Ionite
1 year ago
committed by
GitHub
30 changed files with 1109 additions and 164 deletions
@ -0,0 +1,30 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Globalization; |
||||||
|
using Avalonia.Data.Converters; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Converters; |
||||||
|
|
||||||
|
public class BooleanChoiceMultiConverter : IMultiValueConverter |
||||||
|
{ |
||||||
|
/// <inheritdoc /> |
||||||
|
public object? Convert( |
||||||
|
IList<object?> values, |
||||||
|
Type targetType, |
||||||
|
object? parameter, |
||||||
|
CultureInfo culture |
||||||
|
) |
||||||
|
{ |
||||||
|
if (values.Count < 3) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
if (values[0] is bool boolValue) |
||||||
|
{ |
||||||
|
return boolValue ? values[1] : values[2]; |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
@ -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,70 @@ |
|||||||
|
using System; |
||||||
|
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; |
||||||
|
|
||||||
|
// Trim both to the lower length, to a minimum of 7 characters |
||||||
|
var minLength = Math.Min(7, Math.Min(updateHash.Length, appHash.Length)); |
||||||
|
updateHash = updateHash[..minLength]; |
||||||
|
appHash = appHash[..minLength]; |
||||||
|
|
||||||
|
// If different, we can update |
||||||
|
if (updateHash != appHash) |
||||||
|
{ |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||||
|
|
||||||
|
<!-- ListBoxItem theme that removes ListBox visuals (i.e. for using with radiobuttons) --> |
||||||
|
<ControlTheme x:Key="ListBoxItemBorderlessTheme" TargetType="ListBoxItem"> |
||||||
|
<Setter Property="Template"> |
||||||
|
<ControlTemplate> |
||||||
|
<Panel> |
||||||
|
<ContentPresenter |
||||||
|
Name="PART_ContentPresenter" |
||||||
|
Margin="2,0" |
||||||
|
Padding="{TemplateBinding Padding}" |
||||||
|
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" |
||||||
|
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" |
||||||
|
Background="{TemplateBinding Background}" |
||||||
|
BorderBrush="{TemplateBinding BorderBrush}" |
||||||
|
BorderThickness="{TemplateBinding BorderThickness}" |
||||||
|
Content="{TemplateBinding Content}" |
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}" |
||||||
|
CornerRadius="{TemplateBinding CornerRadius}" /> |
||||||
|
</Panel> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter> |
||||||
|
|
||||||
|
</ControlTheme> |
||||||
|
</ResourceDictionary> |
@ -0,0 +1,236 @@ |
|||||||
|
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 Semver; |
||||||
|
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 isBetaChannelsEnabled = args.User?.IsActiveSupporter == true; |
||||||
|
|
||||||
|
foreach ( |
||||||
|
var card in AvailableUpdateChannelCards.Where( |
||||||
|
c => c.UpdateChannel > UpdateChannel.Stable |
||||||
|
) |
||||||
|
) |
||||||
|
{ |
||||||
|
card.IsSelectable = isBetaChannelsEnabled; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// 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", |
||||||
|
"" |
||||||
|
+ "Support the Stability Matrix Team and get access to early development builds and be the first to test new features. " |
||||||
|
); |
||||||
|
|
||||||
|
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 |
||||||
|
|
||||||
|
// Use maximum version from platforms equal or lower than current |
||||||
|
foreach (var card in AvailableUpdateChannelCards) |
||||||
|
{ |
||||||
|
card.LatestVersion = value?.UpdateChannels |
||||||
|
.Where(kv => kv.Key <= card.UpdateChannel) |
||||||
|
.Select(kv => kv.Value) |
||||||
|
.MaxBy(info => info.Version, SemVersion.PrecedenceComparer) |
||||||
|
?.Version; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
partial void OnPreferredUpdateChannelChanged(UpdateChannel value) |
||||||
|
{ |
||||||
|
CheckForUpdatesCommand.ExecuteAsync(null).SafeFireAndForget(); |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public override string Title => "Updates"; |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public override IconSource IconSource => |
||||||
|
new SymbolIconSource { Symbol = Symbol.Settings, IsFilled = true }; |
||||||
|
} |
@ -0,0 +1,154 @@ |
|||||||
|
<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" |
||||||
|
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" |
||||||
|
d:DataContext="{x:Static mocks:DesignData.UpdateSettingsViewModel}" |
||||||
|
d:DesignHeight="550" |
||||||
|
d:DesignWidth="800" |
||||||
|
x:DataType="vmSettings:UpdateSettingsViewModel" |
||||||
|
mc:Ignorable="d"> |
||||||
|
|
||||||
|
<controls:UserControlBase.Resources> |
||||||
|
<converters:BooleanChoiceMultiConverter x:Key="BoolChoiceMultiConverter" /> |
||||||
|
</controls:UserControlBase.Resources> |
||||||
|
|
||||||
|
<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" |
||||||
|
IsChecked="{Binding $parent[ListBoxItem].IsSelected}" > |
||||||
|
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Left"> |
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5"> |
||||||
|
<TextBlock |
||||||
|
VerticalAlignment="Center" |
||||||
|
Text="{Binding DisplayName}"> |
||||||
|
<TextBlock.Foreground> |
||||||
|
<MultiBinding Converter="{StaticResource BoolChoiceMultiConverter}"> |
||||||
|
<Binding Path="IsSelectable" /> |
||||||
|
<DynamicResource ResourceKey="TextFillColorPrimaryBrush"/> |
||||||
|
<DynamicResource ResourceKey="TextFillColorTertiaryBrush"/> |
||||||
|
</MultiBinding> |
||||||
|
</TextBlock.Foreground> |
||||||
|
</TextBlock> |
||||||
|
<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(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
using Semver; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Core.Extensions; |
||||||
|
|
||||||
|
public static class SemVersionExtensions |
||||||
|
{ |
||||||
|
public static string ToDisplayString(this SemVersion version) |
||||||
|
{ |
||||||
|
var versionString = $"{version.Major}.{version.Minor}.{version.Patch}"; |
||||||
|
|
||||||
|
// Add the build metadata if we have pre-release information |
||||||
|
if (version.PrereleaseIdentifiers.Count > 0) |
||||||
|
{ |
||||||
|
versionString += $"-{version.Prerelease}"; |
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(version.Metadata)) |
||||||
|
{ |
||||||
|
// First 7 characters of the commit hash |
||||||
|
versionString += $"+{version.Metadata[..7]}"; |
||||||
|
} |
||||||
|
} |
||||||
|
return versionString; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
namespace StabilityMatrix.Core.Models.Api.Lykos; |
||||||
|
|
||||||
|
public record GetFilesDownloadResponse |
||||||
|
{ |
||||||
|
public required Uri DownloadUrl { get; set; } |
||||||
|
|
||||||
|
public DateTimeOffset? ExpiresAt { get; set; } |
||||||
|
} |
@ -1,9 +1,15 @@ |
|||||||
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) |
||||||
|
|| (UserRoles.Contains(LykosRole.Developer) && PatreonId is not null); |
||||||
|
} |
||||||
|
@ -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