feat: add automatic update checking with confirmation and progress dialogs

This commit is contained in:
Stone_Red
2026-02-21 17:11:47 +01:00
committed by Hue
parent 1fa4348700
commit cf9c647c03
5 changed files with 219 additions and 47 deletions
+5 -12
View File
@@ -23,6 +23,7 @@ public sealed class AppOrchestrator : IDisposable
private readonly CommandHandler _commandHandler; private readonly CommandHandler _commandHandler;
private readonly NotificationSoundService _notificationSound; private readonly NotificationSoundService _notificationSound;
private readonly AudioPlaybackService _audioPlayback = new(); private readonly AudioPlaybackService _audioPlayback = new();
private readonly UpdateChecker _updateService;
private EchoHubConnection? _connection; private EchoHubConnection? _connection;
private ApiClient? _apiClient; private ApiClient? _apiClient;
@@ -45,10 +46,13 @@ public sealed class AppOrchestrator : IDisposable
_mainWindow = new MainWindow(app); _mainWindow = new MainWindow(app);
_commandHandler = new CommandHandler(); _commandHandler = new CommandHandler();
_notificationSound = new NotificationSoundService(config.Notifications); _notificationSound = new NotificationSoundService(config.Notifications);
_updateService = new UpdateChecker(app);
WireMainWindowEvents(); WireMainWindowEvents();
WireCommandHandlerEvents(); WireCommandHandlerEvents();
_updateService.Start();
_mainWindow.UpdateStatusBar("Disconnected"); _mainWindow.UpdateStatusBar("Disconnected");
} }
@@ -56,6 +60,7 @@ public sealed class AppOrchestrator : IDisposable
{ {
_connection?.DisposeAsync().AsTask().GetAwaiter().GetResult(); _connection?.DisposeAsync().AsTask().GetAwaiter().GetResult();
_apiClient?.Dispose(); _apiClient?.Dispose();
_updateService.Dispose();
} }
// ── Convenience Helpers ──────────────────────────────────────────────── // ── Convenience Helpers ────────────────────────────────────────────────
@@ -456,18 +461,6 @@ public sealed class AppOrchestrator : IDisposable
FetchAndUpdateOnlineUsers(); FetchAndUpdateOnlineUsers();
SaveServerToConfig(result); 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"); }, "Connection failed", "Connect");
} }
+1
View File
@@ -5,6 +5,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AlwaysUpToDate" Version="2.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.3" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.3" />
<PackageReference Include="NetCoreAudio" Version="2.0.1" /> <PackageReference Include="NetCoreAudio" Version="2.0.1" />
+94 -35
View File
@@ -1,47 +1,106 @@
using System.Net.Http.Json; using AlwaysUpToDate;
using System.Text.Json.Serialization;
using EchoHub.Client.UI;
using Serilog;
using Terminal.Gui.App;
namespace EchoHub.Client.Services; namespace EchoHub.Client.Services;
public static class UpdateChecker public sealed class UpdateChecker : IDisposable
{ {
private static readonly Uri ReleaseUrl = private const string ManifestUrl = "https://echohub.voidcube.cloud/api/app/version";
new("https://api.github.com/repos/HueByte/EchoHub/releases/latest");
/// <summary> private readonly Updater _updater;
/// Checks GitHub for a newer release. Returns the new version string if one exists, or null. private readonly IApplication _app;
/// Never throws — all errors are silently swallowed. private UpdateProgressDialog? _progressDialog;
/// </summary>
public static async Task<string?> CheckForUpdateAsync() public static string CurrentVersion => typeof(UpdateChecker).Assembly.GetName().Version?.ToString(3) ?? "0.0.0";
public UpdateChecker(IApplication app)
{ {
try _app = app;
{ _updater = new Updater(TimeSpan.FromHours(1), ManifestUrl, false);
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
http.DefaultRequestHeaders.UserAgent.ParseAdd("EchoHub-Client");
var release = await http.GetFromJsonAsync<GitHubRelease>(ReleaseUrl); _updater.UpdateAvailable += OnUpdateAvailable;
if (release?.TagName is null) _updater.ProgressChanged += OnProgressChanged;
return null; _updater.UpdateStarted += OnUpdateStarted;
_updater.NoUpdateAvailable += OnNoUpdateAvailable;
var tag = release.TagName.TrimStart('v', 'V'); _updater.OnException += OnException;
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;
}
} }
private sealed class GitHubRelease public void Start()
{ {
[JsonPropertyName("tag_name")] #if RELEASE
public string? TagName { get; set; } _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;
} }
} }
@@ -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;
}
}
@@ -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();
}
}