mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +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 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user