Browse Source

Add Update preferences page

pull/324/head
Ionite 1 year ago
parent
commit
7409deb97a
No known key found for this signature in database
  1. 32
      StabilityMatrix.Avalonia/Converters/EnumStringConverter.cs
  2. 24
      StabilityMatrix.Avalonia/Converters/EnumToBooleanConverter.cs
  3. 35
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  4. 54
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  5. 18
      StabilityMatrix.Avalonia/Languages/Resources.resx
  6. 63
      StabilityMatrix.Avalonia/Models/UpdateChannelCard.cs
  7. 1
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  8. 1
      StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
  9. 237
      StabilityMatrix.Avalonia/ViewModels/Settings/UpdateSettingsViewModel.cs
  10. 3
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  11. 50
      StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml
  12. 143
      StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml
  13. 41
      StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml.cs
  14. 18
      StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs
  15. 12
      StabilityMatrix.Core/Models/Api/Lykos/LykosRole.cs
  16. 5
      StabilityMatrix.Core/Models/Settings/Settings.cs
  17. 7
      StabilityMatrix.Core/Updater/IUpdateHelper.cs
  18. 42
      StabilityMatrix.Core/Updater/UpdateHelper.cs
  19. 13
      StabilityMatrix.Core/Updater/UpdateStatusChangedEventArgs.cs

32
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
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not Enum enumValue)
return null;
return enumValue.GetStringValue();
}
/// <inheritdoc />
public object? ConvertBack(
object? value,
Type targetType,
object? parameter,
CultureInfo culture
)
{
if (value is not string stringValue)
return null;
return Enum.Parse(targetType, stringValue);
}
}

24
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;
}
}

35
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -11,6 +11,7 @@ using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NSubstitute; using NSubstitute;
using NSubstitute.ReturnsExtensions; using NSubstitute.ReturnsExtensions;
using Semver;
using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Controls.CodeCompletion;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Models.TagCompletion;
@ -36,6 +37,7 @@ using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater; using StabilityMatrix.Core.Updater;
@ -449,6 +451,37 @@ public static class DesignData
public static AccountSettingsViewModel AccountSettingsViewModel => public static AccountSettingsViewModel AccountSettingsViewModel =>
Services.GetRequiredService<AccountSettingsViewModel>(); Services.GetRequiredService<AccountSettingsViewModel>();
public static UpdateSettingsViewModel UpdateSettingsViewModel
{
get
{
var vm = Services.GetRequiredService<UpdateSettingsViewModel>();
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, UpdateInfo>
{
[UpdateChannel.Stable] = update,
[UpdateChannel.Preview] = update,
[UpdateChannel.Development] = update
},
CheckedAt = DateTimeOffset.UtcNow
};
return vm;
}
}
public static CheckpointBrowserViewModel CheckpointBrowserViewModel => public static CheckpointBrowserViewModel CheckpointBrowserViewModel =>
Services.GetRequiredService<CheckpointBrowserViewModel>(); Services.GetRequiredService<CheckpointBrowserViewModel>();
@ -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 public class Indexer
{ {

54
StabilityMatrix.Avalonia/Languages/Resources.Designer.cs generated

@ -662,6 +662,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Auto Updates.
/// </summary>
public static string Label_AutoUpdates {
get {
return ResourceManager.GetString("Label_AutoUpdates", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Base Model. /// Looks up a localized string similar to Base Model.
/// </summary> /// </summary>
@ -1814,6 +1823,33 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Updates.
/// </summary>
public static string Label_Updates {
get {
return ResourceManager.GetString("Label_Updates", resourceCulture);
}
}
/// <summary>
/// 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..
/// </summary>
public static string Label_UpdatesDevChannelDescription {
get {
return ResourceManager.GetString("Label_UpdatesDevChannelDescription", resourceCulture);
}
}
/// <summary>
/// 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..
/// </summary>
public static string Label_UpdatesPreviewChannelDescription {
get {
return ResourceManager.GetString("Label_UpdatesPreviewChannelDescription", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Upscale. /// Looks up a localized string similar to Upscale.
/// </summary> /// </summary>
@ -1886,6 +1922,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to You&apos;re up to date.
/// </summary>
public static string Label_YouAreUpToDate {
get {
return ResourceManager.GetString("Label_YouAreUpToDate", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Download complete. /// Looks up a localized string similar to Download complete.
/// </summary> /// </summary>
@ -2048,6 +2093,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Last checked: {0}.
/// </summary>
public static string TextTemplate_LastChecked {
get {
return ResourceManager.GetString("TextTemplate_LastChecked", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to {0} has been updated to the latest version. /// Looks up a localized string similar to {0} has been updated to the latest version.
/// </summary> /// </summary>

18
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -786,4 +786,22 @@
<data name="Label_DownloadFailed" xml:space="preserve"> <data name="Label_DownloadFailed" xml:space="preserve">
<value>Download Failed</value> <value>Download Failed</value>
</data> </data>
<data name="Label_AutoUpdates" xml:space="preserve">
<value>Auto Updates</value>
</data>
<data name="Label_UpdatesPreviewChannelDescription" xml:space="preserve">
<value>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.</value>
</data>
<data name="Label_UpdatesDevChannelDescription" xml:space="preserve">
<value>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.</value>
</data>
<data name="Label_Updates" xml:space="preserve">
<value>Updates</value>
</data>
<data name="Label_YouAreUpToDate" xml:space="preserve">
<value>You're up to date</value>
</data>
<data name="TextTemplate_LastChecked" xml:space="preserve">
<value>Last checked: {0}</value>
</data>
</root> </root>

63
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;
/// <summary>
/// Whether the <see cref="LatestVersion"/> is available for update.
/// </summary>
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;
}
}
}

1
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -38,6 +38,7 @@
<PackageReference Include="Dock.Model.Avalonia" Version="11.0.0.3" /> <PackageReference Include="Dock.Model.Avalonia" Version="11.0.0.3" />
<PackageReference Include="Dock.Serializer" Version="11.0.0.3" /> <PackageReference Include="Dock.Serializer" Version="11.0.0.3" />
<PackageReference Include="DynamicData" Version="8.1.1" /> <PackageReference Include="DynamicData" Version="8.1.1" />
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="3.4.3" />
<PackageReference Include="FluentAvalonia.BreadcrumbBar" Version="2.0.2" /> <PackageReference Include="FluentAvalonia.BreadcrumbBar" Version="2.0.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.4" /> <PackageReference Include="FluentAvaloniaUI" Version="2.0.4" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.222" /> <PackageReference Include="FluentIcons.Avalonia" Version="1.1.222" />

1
StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs

@ -25,7 +25,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using DynamicData.Binding; using DynamicData.Binding;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using NLog; using NLog;
using SkiaSharp; using SkiaSharp;
using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Animations;

237
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<SettingsViewModel> 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<UpdateChannelCard> 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<SettingsViewModel> 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;
};
}
/// <inheritdoc />
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();
}
/// <summary>
/// Verify a new channel selection is valid, else returns false.
/// </summary>
/// <param name="card"></param>
/// <returns></returns>
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<AccountSettingsViewModel>(
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;
}
/// <inheritdoc />
public override string Title => "Updates";
/// <inheritdoc />
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true };
}

3
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -35,7 +35,8 @@ public partial class SettingsViewModel : PageViewModelBase
{ {
vmFactory.Get<MainSettingsViewModel>(), vmFactory.Get<MainSettingsViewModel>(),
vmFactory.Get<InferenceSettingsViewModel>(), vmFactory.Get<InferenceSettingsViewModel>(),
vmFactory.Get<AccountSettingsViewModel>() vmFactory.Get<AccountSettingsViewModel>(),
vmFactory.Get<UpdateSettingsViewModel>()
}; };
CurrentPagePath.AddRange(SubPages); CurrentPagePath.AddRange(SubPages);

50
StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml

@ -11,21 +11,25 @@
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" 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:system="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="using:FluentAvalonia.UI.Controls" 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:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vmSettings="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Settings" 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:DataContext="{x:Static mocks:DesignData.MainSettingsViewModel}"
d:DesignHeight="700" d:DesignHeight="700"
d:DesignWidth="800" d:DesignWidth="800"
x:CompileBindings="True" x:CompileBindings="True"
x:DataType="vmSettings:MainSettingsViewModel" x:DataType="vmSettings:MainSettingsViewModel"
Focusable="True"
mc:Ignorable="d"> mc:Ignorable="d">
<controls:UserControlBase.Resources> <controls:UserControlBase.Resources>
<converters:CultureInfoDisplayConverter x:Key="CultureInfoDisplayConverter" /> <converters:CultureInfoDisplayConverter x:Key="CultureInfoDisplayConverter" />
<converters:EnumStringConverter x:Key="EnumStringConverter" />
<converters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
</controls:UserControlBase.Resources> </controls:UserControlBase.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
@ -63,15 +67,14 @@
</ui:SettingsExpander> </ui:SettingsExpander>
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="3" Grid.Row="3"
IsVisible="{Binding SharedState.IsDebugMode}"
Margin="8,0,8,4" Margin="8,0,8,4"
IsClickEnabled="True" ActionIconSource="ChevronRight"
Command="{Binding NavigateToSubPageCommand}" Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:InferenceSettingsViewModel}" CommandParameter="{x:Type vmSettings:InferenceSettingsViewModel}"
Header="Inference (Test)" Header="Inference (Test)"
IconSource="Code" IconSource="Code"
ActionIconSource="ChevronRight"> IsClickEnabled="True"
</ui:SettingsExpander> IsVisible="{Binding SharedState.IsDebugMode}" />
</Grid> </Grid>
<!-- Checkpoints Manager Options --> <!-- Checkpoints Manager Options -->
@ -254,16 +257,16 @@
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="1" Grid.Row="1"
Margin="8,0" Margin="8,0"
IsClickEnabled="True" ActionIconSource="ChevronRight"
Command="{Binding NavigateToSubPageCommand}" Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:AccountSettingsViewModel}" CommandParameter="{x:Type vmSettings:AccountSettingsViewModel}"
Header="{x:Static lang:Resources.Label_Accounts}" Header="{x:Static lang:Resources.Label_Accounts}"
ActionIconSource="ChevronRight"> IsClickEnabled="True">
<ui:SettingsExpander.IconSource> <ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource <fluentIcons:SymbolIconSource
FontSize="10" FontSize="10"
IsFilled="True" IsFilled="True"
Symbol="Person"/> Symbol="Person" />
</ui:SettingsExpander.IconSource> </ui:SettingsExpander.IconSource>
</ui:SettingsExpander> </ui:SettingsExpander>
<ui:SettingsExpander <ui:SettingsExpander
@ -280,14 +283,32 @@
</sg:SpacedGrid> </sg:SpacedGrid>
<!-- System Options --> <!-- System Options -->
<Grid RowDefinitions="auto, auto, auto"> <sg:SpacedGrid RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="4">
<TextBlock <TextBlock
Margin="0,0,0,8" Margin="0,0,0,4"
FontWeight="Medium" FontWeight="Medium"
Text="{x:Static lang:Resources.Label_System}" /> Text="{x:Static lang:Resources.Label_System}" />
<!-- Updates page -->
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="1" Grid.Row="1"
Margin="8,0,8,4" Margin="8,0"
ActionIconSource="ChevronRight"
Command="{Binding NavigateToSubPageCommand}"
CommandParameter="{x:Type vmSettings:UpdateSettingsViewModel}"
Header="{x:Static lang:Resources.Label_Updates}"
IsClickEnabled="True">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource
FontSize="10"
IsFilled="True"
Symbol="ArrowSync" />
</ui:SettingsExpander.IconSource>
</ui:SettingsExpander>
<ui:SettingsExpander
Grid.Row="2"
Margin="8,0"
Description="{x:Static lang:Resources.Label_AddToStartMenu_Details}" Description="{x:Static lang:Resources.Label_AddToStartMenu_Details}"
Header="{x:Static lang:Resources.Label_AddToStartMenu}" Header="{x:Static lang:Resources.Label_AddToStartMenu}"
IconSource="StarAdd" IconSource="StarAdd"
@ -324,8 +345,9 @@
</StackPanel> </StackPanel>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
<ui:SettingsExpander <ui:SettingsExpander
Grid.Row="2" Grid.Row="3"
Margin="8,0" Margin="8,0"
Description="{x:Static lang:Resources.Label_SelectNewDataDirectory_Details}" Description="{x:Static lang:Resources.Label_SelectNewDataDirectory_Details}"
IconSource="MoveToFolder"> IconSource="MoveToFolder">
@ -354,7 +376,7 @@
</Button> </Button>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
</Grid> </sg:SpacedGrid>
<!-- Debug Options --> <!-- Debug Options -->
<Grid IsVisible="{Binding SharedState.IsDebugMode}" RowDefinitions="auto,*"> <Grid IsVisible="{Binding SharedState.IsDebugMode}" RowDefinitions="auto,*">

143
StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml

@ -0,0 +1,143 @@
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.Settings.UpdateSettingsPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
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:sm="clr-namespace:StabilityMatrix.Avalonia"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vmSettings="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Settings"
d:DataContext="{x:Static mocks:DesignData.UpdateSettingsViewModel}"
d:DesignHeight="550"
d:DesignWidth="800"
x:DataType="vmSettings:UpdateSettingsViewModel"
mc:Ignorable="d">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="16,16,24,16" Spacing="2">
<sg:SpacedGrid Margin="0,4,0,16" ColumnDefinitions="*,Auto">
<StackPanel Orientation="Horizontal" Spacing="16">
<fluentIcons:SymbolIcon
Width="64"
Height="64"
FontSize="64"
IsFilled="True"
Symbol="ArrowSync" />
<StackPanel VerticalAlignment="Center">
<TextBlock
Margin="-1,0,0,0"
HorizontalAlignment="Left"
Text="{Binding HeaderText}"
Theme="{DynamicResource SubtitleTextBlockStyle}" />
<TextBlock
HorizontalAlignment="Left"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding SubtitleText}" />
</StackPanel>
</StackPanel>
<Button
Grid.Column="1"
Classes="accent"
Content="{x:Static lang:Resources.Action_CheckForUpdates}"
Command="{Binding CheckForUpdatesCommand}"
HorizontalAlignment="Right">
</Button>
</sg:SpacedGrid>
<!-- Auto updates toggle -->
<ui:SettingsExpander
Description="Periodically checks for updates"
Header="Notify when updates are ready to install">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource Symbol="Megaphone" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding IsAutoCheckUpdatesEnabled}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<!-- Channel radio buttons -->
<ui:SettingsExpander Header="Preferred Update Channel" IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource Symbol="Branch" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpanderItem>
<ListBox
SelectionChanged="ChannelListBox_OnSelectionChanged"
IsEnabled="{Binding IsAutoCheckUpdatesEnabled}"
ItemContainerTheme="{DynamicResource ListBoxItemBorderlessTheme}"
ItemsSource="{Binding AvailableUpdateChannelCards}"
SelectedItem="{Binding SelectedUpdateChannelCard}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:UpdateChannelCard}">
<RadioButton
GroupName="UpdateChannel"
IsEnabled="{Binding IsSelectable}"
IsChecked="{Binding $parent[ListBoxItem].IsSelected}" >
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Left">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock
IsEnabled="False"
VerticalAlignment="Center"
Text="{Binding DisplayName}" />
<Ellipse
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding LatestVersionString, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Width="4"
Height="4"
Fill="{DynamicResource TextFillColorTertiaryBrush}" />
<TextBlock
VerticalAlignment="Center"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
IsVisible="{Binding LatestVersionString, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Text="{Binding LatestVersionString}" />
<!-- Update available icon -->
<fluentIcons:SymbolIcon
ToolTip.Tip="{x:Static lang:Resources.Label_UpdateAvailable}"
IsVisible="{Binding IsLatestVersionUpdateable}"
VerticalAlignment="Center"
FontSize="16"
Foreground="#3592c4"
Symbol="ArrowCircleUpRight" />
</StackPanel>
<TextBlock
FontSize="13"
TextAlignment="Start"
Margin="0,0,0,0"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="{Binding Description}"
Theme="{DynamicResource CaptionTextBlockStyle}" />
</StackPanel>
</RadioButton>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="8" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</controls:UserControlBase>

41
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();
}
}
}

18
StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs

@ -1,9 +1,13 @@
namespace StabilityMatrix.Core.Models.Api.Lykos; namespace StabilityMatrix.Core.Models.Api.Lykos;
public record GetUserResponse( public record GetUserResponse
string Id, {
LykosAccount Account, public required string Id { get; init; }
int UserLevel, public required LykosAccount Account { get; init; }
string PatreonId, public required HashSet<LykosRole> UserRoles { get; init; }
bool IsEmailVerified public string? PatreonId { get; init; }
); public bool IsEmailVerified { get; init; }
public bool IsActiveSupporter =>
UserRoles.Contains(LykosRole.PatreonSupporter) || UserRoles.Contains(LykosRole.Insider);
}

12
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
}

5
StabilityMatrix.Core/Models/Settings/Settings.cs

@ -43,6 +43,11 @@ public class Settings
/// </summary> /// </summary>
public UpdateChannel PreferredUpdateChannel { get; set; } = UpdateChannel.Stable; public UpdateChannel PreferredUpdateChannel { get; set; } = UpdateChannel.Stable;
/// <summary>
/// Whether to check for updates
/// </summary>
public bool CheckForUpdates { get; set; } = true;
/// <summary> /// <summary>
/// The last auto-update version that had a notification dismissed by the user /// The last auto-update version that had a notification dismissed by the user
/// </summary> /// </summary>

7
StabilityMatrix.Core/Updater/IUpdateHelper.cs

@ -5,8 +5,11 @@ namespace StabilityMatrix.Core.Updater;
public interface IUpdateHelper public interface IUpdateHelper
{ {
event EventHandler<UpdateStatusChangedEventArgs>? UpdateStatusChanged;
Task StartCheckingForUpdates(); Task StartCheckingForUpdates();
Task DownloadUpdate(UpdateInfo updateInfo, Task CheckForUpdate();
IProgress<ProgressReport> progress);
Task DownloadUpdate(UpdateInfo updateInfo, IProgress<ProgressReport> progress);
} }

42
StabilityMatrix.Core/Updater/UpdateHelper.cs

@ -29,6 +29,9 @@ public class UpdateHelper : IUpdateHelper
public static FilePath ExecutablePath => UpdateFolder.JoinFile(Compat.GetExecutableName()); public static FilePath ExecutablePath => UpdateFolder.JoinFile(Compat.GetExecutableName());
/// <inheritdoc />
public event EventHandler<UpdateStatusChangedEventArgs>? UpdateStatusChanged;
public UpdateHelper( public UpdateHelper(
ILogger<UpdateHelper> logger, ILogger<UpdateHelper> logger,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
@ -118,7 +121,7 @@ public class UpdateHelper : IUpdateHelper
} }
} }
private async Task CheckForUpdate() public async Task CheckForUpdate()
{ {
try try
{ {
@ -159,12 +162,31 @@ public class UpdateHelper : IUpdateHelper
&& ValidateUpdate(update) && ValidateUpdate(update)
) )
{ {
NotifyUpdateAvailable(update); OnUpdateStatusChanged(
new UpdateStatusChangedEventArgs
{
LatestUpdate = update,
UpdateChannels = updateManifest.Updates.ToDictionary(
kv => kv.Key,
kv => kv.Value.GetInfoForCurrentPlatform()
)!
}
);
return; return;
} }
} }
logger.LogInformation("No update available"); logger.LogInformation("No update available");
OnUpdateStatusChanged(
new UpdateStatusChangedEventArgs
{
UpdateChannels = updateManifest.Updates.ToDictionary(
kv => kv.Key,
kv => kv.Value.GetInfoForCurrentPlatform()
)!
}
);
} }
catch (Exception e) catch (Exception e)
{ {
@ -215,6 +237,22 @@ public class UpdateHelper : IUpdateHelper
return false; 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) private void NotifyUpdateAvailable(UpdateInfo update)
{ {
logger.LogInformation( logger.LogInformation(

13
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<UpdateChannel, UpdateInfo> UpdateChannels { get; init; } =
new Dictionary<UpdateChannel, UpdateInfo>();
public DateTimeOffset CheckedAt { get; init; } = DateTimeOffset.UtcNow;
}
Loading…
Cancel
Save