Implement Auto Update Mechanism

Add Config Options `startMinimized` and `minimizeToTray`
Added `AssemblyVersion` String To Resources
This commit is contained in:
Alan Moon 2025-06-24 15:54:55 -07:00
parent 88806e93a4
commit 68def5ac1f
9 changed files with 210 additions and 30 deletions

View File

@ -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;
}
}

View File

@ -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";
}
}

View File

@ -275,7 +275,6 @@
//
niMain.Icon = (Icon)resources.GetObject("niMain.Icon");
niMain.Text = "QtC.NET Client";
niMain.Visible = true;
niMain.DoubleClick += niMain_DoubleClick;
//
// btnAddRoom

View File

@ -13,15 +13,17 @@ namespace qtc_net_client_2
{
private IApiService _apiService;
private IGatewayService _gatewayService;
private Config _config;
private AudioService AudioService = new();
private List<Room> RoomList = [];
private List<UserInformationDto> OnlineUsers = [];
private List<UserInformationDto> 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;
}
}
}

View File

@ -127,19 +127,19 @@
<value>
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=
</value>
</data>
<metadata name="ctxmChangeStatus.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">

View File

@ -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"))
{
@ -23,12 +31,22 @@ namespace qtc_net_client_2
string configJson = JsonSerializer.Serialize(clientConfig, options: new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText("./config.json", configJson);
} else
{
try
{
// use config in file
Config? fileConfig = JsonSerializer.Deserialize<Config>(File.ReadAllText("./config.json"));
if(fileConfig != null)
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
// this will keep the gateway thread in the main thread (i think)
@ -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);
}

View File

@ -80,6 +80,15 @@ namespace qtc_net_client_2.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to 3.4.
/// </summary>
internal static string AssemblyVersion {
get {
return ResourceManager.GetString("AssemblyVersion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>

View File

@ -169,4 +169,7 @@
<data name="left-horn-animated" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Anims\left-horn-animated.gif;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="AssemblyVersion" xml:space="preserve">
<value>3.4</value>
</data>
</root>

View File

@ -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<ClientUpdateInfo>("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);
}
}
}
}