Merge pull request #12 from Stone-Red-Code/dev

feat: installer & auto update system
This commit is contained in:
Hue
2026-02-21 17:12:47 +01:00
committed by GitHub
12 changed files with 280 additions and 47 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.
+52
View File
@@ -0,0 +1,52 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#expr Exec('cmd.exe', '/C dotnet build -o "' + SourcePath + '\publish" -c Release ' + SourcePath + '..\src\EchoHub.Client\')
#define MyAppName "EchoHub"
#define MyAppVersion GetStringFileInfo("/publish/EchoHub.Client.exe","ProductVersion")
#define MyAppPublisher "Hue"
#define MyAppExeName "EchoHub.Client.exe"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId=c95b1292-3022-4c62-a131-4d46ace370f5
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
SetupIconFile=../assets/hue_icon.ico
; The [Icons] "quicklaunchicon" entry uses {userappdata} but its [Tasks] entry has a proper IsAdminInstallMode Check.
UsedUserAreasWarning=no
; Remove the following line to run in administrative install mode (install for all users.)
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputBaseFilename={#MyAppName}-Installer
OutputDir=.
Compression=lzma
SolidCompression=yes
WizardStyle=modern
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode
[Files]
Source: "publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
+5 -12
View File
@@ -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");
}
+4
View File
@@ -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" />
@@ -19,6 +20,7 @@
<Content Include="appsettings.json" Condition="Exists('appsettings.json')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="hue_icon.ico" />
<Content Include="Assets\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -32,6 +34,8 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageIcon></PackageIcon>
<ApplicationIcon>hue_icon.ico</ApplicationIcon>
</PropertyGroup>
</Project>
+94 -35
View File
@@ -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();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+6
View File
@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<Content Include="hue_icon.ico" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EchoHub.Core\EchoHub.Core.csproj" />
<ProjectReference Include="..\EchoHub.Server.Irc\EchoHub.Server.Irc.csproj" />
@@ -22,6 +26,8 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageIcon></PackageIcon>
<ApplicationIcon>hue_icon.ico</ApplicationIcon>
</PropertyGroup>
</Project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB