1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 13:28:33 +00:00

Submission wizard - Added Lottie animations and the first few steps

This commit is contained in:
Robert 2023-08-09 11:54:35 +02:00
parent 661242ebf9
commit 78cb95aabc
29 changed files with 534 additions and 153 deletions

View File

@ -24,6 +24,7 @@
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Skia.Lottie" Version="11.0.0" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="$(AvaloniaVersion)" />
<PackageReference Include="DryIoc.dll" Version="5.4.0" />
<PackageReference Include="DynamicData" Version="7.13.1" />

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,9 +9,9 @@
x:Class="Artemis.UI.Screens.Workshop.CurrentUser.CurrentUserView"
x:DataType="currentUser:CurrentUserViewModel">
<Panel Name="Container" IsVisible="{CompiledBinding !Loading}">
<Panel Name="Container">
<!-- Signed out -->
<Ellipse Height="{CompiledBinding Bounds.Height, ElementName=Container}" Width="{CompiledBinding Bounds.Height, ElementName=Container}" IsVisible="{CompiledBinding Name, Converter={x:Static StringConverters.IsNullOrEmpty}}">
<Ellipse Height="{CompiledBinding Bounds.Height, ElementName=Container}" Width="{CompiledBinding Bounds.Height, ElementName=Container}" IsVisible="{CompiledBinding IsAnonymous}">
<Ellipse.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Login" Command="{CompiledBinding Login}">
@ -27,7 +27,7 @@
</Ellipse>
<!-- Signed in -->
<Ellipse Height="{CompiledBinding Bounds.Height, ElementName=Container}" Width="{CompiledBinding Bounds.Height, ElementName=Container}" IsVisible="{CompiledBinding Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" Name="UserMenu">
<Ellipse Height="{CompiledBinding Bounds.Height, ElementName=Container}" Width="{CompiledBinding Bounds.Height, ElementName=Container}" IsVisible="{CompiledBinding !IsAnonymous}" Name="UserMenu">
<Ellipse.ContextFlyout>
<Flyout>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="*,*,*" MinWidth="300">
@ -39,6 +39,7 @@
<TextBlock Grid.Column="1" Grid.Row="0" Text="{CompiledBinding Name}" Margin="0 4 0 0"></TextBlock>
<TextBlock Grid.Column="1" Grid.Row="1" Text="{CompiledBinding Email}"></TextBlock>
<controls:HyperlinkButton
IsVisible="{CompiledBinding AllowLogout}"
Grid.Column="1"
Grid.Row="2"
Margin="-8 0 0 0"
@ -54,6 +55,4 @@
</Ellipse.Fill>
</Ellipse>
</Panel>
</UserControl>

View File

@ -19,8 +19,10 @@ namespace Artemis.UI.Screens.Workshop.CurrentUser;
public class CurrentUserViewModel : ActivatableViewModelBase
{
private readonly IAuthenticationService _authenticationService;
private readonly ObservableAsPropertyHelper<bool> _isAnonymous;
private readonly ILogger _logger;
private readonly IWindowService _windowService;
private bool _allowLogout = true;
private Bitmap? _avatar;
private string? _email;
private bool _loading = true;
@ -34,6 +36,8 @@ public class CurrentUserViewModel : ActivatableViewModelBase
_windowService = windowService;
Login = ReactiveCommand.CreateFromTask(ExecuteLogin);
_isAnonymous = this.WhenAnyValue(vm => vm.Loading, vm => vm.Name, (l, n) => l || n == null).ToProperty(this, vm => vm.IsAnonymous);
this.WhenActivated(d =>
{
Task.Run(AutoLogin);
@ -41,41 +45,50 @@ public class CurrentUserViewModel : ActivatableViewModelBase
});
}
public bool IsAnonymous => _isAnonymous.Value;
public bool AllowLogout
{
get => _allowLogout;
set => RaiseAndSetIfChanged(ref _allowLogout, value);
}
public bool Loading
{
get => _loading;
set => RaiseAndSetIfChanged(ref _loading, value);
private set => RaiseAndSetIfChanged(ref _loading, value);
}
public string? UserId
{
get => _userId;
set => RaiseAndSetIfChanged(ref _userId, value);
private set => RaiseAndSetIfChanged(ref _userId, value);
}
public string? Name
{
get => _name;
set => RaiseAndSetIfChanged(ref _name, value);
private set => RaiseAndSetIfChanged(ref _name, value);
}
public string? Email
{
get => _email;
set => RaiseAndSetIfChanged(ref _email, value);
private set => RaiseAndSetIfChanged(ref _email, value);
}
public Bitmap? Avatar
{
get => _avatar;
set => RaiseAndSetIfChanged(ref _avatar, value);
private set => RaiseAndSetIfChanged(ref _avatar, value);
}
public ReactiveCommand<Unit, Unit> Login { get; }
public void Logout()
{
_authenticationService.Logout();
if (AllowLogout)
_authenticationService.Logout();
}
private async Task ExecuteLogin(CancellationToken cancellationToken)

View File

@ -8,13 +8,10 @@
x:DataType="currentUser:WorkshopLoginViewModel">
<StackPanel Width="400">
<TextBlock TextWrapping="Wrap">
In order to login you must enter your Artemis credentials in a browser.
</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="0 15">
If you do not have an account yet you can create one in the browser too. Alternatively you can log in with Google or Discord.
A browser window has opened where you'll be able to create an account and/or log in.
</TextBlock>
<TextBlock>Waiting for login...</TextBlock>
<TextBlock Margin="0 15 0 0">Waiting for log in...</TextBlock>
<ProgressBar Margin="0 5 0 0" IsIndeterminate="True"></ProgressBar>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,48 @@
<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:steps="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps"
xmlns:workshop="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.EntryTypeView"
x:DataType="steps:EntryTypeViewModel">
<UserControl.Resources>
<converters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
</UserControl.Resources>
<StackPanel>
<StackPanel.Styles>
<Styles>
<Style Selector="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
</Styles>
</StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}" TextWrapping="Wrap">
Submission type
</TextBlock>
<TextBlock TextWrapping="Wrap">
Please select the type of content you want to submit to the workshop.
</TextBlock>
<RadioButton GroupName="EntryType"
Margin="0 15 0 0"
IsChecked="{CompiledBinding SelectedEntryType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static workshop:EntryType.Profile}}" >
<RadioButton.Content>
<StackPanel>
<TextBlock>Profile</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">A profile which others can install to enjoy new lighting and interactions.</TextBlock>
</StackPanel>
</RadioButton.Content>
</RadioButton>
<RadioButton GroupName="EntryType"
IsChecked="{CompiledBinding SelectedEntryType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static workshop:EntryType.Layout}}">
<RadioButton.Content>
<StackPanel>
<TextBlock>Layout</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">A layout providing users with a visual representation of a device.</TextBlock>
</StackPanel>
</RadioButton.Content>
</RadioButton>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public partial class EntryTypeView : ReactiveUserControl<EntryTypeViewModel>
{
public EntryTypeView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using Artemis.WebClient.Workshop;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class EntryTypeViewModel : SubmissionViewModel
{
private EntryType? _selectedEntryType;
/// <inheritdoc />
public EntryTypeViewModel()
{
GoBack = ReactiveCommand.Create(() => State.ChangeScreen<WelcomeStepViewModel>());
Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.SelectedEntryType).Select(e => e != null));
}
public EntryType? SelectedEntryType
{
get => _selectedEntryType;
set => RaiseAndSetIfChanged(ref _selectedEntryType, value);
}
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> Continue { get; }
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> GoBack { get; }
private void ExecuteContinue()
{
if (SelectedEntryType == null)
return;
State.EntryType = SelectedEntryType.Value;
if (State.EntryType == EntryType.Profile)
State.ChangeScreen<ProfileSelectionStepViewModel>();
}
}

View File

@ -0,0 +1,33 @@
<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:steps="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps"
mc:Ignorable="d" d:DesignWidth="970" d:DesignHeight="625"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.LoginStepView"
x:DataType="steps:LoginStepViewModel">
<StackPanel>
<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}">First things first</TextBlock>
<TextBlock>
<Run>In order to submit anything to the workshop 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="250" Height="250"></Lottie>
<TextBlock>
<Run>Click continue to (create an account) and log in.</Run>
<LineBreak/>
<Run>You'll also be able to log in with Google or Discord.</Run>
</TextBlock>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public partial class LoginStepView : ReactiveUserControl<LoginStepView>
{
public LoginStepView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Linq;
using System.Reactive;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.CurrentUser;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop.Services;
using FluentAvalonia.UI.Controls;
using IdentityModel;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class LoginStepViewModel : SubmissionViewModel
{
private readonly IAuthenticationService _authenticationService;
private readonly IWindowService _windowService;
public LoginStepViewModel(IAuthenticationService authenticationService, IWindowService windowService)
{
_authenticationService = authenticationService;
_windowService = windowService;
Continue = ReactiveCommand.CreateFromTask(ExecuteLogin);
ShowGoBack = false;
ShowHeader = false;
ContinueText = "Log In";
}
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> Continue { get; }
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> GoBack { get; } = null!;
private async Task ExecuteLogin(CancellationToken ct)
{
ContentDialogResult result = await _windowService.CreateContentDialog().WithViewModel(out WorkshopLoginViewModel _).WithTitle("Workshop login").ShowAsync();
if (result != ContentDialogResult.Primary)
return;
Claim? emailVerified = _authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.EmailVerified);
if (emailVerified?.Value == "true")
State.ChangeScreen<EntryTypeViewModel>();
else
State.ChangeScreen<ValidateEmailStepViewModel>();
}
}

View File

@ -0,0 +1,10 @@
<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:steps="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.ProfileSelectionStepView"
x:DataType="steps:ProfileSelectionStepViewModel">
Welcome to Avalonia!
</UserControl>

View File

@ -0,0 +1,18 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public partial class ProfileSelectionStepView : UserControl
{
public ProfileSelectionStepView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,13 @@
using System.Reactive;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class ProfileSelectionStepViewModel : SubmissionViewModel
{
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> Continue { get; }
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> GoBack { get; }
}

View File

@ -0,0 +1,41 @@
<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:steps="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="970" d:DesignHeight="625"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.ValidateEmailStepView"
x:DataType="steps:ValidateEmailStepViewModel">
<StackPanel >
<StackPanel.Styles>
<Styles>
<Style Selector="TextBlock">
<Setter Property="TextAlignment" Value="Center"></Setter>
</Style>
</Styles>
</StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}" TextWrapping="Wrap">Confirm email address</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run>Before you can continue, please confirm your email address.</Run> (<Run Text="{CompiledBinding Email.Value}"></Run>)
<LineBreak />
<Run>You'll find the confirmation mail in your inbox.</Run>
<LineBreak />
<LineBreak />
<TextBlock>Don't see an email? Check your spam box!</TextBlock>
</TextBlock>
<Lottie Path="/Assets/Animations/email.json" RepeatCount="-1" Width="200" Height="200"></Lottie>
<Button Margin="0 0 0 15" HorizontalAlignment="Center" Command="{CompiledBinding Refresh}">
<StackPanel Orientation="Horizontal" Spacing="5">
<avalonia:MaterialIcon Kind="Refresh" />
<TextBlock>Refresh</TextBlock>
</StackPanel>
</Button>
<TextBlock Theme="{StaticResource CaptionTextBlockStyle}">
PS: We take this step to avoid the workshop getting spammed with low quality content.
</TextBlock>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public partial class ValidateEmailStepView : ReactiveUserControl<ValidateEmailStepViewModel>
{
public ValidateEmailStepView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,78 @@
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Artemis.WebClient.Workshop.Services;
using IdentityModel;
using ReactiveUI;
using Timer = System.Timers.Timer;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class ValidateEmailStepViewModel : SubmissionViewModel
{
private readonly IAuthenticationService _authenticationService;
private ObservableAsPropertyHelper<Claim?>? _email;
public ValidateEmailStepViewModel(IAuthenticationService authenticationService)
{
_authenticationService = authenticationService;
Continue = ReactiveCommand.Create(ExecuteContinue);
Refresh = ReactiveCommand.CreateFromTask(ExecuteRefresh);
ShowGoBack = false;
ShowHeader = false;
this.WhenActivated(d =>
{
_email = authenticationService.GetClaim(JwtClaimTypes.Email).ToProperty(this, vm => vm.Email).DisposeWith(d);
Timer updateTimer = new(TimeSpan.FromSeconds(15));
updateTimer.Elapsed += (_, _) => Task.Run(Update);
updateTimer.Start();
updateTimer.DisposeWith(d);
});
}
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> Continue { get; }
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> GoBack { get; } = null!;
public ReactiveCommand<Unit, Unit> Refresh { get; }
public Claim? Email => _email?.Value;
private async Task Update()
{
try
{
// Use the refresh token to login again, updating claims
await _authenticationService.AutoLogin(true);
Claim? emailVerified = _authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.EmailVerified);
if (emailVerified?.Value == "true")
ExecuteContinue();
}
catch (Exception)
{
// ignored, meh
}
}
private void ExecuteContinue()
{
State.ChangeScreen<EntryTypeViewModel>();
}
private async Task ExecuteRefresh(CancellationToken ct)
{
await Update();
await Task.Delay(1000, ct);
}
}

View File

@ -3,50 +3,17 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:steps="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
mc:Ignorable="d" d:DesignWidth="970" d:DesignHeight="625"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.WelcomeStepView"
x:DataType="steps:WelcomeStepViewModel">
<StackPanel>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Welcome to the Workshop Submission Wizard 🧙</TextBlock>
<TextBlock>Here we'll take you, step by step, through the process of uploading your submission to the workshop.</TextBlock>
<StackPanel Margin="0 50 0 0" >
<TextBlock Theme="{StaticResource TitleTextBlockStyle}" TextAlignment="Center" TextWrapping="Wrap">
Welcome to the Workshop Submission Wizard 🧙
</TextBlock>
<TextBlock TextAlignment="Center" TextWrapping="Wrap">
Here we'll take you, step by step, through the process of uploading your submission to the workshop.
</TextBlock>
<StackPanel IsVisible="{CompiledBinding !IsLoggedIn}" Margin="0 35 0 0">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}" TextWrapping="Wrap">First things first</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run>In order to submit anything to the workshop you must be logged in.</Run>
<LineBreak />
<LineBreak />
<Run>Creating an account is free and we'll not bother you with a newsletter or crap like that. You can also log in with Google or Discord.</Run>
</TextBlock>
<Button Margin="0 15 0 0" Command="{CompiledBinding Login}">
<StackPanel Orientation="Horizontal" Spacing="5">
<avalonia:MaterialIcon Kind="ExternalLink" />
<TextBlock>Create account or log in</TextBlock>
</StackPanel>
</Button>
</StackPanel>
<StackPanel IsVisible="{CompiledBinding ShowMissingVerification}" Margin="0 35 0 0">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}" TextWrapping="Wrap">Confirm email address</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run>Before you can continue, please confirm your email address.</Run> (<Run Text="{CompiledBinding Email.Value}"></Run>)
<LineBreak />
<Run>You'll find the confirmation mail in your inbox.</Run>
<LineBreak />
<LineBreak />
<TextBlock>Don't see an email? Check your spam box!</TextBlock>
</TextBlock>
<Button Margin="0 15 0 5" Command="{CompiledBinding Refresh}">
<StackPanel Orientation="Horizontal" Spacing="5">
<avalonia:MaterialIcon Kind="Refresh" />
<TextBlock>Refresh</TextBlock>
</StackPanel>
</Button>
<TextBlock Theme="{StaticResource CaptionTextBlockStyle}">PS: We take this step to avoid the workshop getting flooded with bogus content.</TextBlock>
</StackPanel>
<Lottie Path="/Assets/Animations/workshop-wizard.json" RepeatCount="1" Width="300" Height="400"></Lottie>
</StackPanel>
</UserControl>

View File

@ -1,50 +1,24 @@
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.CurrentUser;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop.Services;
using IdentityModel;
using ReactiveUI;
using Timer = System.Timers.Timer;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class WelcomeStepViewModel : SubmissionViewModel
{
private readonly IAuthenticationService _authenticationService;
private readonly ObservableAsPropertyHelper<bool> _showMissingVerification;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<Claim?>? _email;
private ObservableAsPropertyHelper<Claim?>? _emailVerified;
private ObservableAsPropertyHelper<bool>? _isLoggedIn;
public WelcomeStepViewModel(IAuthenticationService authenticationService, IWindowService windowService)
public WelcomeStepViewModel(IAuthenticationService authenticationService)
{
_authenticationService = authenticationService;
_windowService = windowService;
_showMissingVerification = this.WhenAnyValue(vm => vm.IsLoggedIn, vm => vm.EmailVerified, (l, v) => l && (v == null || v.Value == "false")).ToProperty(this, vm => vm.ShowMissingVerification);
Continue = ReactiveCommand.CreateFromTask(ExecuteContinue);
ShowHeader = false;
ShowGoBack = false;
Continue = ReactiveCommand.Create(ExecuteContinue);
Login = ReactiveCommand.CreateFromTask(ExecuteLogin);
Refresh = ReactiveCommand.CreateFromTask(ExecuteRefresh);
this.WhenActivated(d =>
{
_isLoggedIn = authenticationService.IsLoggedIn.ToProperty(this, vm => vm.IsLoggedIn).DisposeWith(d);
_emailVerified = authenticationService.GetClaim(JwtClaimTypes.EmailVerified).ToProperty(this, vm => vm.EmailVerified).DisposeWith(d);
_email = authenticationService.GetClaim(JwtClaimTypes.Email).ToProperty(this, vm => vm.Email).DisposeWith(d);
Timer updateTimer = new(TimeSpan.FromSeconds(15));
updateTimer.Elapsed += (_, _) => Task.Run(Update);
updateTimer.Start();
updateTimer.DisposeWith(d);
});
}
/// <inheritdoc />
@ -53,43 +27,20 @@ public class WelcomeStepViewModel : SubmissionViewModel
/// <inheritdoc />
public override ReactiveCommand<Unit, Unit> GoBack { get; } = null!;
public ReactiveCommand<Unit, Unit> Login { get; }
public ReactiveCommand<Unit, Unit> Refresh { get; }
public bool ShowMissingVerification => _showMissingVerification.Value;
public bool IsLoggedIn => _isLoggedIn?.Value ?? false;
public Claim? EmailVerified => _emailVerified?.Value;
public Claim? Email => _email?.Value;
private async Task Update()
private async Task ExecuteContinue()
{
if (EmailVerified?.Value == "true")
return;
bool loggedIn = await _authenticationService.AutoLogin(true);
try
if (!loggedIn)
{
// Use the refresh token to login again, updating claims
await _authenticationService.AutoLogin(true);
State.ChangeScreen<LoginStepViewModel>();
}
catch (Exception)
else
{
// ignored, meh
if (_authenticationService.Claims.Any(c => c.Type == JwtClaimTypes.EmailVerified && c.Value == "true"))
State.ChangeScreen<EntryTypeViewModel>();
else
State.ChangeScreen<ValidateEmailStepViewModel>();
}
}
private void ExecuteContinue()
{
throw new NotImplementedException();
}
private async Task ExecuteLogin(CancellationToken ct)
{
await _windowService.CreateContentDialog().WithViewModel(out WorkshopLoginViewModel _).WithTitle("Workshop login").ShowAsync();
}
private async Task ExecuteRefresh(CancellationToken ct)
{
await Update();
await Task.Delay(1000, ct);
}
}

View File

@ -0,0 +1,43 @@
using System.Reactive;
using Artemis.UI.Shared;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard;
public abstract class SubmissionViewModel : ActivatableViewModelBase
{
private string _continueText = "Continue";
private bool _showFinish;
private bool _showGoBack = true;
private bool _showHeader = true;
public SubmissionWizardViewModel WizardViewModel { get; set; } = null!;
public SubmissionWizardState State { get; set; } = null!;
public abstract ReactiveCommand<Unit, Unit> Continue { get; }
public abstract ReactiveCommand<Unit, Unit> GoBack { get; }
public bool ShowHeader
{
get => _showHeader;
set => RaiseAndSetIfChanged(ref _showHeader, value);
}
public bool ShowGoBack
{
get => _showGoBack;
set => RaiseAndSetIfChanged(ref _showGoBack, value);
}
public bool ShowFinish
{
get => _showFinish;
set => RaiseAndSetIfChanged(ref _showFinish, value);
}
public string ContinueText
{
get => _continueText;
set => RaiseAndSetIfChanged(ref _continueText, value);
}
}

View File

@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.IO;
using Artemis.WebClient.Workshop;
using DryIoc;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard;
public class SubmissionWizardState
{
private readonly SubmissionWizardViewModel _wizardViewModel;
private readonly IContainer _container;
public SubmissionWizardState(SubmissionWizardViewModel wizardViewModel, IContainer container)
{
_wizardViewModel = wizardViewModel;
_container = container;
}
public EntryType EntryType { get; set; }
public string Name { get; set; } = string.Empty;
public Stream? Icon { get; set; }
public string Summary { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public List<int> Categories { get; set; } = new();
public List<int> Tags { get; set; } = new();
public List<Stream> Images { get; set; } = new();
public void ChangeScreen<TSubmissionViewModel>() where TSubmissionViewModel : SubmissionViewModel
{
_wizardViewModel.Screen = _container.Resolve<TSubmissionViewModel>();
}
}

View File

@ -17,7 +17,8 @@
<Grid Margin="15" RowDefinitions="Auto,*,Auto">
<Grid RowDefinitions="*,*" ColumnDefinitions="Auto,*,Auto" Margin="0 0 0 15">
<ContentControl Grid.Column="0" Grid.RowSpan="2" Width="65" Height="65" VerticalAlignment="Center" Margin="0 0 20 0" Content="{CompiledBinding CurrentUserViewModel}"/>
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="36" VerticalAlignment="Bottom" Text="{CompiledBinding CurrentUserViewModel.Name, FallbackValue=Not logged in}"/>
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="36" VerticalAlignment="Bottom" Text="{CompiledBinding CurrentUserViewModel.Name}" IsVisible="{CompiledBinding !CurrentUserViewModel.IsAnonymous}"/>
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="36" VerticalAlignment="Bottom" Text="Anon 🕵️" IsVisible="{CompiledBinding CurrentUserViewModel.IsAnonymous}"/>
<StackPanel Grid.Row="0" Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Bottom" Orientation="Horizontal">
<controls:HyperlinkButton Classes="icon-button" ToolTip.Tip="View Wiki" NavigateUri="https://wiki.artemis-rgb.com?mtm_campaign=artemis&amp;mtm_kwd=workshop-wizard">
@ -44,9 +45,7 @@
<Button Command="{CompiledBinding Screen.GoBack}" IsVisible="{CompiledBinding Screen.ShowGoBack}">
Back
</Button>
<Button Command="{CompiledBinding Screen.Continue}" IsVisible="{CompiledBinding !Screen.ShowFinish}" Width="80">
Continue
</Button>
<Button Command="{CompiledBinding Screen.Continue}" IsVisible="{CompiledBinding !Screen.ShowFinish}" Width="80" Content="{CompiledBinding Screen.ContinueText}"/>
<Button Command="{CompiledBinding Screen.Continue}" IsVisible="{CompiledBinding Screen.ShowFinish}" Width="80">
Finish
</Button>

View File

@ -1,19 +1,24 @@
using System.Reactive;
using Artemis.UI.Screens.Workshop.CurrentUser;
using Artemis.UI.Screens.Workshop.CurrentUser;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
using Artemis.UI.Shared;
using ReactiveUI;
using DryIoc;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard;
public class SubmissionWizardViewModel : DialogViewModelBase<bool>
{
private readonly SubmissionWizardState _state;
private SubmissionViewModel _screen;
public SubmissionWizardViewModel(CurrentUserViewModel currentUserViewModel, WelcomeStepViewModel welcomeStepViewModel)
public SubmissionWizardViewModel(IContainer container, CurrentUserViewModel currentUserViewModel, WelcomeStepViewModel welcomeStepViewModel)
{
_state = new SubmissionWizardState(this, container);
_screen = welcomeStepViewModel;
_screen.WizardViewModel = this;
_screen.State = _state;
CurrentUserViewModel = currentUserViewModel;
CurrentUserViewModel.AllowLogout = false;
}
public CurrentUserViewModel CurrentUserViewModel { get; }
@ -21,27 +26,11 @@ public class SubmissionWizardViewModel : DialogViewModelBase<bool>
public SubmissionViewModel Screen
{
get => _screen;
set => RaiseAndSetIfChanged(ref _screen, value);
}
}
public abstract class SubmissionViewModel : ActivatableViewModelBase
{
private bool _showFinish;
private bool _showGoBack = true;
public abstract ReactiveCommand<Unit, Unit> Continue { get; }
public abstract ReactiveCommand<Unit, Unit> GoBack { get; }
public bool ShowGoBack
{
get => _showGoBack;
set => RaiseAndSetIfChanged(ref _showGoBack, value);
}
public bool ShowFinish
{
get => _showFinish;
set => RaiseAndSetIfChanged(ref _showFinish, value);
set
{
value.WizardViewModel = this;
value.State = _state;
RaiseAndSetIfChanged(ref _screen, value);
}
}
}

View File

@ -79,14 +79,14 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
if (response.IsError)
{
if (response.Error == OidcConstants.TokenErrors.ExpiredToken)
if (response.Error is OidcConstants.TokenErrors.ExpiredToken or OidcConstants.TokenErrors.InvalidGrant)
return false;
throw new ArtemisWebClientException("Failed to request refresh token: " + response.Error);
}
SetCurrentUser(response);
return false;
return true;
}
private static byte[] HashSha256(string inputString)

View File

@ -2,6 +2,6 @@ namespace Artemis.WebClient.Workshop;
public static class WorkshopConstants
{
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
public const string AUTHORITY_URL = "https://localhost:5001";
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
}