diff --git a/assets/hue_icon.ico b/assets/hue_icon.ico new file mode 100644 index 0000000..dd2668c Binary files /dev/null and b/assets/hue_icon.ico differ diff --git a/assets/hue_icon.png b/assets/hue_icon.png new file mode 100644 index 0000000..92c03cd Binary files /dev/null and b/assets/hue_icon.png differ diff --git a/installer/EchoHub-Installer.exe b/installer/EchoHub-Installer.exe new file mode 100644 index 0000000..22a0eb0 Binary files /dev/null and b/installer/EchoHub-Installer.exe differ diff --git a/installer/Installer.iss b/installer/Installer.iss new file mode 100644 index 0000000..b833071 --- /dev/null +++ b/installer/Installer.iss @@ -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 + 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..27b0268 100644 --- a/src/EchoHub.Client/EchoHub.Client.csproj +++ b/src/EchoHub.Client/EchoHub.Client.csproj @@ -5,6 +5,7 @@ + @@ -19,6 +20,7 @@ PreserveNewest + PreserveNewest @@ -32,6 +34,8 @@ net10.0 enable enable + + hue_icon.ico 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(); + } +} diff --git a/src/EchoHub.Client/hue_icon.ico b/src/EchoHub.Client/hue_icon.ico new file mode 100644 index 0000000..dd2668c Binary files /dev/null and b/src/EchoHub.Client/hue_icon.ico differ diff --git a/src/EchoHub.Server/EchoHub.Server.csproj b/src/EchoHub.Server/EchoHub.Server.csproj index 93bbe03..9795f48 100644 --- a/src/EchoHub.Server/EchoHub.Server.csproj +++ b/src/EchoHub.Server/EchoHub.Server.csproj @@ -1,5 +1,9 @@ + + + + @@ -22,6 +26,8 @@ net10.0 enable enable + + hue_icon.ico diff --git a/src/EchoHub.Server/hue_icon.ico b/src/EchoHub.Server/hue_icon.ico new file mode 100644 index 0000000..dd2668c Binary files /dev/null and b/src/EchoHub.Server/hue_icon.ico differ