From 68def5ac1f4ab3e218f0a5ada4f3b7812d65cc1d Mon Sep 17 00:00:00 2001 From: AlanMoonbase Date: Tue, 24 Jun 2025 15:54:55 -0700 Subject: [PATCH] Implement Auto Update Mechanism Add Config Options `startMinimized` and `minimizeToTray` Added `AssemblyVersion` String To Resources --- .../ClientModel/ClientUpdateInfo.cs | 25 +++++ qtc-net-client-2/ClientModel/Config.cs | 9 ++ qtc-net-client-2/Forms/Main.Designer.cs | 1 - qtc-net-client-2/Forms/Main.cs | 12 +- qtc-net-client-2/Forms/Main.resx | 36 +++--- qtc-net-client-2/Program.cs | 40 +++++-- .../Properties/Resources.Designer.cs | 9 ++ qtc-net-client-2/Properties/Resources.resx | 3 + qtc-net-client-2/Services/UpdateService.cs | 105 ++++++++++++++++++ 9 files changed, 210 insertions(+), 30 deletions(-) create mode 100644 qtc-net-client-2/ClientModel/ClientUpdateInfo.cs create mode 100644 qtc-net-client-2/Services/UpdateService.cs diff --git a/qtc-net-client-2/ClientModel/ClientUpdateInfo.cs b/qtc-net-client-2/ClientModel/ClientUpdateInfo.cs new file mode 100644 index 0000000..b3c9c8c --- /dev/null +++ b/qtc-net-client-2/ClientModel/ClientUpdateInfo.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace qtc_net_client_2.ClientModel +{ + public class ClientUpdateInfo + { + [JsonPropertyName("clientFileName")] + [JsonRequired] + public string ClientFileName { get; set; } = "QtCNetChat.exe"; + [JsonPropertyName("version")] + [JsonRequired] + public string Version { get; set; } = string.Empty; + [JsonPropertyName("downloadUri")] + [JsonRequired] + public string DownloadUrl { get; set; } = string.Empty; + [JsonPropertyName("updateMandatory")] + [JsonRequired] + public bool IsUpdateMandatory { get; set; } = false; + } +} diff --git a/qtc-net-client-2/ClientModel/Config.cs b/qtc-net-client-2/ClientModel/Config.cs index b38fb88..1b0fce0 100644 --- a/qtc-net-client-2/ClientModel/Config.cs +++ b/qtc-net-client-2/ClientModel/Config.cs @@ -9,9 +9,18 @@ namespace qtc_net_client_2.ClientModel { public class Config { + [JsonPropertyName("startMinimized")] + [JsonRequired] + public bool StartMinimized { get; set; } = false; + [JsonPropertyName("minimizeToTray")] + [JsonRequired] + public bool MinimizeToTray { get; set; } = true; + [JsonPropertyName("apiEndpoint")] + [JsonRequired] public string ApiEndpoint { get; set; } = "https://qtc.alanmoon.net/api"; [JsonPropertyName("gatewayEndpoint")] + [JsonRequired] public string GatewayEndpoint { get; set; } = "https://qtc.alanmoon.net/chat"; } } diff --git a/qtc-net-client-2/Forms/Main.Designer.cs b/qtc-net-client-2/Forms/Main.Designer.cs index 0adef81..faf4f2e 100644 --- a/qtc-net-client-2/Forms/Main.Designer.cs +++ b/qtc-net-client-2/Forms/Main.Designer.cs @@ -275,7 +275,6 @@ // niMain.Icon = (Icon)resources.GetObject("niMain.Icon"); niMain.Text = "QtC.NET Client"; - niMain.Visible = true; niMain.DoubleClick += niMain_DoubleClick; // // btnAddRoom diff --git a/qtc-net-client-2/Forms/Main.cs b/qtc-net-client-2/Forms/Main.cs index 1384459..5982d8c 100644 --- a/qtc-net-client-2/Forms/Main.cs +++ b/qtc-net-client-2/Forms/Main.cs @@ -13,15 +13,17 @@ namespace qtc_net_client_2 { private IApiService _apiService; private IGatewayService _gatewayService; + private Config _config; private AudioService AudioService = new(); private List RoomList = []; private List OnlineUsers = []; private List Contacts = []; - public Main(IApiService apiService, IGatewayService gatewayService) + public Main(IApiService apiService, IGatewayService gatewayService, Config config) { _apiService = apiService; _gatewayService = gatewayService; + _config = config; InitializeComponent(); } @@ -199,13 +201,17 @@ namespace qtc_net_client_2 private void frmMain_Resize(object sender, EventArgs e) { - if (WindowState == FormWindowState.Minimized) Hide(); + if (WindowState == FormWindowState.Minimized && _config.MinimizeToTray) { Hide(); niMain.Visible = true; niMain.ShowBalloonTip(10, + "I'm over here!", + "QtC.NET Mimimizes To Tray By Default. To Change This Behaviour, Refer To config.json", + ToolTipIcon.Info); } } private void niMain_DoubleClick(object sender, EventArgs e) { Show(); WindowState = FormWindowState.Normal; + niMain.Visible = false; } private async void frmMain_FormClosed(object sender, FormClosedEventArgs e) @@ -364,6 +370,8 @@ namespace qtc_net_client_2 var current = DateTime.UtcNow; if (current > _apiService.CurrentUser.LastCurrencySpin.ToUniversalTime() || _apiService.CurrentUser.LastCurrencySpin == new DateTime()) llblClaimSpin.Visible = true; else llblClaimSpin.Visible = false; + + if(_config.StartMinimized) WindowState = FormWindowState.Minimized; } } } diff --git a/qtc-net-client-2/Forms/Main.resx b/qtc-net-client-2/Forms/Main.resx index 646548f..1cfdd5e 100644 --- a/qtc-net-client-2/Forms/Main.resx +++ b/qtc-net-client-2/Forms/Main.resx @@ -127,19 +127,19 @@ AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu - SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAA5ggAAAJNU0Z0AUkBTAIBAQIB - AAGoAQABqAEAARABAAEQAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABQAMAARADAAEBAQABIAYAARAS + SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAA5AgAAAJNU0Z0AUkBTAIBAQIB + AAGwAQABsAEAARABAAEQAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABQAMAARADAAEBAQABIAYAARAS AANnAe8CZwFZAe8BZwFdAVkB7wFnAVsBWQHvAWcBWwFZAe8BZwJZAe8BZwFkAVkB7wNnAe8DZwHvA2cB - 7wNnAe8DZwHvA2cB7wNnAe8DZwHvA2cB7zgAAzMBUQN8AfWAAAP4Af8BuQGVATwB/wGDAX0BbgH/AYQB + 7wNnAe8DZwHvA2cB7wNnAe8DZwHvA2cB7zgAAzMBUQN5AfWAAAP4Af8BuQGVATwB/wGDAX0BbgH/AYQB fQFsAf8BqgGEAScB/wGsAXsBAAH/AcwBvAGUAf8DfgH/A34B/wN+Af8DfgH/A34B/wN+Af8DfgH/A34B /wOOAf84AAMSARgDPwFtgAAE/wGXAYsBbQH/AoEBgAH/AYIBgQGAAf8BmAGIAWAB/wHKAZABAAH/Ad0B zAGfAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOTAf80AANfAdMDPQFnhAAE/wGGAYQB fQH/A4EB/wOBAf8BhwGDAXoB/wHPAZQBAAH/Ad4BzAGfAf8D4AH/A+AB/wPgAf8D4AH/A+AB/wPgAf8D 4AH/A+AB/wO8Af80AANaAcIDNAFThAAE/wGLAYYBegH/A4EB/wOBAf8BjgGGAXEB/wHPAZQBAAH/Ad4B - zAGfIf8DygH/EAADDQERAz8BbANTAacBXAJZAb4BWAJWAbMBSAJHAYMDIQEwBAADdgHzAzoBYIgABP8B + zAGfIf8DygH/EAADDQERAz8BbANTAacBXAJZAb4BWAJWAbMBSAJHAYMDIQEwBAADcwHzAzoBYIgABP8B rAGWAWAB/wGDAYIBfwH/AYUBggF9Af8BswGTAUQB/wHPAZQBAAH/Ad4BzAGfAf8DsAH/A7AB/wOwAf8D - sAH/A7AB/wOwAf8DsAH/A7AB/wOoAf8IAAMaASQDUgGgAWkBYwFIAfYBogFzAQAB/wGuAXwBAAH/AbAB - fQEAAf8BqAF4AQAB/wGVAWoBAAH/AYABaQEVAf4BXAJZAcYDVwG1AxYBHogABP8B2QGqATcB/wG+AZgB + sAH/A7AB/wOwAf8DsAH/A7AB/wOoAf8IAAMaASQDUgGgAWcBYwFIAfYBogFzAQAB/wGuAXwBAAH/AbAB + fQEAAf8BqAF4AQAB/wGVAWoBAAH/AYABagEWAf4BXAJZAcYDVwG1AxYBHogABP8B2QGqATcB/wG+AZgB OAH/AcABmAE2Af8B3AGiARQB/wHPAZQBAAH/Ad4BzAGfAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8D gQH/A4EB/wOTAf8EAAMgAS0CYwFaAekBvwGIAQAB/wHNAZUBCgH/AbABiAEnAf8BcwFkAT8B/wFNAUsB RwH/AU4BSwFCAf8BZgFXATEB/wGaAXQBFwH/AaQBdgEDAf8BcAFPAQAB/wNDAXYEAYQABP8B4QGuATEB @@ -150,22 +150,22 @@ uwH/A6wB/wNdAf8DTAH/A0sB/wNEAf8DDwH/A7MB/wNmAf8BVAFMAToB/wGuAX0BBAH/A10BzIQABP8B lAGKAXMB/wOBAf8DgQH/AZoBigFjAf8BzwGUAQAB/wHeAcwBnwH/A9AB/wPQAf8D0AH/A9AB/wPQAf8D 0AH/A9AB/wPQAf8DtQH/AmoBYQHmAe0BrQEQAf8B9AHQAXYB/wP6Af8D+gH/A30B/wN/Af8DgAH/A4AB - /wN+Af8DhAH/A7sB/wNqAf8BqAGAARwB/wFrAWMBSAH2hAAE/wGGAYMBfgH/A4EB/wOBAf8BhgGDAXsB - /wHPAZQBAAH/Ad4BzAGfAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOTAf8BawFoAWIB - 7gHvAbQBIQH/AfcB3AGXCf8DkQH/A4gB/wOHAf8DhwH/A4EB/wNXAf8D5gH/A6MB/wG2AZEBNgH/AYYB - agE+AfmEAAT/AYsBhgF5Af8DgQH/A4EB/wGOAYYBcQH/Ac8BlAEAAf8B3gHMAZ8B/wOhAf8DoQH/A6EB + /wN+Af8DhAH/A7sB/wNqAf8BqAGAARwB/wFpAWMBSAH2hAAE/wGGAYMBfgH/A4EB/wOBAf8BhgGDAXsB + /wHPAZQBAAH/Ad4BzAGfAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOBAf8DgQH/A4EB/wOTAf8BagFnAWIB + 7gHvAbQBIQH/AfcB3AGXCf8DkQH/A4gB/wOHAf8DhwH/A4EB/wNXAf8D5gH/A6MB/wG2AZEBNgH/AYUB + agFAAfmEAAT/AYsBhgF5Af8DgQH/A4EB/wGOAYYBcQH/Ac8BlAEAAf8B3gHMAZ8B/wOhAf8DoQH/A6EB /wOhAf8DoQH/A6EB/wOhAf8DoQH/A6EB/wNLAY0B8AG+AT8B/wH0Ac0BbCH/A/sB/wPQAf8B1wGnATEB /wJhAV0B0YQABP8BpwGUAWcB/wGDAYIBfwH/AYUBgwF+Af8BsAGUAU4B/wHTAZcBAgH/AeABzgGfAf8D - 5wH/A+cB/wPnAf8D5wH/A+cB/wPnAf8D5wH/A+cB/wPAAf8DBwEJAWsBaQFiAe4B8QG8ATsB/wH6AeoB + 5wH/A+cB/wPnAf8D5wH/A+cB/wPnAf8D5wH/A+cB/wPAAf8DBwEJAWoBaAFiAe4B8QG8ATsB/wH6AeoB wgH/A9wB/wN3Af8DaAH/A2gB/wNoAf8DMgn/AfIB3QGpAf8B6gGpAQgB/wM+AWqEAAT/AdcBsQFSAf8B mgGPAXQB/wGgAZIBbQH/AeABrwE3Af8B5wGpARAB/wHrAdUBoAH/A4EB/wOBAf8DgQH/A4EB/wOBAf8D - gQH/A4EB/wOBAf8DkwH/BAADMwFRAWsBaQFoAfAB8wHGAVgB/wH6AecBuBb/Af4B+wH/AfkB4gGqAf8B - 7wG4AS0B/wNOAZYEAoQABP8B9AHNAWwB/wH0AcsBZgH/AfQBywFlAf8B9AHLAWUB/wHxAcEBSQH/AfkB - 4wGsAf8DiQH/A4kB/wOJAf8DiQH/A4kB/wOJAf8DiQH/A4kB/wOaAf8IAAMiATEDXwHJAaIBjQFnAfoB - 8wHKAWUB/wH5AeEBpgH/AfsB7QHMAf8B+wHsAcgB/wH4Ad0BmwH/AcgBmgFmAf4CZQFeAeIDPQFoBAGI - AED/EAADDwETA0cBggNkAdsBtgGhAW8B/ANnAeoDVAGoAygBO5QAAUIBTQE+BwABPgMAASgDAAFAAwAB - EAMAAQEBAAEBBQABgBcAA/8DAAH/AfwGAAH/AfwGAAH/AfkGAAH/AfkGAAHwARMGAAHAAQMGAAGAAQEG - AAGAAQEHAAEBBwABAQcAAQEHAAEBBwABAQYAAYABAQYAAcABAwYAAfABHwQACw== + gQH/A4EB/wOBAf8DkwH/BAADMwFRAWoCaAHwAfMBxgFYAf8B+gHnAbgW/wH+AfsB/wH5AeIBqgH/Ae8B + uAEtAf8DTgGWBAKEAAT/AfQBzQFsAf8B9AHLAWYB/wH0AcsBZQH/AfQBywFlAf8B8QHBAUkB/wH5AeMB + rAH/A4kB/wOJAf8DiQH/A4kB/wOJAf8DiQH/A4kB/wOJAf8DmgH/CAADIgExA18ByQGgAYoBZwH6AfMB + ygFlAf8B+QHhAaYB/wH7Ae0BzAH/AfsB7AHIAf8B+AHdAZsB/wHGAZgBZwH+AmUBXgHiAz0BaAQBiABA + /xAAAw8BEwNHAYIDZAHbAbQBngFvAfwDZwHqA1QBqAMoATuUAAFCAU0BPgcAAT4DAAEoAwABQAMAARAD + AAEBAQABAQUAAYAXAAP/AwAB/wH8BgAB/wH8BgAB/wH5BgAB/wH5BgAB8AETBgABwAEDBgABgAEBBgAB + gAEBBwABAQcAAQEHAAEBBwABAQcAAQEGAAGAAQEGAAHAAQMGAAHwAR8EAAs= diff --git a/qtc-net-client-2/Program.cs b/qtc-net-client-2/Program.cs index d8af02e..a26675a 100644 --- a/qtc-net-client-2/Program.cs +++ b/qtc-net-client-2/Program.cs @@ -1,4 +1,5 @@ using qtc_net_client_2.ClientModel; +using qtc_net_client_2.Services; using QtCNETAPI.Services.ApiService; using QtCNETAPI.Services.GatewayService; using System.Text.Json; @@ -16,6 +17,13 @@ namespace qtc_net_client_2 { Config clientConfig = new Config(); + if(System.Diagnostics.Debugger.IsAttached) + { + // use localhost + clientConfig.ApiEndpoint = "http://localhost:5268/api"; + clientConfig.GatewayEndpoint = "http://localhost:5268/chat"; + } + // find config file if(!File.Exists("./config.json")) { @@ -24,10 +32,20 @@ namespace qtc_net_client_2 File.WriteAllText("./config.json", configJson); } else { - // use config in file - Config? fileConfig = JsonSerializer.Deserialize(File.ReadAllText("./config.json")); - if(fileConfig != null) - clientConfig = fileConfig; + try + { + // use config in file + Config? fileConfig = JsonSerializer.Deserialize(File.ReadAllText("./config.json")); + if (fileConfig != null) + { + clientConfig = fileConfig; + } + } catch (JsonException) + { + // there was a missing property, reset config to default + string configJson = JsonSerializer.Serialize(clientConfig, options: new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText("./config.json", configJson); + } } // instantiate new ApiService and GatewayService for this session @@ -36,23 +54,27 @@ namespace qtc_net_client_2 IGatewayService gateway = new GatewayService(clientConfig.GatewayEndpoint, api); // ping api - var pingResult = api.PingServerAsync().GetAwaiter().GetResult(); + var pingResult = await api.PingServerAsync(); if (!pingResult.Success) { - MessageBox.Show("The API Specified In The Config Could Not Be Reached.\nCheck The URL Specified, Otherwise Contact The Server Admin.", "Uh Oh.", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show("The Server Specified In The Config Could Not Be Reached.\nCheck The URL Specified, Otherwise Contact The Server Admin.", "Uh Oh.", MessageBoxButtons.OK, MessageBoxIcon.Error); Environment.Exit(1); } + // check for updates + UpdateService updateService = new(); + await updateService.CheckForUpdatesAsync(); + // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); - Application.Run(new Main(api, gateway)); + Application.Run(new Main(api, gateway, clientConfig)); // if application loop is exited, dispose everything await gateway.DisposeAsync(); - api = null; - gateway = null; + api = null!; // shut up compiler >:( + gateway = null!; Environment.Exit(0); } diff --git a/qtc-net-client-2/Properties/Resources.Designer.cs b/qtc-net-client-2/Properties/Resources.Designer.cs index 63bca4a..05a2309 100644 --- a/qtc-net-client-2/Properties/Resources.Designer.cs +++ b/qtc-net-client-2/Properties/Resources.Designer.cs @@ -80,6 +80,15 @@ namespace qtc_net_client_2.Properties { } } + /// + /// Looks up a localized string similar to 3.4. + /// + internal static string AssemblyVersion { + get { + return ResourceManager.GetString("AssemblyVersion", resourceCulture); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/qtc-net-client-2/Properties/Resources.resx b/qtc-net-client-2/Properties/Resources.resx index d3e583e..1648a9a 100644 --- a/qtc-net-client-2/Properties/Resources.resx +++ b/qtc-net-client-2/Properties/Resources.resx @@ -169,4 +169,7 @@ ..\Anims\left-horn-animated.gif;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + 3.4 + \ No newline at end of file diff --git a/qtc-net-client-2/Services/UpdateService.cs b/qtc-net-client-2/Services/UpdateService.cs new file mode 100644 index 0000000..5312eef --- /dev/null +++ b/qtc-net-client-2/Services/UpdateService.cs @@ -0,0 +1,105 @@ +using qtc_net_client_2.ClientModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Diagnostics; +using qtc_net_client_2.Properties; +using System.Reflection; + +namespace qtc_net_client_2.Services +{ + public class UpdateService + { + public async Task CheckForUpdatesAsync() + { + if (Debugger.IsAttached) return; + + // get client update info + HttpClient client = new(); + client.BaseAddress = new Uri("https://qtcclient.alanmoon.net"); + + try + { + ClientUpdateInfo? updateInfo = await client.GetFromJsonAsync("clientinfo.json"); + if (updateInfo != null) + { + if (updateInfo.Version != Resources.AssemblyVersion) + { + // inform the user an update is available + if (!updateInfo.IsUpdateMandatory) + { + var result = MessageBox.Show( + $"An Update For QtC.NET Is Available. You Can View The Changelog At {updateInfo.DownloadUrl}. Do You Want To Update Now?", + "Update Available", + MessageBoxButtons.YesNo, + MessageBoxIcon.Information); + if (result == DialogResult.Yes) await UpdateAsync(updateInfo); + } + else + { + var result = MessageBox.Show( + "Your QtC.NET Client Is Not Up To Date. Using This Version Of The Client With The Current Version Of The Server Could Lead To Data Loss.\n" + + $"You Can View The Changelog At {updateInfo.DownloadUrl}. Do You Want To Update Now?", + "Update Required", + MessageBoxButtons.YesNo, + MessageBoxIcon.Error); + if (result == DialogResult.Yes) await UpdateAsync(updateInfo); + else Environment.Exit(1); + } + } + } + } catch (HttpRequestException ex) + { + Debug.WriteLine("Client Update HTTP Request Failed - " + ex.Message); + } catch (JsonException) + { + Debug.WriteLine("Client Update Info From Server Invalid"); + } + } + + private async Task UpdateAsync(ClientUpdateInfo updateInfo) + { + HttpClient client = new(); + client.BaseAddress = new Uri("https://qtcclient.alanmoon.net"); + + try + { + // move old client to backup file + File.Move($"./{updateInfo.ClientFileName}", $"{updateInfo.ClientFileName}.bak"); + + // download new client version + var clientFileStream = await client.GetStreamAsync(updateInfo.ClientFileName); + using(var fs = new FileStream($"./{updateInfo.ClientFileName}", FileMode.Create)) + { + clientFileStream.CopyTo(fs); + fs.Dispose(); + } + clientFileStream.Dispose(); + + // restart the process + Process process = new Process(); + process.StartInfo = new ProcessStartInfo { FileName = $"./{updateInfo.ClientFileName}", WorkingDirectory = Environment.CurrentDirectory }; + + process.Start(); + Environment.Exit(0); + } catch (HttpRequestException ex) + { + MessageBox.Show($"Update Failed. Please Check Your Internet Connection.\n{ex.Message}", + "Update Failed", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + catch (UnauthorizedAccessException ex) + { + MessageBox.Show($"Update Failed. Permissions In Client Folder Are Wrong.\n{ex.Message}", + "Update Failed", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + } + } +}