Implemented most of the updating mechanism

This commit is contained in:
Robert committed 2023-02-24 22:54:17 +01:00
1 parent 0cd65a2ebf
commit acd005e4a2
27 files changed
+1055 -383

No files matched your search

@@ -10,6 +10,7 @@ public class SettingsViewModel : MainScreenViewModel
GeneralTabViewModel generalTabViewModel,
PluginsTabViewModel pluginsTabViewModel,
DevicesTabViewModel devicesTabViewModel,
ReleasesTabViewModel releasesTabViewModel,
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
{
SettingTabs = new ObservableCollection<ActivatableViewModelBase>
@@ -17,6 +18,7 @@ public class SettingsViewModel : MainScreenViewModel
generalTabViewModel,
pluginsTabViewModel,
devicesTabViewModel,
releasesTabViewModel,
aboutTabViewModel
};
}
@@ -0,0 +1,26 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
x:Class="Artemis.UI.Screens.Settings.ReleasesTabView"
x:DataType="settings:ReleasesTabViewModel">
<Grid ColumnDefinitions="300,*" Margin="0 10">
<Border Classes="card-condensed" Grid.Column="0" Margin="0 0 10 0">
<ListBox Items="{CompiledBinding ReleaseViewModels}" SelectedItem="{CompiledBinding SelectedReleaseViewModel}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="updating:ReleaseViewModel">
<StackPanel Margin="4">
<TextBlock Text="{CompiledBinding Version}" VerticalAlignment="Center" />
<TextBlock Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<ContentControl Grid.Column="1" Content="{CompiledBinding SelectedReleaseViewModel}"/>
</Grid>
</UserControl>
@@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings;
public class ReleasesTabView : ReactiveUserControl<ReleasesTabViewModel>
{
public ReleasesTabView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -0,0 +1,106 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Extensions;
using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Updating;
using Avalonia.Threading;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using Serilog;
using StrawberryShake;
namespace Artemis.UI.Screens.Settings;
public class ReleasesTabViewModel : ActivatableViewModelBase
{
private readonly ILogger _logger;
private readonly IUpdatingClient _updatingClient;
private readonly INotificationService _notificationService;
private readonly SourceList<IGetReleases_PublishedReleases_Nodes> _releases;
private IGetReleases_PublishedReleases_PageInfo? _lastPageInfo;
private bool _loading;
private ReleaseViewModel? _selectedReleaseViewModel;
public ReleasesTabViewModel(ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, IReleaseVmFactory releaseVmFactory, INotificationService notificationService)
{
_logger = logger;
_updatingClient = updatingClient;
_notificationService = notificationService;
_releases = new SourceList<IGetReleases_PublishedReleases_Nodes>();
_releases.Connect()
.Sort(SortExpressionComparer<IGetReleases_PublishedReleases_Nodes>.Descending(p => p.CreatedAt))
.Transform(r => releaseVmFactory.ReleaseListViewModel(r.Id, r.Version, r.CreatedAt))
.ObserveOn(AvaloniaScheduler.Instance)
.Bind(out ReadOnlyObservableCollection<ReleaseViewModel> releaseViewModels)
.Subscribe();
DisplayName = "Releases";
ReleaseViewModels = releaseViewModels;
this.WhenActivated(async d =>
{
await updateService.CacheLatestRelease();
await GetMoreReleases(d.AsCancellationToken());
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault();
});
}
public ReadOnlyObservableCollection<ReleaseViewModel> ReleaseViewModels { get; }
public ReleaseViewModel? SelectedReleaseViewModel
{
get => _selectedReleaseViewModel;
set => RaiseAndSetIfChanged(ref _selectedReleaseViewModel, value);
}
public bool Loading
{
get => _loading;
private set => RaiseAndSetIfChanged(ref _loading, value);
}
public async Task GetMoreReleases(CancellationToken cancellationToken)
{
if (_lastPageInfo != null && !_lastPageInfo.HasNextPage)
return;
try
{
Loading = true;
IOperationResult<IGetReleasesResult> result = await _updatingClient.GetReleases.ExecuteAsync("feature/gh-actions", Platform.Windows, 20, _lastPageInfo?.EndCursor, cancellationToken);
if (result.Data?.PublishedReleases?.Nodes == null)
return;
_lastPageInfo = result.Data.PublishedReleases.PageInfo;
_releases.AddRange(result.Data.PublishedReleases.Nodes);
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
_logger.Warning(e, "Failed to retrieve releases");
_notificationService.CreateNotification()
.WithTitle("Failed to retrieve releases")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
finally
{
Loading = false;
}
}
}
@@ -1,29 +0,0 @@
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings.Updating;
public partial class ReleaseAvailableView : ReactiveCoreWindow<ReleaseAvailableViewModel>
{
public ReleaseAvailableView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void Button_OnClick(object? sender, RoutedEventArgs e)
{
Close();
}
}
@@ -1,76 +0,0 @@
using System.Reactive;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Extensions;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Updating;
using ReactiveUI;
using Serilog;
using StrawberryShake;
namespace Artemis.UI.Screens.Settings.Updating;
public class ReleaseAvailableViewModel : ActivatableViewModelBase
{
private readonly string _nextReleaseId;
private readonly ILogger _logger;
private readonly IUpdateService _updateService;
private readonly IUpdatingClient _updatingClient;
private readonly INotificationService _notificationService;
private IGetReleaseById_Release? _release;
public ReleaseAvailableViewModel(string nextReleaseId, ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, INotificationService notificationService)
{
_nextReleaseId = nextReleaseId;
_logger = logger;
_updateService = updateService;
_updatingClient = updatingClient;
_notificationService = notificationService;
CurrentVersion = _updateService.CurrentVersion ?? "Development build";
Install = ReactiveCommand.Create(ExecuteInstall, this.WhenAnyValue(vm => vm.Release).Select(r => r != null));
this.WhenActivated(async d => await RetrieveRelease(d.AsCancellationToken()));
}
private void ExecuteInstall()
{
_updateService.InstallRelease(_nextReleaseId);
}
private async Task RetrieveRelease(CancellationToken cancellationToken)
{
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(_nextReleaseId, cancellationToken);
// Borrow GraphQLClientException for messaging, how lazy of me..
if (result.Errors.Count > 0)
{
GraphQLClientException exception = new(result.Errors);
_logger.Error(exception, "Failed to retrieve release details");
_notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage(exception.Message).Show();
return;
}
if (result.Data?.Release == null)
{
_notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage("Release not found").Show();
return;
}
Release = result.Data.Release;
}
public string CurrentVersion { get; }
public IGetReleaseById_Release? Release
{
get => _release;
set => RaiseAndSetIfChanged(ref _release, value);
}
public ReactiveCommand<Unit, Unit> Install { get; }
public ReactiveCommand<Unit, Unit> AskLater { get; }
}
@@ -1,40 +0,0 @@
<controls:CoreWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
mc:Ignorable="d"
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseInstallerView"
x:DataType="updating:ReleaseInstallerViewModel"
Title="Artemis | Updating"
ShowAsDialog="True"
Width="465" Height="260"
Padding="15"
CanResize="False"
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,Auto,*,Auto" Width="450" Height="200">
<TextBlock Grid.Row="0" Classes="h4" TextWrapping="Wrap">
Downloading &amp; installing update...
</TextBlock>
<TextBlock Grid.Row="1" Classes="subtitle" TextWrapping="Wrap">
This should not take long, when finished Artemis must restart.
</TextBlock>
<StackPanel Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Stretch" IsVisible="{CompiledBinding !Ready}">
<ProgressBar Value="{CompiledBinding OverallProgress}" ></ProgressBar>
<ProgressBar Margin="0 15 0 0" Value="{CompiledBinding StepProgress}"></ProgressBar>
</StackPanel>
<TextBlock Grid.Row="2" IsVisible="{CompiledBinding Ready}" VerticalAlignment="Top" Margin="0 15 0 0">Done, click restart to apply the update 🫡</TextBlock>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Spacing="15" Grid.Row="3" Margin="0 15 0 0" Height="30">
<CheckBox IsVisible="{CompiledBinding !Ready}" IsChecked="{CompiledBinding RestartWhenFinished}">Restart when finished</CheckBox>
<Button IsVisible="{CompiledBinding Ready}" Command="{CompiledBinding Restart}" Classes="accent">Restart</Button>
<Button Click="Cancel_OnClick">Cancel</Button>
</StackPanel>
</Grid>
</controls:CoreWindow>
@@ -1,28 +0,0 @@
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Settings.Updating;
public partial class ReleaseInstallerView : ReactiveCoreWindow<ReleaseInstallerViewModel>
{
public ReleaseInstallerView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void Cancel_OnClick(object? sender, RoutedEventArgs e)
{
Close();
}
}
@@ -1,81 +0,0 @@
using System;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Extensions;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using ReactiveUI;
namespace Artemis.UI.Screens.Settings.Updating;
public class ReleaseInstallerViewModel : ActivatableViewModelBase
{
private readonly ReleaseInstaller _releaseInstaller;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<float>? _overallProgress;
private ObservableAsPropertyHelper<float>? _stepProgress;
private bool _ready;
private bool _restartWhenFinished;
public ReleaseInstallerViewModel(ReleaseInstaller releaseInstaller, IWindowService windowService)
{
_releaseInstaller = releaseInstaller;
_windowService = windowService;
Restart = ReactiveCommand.Create(() => Utilities.ApplyUpdate(false));
this.WhenActivated(d =>
{
_overallProgress = Observable.FromEventPattern<float>(x => _releaseInstaller.OverallProgress.ProgressChanged += x, x => _releaseInstaller.OverallProgress.ProgressChanged -= x)
.Select(e => e.EventArgs)
.ToProperty(this, vm => vm.OverallProgress)
.DisposeWith(d);
_stepProgress = Observable.FromEventPattern<float>(x => _releaseInstaller.StepProgress.ProgressChanged += x, x => _releaseInstaller.StepProgress.ProgressChanged -= x)
.Select(e => e.EventArgs)
.ToProperty(this, vm => vm.StepProgress)
.DisposeWith(d);
Task.Run(() => InstallUpdate(d.AsCancellationToken()));
});
}
public ReactiveCommand<Unit, Unit> Restart { get; }
public float OverallProgress => _overallProgress?.Value ?? 0;
public float StepProgress => _stepProgress?.Value ?? 0;
public bool Ready
{
get => _ready;
set => RaiseAndSetIfChanged(ref _ready, value);
}
public bool RestartWhenFinished
{
get => _restartWhenFinished;
set => RaiseAndSetIfChanged(ref _restartWhenFinished, value);
}
private async Task InstallUpdate(CancellationToken cancellationToken)
{
try
{
await _releaseInstaller.InstallAsync(cancellationToken);
Ready = true;
if (RestartWhenFinished)
Utilities.ApplyUpdate(false);
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
_windowService.ShowExceptionDialog("Something went wrong while installing the update", e);
}
}
}
@@ -1,49 +1,163 @@
<controls:CoreWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:mdc="clr-namespace:Markdown.Avalonia.Controls;assembly=Markdown.Avalonia"
xmlns:mde="clr-namespace:Markdown.Avalonia.Extensions;assembly=Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseAvailableView"
x:DataType="updating:ReleaseAvailableViewModel"
Title="Artemis | Update available"
Width="750"
Height="750"
WindowStartupLocation="CenterOwner">
<Grid Margin="15" RowDefinitions="Auto,*,Auto">
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
xmlns:mdc="clr-namespace:Markdown.Avalonia.Controls;assembly=Markdown.Avalonia"
xmlns:mde="clr-namespace:Markdown.Avalonia.Extensions;assembly=Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseView"
x:DataType="updating:ReleaseViewModel">
<UserControl.Resources>
<converters:BytesToStringConverter x:Key="BytesToStringConverter" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector=":is(Control).fade-in">
<Setter Property="Opacity" Value="0"></Setter>
</Style>
<Style Selector=":is(Control).fade-in[IsVisible=True]">
<Style.Animations>
<Animation Duration="0:00:00.250" FillMode="Forward" Easing="CubicEaseInOut">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="0.0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Grid.info-container">
<Setter Property="Margin" Value="10" />
</Style>
<Style Selector="avalonia1|MaterialIcon.info-icon">
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Margin" Value="0 3 10 0" />
</Style>
<Style Selector="TextBlock.info-title">
<Setter Property="Margin" Value="0 0 0 5" />
<Setter Property="Opacity" Value="0.8" />
</Style>
<Style Selector="TextBlock.info-body">
</Style>
<Style Selector="TextBlock.info-link">
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight3}" />
</Style>
<Style Selector="TextBlock.info-link:pointerover">
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight1}" />
</Style>
</UserControl.Styles>
<TextBlock Grid.Row="0" Classes="h4">
A new Artemis update is available! 🥳
</TextBlock>
<Grid RowDefinitions="Auto,*" IsVisible="{CompiledBinding !Loading}" Classes="fade-in">
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
<StackPanel>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Classes="h4 no-margin">Release info</TextBlock>
<StackPanel Grid.Row="1" IsVisible="{CompiledBinding Release, Converter={x:Static ObjectConverters.IsNull}}" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock>Retrieving release...</TextBlock>
<ProgressBar IsIndeterminate="True"></ProgressBar>
</StackPanel>
<Panel Grid.Column="1" IsVisible="{CompiledBinding InstallationAvailable}">
<!-- Install progress -->
<Grid ColumnDefinitions="*,*"
RowDefinitions="*,*"
IsVisible="{CompiledBinding InstallationInProgress}">
<ProgressBar Grid.Column="0"
Grid.Row="0"
Width="300"
Value="{CompiledBinding ReleaseInstaller.Progress, FallbackValue=0}">
</ProgressBar>
<TextBlock Grid.Column="0"
Grid.Row="1"
Classes="subtitle"
TextAlignment="Right"
Text="{CompiledBinding ReleaseInstaller.Status, FallbackValue=Installing}" />
<Button Grid.Column="1" Grid.Row="0" Grid.RowSpan="2"
Classes="accent"
Margin="15 0 0 0"
Width="80"
VerticalAlignment="Center"
Command="{CompiledBinding CancelInstall}">
Cancel
</Button>
</Grid>
<Border Grid.Row="1" Classes="card" IsVisible="{CompiledBinding Release, Converter={x:Static ObjectConverters.IsNotNull}}">
<Grid RowDefinitions="Auto,*">
<StackPanel Grid.Row="0">
<StackPanel Orientation="Horizontal">
<TextBlock Text="You are currently running version " />
<TextBlock Text="{CompiledBinding CurrentVersion, Mode=OneWay}"></TextBlock>
<TextBlock Text=" while the latest build is " />
<TextBlock Text="{CompiledBinding Release.Version, Mode=OneWay, FallbackValue='Unknown'}"></TextBlock>
<TextBlock Text="." />
</StackPanel>
<Panel IsVisible="{CompiledBinding !InstallationInProgress}" HorizontalAlignment="Right">
<!-- Install button -->
<Button Classes="accent"
Width="80"
Command="{CompiledBinding Install}"
IsVisible="{CompiledBinding !InstallationFinished}">
Install
</Button>
<TextBlock Text="Updating Artemis will give you the latest bug(fixes), features and improvements." />
<Separator Classes="card-separator" />
</StackPanel>
<!-- Restart button -->
<Grid ColumnDefinitions="*,*" IsVisible="{CompiledBinding InstallationFinished}">
<TextBlock Grid.Column="0"
Grid.Row="0"
Classes="subtitle"
TextAlignment="Right"
VerticalAlignment="Center">
Ready, restart to install
</TextBlock>
<Button Grid.Column="1" Grid.Row="0"
Classes="accent"
Margin="15 0 0 0"
Width="80"
Command="{CompiledBinding Restart}"
IsVisible="{CompiledBinding InstallationFinished}">
Restart
</Button>
</Grid>
</Panel>
</Panel>
<avalonia:MarkdownScrollViewer Grid.Row="1"
VerticalAlignment="Top"
Markdown="{CompiledBinding Release.Changelog}">
</Grid>
<Separator Classes="card-separator" />
<Grid Margin="-5 -10" ColumnDefinitions="*,*,*">
<Grid Grid.Column="0" ColumnDefinitions="*,*" RowDefinitions="*,*,*" Classes="info-container" HorizontalAlignment="Left">
<avalonia1:MaterialIcon Kind="Calendar" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Release date</TextBlock>
<TextBlock Grid.Column="1"
Grid.Row="1"
Classes="info-body"
Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}, FallbackValue=Loading...}" />
</Grid>
<Grid Grid.Column="1" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Center">
<avalonia1:MaterialIcon Kind="Git" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Source</TextBlock>
<TextBlock Grid.Column="1"
Grid.Row="1"
Classes="info-body info-link"
Cursor="Hand"
PointerReleased="InputElement_OnPointerReleased"
Text="{CompiledBinding ShortCommit, FallbackValue=Loading...}" />
</Grid>
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
<avalonia1:MaterialIcon Kind="BoxOutline" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">File size</TextBlock>
<TextBlock Grid.Column="1"
Grid.Row="1"
Classes="info-body"
Text="{CompiledBinding FileSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay, FallbackValue=Loading...}" />
</Grid>
</Grid>
</StackPanel>
</Border>
<Border Grid.Row="1" Classes="card">
<Grid RowDefinitions="Auto,Auto,*">
<TextBlock Grid.Row="0" Classes="h5 no-margin">Release notes</TextBlock>
<Separator Grid.Row="1" Classes="card-separator" />
<avalonia:MarkdownScrollViewer Grid.Row="2" Markdown="{CompiledBinding Changelog}">
<avalonia:MarkdownScrollViewer.Styles>
<Style Selector="ctxt|CTextBlock">
<Style.Setters>
@@ -207,10 +321,6 @@
</avalonia:MarkdownScrollViewer>
</Grid>
</Border>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Spacing="5" Grid.Row="2" Margin="0 15 0 0">
<Button Classes="accent" Command="{CompiledBinding Install}" Click="Button_OnClick">Install update</Button>
<Button Click="Button_OnClick">Ask later</Button>
</StackPanel>
</Grid>
</controls:CoreWindow>
</UserControl>
@@ -0,0 +1,23 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings.Updating;
public class ReleaseView : ReactiveUserControl<ReleaseViewModel>
{
public ReleaseView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
ViewModel?.NavigateToSource();
}
}
@@ -0,0 +1,202 @@
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Extensions;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Updating;
using ReactiveUI;
using Serilog;
using StrawberryShake;
namespace Artemis.UI.Screens.Settings.Updating;
public class ReleaseViewModel : ActivatableViewModelBase
{
private readonly ILogger _logger;
private readonly INotificationService _notificationService;
private readonly string _releaseId;
private readonly Platform _updatePlatform;
private readonly IUpdatingClient _updatingClient;
private readonly IWindowService _windowService;
private CancellationTokenSource? _installerCts;
private string _changelog = string.Empty;
private string _commit = string.Empty;
private string _shortCommit = string.Empty;
private long _fileSize;
private bool _installationAvailable;
private bool _installationFinished;
private bool _installationInProgress;
private bool _loading = true;
private bool _retrievedDetails;
public ReleaseViewModel(string releaseId,
string version,
DateTimeOffset createdAt,
ILogger logger,
IUpdatingClient updatingClient,
INotificationService notificationService,
IUpdateService updateService,
IWindowService windowService)
{
_releaseId = releaseId;
_logger = logger;
_updatingClient = updatingClient;
_notificationService = notificationService;
_windowService = windowService;
if (OperatingSystem.IsWindows())
_updatePlatform = Platform.Windows;
else if (OperatingSystem.IsLinux())
_updatePlatform = Platform.Linux;
else if (OperatingSystem.IsMacOS())
_updatePlatform = Platform.Osx;
else
throw new PlatformNotSupportedException("Cannot auto update on the current platform");
Version = version;
CreatedAt = createdAt;
ReleaseInstaller = updateService.GetReleaseInstaller(_releaseId);
Install = ReactiveCommand.CreateFromTask(ExecuteInstall);
Restart = ReactiveCommand.Create(() => Utilities.ApplyUpdate(false));
CancelInstall = ReactiveCommand.Create(() => _installerCts?.Cancel());
this.WhenActivated(d =>
{
// There's no point in running anything but the latest version of the current channel.
// Perhaps later that won't be true anymore, then we could consider allowing to install
// older versions with compatible database versions.
InstallationAvailable = updateService.CachedLatestRelease?.Id == _releaseId;
RetrieveDetails(d.AsCancellationToken()).ToObservable();
Disposable.Create(_installerCts, cts => cts?.Cancel()).DisposeWith(d);
});
}
public ReactiveCommand<Unit, Unit> Restart { get; set; }
public ReactiveCommand<Unit, Unit> Install { get; }
public ReactiveCommand<Unit, Unit> CancelInstall { get; }
public string Version { get; }
public DateTimeOffset CreatedAt { get; }
public ReleaseInstaller ReleaseInstaller { get; }
public string Changelog
{
get => _changelog;
set => RaiseAndSetIfChanged(ref _changelog, value);
}
public string Commit
{
get => _commit;
set => RaiseAndSetIfChanged(ref _commit, value);
}
public string ShortCommit
{
get => _shortCommit;
set => RaiseAndSetIfChanged(ref _shortCommit, value);
}
public long FileSize
{
get => _fileSize;
set => RaiseAndSetIfChanged(ref _fileSize, value);
}
public bool Loading
{
get => _loading;
private set => RaiseAndSetIfChanged(ref _loading, value);
}
public bool InstallationAvailable
{
get => _installationAvailable;
set => RaiseAndSetIfChanged(ref _installationAvailable, value);
}
public bool InstallationInProgress
{
get => _installationInProgress;
set => RaiseAndSetIfChanged(ref _installationInProgress, value);
}
public bool InstallationFinished
{
get => _installationFinished;
set => RaiseAndSetIfChanged(ref _installationFinished, value);
}
public void NavigateToSource()
{
Utilities.OpenUrl($"https://github.com/Artemis-RGB/Artemis/commit/{Commit}");
}
private async Task ExecuteInstall(CancellationToken cancellationToken)
{
_installerCts = new CancellationTokenSource();
try
{
InstallationInProgress = true;
await ReleaseInstaller.InstallAsync(_installerCts.Token);
InstallationFinished = true;
}
catch (TaskCanceledException)
{
// ignored
}
catch (Exception e)
{
_windowService.ShowExceptionDialog("Failed to install update", e);
}
finally
{
InstallationInProgress = false;
}
}
private async Task RetrieveDetails(CancellationToken cancellationToken)
{
if (_retrievedDetails)
return;
try
{
Loading = true;
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken);
IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease;
if (release == null)
return;
Changelog = release.Changelog;
Commit = release.Commit;
ShortCommit = release.Commit.Substring(0, 7);
FileSize = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform)?.FileInfo.DownloadSize ?? 0;
_retrievedDetails = true;
}
catch (Exception e)
{
_logger.Warning(e, "Failed to retrieve release details");
_notificationService.CreateNotification()
.WithTitle("Failed to retrieve details")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
finally
{
Loading = false;
}
}
}