From cf9c647c03a006bb8964aa58b5bf78f311415328 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:41:47 +0100 Subject: [PATCH] feat: add automatic update checking with confirmation and progress dialogs --- src/EchoHub.Client/AppOrchestrator.cs | 17 +-- src/EchoHub.Client/EchoHub.Client.csproj | 1 + src/EchoHub.Client/Services/UpdateChecker.cs | 129 +++++++++++++----- src/EchoHub.Client/UI/UpdateConfirmDialog.cs | 58 ++++++++ src/EchoHub.Client/UI/UpdateProgressDialog.cs | 61 +++++++++ 5 files changed, 219 insertions(+), 47 deletions(-) create mode 100644 src/EchoHub.Client/UI/UpdateConfirmDialog.cs create mode 100644 src/EchoHub.Client/UI/UpdateProgressDialog.cs diff --git a/src/EchoHub.Client/AppOrchestrator.cs b/src/EchoHub.Client/AppOrchestrator.cs index 2898fed..698b5da 100644 --- a/src/EchoHub.Client/AppOrchestrator.cs +++ b/src/EchoHub.Client/AppOrchestrator.cs @@ -23,6 +23,7 @@ public sealed class AppOrchestrator : IDisposable private readonly CommandHandler _commandHandler; private readonly NotificationSoundService _notificationSound; private readonly AudioPlaybackService _audioPlayback = new(); + private readonly UpdateChecker _updateService; private EchoHubConnection? _connection; private ApiClient? _apiClient; @@ -45,10 +46,13 @@ public sealed class AppOrchestrator : IDisposable _mainWindow = new MainWindow(app); _commandHandler = new CommandHandler(); _notificationSound = new NotificationSoundService(config.Notifications); + _updateService = new UpdateChecker(app); WireMainWindowEvents(); WireCommandHandlerEvents(); + _updateService.Start(); + _mainWindow.UpdateStatusBar("Disconnected"); } @@ -56,6 +60,7 @@ public sealed class AppOrchestrator : IDisposable { _connection?.DisposeAsync().AsTask().GetAwaiter().GetResult(); _apiClient?.Dispose(); + _updateService.Dispose(); } // ── Convenience Helpers ──────────────────────────────────────────────── @@ -456,18 +461,6 @@ public sealed class AppOrchestrator : IDisposable FetchAndUpdateOnlineUsers(); SaveServerToConfig(result); - - // Check for newer version in the background - _ = Task.Run(async () => - { - var newVersion = await UpdateChecker.CheckForUpdateAsync(); - if (newVersion is not null) - { - InvokeUI(() => _mainWindow.AddSystemMessage( - HubConstants.DefaultChannel, - $"A new version of EchoHub is available: v{newVersion} (current: v{MainWindow.AppVersion}). Visit https://github.com/HueByte/EchoHub/releases")); - } - }); }, "Connection failed", "Connect"); } diff --git a/src/EchoHub.Client/EchoHub.Client.csproj b/src/EchoHub.Client/EchoHub.Client.csproj index 53628a9..a4991f2 100644 --- a/src/EchoHub.Client/EchoHub.Client.csproj +++ b/src/EchoHub.Client/EchoHub.Client.csproj @@ -5,6 +5,7 @@ + diff --git a/src/EchoHub.Client/Services/UpdateChecker.cs b/src/EchoHub.Client/Services/UpdateChecker.cs index d73c9df..9a204ad 100644 --- a/src/EchoHub.Client/Services/UpdateChecker.cs +++ b/src/EchoHub.Client/Services/UpdateChecker.cs @@ -1,47 +1,106 @@ -using System.Net.Http.Json; -using System.Text.Json.Serialization; +using AlwaysUpToDate; + +using EchoHub.Client.UI; + +using Serilog; + +using Terminal.Gui.App; namespace EchoHub.Client.Services; -public static class UpdateChecker +public sealed class UpdateChecker : IDisposable { - private static readonly Uri ReleaseUrl = - new("https://api.github.com/repos/HueByte/EchoHub/releases/latest"); + private const string ManifestUrl = "https://echohub.voidcube.cloud/api/app/version"; - /// - /// Checks GitHub for a newer release. Returns the new version string if one exists, or null. - /// Never throws — all errors are silently swallowed. - /// - public static async Task CheckForUpdateAsync() + private readonly Updater _updater; + private readonly IApplication _app; + private UpdateProgressDialog? _progressDialog; + + public static string CurrentVersion => typeof(UpdateChecker).Assembly.GetName().Version?.ToString(3) ?? "0.0.0"; + + public UpdateChecker(IApplication app) { - try - { - using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; - http.DefaultRequestHeaders.UserAgent.ParseAdd("EchoHub-Client"); + _app = app; + _updater = new Updater(TimeSpan.FromHours(1), ManifestUrl, false); - var release = await http.GetFromJsonAsync(ReleaseUrl); - if (release?.TagName is null) - return null; - - var tag = release.TagName.TrimStart('v', 'V'); - if (!Version.TryParse(tag, out var latest)) - return null; - - var currentStr = typeof(UpdateChecker).Assembly.GetName().Version?.ToString(3); - if (currentStr is null || !Version.TryParse(currentStr, out var current)) - return null; - - return latest > current ? tag : null; - } - catch - { - return null; - } + _updater.UpdateAvailable += OnUpdateAvailable; + _updater.ProgressChanged += OnProgressChanged; + _updater.UpdateStarted += OnUpdateStarted; + _updater.NoUpdateAvailable += OnNoUpdateAvailable; + _updater.OnException += OnException; } - private sealed class GitHubRelease + public void Start() { - [JsonPropertyName("tag_name")] - public string? TagName { get; set; } +#if RELEASE + _updater.Start(); +#endif + } + + private async void OnUpdateAvailable(string version, string changelogUrl) + { + Log.Information("Update available: v{Version}", version); + + var confirmed = false; + _app.Invoke(() => + { + confirmed = UpdateConfirmDialog.Show(_app, CurrentVersion, version); + + + if (confirmed) + { + _progressDialog = new UpdateProgressDialog(_app, version); + + // Start the update; progress is reported via OnProgressChanged + _ = Task.Run(async () => + { + await _updater.UpdateAsync(); + }); + + _progressDialog?.Show(); + } + }); + } + + private void OnProgressChanged(UpdateStep step, long itemsProcessed, long? totalItems, double? progressPercentage) + { + var fraction = progressPercentage.HasValue ? (float)(progressPercentage.Value / 100.0) : 0f; + var statusText = $"{step}: {itemsProcessed}/{totalItems ?? 0} ({progressPercentage ?? 0:F0}%)"; + + if (!progressPercentage.HasValue) + { + statusText = $"{step}..."; + } + + _progressDialog?.UpdateProgress(fraction, statusText); + } + + private void OnUpdateStarted(string version) + { + Log.Information("Update started: v{Version}", version); + } + + private void OnNoUpdateAvailable() + { + Log.Debug("No update available"); + } + + private void OnException(Exception exception) + { + Log.Error(exception, "Update check failed"); + _app.Invoke(() => + { + _progressDialog?.Close(); + _progressDialog = null; + }); + } + + public void Dispose() + { + _updater.UpdateAvailable -= OnUpdateAvailable; + _updater.ProgressChanged -= OnProgressChanged; + _updater.UpdateStarted -= OnUpdateStarted; + _updater.NoUpdateAvailable -= OnNoUpdateAvailable; + _updater.OnException -= OnException; } } diff --git a/src/EchoHub.Client/UI/UpdateConfirmDialog.cs b/src/EchoHub.Client/UI/UpdateConfirmDialog.cs new file mode 100644 index 0000000..d305377 --- /dev/null +++ b/src/EchoHub.Client/UI/UpdateConfirmDialog.cs @@ -0,0 +1,58 @@ +using Terminal.Gui.App; +using Terminal.Gui.Views; +using Terminal.Gui.ViewBase; + +namespace EchoHub.Client.UI; + +public sealed class UpdateConfirmDialog +{ + public static bool Show(IApplication app, string currentVersion, string newVersion) + { + var confirmed = false; + + var dialog = new Dialog { Title = "Update Available", Width = 50, Height = 10 }; + + var messageLabel = new Label + { + Text = $"A new version of EchoHub is available.\n\n Current: {currentVersion}\n Latest: {newVersion}", + X = 1, + Y = 1, + Width = Dim.Fill(2), + Height = 4 + }; + + var updateButton = new Button + { + Text = "Update", + IsDefault = true, + X = Pos.Center() - 10, + Y = 6 + }; + + var cancelButton = new Button + { + Text = "Cancel", + X = Pos.Center() + 5, + Y = 6 + }; + + updateButton.Accepting += (s, e) => + { + confirmed = true; + e.Handled = true; + app.RequestStop(); + }; + + cancelButton.Accepting += (s, e) => + { + confirmed = false; + e.Handled = true; + app.RequestStop(); + }; + + dialog.Add(messageLabel, updateButton, cancelButton); + app.Run(dialog); + + return confirmed; + } +} diff --git a/src/EchoHub.Client/UI/UpdateProgressDialog.cs b/src/EchoHub.Client/UI/UpdateProgressDialog.cs new file mode 100644 index 0000000..1d71f04 --- /dev/null +++ b/src/EchoHub.Client/UI/UpdateProgressDialog.cs @@ -0,0 +1,61 @@ +using Terminal.Gui.App; +using Terminal.Gui.Views; +using Terminal.Gui.ViewBase; + +namespace EchoHub.Client.UI; + +public sealed class UpdateProgressDialog +{ + private readonly Dialog _dialog; + private readonly ProgressBar _progressBar; + private readonly Label _infoLabel; + private readonly IApplication _app; + + public UpdateProgressDialog(IApplication app, string newVersion) + { + _app = app; + + _dialog = new Dialog { Title = $"Updating to {newVersion}", Width = 50, Height = 10 }; + + _infoLabel = new Label + { + Text = "Preparing update...", + X = 1, + Y = 1, + Width = Dim.Fill(2) + }; + + _progressBar = new ProgressBar + { + X = 1, + Y = 3, + Width = Dim.Fill(2), + Fraction = 0f + }; + + var cancelButton = new Button + { + Text = "Cancel", + X = Pos.Center(), + Y = 6 + }; + + _dialog.Add(_infoLabel, _progressBar); + } + + public void UpdateProgress(float fraction, string statusText) + { + _progressBar.Fraction = fraction; + _infoLabel.Text = statusText; + } + + public void Show() + { + _app.Run(_dialog); + } + + public void Close() + { + _app.RequestStop(); + } +}