Ionite
1 year ago
19 changed files with 773 additions and 30 deletions
@ -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); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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 }; |
||||
} |
@ -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> |
@ -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(); |
||||
} |
||||
} |
||||
} |
@ -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<LykosRole> UserRoles { get; init; } |
||||
public string? PatreonId { get; init; } |
||||
public bool IsEmailVerified { get; init; } |
||||
|
||||
public bool IsActiveSupporter => |
||||
UserRoles.Contains(LykosRole.PatreonSupporter) || UserRoles.Contains(LykosRole.Insider); |
||||
} |
||||
|
@ -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 |
||||
} |
@ -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…
Reference in new issue