Plugins - Fixed cancelling prerequisite installation

Plugins - Tweaked prerequisite installation popup size
Account settings - Added personal access token management
Workshop - Added plugin loading, installing and removal
This commit is contained in:
RobertBeekman committed 2024-02-16 23:04:24 +01:00
1 parent 21b8112de5
commit f2c8de1746
33 files changed
+516 -113

No files matched your search

@@ -0,0 +1,19 @@
<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:account="clr-namespace:Artemis.UI.Screens.Settings.Account"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.Account.CreatePersonalAccessTokenView"
x:DataType="account:CreatePersonalAccessTokenViewModel">
<StackPanel Spacing="5" Width="300">
<Label>Description</Label>
<TextBox Text="{CompiledBinding Description}"/>
<Label>Expiration date</Label>
<CalendarDatePicker SelectedDate="{CompiledBinding ExpirationDate}"
DisplayDateStart="{CompiledBinding StartDate}"
DisplayDateEnd="{CompiledBinding EndDate}"
HorizontalAlignment="Stretch"/>
</StackPanel>
</UserControl>
@@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings.Account;
public partial class CreatePersonalAccessTokenView : ReactiveUserControl<CreatePersonalAccessTokenViewModel>
{
public CreatePersonalAccessTokenView()
{
InitializeComponent();
}
}
@@ -0,0 +1,46 @@
using System;
using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
namespace Artemis.UI.Screens.Settings.Account;
public partial class CreatePersonalAccessTokenViewModel : ContentDialogViewModelBase
{
private readonly IUserManagementService _userManagementService;
private readonly IWindowService _windowService;
[Notify] private string _description = string.Empty;
[Notify] private DateTime _expirationDate = DateTime.Today.AddDays(180);
public CreatePersonalAccessTokenViewModel(IUserManagementService userManagementService, IWindowService windowService)
{
_userManagementService = userManagementService;
_windowService = windowService;
Submit = ReactiveCommand.CreateFromTask(ExecuteSubmit, ValidationContext.Valid);
this.ValidationRule(vm => vm.Description, e => e != null, "You must specify a description");
this.ValidationRule(vm => vm.Description, e => e == null || e.Length >= 10, "You must specify a description of at least 10 characters");
this.ValidationRule(vm => vm.Description, e => e == null || e.Length <= 150, "You must specify a description of less than 150 characters");
this.ValidationRule(vm => vm.ExpirationDate, e => e >= DateTime.Today.AddDays(1), "Expiration date must be at least 24 hours from now");
}
public DateTime StartDate => DateTime.Today.AddDays(1);
public DateTime EndDate => DateTime.Today.AddDays(365);
public ReactiveCommand<Unit, Unit> Submit { get; }
private async Task ExecuteSubmit(CancellationToken cts)
{
string result = await _userManagementService.CreatePersonAccessToken(Description, ExpirationDate, cts);
await _windowService.CreateContentDialog()
.WithTitle("Personal Access Token")
.WithViewModel(out PersonalAccessTokenViewModel _, result)
.WithFullScreen()
.ShowAsync();
}
}
@@ -0,0 +1,19 @@
<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:account="clr-namespace:Artemis.UI.Screens.Settings.Account"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.Account.PersonalAccessTokenView"
x:DataType="account:PersonalAccessTokenViewModel">
<StackPanel>
<TextBlock>
Your token has been created, please copy it now as you cannot view it again later.
</TextBlock>
<TextBox Margin="0 10 0 0"
VerticalAlignment="Top"
TextWrapping="Wrap"
IsReadOnly="True"
Text="{CompiledBinding Token, Mode=OneWay}" />
</StackPanel>
</UserControl>
@@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Settings.Account;
public partial class PersonalAccessTokenView : ReactiveUserControl<PersonalAccessTokenViewModel>
{
public PersonalAccessTokenView()
{
InitializeComponent();
}
}
@@ -0,0 +1,13 @@
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.Settings.Account;
public class PersonalAccessTokenViewModel : ContentDialogViewModelBase
{
public string Token { get; }
public PersonalAccessTokenViewModel(string token)
{
Token = token;
}
}
@@ -5,36 +5,41 @@
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:loaders="clr-namespace:AsyncImageLoader.Loaders;assembly=AsyncImageLoader.Avalonia"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:models="clr-namespace:Artemis.WebClient.Workshop.Models;assembly=Artemis.WebClient.Workshop"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Settings.AccountTabView"
x:DataType="settings:AccountTabViewModel">
<UserControl.Resources>
<converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources>
<Panel>
<StackPanel IsVisible="{CompiledBinding !IsLoggedIn^}" Margin="0 50 0 0">
<StackPanel.Styles>
<Styles>
<Style Selector="TextBlock">
<Setter Property="TextAlignment" Value="Center"></Setter>
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
</Styles>
</StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">You are not logged in</TextBlock>
<TextBlock>
<Run>In order to manage your account you must be logged in.</Run>
<LineBreak />
<Run>Creating an account is free and we'll not bother you with a newsletter or crap like that.</Run>
</TextBlock>
<Lottie Path="/Assets/Animations/login-pending.json" RepeatCount="1" Width="350" Height="350"></Lottie>
<TextBlock>
<Run>Click Log In below to (create an account) and log in.</Run>
<LineBreak />
<Run>You'll also be able to log in with Google or Discord.</Run>
</TextBlock>
<Button HorizontalAlignment="Center" Command="{CompiledBinding Login}" Margin="0 25 0 0">Login</Button>
</StackPanel>
<!-- <StackPanel IsVisible="{CompiledBinding !IsLoggedIn^}" Margin="0 50 0 0"> -->
<!-- <StackPanel.Styles> -->
<!-- <Styles> -->
<!-- <Style Selector="TextBlock"> -->
<!-- <Setter Property="TextAlignment" Value="Center"></Setter> -->
<!-- <Setter Property="TextWrapping" Value="Wrap"></Setter> -->
<!-- </Style> -->
<!-- </Styles> -->
<!-- </StackPanel.Styles> -->
<!-- <TextBlock Theme="{StaticResource TitleTextBlockStyle}">You are not logged in</TextBlock> -->
<!-- <TextBlock> -->
<!-- <Run>In order to manage your account you must be logged in.</Run> -->
<!-- <LineBreak /> -->
<!-- <Run>Creating an account is free and we'll not bother you with a newsletter or crap like that.</Run> -->
<!-- </TextBlock> -->
<!-- -->
<!-- <Lottie Path="/Assets/Animations/login-pending.json" RepeatCount="1" Width="350" Height="350"></Lottie> -->
<!-- -->
<!-- <TextBlock> -->
<!-- <Run>Click Log In below to (create an account) and log in.</Run> -->
<!-- <LineBreak /> -->
<!-- <Run>You'll also be able to log in with Google or Discord.</Run> -->
<!-- </TextBlock> -->
<!-- <Button HorizontalAlignment="Center" Command="{CompiledBinding Login}" Margin="0 25 0 0">Login</Button> -->
<!-- </StackPanel> -->
<ScrollViewer IsVisible="{CompiledBinding IsLoggedIn^}" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="15" MaxWidth="1000">
@@ -46,7 +51,7 @@
</asyncImageLoader:AdvancedImage.Loader>
</asyncImageLoader:AdvancedImage>
</Border>
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0" VerticalAlignment="Bottom" Text="{CompiledBinding Name}" Classes="h3 no-margin"/>
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0" VerticalAlignment="Bottom" Text="{CompiledBinding Name}" Classes="h3 no-margin" />
<TextBlock Classes="subtitle" Grid.Column="1" Grid.Row="1" Padding="0" VerticalAlignment="Top" Text="{CompiledBinding Email}" />
</Grid>
@@ -66,8 +71,8 @@
<Button Width="150" Content="Change password" Command="{CompiledBinding ChangePasswordAddress}" />
</StackPanel>
</Grid>
<Border Classes="card-separator" IsVisible="{CompiledBinding CanChangePassword}"/>
<Border Classes="card-separator" IsVisible="{CompiledBinding CanChangePassword}" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" VerticalAlignment="Center">
<TextBlock>
@@ -99,16 +104,45 @@
</StackPanel>
</Border>
<!-- <TextBlock Classes="card-title"> -->
<!-- Personal access tokens -->
<!-- </TextBlock> -->
<!-- <Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0"> -->
<!-- <StackPanel> -->
<!-- <TextBlock>TODO :)</TextBlock> -->
<!-- </StackPanel> -->
<!-- </Border> -->
<TextBlock Classes="card-title">
Personal access tokens
</TextBlock>
<Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0">
<StackPanel>
<Button HorizontalAlignment="Right" Command="{CompiledBinding GenerateToken}">Generate token</Button>
<Grid ColumnDefinitions="2*,*,*,*" Margin="12 6">
<TextBlock Grid.Column="0">Description</TextBlock>
<TextBlock Grid.Column="1">Created at</TextBlock>
<TextBlock Grid.Column="2" Grid.ColumnSpan="2">Expires at</TextBlock>
</Grid>
<ItemsControl ItemsSource="{CompiledBinding PersonalAccessTokens}">
<ItemsControl.Styles>
<Style Selector="ContentPresenter:nth-child(even) > Border">
<Setter Property="Background" Value="{StaticResource ControlStrokeColorOnAccentDefault}"></Setter>
</Style>
</ItemsControl.Styles>
<ItemsControl.DataTemplates>
<DataTemplate DataType="models:PersonalAccessToken">
<Border CornerRadius="4" Padding="12 6">
<Grid ColumnDefinitions="2*,*,*,*">
<TextBlock Grid.Column="0" VerticalAlignment="Center" Text="{CompiledBinding Description}" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" Text="{CompiledBinding CreationTime, Converter={StaticResource DateTimeConverter}}" />
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{CompiledBinding Expiration, Converter={StaticResource DateTimeConverter}}" />
<Button Grid.Column="3" HorizontalAlignment="Right"
Classes="icon-button"
Command="{Binding $parent[settings:AccountTabView].DataContext.DeleteToken}"
CommandParameter="{CompiledBinding}">
<avalonia:MaterialIcon Kind="Trash" />
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Panel>
</UserControl>
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
@@ -12,6 +15,7 @@ using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using IdentityModel;
using PropertyChanged.SourceGenerator;
@@ -29,6 +33,7 @@ public partial class AccountTabViewModel : RoutableScreen
[Notify(Setter.Private)] private string? _name;
[Notify(Setter.Private)] private string? _email;
[Notify(Setter.Private)] private string? _avatarUrl;
[Notify(Setter.Private)] private ObservableCollection<PersonalAccessToken> _personalAccessTokens = new();
public AccountTabViewModel(IWindowService windowService, IAuthenticationService authenticationService, IUserManagementService userManagementService)
{
@@ -36,10 +41,11 @@ public partial class AccountTabViewModel : RoutableScreen
_authenticationService = authenticationService;
_userManagementService = userManagementService;
_authenticationService.AutoLogin(true);
DisplayName = "Account";
IsLoggedIn = _authenticationService.IsLoggedIn;
DeleteToken = ReactiveCommand.CreateFromTask<PersonalAccessToken>(ExecuteDeleteToken);
this.WhenActivated(d =>
{
_canChangePassword = _authenticationService.GetClaim(JwtClaimTypes.AuthenticationMethod).Select(c => c?.Value == "pwd").ToProperty(this, vm => vm.CanChangePassword);
@@ -50,12 +56,13 @@ public partial class AccountTabViewModel : RoutableScreen
public bool CanChangePassword => _canChangePassword?.Value ?? false;
public IObservable<bool> IsLoggedIn { get; }
public ReactiveCommand<PersonalAccessToken,Unit> DeleteToken { get; }
public async Task Login()
{
await _windowService.CreateContentDialog().WithViewModel(out WorkshopLoginViewModel _).WithTitle("Account login").ShowAsync();
}
public async Task ChangeAvatar()
{
string[]? result = await _windowService.CreateOpenFileDialog().HavingFilter(f => f.WithBitmaps()).ShowAsync();
@@ -85,7 +92,7 @@ public partial class AccountTabViewModel : RoutableScreen
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
.ShowAsync();
}
public async Task ChangePasswordAddress()
{
await _windowService.CreateContentDialog().WithTitle("Change password")
@@ -94,7 +101,7 @@ public partial class AccountTabViewModel : RoutableScreen
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
.ShowAsync();
}
public async Task RemoveAccount()
{
await _windowService.CreateContentDialog().WithTitle("Remove account")
@@ -104,11 +111,36 @@ public partial class AccountTabViewModel : RoutableScreen
.ShowAsync();
}
private void LoadCurrentUser()
public async Task GenerateToken()
{
await _windowService.CreateContentDialog().WithTitle("Create Personal Access Token")
.WithViewModel(out CreatePersonalAccessTokenViewModel vm)
.WithCloseButtonText("Cancel")
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
.ShowAsync();
List<PersonalAccessToken> personalAccessTokens = await _userManagementService.GetPersonAccessTokens(CancellationToken.None);
PersonalAccessTokens = new ObservableCollection<PersonalAccessToken>(personalAccessTokens);
}
private async Task ExecuteDeleteToken(PersonalAccessToken token)
{
bool confirm = await _windowService.ShowConfirmContentDialog("Delete Personal Access Token", "Are you sure you want to delete this token? Any services using it will stop working");
if (!confirm)
return;
await _userManagementService.DeletePersonAccessToken(token, CancellationToken.None);
PersonalAccessTokens.Remove(token);
}
private async Task LoadCurrentUser()
{
string? userId = _authenticationService.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
Name = _authenticationService.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value;
AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{userId}";
List<PersonalAccessToken> personalAccessTokens = await _userManagementService.GetPersonAccessTokens(CancellationToken.None);
PersonalAccessTokens = new ObservableCollection<PersonalAccessToken>(personalAccessTokens);
}
}