mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 00:26:07 +02:00
feat: add automatic update checking with confirmation and progress dialogs
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AlwaysUpToDate" Version="2.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.3" />
|
||||
<PackageReference Include="NetCoreAudio" Version="2.0.1" />
|
||||
|
||||
@@ -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";
|
||||
|
||||
/// <summary>
|
||||
/// Checks GitHub for a newer release. Returns the new version string if one exists, or null.
|
||||
/// Never throws — all errors are silently swallowed.
|
||||
/// </summary>
|
||||
public static async Task<string?> 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<GitHubRelease>(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user