Merge branch 'development' into feature/workshop

This commit is contained in:
Robert committed 2023-07-06 18:41:59 +02:00
commit 229d93901b
76 files changed
+2016 -854

No files matched your search

@@ -0,0 +1,20 @@
using System;
namespace Artemis.UI.Screens.Settings;
public class SettingsTab
{
public SettingsTab(string path, string name)
{
Path = path;
Name = name;
}
public string Path { get; set; }
public string Name { get; set; }
public bool Matches(string path)
{
return path.StartsWith($"settings/{Path}", StringComparison.InvariantCultureIgnoreCase);
}
}
@@ -3,16 +3,25 @@
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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.SettingsView"
x:DataType="settings:SettingsViewModel">
<Border Classes="router-container">
<TabControl Margin="12" ItemsSource="{CompiledBinding SettingTabs}" SelectedItem="{CompiledBinding SelectedTab}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{CompiledBinding DisplayName}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
<Grid RowDefinitions="Auto,*">
<TabStrip Grid.Row="0" Margin="12" ItemsSource="{CompiledBinding SettingTabs}" SelectedItem="{CompiledBinding SelectedTab}">
<TabStrip.ItemTemplate>
<DataTemplate>
<TextBlock Text="{CompiledBinding Name}" />
</DataTemplate>
</TabStrip.ItemTemplate>
</TabStrip>
<controls:Frame Grid.Row="1" Name="TabFrame" IsNavigationStackEnabled="False" CacheSize="0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory/>
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</Grid>
</Border>
</UserControl>
@@ -1,5 +1,11 @@
using Avalonia.Markup.Xaml;
using System;
using System.Reactive.Disposables;
using Artemis.UI.Shared;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using FluentAvalonia.UI.Media.Animation;
using FluentAvalonia.UI.Navigation;
using ReactiveUI;
namespace Artemis.UI.Screens.Settings;
@@ -8,6 +14,11 @@ public partial class SettingsView : ReactiveUserControl<SettingsViewModel>
public SettingsView()
{
InitializeComponent();
this.WhenActivated(d => { ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d); });
}
private void Navigate(ViewModelBase viewModel)
{
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel, new FrameNavigationOptions {TransitionInfoOverride = new SlideNavigationTransitionInfo()}));
}
}
@@ -1,36 +1,57 @@
using System.Collections.ObjectModel;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using ReactiveUI;
namespace Artemis.UI.Screens.Settings;
public class SettingsViewModel : MainScreenViewModel
public class SettingsViewModel : RoutableScreen<ActivatableViewModelBase>, IMainScreenViewModel
{
private ActivatableViewModelBase _selectedTab;
private readonly IRouter _router;
private SettingsTab? _selectedTab;
public SettingsViewModel(IScreen hostScreen,
GeneralTabViewModel generalTabViewModel,
PluginsTabViewModel pluginsTabViewModel,
DevicesTabViewModel devicesTabViewModel,
ReleasesTabViewModel releasesTabViewModel,
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
public SettingsViewModel(IRouter router)
{
SettingTabs = new ObservableCollection<ActivatableViewModelBase>
_router = router;
SettingTabs = new ObservableCollection<SettingsTab>
{
generalTabViewModel,
pluginsTabViewModel,
devicesTabViewModel,
releasesTabViewModel,
aboutTabViewModel
new("general", "General"),
new("plugins", "Plugins"),
new("devices", "Devices"),
new("releases", "Releases"),
new("about", "About"),
};
_selectedTab = generalTabViewModel;
// Navigate on tab change
this.WhenActivated(d => this.WhenAnyValue(vm => vm.SelectedTab)
.WhereNotNull()
.Subscribe(s => _router.Navigate($"settings/{s.Path}", new RouterNavigationOptions {IgnoreOnPartialMatch = true}))
.DisposeWith(d));
}
public ObservableCollection<ActivatableViewModelBase> SettingTabs { get; }
public ObservableCollection<SettingsTab> SettingTabs { get; }
public ActivatableViewModelBase SelectedTab
public SettingsTab? SelectedTab
{
get => _selectedTab;
set => RaiseAndSetIfChanged(ref _selectedTab, value);
}
public ViewModelBase? TitleBarViewModel => null;
/// <inheritdoc />
public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
// Display tab change on navigate
SelectedTab = SettingTabs.FirstOrDefault(t => t.Matches(args.Path));
// Always show a tab, if there is none forward to the first
if (SelectedTab == null)
await _router.Navigate($"settings/{SettingTabs.First().Path}");
}
}
@@ -32,6 +32,7 @@
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Grid>
</UserControl>
@@ -49,7 +49,7 @@ public class PluginsTabViewModel : ActivatableViewModelBase
this.WhenActivated(d =>
{
Dispatcher.UIThread.Post(() => plugins.AddRange(_pluginManagementService.GetAllPlugins()), DispatcherPriority.Background);
plugins.AddRange(_pluginManagementService.GetAllPlugins());
Observable.FromEventPattern<PluginEventArgs>(x => _pluginManagementService.PluginLoaded += x, x => _pluginManagementService.PluginLoaded -= x)
.Subscribe(a => plugins.Add(a.EventArgs.Plugin))
.DisposeWith(d);
@@ -6,6 +6,7 @@
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
x:Class="Artemis.UI.Screens.Settings.ReleasesTabView"
x:DataType="settings:ReleasesTabViewModel">
@@ -34,31 +35,16 @@
</controls:HyperlinkButton>
</StackPanel>
<Grid ColumnDefinitions="300,*" Margin="0 10" IsVisible="{CompiledBinding ReleaseViewModels.Count}">
<Grid ColumnDefinitions="300,*" Margin="10" IsVisible="{CompiledBinding ReleaseViewModels.Count}">
<Border Classes="card-condensed" Grid.Column="0" Margin="0 0 10 0">
<ListBox ItemsSource="{CompiledBinding ReleaseViewModels}" SelectedItem="{CompiledBinding SelectedReleaseViewModel}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="updating:ReleaseViewModel">
<Panel>
<Grid Margin="4" IsVisible="{CompiledBinding ShowStatusIndicator}" RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" Text="{CompiledBinding Version}" VerticalAlignment="Center" FontWeight="SemiBold" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
<avalonia:MaterialIcon Classes="status-icon" Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Kind="CheckCircle" ToolTip.Tip="Current version"
IsVisible="{CompiledBinding IsCurrentVersion}" />
<avalonia:MaterialIcon Classes="status-icon" Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Kind="History" ToolTip.Tip="Previous version"
IsVisible="{CompiledBinding IsPreviousVersion}" />
</Grid>
<StackPanel Margin="4" IsVisible="{CompiledBinding !ShowStatusIndicator}">
<TextBlock Text="{CompiledBinding Version}" VerticalAlignment="Center" />
<TextBlock Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
</StackPanel>
</Panel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ListBox ItemsSource="{CompiledBinding ReleaseViewModels}" SelectedItem="{CompiledBinding SelectedReleaseViewModel}"/>
</Border>
<ContentControl Grid.Column="1" Content="{CompiledBinding SelectedReleaseViewModel}" />
<controls:Frame Grid.Column="1" Name="ReleaseFrame" CacheSize="0" IsNavigationStackEnabled="False">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</Grid>
</Panel>
</Panel>
@@ -1,5 +1,9 @@
using Avalonia.Markup.Xaml;
using System;
using System.Reactive.Disposables;
using Artemis.UI.Shared;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Settings;
@@ -8,6 +12,22 @@ public partial class ReleasesTabView : ReactiveUserControl<ReleasesTabViewModel>
public ReleasesTabView()
{
InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d));
}
private void Navigate(ViewModelBase viewModel)
{
Dispatcher.UIThread.Invoke(() =>
{
try
{
ReleaseFrame.NavigateFromObject(viewModel);
}
catch (Exception e)
{
// ignored
}
});
}
}
@@ -1,14 +1,15 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
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.Routing;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Updating;
@@ -21,47 +22,46 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Settings;
public class ReleasesTabViewModel : ActivatableViewModelBase
public class ReleasesTabViewModel : RoutableScreen<ReleaseDetailsViewModel>
{
private readonly ILogger _logger;
private readonly IUpdateService _updateService;
private readonly IUpdatingClient _updatingClient;
private readonly INotificationService _notificationService;
private readonly IRouter _router;
private readonly SourceList<IGetReleases_PublishedReleases_Nodes> _releases;
private IGetReleases_PublishedReleases_PageInfo? _lastPageInfo;
private bool _loading;
private IGetReleases_PublishedReleases_PageInfo? _lastPageInfo;
private ReleaseViewModel? _selectedReleaseViewModel;
public ReleasesTabViewModel(ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, IReleaseVmFactory releaseVmFactory, INotificationService notificationService)
public ReleasesTabViewModel(ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, IReleaseVmFactory releaseVmFactory, INotificationService notificationService,
IRouter router)
{
_logger = logger;
_updateService = updateService;
_updatingClient = updatingClient;
_notificationService = notificationService;
_router = router;
_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))
.Transform(releaseVmFactory.ReleaseListViewModel)
.ObserveOn(AvaloniaScheduler.Instance)
.Bind(out ReadOnlyObservableCollection<ReleaseViewModel> releaseViewModels)
.Subscribe();
DisplayName = "Releases";
RecycleScreen = false;
ReleaseViewModels = releaseViewModels;
Channel = _updateService.Channel;
this.WhenActivated(async d =>
{
await _updateService.CacheLatestRelease();
await GetMoreReleases(d.AsCancellationToken());
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(r => r.Version == PreselectVersion) ?? ReleaseViewModels.FirstOrDefault();
});
}
this.WhenAnyValue(vm => vm.SelectedReleaseViewModel).WhereNotNull().Subscribe(r => _router.Navigate($"settings/releases/{r.Release.Id}"));
this.WhenActivated(d => _updateService.CacheLatestRelease().ToObservable().Subscribe().DisposeWith(d));
}
public ReadOnlyObservableCollection<ReleaseViewModel> ReleaseViewModels { get; }
public string Channel { get; }
public string? PreselectVersion { get; set; }
public ReleaseViewModel? SelectedReleaseViewModel
{
@@ -109,4 +109,22 @@ public class ReleasesTabViewModel : ActivatableViewModelBase
Loading = false;
}
}
/// <inheritdoc />
public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
if (!ReleaseViewModels.Any())
await GetMoreReleases(cancellationToken);
// If there is an ID parameter further down the path, preselect it
if (args.RouteParameters.Length > 0 && args.RouteParameters[0] is Guid releaseId)
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(vm => vm.Release.Id == releaseId);
// Otherwise forward to the last release
else
{
ReleaseViewModel? lastRelease = ReleaseViewModels.FirstOrDefault(r => r.IsCurrentVersion) ?? ReleaseViewModels.FirstOrDefault();
if (lastRelease != null)
await _router.Navigate($"settings/releases/{lastRelease.Release.Id}");
}
}
}
@@ -0,0 +1,152 @@
<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:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:converters1="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseDetailsView"
x:DataType="updating:ReleaseDetailsViewModel">
<UserControl.Resources>
<converters:BytesToStringConverter x:Key="BytesToStringConverter" />
<converters1:SubstringConverter x:Key="SubstringConverter" />
</UserControl.Resources>
<UserControl.Styles>
<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>
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
<StackPanel>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Classes="h4 no-margin">Release info</TextBlock>
<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>
<Panel IsVisible="{CompiledBinding !InstallationInProgress}" HorizontalAlignment="Right">
<!-- Install button -->
<Button Classes="accent"
Width="80"
Command="{CompiledBinding Install}"
IsVisible="{CompiledBinding !InstallationFinished}">
Install
</Button>
<!-- 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>
</Grid>
<Border 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 Release.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 Release.Commit, Converter={StaticResource SubstringConverter}, ConverterParameter=7, 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>
<Border Grid.Row="1" Classes="card-separator" />
<avalonia:MarkdownScrollViewer Grid.Row="2" Markdown="{CompiledBinding Release.Changelog}" MarkdownStyleName="FluentAvalonia">
<avalonia:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml"/>
</avalonia:MarkdownScrollViewer.Styles>
</avalonia:MarkdownScrollViewer>
</Grid>
</Border>
</Grid>
</UserControl>
@@ -0,0 +1,19 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings.Updating;
public partial class ReleaseDetailsView : ReactiveUserControl<ReleaseDetailsViewModel>
{
public ReleaseDetailsView()
{
InitializeComponent();
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
ViewModel?.NavigateToSource();
}
}
@@ -0,0 +1,195 @@
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
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 ReleaseDetailsViewModel : RoutableScreen<ViewModelBase, ReleaseDetailsViewModelParameters>
{
private readonly ObservableAsPropertyHelper<long> _fileSize;
private readonly ILogger _logger;
private readonly INotificationService _notificationService;
private readonly IUpdateService _updateService;
private readonly IUpdatingClient _updatingClient;
private bool _installationAvailable;
private bool _installationFinished;
private bool _installationInProgress;
private CancellationTokenSource? _installerCts;
private bool _loading = true;
private IGetReleaseById_PublishedRelease? _release;
private ReleaseInstaller? _releaseInstaller;
public ReleaseDetailsViewModel(ILogger logger, IUpdatingClient updatingClient, INotificationService notificationService, IUpdateService updateService)
{
_logger = logger;
_updatingClient = updatingClient;
_notificationService = notificationService;
_updateService = updateService;
Platform updatePlatform;
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");
Install = ReactiveCommand.CreateFromTask(ExecuteInstall);
Restart = ReactiveCommand.Create(ExecuteRestart);
CancelInstall = ReactiveCommand.Create(() => _installerCts?.Cancel());
_fileSize = this.WhenAnyValue(vm => vm.Release)
.Select(release => release?.Artifacts.FirstOrDefault(a => a.Platform == updatePlatform)?.FileInfo.DownloadSize ?? 0)
.ToProperty(this, vm => vm.FileSize);
this.WhenActivated(d => Disposable.Create(_installerCts, cts => cts?.Cancel()).DisposeWith(d));
}
public ReactiveCommand<Unit, Unit> Restart { get; }
public ReactiveCommand<Unit, Unit> Install { get; }
public ReactiveCommand<Unit, Unit> CancelInstall { get; }
public long FileSize => _fileSize.Value;
public IGetReleaseById_PublishedRelease? Release
{
get => _release;
set => RaiseAndSetIfChanged(ref _release, value);
}
public ReleaseInstaller? ReleaseInstaller
{
get => _releaseInstaller;
set => RaiseAndSetIfChanged(ref _releaseInstaller, 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()
{
if (Release != null)
Utilities.OpenUrl($"https://github.com/Artemis-RGB/Artemis/commit/{Release.Commit}");
}
/// <inheritdoc />
public override async Task OnNavigating(ReleaseDetailsViewModelParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
// 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 == parameters.ReleaseId;
await RetrieveDetails(parameters.ReleaseId, cancellationToken);
ReleaseInstaller = _updateService.GetReleaseInstaller(parameters.ReleaseId);
}
private void ExecuteRestart()
{
_updateService.RestartForUpdate(false);
}
private async Task ExecuteInstall(CancellationToken cancellationToken)
{
if (ReleaseInstaller == null)
return;
_installerCts = new CancellationTokenSource();
try
{
InstallationInProgress = true;
await ReleaseInstaller.InstallAsync(_installerCts.Token);
InstallationFinished = true;
}
catch (Exception e)
{
if (_installerCts.IsCancellationRequested)
return;
_logger.Warning(e, "Failed to install update through UI");
_notificationService.CreateNotification()
.WithTitle("Failed to install update")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
finally
{
InstallationInProgress = false;
}
}
private async Task RetrieveDetails(Guid releaseId, CancellationToken cancellationToken)
{
try
{
Loading = true;
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(releaseId, cancellationToken);
IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease;
if (release == null)
return;
Release = release;
}
catch (TaskCanceledException)
{
// ignored
}
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;
}
}
}
public class ReleaseDetailsViewModelParameters
{
public Guid ReleaseId { get; set; }
}
@@ -3,163 +3,22 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.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"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
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>
<Grid RowDefinitions="Auto,*" IsVisible="{CompiledBinding Commit, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" 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>
<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>
<Panel IsVisible="{CompiledBinding !InstallationInProgress}" HorizontalAlignment="Right">
<!-- Install button -->
<Button Classes="accent"
Width="80"
Command="{CompiledBinding Install}"
IsVisible="{CompiledBinding !InstallationFinished}">
Install
</Button>
<!-- 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>
</Grid>
<Border 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>
<Border Grid.Row="1" Classes="card-separator" />
<avalonia:MarkdownScrollViewer Grid.Row="2" Markdown="{CompiledBinding Changelog}" MarkdownStyleName="FluentAvalonia">
<avalonia:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml"/>
</avalonia:MarkdownScrollViewer.Styles>
</avalonia:MarkdownScrollViewer>
</Grid>
</Border>
</Grid>
</UserControl>
<Panel>
<Grid Margin="4" IsVisible="{CompiledBinding ShowStatusIndicator}" RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" Text="{CompiledBinding Release.Version}" VerticalAlignment="Center" FontWeight="SemiBold" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{CompiledBinding Release.CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
<avalonia:MaterialIcon Classes="status-icon" Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Kind="CheckCircle" ToolTip.Tip="Current version"
IsVisible="{CompiledBinding IsCurrentVersion}" />
<avalonia:MaterialIcon Classes="status-icon" Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Kind="History" ToolTip.Tip="Previous version"
IsVisible="{CompiledBinding IsPreviousVersion}" />
</Grid>
<StackPanel Margin="4" IsVisible="{CompiledBinding !ShowStatusIndicator}">
<TextBlock Text="{CompiledBinding Release.Version}" VerticalAlignment="Center" />
<TextBlock Text="{CompiledBinding Release.CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
</StackPanel>
</Panel>
</UserControl>
@@ -1,19 +1,18 @@
using Avalonia.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings.Updating;
public partial class ReleaseView : ReactiveUserControl<ReleaseViewModel>
public partial class ReleaseView : UserControl
{
public ReleaseView()
{
InitializeComponent();
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
private void InitializeComponent()
{
ViewModel?.NavigateToSource();
AvaloniaXamlLoader.Load(this);
}
}
@@ -1,220 +1,22 @@
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
public class ReleaseViewModel : ViewModelBase
{
private readonly ILogger _logger;
private readonly INotificationService _notificationService;
private readonly IUpdateService _updateService;
private readonly Platform _updatePlatform;
private readonly IUpdatingClient _updatingClient;
private CancellationTokenSource? _installerCts;
private string? _changelog;
private string? _commit;
private string? _shortCommit;
private long _fileSize;
private bool _installationAvailable;
private bool _installationFinished;
private bool _installationInProgress;
private bool _loading = true;
private bool _retrievedDetails;
public IGetReleases_PublishedReleases_Nodes Release { get; }
public ReleaseViewModel(Guid releaseId,
string version,
DateTimeOffset createdAt,
ILogger logger,
IUpdatingClient updatingClient,
INotificationService notificationService,
IUpdateService updateService)
public ReleaseViewModel(IUpdateService updateService, IGetReleases_PublishedReleases_Nodes release)
{
_logger = logger;
_updatingClient = updatingClient;
_notificationService = notificationService;
_updateService = updateService;
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");
ReleaseId = releaseId;
Version = version;
CreatedAt = createdAt;
ReleaseInstaller = updateService.GetReleaseInstaller(ReleaseId);
Install = ReactiveCommand.CreateFromTask(ExecuteInstall);
Restart = ReactiveCommand.Create(ExecuteRestart);
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);
});
Release = release;
}
public Guid ReleaseId { get; }
private void ExecuteRestart()
{
_updateService.RestartForUpdate(false);
}
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 bool IsCurrentVersion => Version == Constants.CurrentVersion;
public bool IsPreviousVersion => Version == _updateService.PreviousVersion;
public bool ShowStatusIndicator => IsCurrentVersion || IsPreviousVersion;
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 (Exception e)
{
if (_installerCts.IsCancellationRequested)
return;
_logger.Warning(e, "Failed to install update through UI");
_notificationService.CreateNotification()
.WithTitle("Failed to install update")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
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 (TaskCanceledException)
{
// ignored
}
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;
}
}
public bool IsCurrentVersion => Release.Version == Constants.CurrentVersion;
public bool IsPreviousVersion => Release.Version == _updateService.PreviousVersion;
public bool ShowStatusIndicator => IsCurrentVersion || IsPreviousVersion;
}