From 7409deb97a4f1693e39454db65d0b4ca934b6937 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 19 Nov 2023 00:02:45 -0500 Subject: [PATCH] Add Update preferences page --- .../Converters/EnumStringConverter.cs | 32 +++ .../Converters/EnumToBooleanConverter.cs | 24 ++ .../DesignData/DesignData.cs | 35 ++- .../Languages/Resources.Designer.cs | 54 ++++ .../Languages/Resources.resx | 18 ++ .../Models/UpdateChannelCard.cs | 63 +++++ .../StabilityMatrix.Avalonia.csproj | 1 + .../Settings/MainSettingsViewModel.cs | 1 - .../Settings/UpdateSettingsViewModel.cs | 237 ++++++++++++++++++ .../ViewModels/SettingsViewModel.cs | 3 +- .../Views/Settings/MainSettingsPage.axaml | 54 ++-- .../Views/Settings/UpdateSettingsPage.axaml | 143 +++++++++++ .../Settings/UpdateSettingsPage.axaml.cs | 41 +++ .../Models/Api/Lykos/GetUserResponse.cs | 18 +- .../Models/Api/Lykos/LykosRole.cs | 12 + .../Models/Settings/Settings.cs | 5 + StabilityMatrix.Core/Updater/IUpdateHelper.cs | 7 +- StabilityMatrix.Core/Updater/UpdateHelper.cs | 42 +++- .../Updater/UpdateStatusChangedEventArgs.cs | 13 + 19 files changed, 773 insertions(+), 30 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Converters/EnumStringConverter.cs create mode 100644 StabilityMatrix.Avalonia/Converters/EnumToBooleanConverter.cs create mode 100644 StabilityMatrix.Avalonia/Models/UpdateChannelCard.cs create mode 100644 StabilityMatrix.Avalonia/ViewModels/Settings/UpdateSettingsViewModel.cs create mode 100644 StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml create mode 100644 StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml.cs create mode 100644 StabilityMatrix.Core/Models/Api/Lykos/LykosRole.cs create mode 100644 StabilityMatrix.Core/Updater/UpdateStatusChangedEventArgs.cs diff --git a/StabilityMatrix.Avalonia/Converters/EnumStringConverter.cs b/StabilityMatrix.Avalonia/Converters/EnumStringConverter.cs new file mode 100644 index 00000000..15ceaa4e --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/EnumStringConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using StabilityMatrix.Core.Extensions; + +namespace StabilityMatrix.Avalonia.Converters; + +public class EnumStringConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not Enum enumValue) + return null; + + return enumValue.GetStringValue(); + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture + ) + { + if (value is not string stringValue) + return null; + + return Enum.Parse(targetType, stringValue); + } +} diff --git a/StabilityMatrix.Avalonia/Converters/EnumToBooleanConverter.cs b/StabilityMatrix.Avalonia/Converters/EnumToBooleanConverter.cs new file mode 100644 index 00000000..0a047b43 --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/EnumToBooleanConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; + +namespace StabilityMatrix.Avalonia.Converters; + +public class EnumToBooleanConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value?.Equals(parameter); + } + + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture + ) + { + return value?.Equals(true) == true ? parameter : BindingOperations.DoNothing; + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 9811c32d..2addd46f 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -11,6 +11,7 @@ using DynamicData.Binding; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using NSubstitute.ReturnsExtensions; +using Semver; using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.TagCompletion; @@ -36,6 +37,7 @@ using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Updater; @@ -449,6 +451,37 @@ public static class DesignData public static AccountSettingsViewModel AccountSettingsViewModel => Services.GetRequiredService(); + public static UpdateSettingsViewModel UpdateSettingsViewModel + { + get + { + var vm = Services.GetRequiredService(); + + var update = new UpdateInfo + { + Version = SemVersion.Parse("2.0.1"), + ReleaseDate = DateTimeOffset.Now, + Url = new Uri("https://example.org"), + Changelog = new Uri("https://example.org"), + HashBlake3 = "", + Signature = "", + }; + + vm.UpdateStatus = new UpdateStatusChangedEventArgs + { + LatestUpdate = update, + UpdateChannels = new Dictionary + { + [UpdateChannel.Stable] = update, + [UpdateChannel.Preview] = update, + [UpdateChannel.Development] = update + }, + CheckedAt = DateTimeOffset.UtcNow + }; + return vm; + } + } + public static CheckpointBrowserViewModel CheckpointBrowserViewModel => Services.GetRequiredService(); @@ -760,7 +793,7 @@ The gallery images are often inpainted, but you will get something very similar ) ); - public static Indexer Types => new(); + public static Indexer Types { get; } = new(); public class Indexer { diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index 36b4d489..888d15e2 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -662,6 +662,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Auto Updates. + /// + public static string Label_AutoUpdates { + get { + return ResourceManager.GetString("Label_AutoUpdates", resourceCulture); + } + } + /// /// Looks up a localized string similar to Base Model. /// @@ -1814,6 +1823,33 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Updates. + /// + public static string Label_Updates { + get { + return ResourceManager.GetString("Label_Updates", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to For technical users. Be the first to access our Development builds from feature branches as soon as they are available. There may be some rough edges and bugs as we experiment with new features.. + /// + public static string Label_UpdatesDevChannelDescription { + get { + return ResourceManager.GetString("Label_UpdatesDevChannelDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to For early adopters. Preview builds will be more reliable than those from the Dev channel, and will be available closer to stable releases. Your feedback will help us greatly in discovering issues and polishing design elements.. + /// + public static string Label_UpdatesPreviewChannelDescription { + get { + return ResourceManager.GetString("Label_UpdatesPreviewChannelDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Upscale. /// @@ -1886,6 +1922,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to You're up to date. + /// + public static string Label_YouAreUpToDate { + get { + return ResourceManager.GetString("Label_YouAreUpToDate", resourceCulture); + } + } + /// /// Looks up a localized string similar to Download complete. /// @@ -2048,6 +2093,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Last checked: {0}. + /// + public static string TextTemplate_LastChecked { + get { + return ResourceManager.GetString("TextTemplate_LastChecked", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} has been updated to the latest version. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index def18acc..531d2418 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -786,4 +786,22 @@ Download Failed + + Auto Updates + + + For early adopters. Preview builds will be more reliable than those from the Dev channel, and will be available closer to stable releases. Your feedback will help us greatly in discovering issues and polishing design elements. + + + For technical users. Be the first to access our Development builds from feature branches as soon as they are available. There may be some rough edges and bugs as we experiment with new features. + + + Updates + + + You're up to date + + + Last checked: {0} + diff --git a/StabilityMatrix.Avalonia/Models/UpdateChannelCard.cs b/StabilityMatrix.Avalonia/Models/UpdateChannelCard.cs new file mode 100644 index 00000000..6a098388 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/UpdateChannelCard.cs @@ -0,0 +1,63 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Semver; +using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.Update; + +namespace StabilityMatrix.Avalonia.Models; + +public partial class UpdateChannelCard : ObservableObject +{ + public UpdateChannel UpdateChannel { get; init; } + + public string DisplayName => UpdateChannel.GetStringValue(); + + public string? Description { get; init; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(LatestVersionString))] + [NotifyPropertyChangedFor(nameof(IsLatestVersionUpdateable))] + private SemVersion? latestVersion; + + public string? LatestVersionString => + LatestVersion is null ? null : $"Latest: v{LatestVersion}"; + + [ObservableProperty] + private bool isSelectable = true; + + /// + /// Whether the is available for update. + /// + public bool IsLatestVersionUpdateable + { + get + { + if (LatestVersion is null) + { + return false; + } + + switch (LatestVersion.ComparePrecedenceTo(Compat.AppVersion)) + { + case > 0: + // Newer version available + return true; + case 0: + { + // Same version available, check if we both have commit hash metadata + var updateHash = LatestVersion.Metadata; + var appHash = Compat.AppVersion.Metadata; + // If different, we can update + if (updateHash != appHash) + { + return true; + } + + break; + } + } + + return false; + } + } +} diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index fb16d35f..bf80f2c1 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -38,6 +38,7 @@ + diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs index abd4cb82..163abc04 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs @@ -25,7 +25,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData.Binding; using FluentAvalonia.UI.Controls; -using FluentAvalonia.UI.Media.Animation; using NLog; using SkiaSharp; using StabilityMatrix.Avalonia.Animations; diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/UpdateSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/UpdateSettingsViewModel.cs new file mode 100644 index 00000000..c3a6f417 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Settings/UpdateSettingsViewModel.cs @@ -0,0 +1,237 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AsyncAwaitBestPractices; +using Avalonia.Controls; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Exceptionless.DateTimeExtensions; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Media.Animation; +using StabilityMatrix.Avalonia.Languages; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.Views.Settings; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Models.Update; +using StabilityMatrix.Core.Processes; +using StabilityMatrix.Core.Services; +using StabilityMatrix.Core.Updater; +using Symbol = FluentIcons.Common.Symbol; +using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; + +namespace StabilityMatrix.Avalonia.ViewModels.Settings; + +[View(typeof(UpdateSettingsPage))] +[Singleton, ManagedService] +public partial class UpdateSettingsViewModel : PageViewModelBase +{ + private readonly IUpdateHelper updateHelper; + private readonly IAccountsService accountsService; + private readonly INavigationService settingsNavService; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsUpdateAvailable))] + [NotifyPropertyChangedFor(nameof(HeaderText))] + [NotifyPropertyChangedFor(nameof(SubtitleText))] + private UpdateStatusChangedEventArgs? updateStatus; + + public bool IsUpdateAvailable => UpdateStatus?.LatestUpdate != null; + + public string HeaderText => + IsUpdateAvailable ? Resources.Label_UpdateAvailable : Resources.Label_YouAreUpToDate; + + public string? SubtitleText => + UpdateStatus is null + ? null + : string.Format( + Resources.TextTemplate_LastChecked, + UpdateStatus.CheckedAt.ToApproximateAgeString() + ); + + [ObservableProperty] + private bool isAutoCheckUpdatesEnabled = true; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(SelectedUpdateChannelCard))] + private UpdateChannel preferredUpdateChannel = UpdateChannel.Stable; + + public UpdateChannelCard? SelectedUpdateChannelCard + { + get => AvailableUpdateChannelCards.First(c => c.UpdateChannel == PreferredUpdateChannel); + set => PreferredUpdateChannel = value?.UpdateChannel ?? UpdateChannel.Stable; + } + + public IReadOnlyList AvailableUpdateChannelCards { get; } = + new UpdateChannelCard[] + { + new() + { + UpdateChannel = UpdateChannel.Development, + Description = Resources.Label_UpdatesDevChannelDescription + }, + new() + { + UpdateChannel = UpdateChannel.Preview, + Description = Resources.Label_UpdatesPreviewChannelDescription + }, + new() { UpdateChannel = UpdateChannel.Stable } + }; + + public UpdateSettingsViewModel( + ISettingsManager settingsManager, + IUpdateHelper updateHelper, + IAccountsService accountsService, + INavigationService settingsNavService + ) + { + this.updateHelper = updateHelper; + this.accountsService = accountsService; + this.settingsNavService = settingsNavService; + + settingsManager.RelayPropertyFor( + this, + vm => vm.PreferredUpdateChannel, + settings => settings.PreferredUpdateChannel, + true + ); + + settingsManager.RelayPropertyFor( + this, + vm => vm.IsAutoCheckUpdatesEnabled, + settings => settings.CheckForUpdates, + true + ); + + accountsService.LykosAccountStatusUpdate += (_, args) => + { + var isSelectable = args.IsPatreonConnected; + + foreach ( + var card in AvailableUpdateChannelCards.Where( + c => c.UpdateChannel > UpdateChannel.Stable + ) + ) + { + card.IsSelectable = isSelectable; + } + }; + + // On update status changed + updateHelper.UpdateStatusChanged += (_, args) => + { + UpdateStatus = args; + }; + } + + /// + public override async Task OnLoadedAsync() + { + if (UpdateStatus is null) + { + await CheckForUpdates(); + } + OnPropertyChanged(nameof(SubtitleText)); + } + + [RelayCommand] + private async Task CheckForUpdates() + { + if (Design.IsDesignMode) + { + return; + } + await updateHelper.CheckForUpdate(); + } + + /// + /// Verify a new channel selection is valid, else returns false. + /// + /// + /// + public bool VerifyChannelSelection(UpdateChannelCard card) + { + if (card.UpdateChannel == UpdateChannel.Stable) + { + return true; + } + + if (accountsService.LykosStatus?.User?.IsActiveSupporter == true) + { + return true; + } + + return false; + } + + public void ShowLoginRequiredDialog() + { + Dispatcher.UIThread + .InvokeAsync(async () => + { + var dialog = DialogHelper.CreateTaskDialog("Become a Supporter", "uwu"); + + dialog.Buttons = new[] + { + new(Resources.Label_Accounts, TaskDialogStandardResult.OK), + TaskDialogButton.CloseButton + }; + + dialog.Commands = new[] + { + new TaskDialogCommand + { + Text = "Patreon", + Description = "https://patreon.com/StabilityMatrix", + Command = new RelayCommand(() => + { + ProcessRunner.OpenUrl("https://patreon.com/StabilityMatrix"); + }) + } + }; + + if (await dialog.ShowAsync(true) is TaskDialogStandardResult.OK) + { + settingsNavService.NavigateTo( + new SuppressNavigationTransitionInfo() + ); + } + }) + .SafeFireAndForget(); + } + + partial void OnUpdateStatusChanged(UpdateStatusChangedEventArgs? value) + { + // Update the update channel cards + foreach (var card in AvailableUpdateChannelCards) + { + card.LatestVersion = value?.UpdateChannels + .GetValueOrDefault(card.UpdateChannel) + ?.Version; + } + } + + partial void OnPreferredUpdateChannelChanged(UpdateChannel oldValue, UpdateChannel newValue) + { + if (newValue == UpdateChannel.Stable) + { + return; + } + + if (accountsService.LykosStatus?.User?.IsActiveSupporter == true) + { + return; + } + + PreferredUpdateChannel = UpdateChannel.Stable; + } + + /// + public override string Title => "Updates"; + + /// + public override IconSource IconSource => + new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true }; +} diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index e915c44a..a7fdf1a8 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -35,7 +35,8 @@ public partial class SettingsViewModel : PageViewModelBase { vmFactory.Get(), vmFactory.Get(), - vmFactory.Get() + vmFactory.Get(), + vmFactory.Get() }; CurrentPagePath.AddRange(SubPages); diff --git a/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml index e9d960f0..04be95a7 100644 --- a/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml @@ -11,21 +11,25 @@ xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" + xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models" + xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia" xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:ui="using:FluentAvalonia.UI.Controls" + xmlns:update="clr-namespace:StabilityMatrix.Core.Models.Update;assembly=StabilityMatrix.Core" xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:vmSettings="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Settings" - xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia" - Focusable="True" d:DataContext="{x:Static mocks:DesignData.MainSettingsViewModel}" d:DesignHeight="700" d:DesignWidth="800" x:CompileBindings="True" x:DataType="vmSettings:MainSettingsViewModel" + Focusable="True" mc:Ignorable="d"> - + + + @@ -63,17 +67,16 @@ - + IsClickEnabled="True" + IsVisible="{Binding SharedState.IsDebugMode}" /> - + + IsClickEnabled="True"> + Symbol="Person" /> - + + + + + + + + + + @@ -354,7 +376,7 @@ - + diff --git a/StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml b/StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml new file mode 100644 index 00000000..90a59e0a --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml.cs b/StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml.cs new file mode 100644 index 00000000..08dd277b --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml.cs @@ -0,0 +1,41 @@ +using System.Linq; +using Avalonia.Controls; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.ViewModels.Settings; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Models.Update; + +namespace StabilityMatrix.Avalonia.Views.Settings; + +[Singleton] +public partial class UpdateSettingsPage : UserControlBase +{ + public UpdateSettingsPage() + { + InitializeComponent(); + } + + private void ChannelListBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + var listBox = (ListBox)sender!; + + if (e.AddedItems.Count == 0 || e.AddedItems[0] is not UpdateChannelCard item) + { + return; + } + + var vm = (UpdateSettingsViewModel)DataContext!; + + if (!vm.VerifyChannelSelection(item)) + { + listBox.Selection.Clear(); + + listBox.Selection.SelectedItem = vm.AvailableUpdateChannelCards.First( + c => c.UpdateChannel == UpdateChannel.Stable + ); + + vm.ShowLoginRequiredDialog(); + } + } +} diff --git a/StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs b/StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs index f79a8ae7..cb256d7e 100644 --- a/StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs +++ b/StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs @@ -1,9 +1,13 @@ namespace StabilityMatrix.Core.Models.Api.Lykos; -public record GetUserResponse( - string Id, - LykosAccount Account, - int UserLevel, - string PatreonId, - bool IsEmailVerified -); +public record GetUserResponse +{ + public required string Id { get; init; } + public required LykosAccount Account { get; init; } + public required HashSet UserRoles { get; init; } + public string? PatreonId { get; init; } + public bool IsEmailVerified { get; init; } + + public bool IsActiveSupporter => + UserRoles.Contains(LykosRole.PatreonSupporter) || UserRoles.Contains(LykosRole.Insider); +} diff --git a/StabilityMatrix.Core/Models/Api/Lykos/LykosRole.cs b/StabilityMatrix.Core/Models/Api/Lykos/LykosRole.cs new file mode 100644 index 00000000..d3bdca64 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Lykos/LykosRole.cs @@ -0,0 +1,12 @@ +namespace StabilityMatrix.Core.Models.Api.Lykos; + +public enum LykosRole +{ + Unknown = -1, + Basic = 0, + Supporter = 1, + PatreonSupporter = 2, + Insider = 3, + BetaTester = 4, + Developer = 5 +} diff --git a/StabilityMatrix.Core/Models/Settings/Settings.cs b/StabilityMatrix.Core/Models/Settings/Settings.cs index 15d0ae8f..897b2fdd 100644 --- a/StabilityMatrix.Core/Models/Settings/Settings.cs +++ b/StabilityMatrix.Core/Models/Settings/Settings.cs @@ -43,6 +43,11 @@ public class Settings /// public UpdateChannel PreferredUpdateChannel { get; set; } = UpdateChannel.Stable; + /// + /// Whether to check for updates + /// + public bool CheckForUpdates { get; set; } = true; + /// /// The last auto-update version that had a notification dismissed by the user /// diff --git a/StabilityMatrix.Core/Updater/IUpdateHelper.cs b/StabilityMatrix.Core/Updater/IUpdateHelper.cs index 99f1b652..ce1349e5 100644 --- a/StabilityMatrix.Core/Updater/IUpdateHelper.cs +++ b/StabilityMatrix.Core/Updater/IUpdateHelper.cs @@ -5,8 +5,11 @@ namespace StabilityMatrix.Core.Updater; public interface IUpdateHelper { + event EventHandler? UpdateStatusChanged; + Task StartCheckingForUpdates(); - Task DownloadUpdate(UpdateInfo updateInfo, - IProgress progress); + Task CheckForUpdate(); + + Task DownloadUpdate(UpdateInfo updateInfo, IProgress progress); } diff --git a/StabilityMatrix.Core/Updater/UpdateHelper.cs b/StabilityMatrix.Core/Updater/UpdateHelper.cs index 7ca9586f..dcaf8049 100644 --- a/StabilityMatrix.Core/Updater/UpdateHelper.cs +++ b/StabilityMatrix.Core/Updater/UpdateHelper.cs @@ -29,6 +29,9 @@ public class UpdateHelper : IUpdateHelper public static FilePath ExecutablePath => UpdateFolder.JoinFile(Compat.GetExecutableName()); + /// + public event EventHandler? UpdateStatusChanged; + public UpdateHelper( ILogger logger, IHttpClientFactory httpClientFactory, @@ -118,7 +121,7 @@ public class UpdateHelper : IUpdateHelper } } - private async Task CheckForUpdate() + public async Task CheckForUpdate() { try { @@ -159,12 +162,31 @@ public class UpdateHelper : IUpdateHelper && ValidateUpdate(update) ) { - NotifyUpdateAvailable(update); + OnUpdateStatusChanged( + new UpdateStatusChangedEventArgs + { + LatestUpdate = update, + UpdateChannels = updateManifest.Updates.ToDictionary( + kv => kv.Key, + kv => kv.Value.GetInfoForCurrentPlatform() + )! + } + ); return; } } logger.LogInformation("No update available"); + + OnUpdateStatusChanged( + new UpdateStatusChangedEventArgs + { + UpdateChannels = updateManifest.Updates.ToDictionary( + kv => kv.Key, + kv => kv.Value.GetInfoForCurrentPlatform() + )! + } + ); } catch (Exception e) { @@ -215,6 +237,22 @@ public class UpdateHelper : IUpdateHelper return false; } + private void OnUpdateStatusChanged(UpdateStatusChangedEventArgs args) + { + UpdateStatusChanged?.Invoke(this, args); + + if (args.LatestUpdate is { } update) + { + logger.LogInformation( + "Update available {AppVer} -> {UpdateVer}", + Compat.AppVersion, + update.Version + ); + + EventManager.Instance.OnUpdateAvailable(update); + } + } + private void NotifyUpdateAvailable(UpdateInfo update) { logger.LogInformation( diff --git a/StabilityMatrix.Core/Updater/UpdateStatusChangedEventArgs.cs b/StabilityMatrix.Core/Updater/UpdateStatusChangedEventArgs.cs new file mode 100644 index 00000000..8eebe4fc --- /dev/null +++ b/StabilityMatrix.Core/Updater/UpdateStatusChangedEventArgs.cs @@ -0,0 +1,13 @@ +using StabilityMatrix.Core.Models.Update; + +namespace StabilityMatrix.Core.Updater; + +public class UpdateStatusChangedEventArgs : EventArgs +{ + public UpdateInfo? LatestUpdate { get; init; } + + public IReadOnlyDictionary UpdateChannels { get; init; } = + new Dictionary(); + + public DateTimeOffset CheckedAt { get; init; } = DateTimeOffset.UtcNow; +}