mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-12 13:28:33 +00:00
Workshop - Added profile uploading
Submission wizard - Final upload steps
This commit is contained in:
parent
e52faf1c9f
commit
ad4da3032d
1
src/Artemis.UI/Assets/Animations/success.json
Normal file
1
src/Artemis.UI/Assets/Animations/success.json
Normal file
File diff suppressed because one or more lines are too long
@ -14,7 +14,11 @@ public class BitmapExtensions
|
||||
|
||||
public static Bitmap LoadAndResize(Stream stream, int size)
|
||||
{
|
||||
using SKBitmap source = SKBitmap.Decode(stream);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
using MemoryStream copy = new();
|
||||
stream.CopyTo(copy);
|
||||
copy.Seek(0, SeekOrigin.Begin);
|
||||
using SKBitmap source = SKBitmap.Decode(copy);
|
||||
return Resize(source, size);
|
||||
}
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
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">
|
||||
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.EntryTypeStepView"
|
||||
x:DataType="steps:EntryTypeStepViewModel">
|
||||
<UserControl.Resources>
|
||||
<converters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
|
||||
</UserControl.Resources>
|
||||
@ -2,9 +2,9 @@ using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||
|
||||
public partial class EntryTypeView : ReactiveUserControl<EntryTypeViewModel>
|
||||
public partial class EntryTypeStepView : ReactiveUserControl<EntryTypeStepViewModel>
|
||||
{
|
||||
public EntryTypeView()
|
||||
public EntryTypeStepView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
@ -6,12 +6,12 @@ using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||
|
||||
public class EntryTypeViewModel : SubmissionViewModel
|
||||
public class EntryTypeStepViewModel : SubmissionViewModel
|
||||
{
|
||||
private EntryType? _selectedEntryType;
|
||||
|
||||
/// <inheritdoc />
|
||||
public EntryTypeViewModel()
|
||||
public EntryTypeStepViewModel()
|
||||
{
|
||||
GoBack = ReactiveCommand.Create(() => State.ChangeScreen<WelcomeStepViewModel>());
|
||||
Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.SelectedEntryType).Select(e => e != null));
|
||||
@ -43,7 +43,7 @@ public class LoginStepViewModel : SubmissionViewModel
|
||||
|
||||
Claim? emailVerified = _authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.EmailVerified);
|
||||
if (emailVerified?.Value == "true")
|
||||
State.ChangeScreen<EntryTypeViewModel>();
|
||||
State.ChangeScreen<EntryTypeStepViewModel>();
|
||||
else
|
||||
State.ChangeScreen<ValidateEmailStepViewModel>();
|
||||
}
|
||||
|
||||
@ -62,6 +62,6 @@ public class ProfileAdaptionHintsStepViewModel : SubmissionViewModel
|
||||
if (Layers.Any(l => l.AdaptionHintCount == 0))
|
||||
return;
|
||||
|
||||
State.ChangeScreen<EntrySpecificationsStepViewModel>();
|
||||
State.ChangeScreen<SpecificationsStepViewModel>();
|
||||
}
|
||||
}
|
||||
@ -43,7 +43,7 @@ public class ProfileSelectionStepViewModel : SubmissionViewModel
|
||||
|
||||
ProfilePreview = profilePreviewViewModel;
|
||||
|
||||
GoBack = ReactiveCommand.Create(() => State.ChangeScreen<EntryTypeViewModel>());
|
||||
GoBack = ReactiveCommand.Create(() => State.ChangeScreen<EntryTypeStepViewModel>());
|
||||
Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.SelectedProfile).Select(p => p != null));
|
||||
|
||||
this.WhenAnyValue(vm => vm.SelectedProfile).Subscribe(p => Update(p));
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
xmlns:categories="clr-namespace:Artemis.UI.Screens.Workshop.Categories"
|
||||
xmlns:tagsInput="clr-namespace:Artemis.UI.Shared.TagsInput;assembly=Artemis.UI.Shared"
|
||||
mc:Ignorable="d" d:DesignWidth="970" d:DesignHeight="625"
|
||||
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.EntrySpecificationsStepView"
|
||||
x:DataType="steps:EntrySpecificationsStepViewModel">
|
||||
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.SpecificationsStepView"
|
||||
x:DataType="steps:SpecificationsStepViewModel">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<StackPanel Grid.Row="0">
|
||||
<StackPanel.Styles>
|
||||
@ -5,9 +5,9 @@ using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||
|
||||
public partial class EntrySpecificationsStepView : ReactiveUserControl<EntrySpecificationsStepViewModel>
|
||||
public partial class SpecificationsStepView : ReactiveUserControl<SpecificationsStepViewModel>
|
||||
{
|
||||
public EntrySpecificationsStepView()
|
||||
public SpecificationsStepView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
@ -13,7 +12,6 @@ using Artemis.UI.Screens.Workshop.Categories;
|
||||
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.Extensions;
|
||||
using Avalonia.Threading;
|
||||
using DynamicData;
|
||||
using DynamicData.Aggregation;
|
||||
@ -26,18 +24,17 @@ using Bitmap = Avalonia.Media.Imaging.Bitmap;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||
|
||||
public class EntrySpecificationsStepViewModel : SubmissionViewModel
|
||||
public class SpecificationsStepViewModel : SubmissionViewModel
|
||||
{
|
||||
private readonly IWindowService _windowService;
|
||||
private ObservableAsPropertyHelper<bool>? _categoriesValid;
|
||||
private ObservableAsPropertyHelper<bool>? _iconValid;
|
||||
private string _description = string.Empty;
|
||||
private Bitmap? _iconBitmap;
|
||||
private bool _isDirty;
|
||||
private string _name = string.Empty;
|
||||
private string _summary = string.Empty;
|
||||
private Bitmap? _iconBitmap;
|
||||
|
||||
public EntrySpecificationsStepViewModel(IWorkshopClient workshopClient, IWindowService windowService)
|
||||
public SpecificationsStepViewModel(IWorkshopClient workshopClient, IWindowService windowService)
|
||||
{
|
||||
_windowService = windowService;
|
||||
GoBack = ReactiveCommand.Create(ExecuteGoBack);
|
||||
@ -45,47 +42,18 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
|
||||
SelectIcon = ReactiveCommand.CreateFromTask(ExecuteSelectIcon);
|
||||
ClearIcon = ReactiveCommand.Create(ExecuteClearIcon);
|
||||
|
||||
workshopClient.GetCategories
|
||||
.Watch(ExecutionStrategy.CacheFirst)
|
||||
.SelectOperationResult(c => c.Categories)
|
||||
.ToObservableChangeSet(c => c.Id)
|
||||
.Transform(c => new CategoryViewModel(c))
|
||||
.Bind(out ReadOnlyObservableCollection<CategoryViewModel> categoryViewModels)
|
||||
.Subscribe();
|
||||
Categories = categoryViewModels;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
DisplayName = $"{State.EntryType} Information";
|
||||
|
||||
// Basic fields
|
||||
Name = State.Name;
|
||||
Summary = State.Summary;
|
||||
Description = State.Description;
|
||||
|
||||
// Categories
|
||||
foreach (CategoryViewModel categoryViewModel in Categories)
|
||||
categoryViewModel.IsSelected = State.Categories.Contains(categoryViewModel.Id);
|
||||
// Load categories
|
||||
Observable.FromAsync(workshopClient.GetCategories.ExecuteAsync).Subscribe(PopulateCategories).DisposeWith(d);
|
||||
|
||||
// Tags
|
||||
Tags.Clear();
|
||||
Tags.AddRange(State.Tags);
|
||||
// Apply the state
|
||||
ApplyFromState();
|
||||
|
||||
// Icon
|
||||
if (State.Icon != null)
|
||||
{
|
||||
State.Icon.Seek(0, SeekOrigin.Begin);
|
||||
IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128);
|
||||
}
|
||||
|
||||
IsDirty = false;
|
||||
this.ClearValidationRules();
|
||||
|
||||
Disposable.Create(() =>
|
||||
{
|
||||
IconBitmap?.Dispose();
|
||||
IconBitmap = null;
|
||||
}).DisposeWith(d);
|
||||
Disposable.Create(ExecuteClearIcon).DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
@ -94,7 +62,7 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
|
||||
public ReactiveCommand<Unit, Unit> SelectIcon { get; }
|
||||
public ReactiveCommand<Unit, Unit> ClearIcon { get; }
|
||||
|
||||
public ReadOnlyObservableCollection<CategoryViewModel> Categories { get; }
|
||||
public ObservableCollection<CategoryViewModel> Categories { get; } = new();
|
||||
public ObservableCollection<string> Tags { get; } = new();
|
||||
public bool CategoriesValid => _categoriesValid?.Value ?? true;
|
||||
public bool IconValid => _iconValid?.Value ?? true;
|
||||
@ -117,12 +85,6 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
|
||||
set => RaiseAndSetIfChanged(ref _description, value);
|
||||
}
|
||||
|
||||
public bool IsDirty
|
||||
{
|
||||
get => _isDirty;
|
||||
set => RaiseAndSetIfChanged(ref _isDirty, value);
|
||||
}
|
||||
|
||||
public Bitmap? IconBitmap
|
||||
{
|
||||
get => _iconBitmap;
|
||||
@ -131,6 +93,9 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
|
||||
|
||||
private void ExecuteGoBack()
|
||||
{
|
||||
// Apply what's there so far
|
||||
ApplyToState();
|
||||
|
||||
switch (State.EntryType)
|
||||
{
|
||||
case EntryType.Layout:
|
||||
@ -147,35 +112,20 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
|
||||
|
||||
private void ExecuteContinue()
|
||||
{
|
||||
if (!IsDirty)
|
||||
if (!ValidationContext.Validations.Any())
|
||||
{
|
||||
SetupDataValidation();
|
||||
IsDirty = true;
|
||||
|
||||
// The ValidationContext seems to update asynchronously, so stop and schedule a retry
|
||||
SetupDataValidation();
|
||||
Dispatcher.UIThread.Post(ExecuteContinue);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyToState();
|
||||
|
||||
if (!ValidationContext.GetIsValid())
|
||||
return;
|
||||
|
||||
State.Name = Name;
|
||||
State.Summary = Summary;
|
||||
State.Description = Description;
|
||||
State.Categories = Categories.Where(c => c.IsSelected).Select(c => c.Id).ToList();
|
||||
State.Tags = new List<string>(Tags);
|
||||
|
||||
State.Icon?.Dispose();
|
||||
if (IconBitmap != null)
|
||||
{
|
||||
State.Icon = new MemoryStream();
|
||||
IconBitmap.Save(State.Icon);
|
||||
}
|
||||
else
|
||||
{
|
||||
State.Icon = null;
|
||||
}
|
||||
|
||||
State.ChangeScreen<SubmitStepViewModel>();
|
||||
}
|
||||
|
||||
private async Task ExecuteSelectIcon()
|
||||
@ -197,6 +147,13 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
|
||||
IconBitmap = null;
|
||||
}
|
||||
|
||||
private void PopulateCategories(IOperationResult<IGetCategoriesResult> result)
|
||||
{
|
||||
Categories.Clear();
|
||||
if (result.Data != null)
|
||||
Categories.AddRange(result.Data.Categories.Select(c => new CategoryViewModel(c) {IsSelected = State.Categories.Contains(c.Id)}));
|
||||
}
|
||||
|
||||
private void SetupDataValidation()
|
||||
{
|
||||
// Hopefully this can be avoided in the future
|
||||
@ -204,17 +161,56 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
|
||||
this.ValidationRule(vm => vm.Name, s => !string.IsNullOrWhiteSpace(s), "Name is required");
|
||||
this.ValidationRule(vm => vm.Summary, s => !string.IsNullOrWhiteSpace(s), "Summary is required");
|
||||
this.ValidationRule(vm => vm.Description, s => !string.IsNullOrWhiteSpace(s), "Description is required");
|
||||
|
||||
|
||||
// These don't use inputs that support validation messages, do so manually
|
||||
ValidationHelper iconRule = this.ValidationRule(vm => vm.IconBitmap, s => s != null, "Icon required");
|
||||
ValidationHelper categoriesRule = this.ValidationRule(vm => vm.Categories, Categories.ToObservableChangeSet()
|
||||
.AutoRefresh(c => c.IsSelected)
|
||||
.Filter(c => c.IsSelected)
|
||||
.IsEmpty()
|
||||
.CombineLatest(this.WhenAnyValue(vm => vm.IsDirty), (empty, dirty) => !dirty || !empty),
|
||||
ValidationHelper categoriesRule = this.ValidationRule(vm => vm.Categories, Categories.ToObservableChangeSet().AutoRefresh(c => c.IsSelected).Filter(c => c.IsSelected).IsNotEmpty(),
|
||||
"At least one category must be selected"
|
||||
);
|
||||
_iconValid = iconRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.IconValid);
|
||||
_categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid);
|
||||
}
|
||||
|
||||
private void ApplyFromState()
|
||||
{
|
||||
// Basic fields
|
||||
Name = State.Name;
|
||||
Summary = State.Summary;
|
||||
Description = State.Description;
|
||||
|
||||
// Tags
|
||||
Tags.Clear();
|
||||
Tags.AddRange(State.Tags);
|
||||
|
||||
// Icon
|
||||
if (State.Icon != null)
|
||||
{
|
||||
State.Icon.Seek(0, SeekOrigin.Begin);
|
||||
IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyToState()
|
||||
{
|
||||
// Basic fields
|
||||
State.Name = Name;
|
||||
State.Summary = Summary;
|
||||
State.Description = Description;
|
||||
|
||||
// Categories and tasks
|
||||
State.Categories = Categories.Where(c => c.IsSelected).Select(c => c.Id).ToList();
|
||||
State.Tags = new List<string>(Tags);
|
||||
|
||||
// Icon
|
||||
State.Icon?.Dispose();
|
||||
if (IconBitmap != null)
|
||||
{
|
||||
State.Icon = new MemoryStream();
|
||||
IconBitmap.Save(State.Icon);
|
||||
}
|
||||
else
|
||||
{
|
||||
State.Icon = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
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.SubmitStepView"
|
||||
x:DataType="steps:SubmitStepViewModel">
|
||||
<StackPanel>
|
||||
<StackPanel.Styles>
|
||||
<Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"></Setter>
|
||||
</Style>
|
||||
</Styles>
|
||||
</StackPanel.Styles>
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">
|
||||
Ready to submit?
|
||||
</TextBlock>
|
||||
<TextBlock>
|
||||
We have all the information we need, are you ready to submit the following to the workshop?
|
||||
</TextBlock>
|
||||
|
||||
<Border Classes="card" Margin="0 15 0 0" >
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<!-- Icon -->
|
||||
<Border Grid.Column="0"
|
||||
CornerRadius="12"
|
||||
Background="{StaticResource ControlStrokeColorOnAccentDefault}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 10 0"
|
||||
Width="80"
|
||||
Height="80"
|
||||
ClipToBounds="True">
|
||||
<Image Source="{CompiledBinding IconBitmap}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"></Image>
|
||||
</Border>
|
||||
|
||||
<!-- Body -->
|
||||
<Grid Grid.Column="1" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Row="0" Margin="0 0 0 5" TextTrimming="CharacterEllipsis" >
|
||||
<Run Classes="h5" Text="{CompiledBinding State.Name, FallbackValue=Title}" />
|
||||
<Run Classes="subtitle">by</Run>
|
||||
<Run Classes="subtitle" Text="{CompiledBinding CurrentUser, FallbackValue=Author}" />
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="1"
|
||||
Classes="subtitle"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="{CompiledBinding State.Summary, FallbackValue=Summary}"></TextBlock>
|
||||
|
||||
<ItemsControl Grid.Row="2" ItemsSource="{CompiledBinding Categories}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8"></StackPanel>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia:MaterialIcon>
|
||||
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,19 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||
|
||||
public partial class SubmitStepView : ReactiveUserControl<SubmitStepViewModel>
|
||||
{
|
||||
public SubmitStepView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.UI.Screens.Workshop.Categories;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.Services;
|
||||
using IdentityModel;
|
||||
using ReactiveUI;
|
||||
using StrawberryShake;
|
||||
using System;
|
||||
using System.IO;
|
||||
using Avalonia.Media.Imaging;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||
|
||||
public class SubmitStepViewModel : SubmissionViewModel
|
||||
{
|
||||
private ReadOnlyObservableCollection<CategoryViewModel>? _categories;
|
||||
private Bitmap? _iconBitmap;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SubmitStepViewModel(IAuthenticationService authenticationService, IWorkshopClient workshopClient)
|
||||
{
|
||||
CurrentUser = authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Name)?.Value;
|
||||
GoBack = ReactiveCommand.Create(() => State.ChangeScreen<SpecificationsStepViewModel>());
|
||||
Continue = ReactiveCommand.Create(() => State.ChangeScreen<UploadStepViewModel>());
|
||||
|
||||
ContinueText = "Submit";
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
if (State.Icon != null)
|
||||
{
|
||||
State.Icon.Seek(0, SeekOrigin.Begin);
|
||||
IconBitmap = new Bitmap(State.Icon);
|
||||
IconBitmap.DisposeWith(d);
|
||||
}
|
||||
Observable.FromAsync(workshopClient.GetCategories.ExecuteAsync).Subscribe(PopulateCategories).DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public Bitmap? IconBitmap
|
||||
{
|
||||
get => _iconBitmap;
|
||||
set => RaiseAndSetIfChanged(ref _iconBitmap, value);
|
||||
}
|
||||
|
||||
public string? CurrentUser { get; }
|
||||
|
||||
public ReadOnlyObservableCollection<CategoryViewModel>? Categories
|
||||
{
|
||||
get => _categories;
|
||||
set => RaiseAndSetIfChanged(ref _categories, value);
|
||||
}
|
||||
|
||||
public override ReactiveCommand<Unit, Unit> Continue { get; }
|
||||
|
||||
public override ReactiveCommand<Unit, Unit> GoBack { get; }
|
||||
|
||||
private void PopulateCategories(IOperationResult<IGetCategoriesResult> result)
|
||||
{
|
||||
if (result.Data == null)
|
||||
Categories = null;
|
||||
else
|
||||
{
|
||||
Categories = new ReadOnlyObservableCollection<CategoryViewModel>(
|
||||
new ObservableCollection<CategoryViewModel>(result.Data.Categories.Where(c => State.Categories.Contains(c.Id)).Select(c => new CategoryViewModel(c)))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
<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="900"
|
||||
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.UploadStepView"
|
||||
x:DataType="steps:UploadStepViewModel">
|
||||
<StackPanel 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}" TextAlignment="Center" TextWrapping="Wrap">
|
||||
Uploading your submission...
|
||||
</TextBlock>
|
||||
<TextBlock TextAlignment="Center" TextWrapping="Wrap">
|
||||
Wooo, the final step, that was pretty easy, right!?
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel IsVisible="{CompiledBinding Finished}" Margin="0 100 0 0">
|
||||
<Lottie Path="/Assets/Animations/success.json" RepeatCount="1" Width="300" Height="300"></Lottie>
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">All done! Hit finish to view your submission.</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,19 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||
|
||||
public partial class UploadStepView : ReactiveUserControl<UploadStepViewModel>
|
||||
{
|
||||
public UploadStepView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.UploadHandlers;
|
||||
using ReactiveUI;
|
||||
using StrawberryShake;
|
||||
using System.Reactive.Disposables;
|
||||
using Artemis.Core;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
|
||||
|
||||
public class UploadStepViewModel : SubmissionViewModel
|
||||
{
|
||||
private readonly IWorkshopClient _workshopClient;
|
||||
private readonly EntryUploadHandlerFactory _entryUploadHandlerFactory;
|
||||
private readonly IWindowService _windowService;
|
||||
private bool _finished;
|
||||
|
||||
/// <inheritdoc />
|
||||
public UploadStepViewModel(IWorkshopClient workshopClient, EntryUploadHandlerFactory entryUploadHandlerFactory, IWindowService windowService)
|
||||
{
|
||||
_workshopClient = workshopClient;
|
||||
_entryUploadHandlerFactory = entryUploadHandlerFactory;
|
||||
_windowService = windowService;
|
||||
|
||||
ShowGoBack = false;
|
||||
ContinueText = "Finish";
|
||||
Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.Finished));
|
||||
|
||||
this.WhenActivated(d => Observable.FromAsync(ExecuteUpload).Subscribe().DisposeWith(d));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ReactiveCommand<Unit, Unit> Continue { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ReactiveCommand<Unit, Unit> GoBack { get; } = null!;
|
||||
|
||||
public bool Finished
|
||||
{
|
||||
get => _finished;
|
||||
set => RaiseAndSetIfChanged(ref _finished, value);
|
||||
}
|
||||
|
||||
public async Task ExecuteUpload(CancellationToken cancellationToken)
|
||||
{
|
||||
IOperationResult<IAddEntryResult> result = await _workshopClient.AddEntry.ExecuteAsync(new CreateEntryInput
|
||||
{
|
||||
EntryType = State.EntryType,
|
||||
Name = State.Name,
|
||||
Summary = State.Summary,
|
||||
Description = State.Description,
|
||||
Categories = State.Categories,
|
||||
Tags = State.Tags
|
||||
}, cancellationToken);
|
||||
|
||||
Guid? entryId = result.Data?.AddEntry?.Id;
|
||||
if (result.IsErrorResult() || entryId == null)
|
||||
{
|
||||
await _windowService.ShowConfirmContentDialog("Failed to create workshop entry", result.Errors.ToString() ?? "Not even an error message", "Close", null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
// Create the workshop entry
|
||||
try
|
||||
{
|
||||
IEntryUploadHandler uploadHandler = _entryUploadHandlerFactory.CreateHandler(State.EntryType);
|
||||
EntryUploadResult uploadResult = await uploadHandler.CreateReleaseAsync(entryId.Value, State.EntrySource!, cancellationToken);
|
||||
if (!uploadResult.IsSuccess)
|
||||
{
|
||||
string? message = uploadResult.Message;
|
||||
if (message != null)
|
||||
message += "\r\n\r\n";
|
||||
else
|
||||
message = "";
|
||||
message += "Your submission has still been saved, you may try to upload a new release";
|
||||
await _windowService.ShowConfirmContentDialog("Failed to upload workshop entry", message, "Close", null);
|
||||
return;
|
||||
}
|
||||
|
||||
Finished = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Something went wrong when creating a release :c
|
||||
// We'll keep the workshop entry so that the user can make changes and try again
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteContinue()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@ -73,7 +73,7 @@ public class ValidateEmailStepViewModel : SubmissionViewModel
|
||||
|
||||
private void ExecuteContinue()
|
||||
{
|
||||
State.ChangeScreen<EntryTypeViewModel>();
|
||||
State.ChangeScreen<EntryTypeStepViewModel>();
|
||||
}
|
||||
|
||||
private async Task ExecuteRefresh(CancellationToken ct)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
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"
|
||||
mc:Ignorable="d" d:DesignWidth="970" d:DesignHeight="900"
|
||||
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.WelcomeStepView"
|
||||
x:DataType="steps:WelcomeStepViewModel">
|
||||
<StackPanel Margin="0 50 0 0" >
|
||||
@ -14,6 +14,6 @@
|
||||
Here we'll take you, step by step, through the process of uploading your submission to the workshop.
|
||||
</TextBlock>
|
||||
|
||||
<Lottie Path="/Assets/Animations/workshop-wizard.json" RepeatCount="1" Width="300" Height="400"></Lottie>
|
||||
<Lottie Path="/Assets/Animations/workshop-wizard.json" RepeatCount="1" Width="500" Height="700"></Lottie>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -38,7 +38,7 @@ public class WelcomeStepViewModel : SubmissionViewModel
|
||||
else
|
||||
{
|
||||
if (_authenticationService.Claims.Any(c => c.Type == JwtClaimTypes.EmailVerified && c.Value == "true"))
|
||||
State.ChangeScreen<EntryTypeViewModel>();
|
||||
State.ChangeScreen<EntryTypeStepViewModel>();
|
||||
else
|
||||
State.ChangeScreen<ValidateEmailStepViewModel>();
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
Icon="/Assets/Images/Logo/application.ico"
|
||||
Title="Artemis | Workshop submission wizard"
|
||||
Width="1000"
|
||||
Height="735"
|
||||
Height="950"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Grid Margin="15" RowDefinitions="Auto,*,Auto">
|
||||
<Grid RowDefinitions="*,*" ColumnDefinitions="Auto,*,Auto" Margin="0 0 0 15">
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
using System.Reflection;
|
||||
using Artemis.WebClient.Workshop.Extensions;
|
||||
using Artemis.WebClient.Workshop.Repositories;
|
||||
using Artemis.WebClient.Workshop.Services;
|
||||
using Artemis.WebClient.Workshop.State;
|
||||
using Artemis.WebClient.Workshop.UploadHandlers;
|
||||
using DryIoc;
|
||||
using DryIoc.Microsoft.DependencyInjection;
|
||||
using IdentityModel.Client;
|
||||
@ -18,11 +22,17 @@ public static class ContainerExtensions
|
||||
/// <param name="container">The builder building the current container</param>
|
||||
public static void RegisterWorkshopClient(this IContainer container)
|
||||
{
|
||||
Assembly[] workshopAssembly = {typeof(WorkshopConstants).Assembly};
|
||||
|
||||
ServiceCollection serviceCollection = new();
|
||||
serviceCollection
|
||||
.AddHttpClient()
|
||||
.AddWorkshopClient()
|
||||
.AddHttpMessageHandler<WorkshopClientStoreAccessor, AuthenticationDelegatingHandler>()
|
||||
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL + "/graphql"));
|
||||
serviceCollection.AddHttpClient(WorkshopConstants.WORKSHOP_CLIENT_NAME)
|
||||
.AddHttpMessageHandler<AuthenticationDelegatingHandler>()
|
||||
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL));
|
||||
|
||||
serviceCollection.AddSingleton<IDiscoveryCache>(r =>
|
||||
{
|
||||
@ -34,5 +44,8 @@ public static class ContainerExtensions
|
||||
|
||||
container.Register<IAuthenticationRepository, AuthenticationRepository>(Reuse.Singleton);
|
||||
container.Register<IAuthenticationService, AuthenticationService>(Reuse.Singleton);
|
||||
|
||||
container.Register<EntryUploadHandlerFactory>(Reuse.Transient);
|
||||
container.RegisterMany(workshopAssembly, type => type.IsAssignableTo<IEntryUploadHandler>(), Reuse.Transient);
|
||||
}
|
||||
}
|
||||
22
src/Artemis.WebClient.Workshop/Entities/Release.cs
Normal file
22
src/Artemis.WebClient.Workshop/Entities/Release.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Artemis.Web.Workshop.Entities;
|
||||
|
||||
public class Release
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[MaxLength(64)]
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
public long Downloads { get; set; }
|
||||
|
||||
public long DownloadSize { get; set; }
|
||||
|
||||
[MaxLength(32)]
|
||||
public string? Md5Hash { get; set; }
|
||||
|
||||
public Guid EntryId { get; set; }
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using StrawberryShake;
|
||||
|
||||
namespace Artemis.WebClient.Workshop.Extensions;
|
||||
|
||||
public static class ClientBuilderExtensions
|
||||
{
|
||||
public static IClientBuilder<T> AddHttpMessageHandler<T, THandler>(this IClientBuilder<T> builder) where THandler : DelegatingHandler where T : IStoreAccessor
|
||||
{
|
||||
builder.Services.Configure<HttpClientFactoryOptions>(
|
||||
builder.ClientName,
|
||||
options => options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(b.Services.GetRequiredService<THandler>()))
|
||||
);
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
mutation AddEntry ($input: CreateEntryInput!) {
|
||||
addEntry(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Artemis.WebClient.Workshop.Services;
|
||||
|
||||
public class AuthenticationDelegatingHandler : DelegatingHandler
|
||||
{
|
||||
private readonly IAuthenticationService _authenticationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public AuthenticationDelegatingHandler(IAuthenticationService authenticationService)
|
||||
{
|
||||
_authenticationService = authenticationService;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
string? token = await _authenticationService.GetBearer();
|
||||
if (token != null)
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
using Artemis.WebClient.Workshop.UploadHandlers.Implementations;
|
||||
using DryIoc;
|
||||
|
||||
namespace Artemis.WebClient.Workshop.UploadHandlers;
|
||||
|
||||
public class EntryUploadHandlerFactory
|
||||
{
|
||||
private readonly IContainer _container;
|
||||
|
||||
public EntryUploadHandlerFactory(IContainer container)
|
||||
{
|
||||
_container = container;
|
||||
}
|
||||
|
||||
public IEntryUploadHandler CreateHandler(EntryType entryType)
|
||||
{
|
||||
return entryType switch
|
||||
{
|
||||
EntryType.Profile => _container.Resolve<ProfileEntryUploadHandler>(),
|
||||
EntryType.Layout => _container.Resolve<LayoutEntryUploadHandler>(),
|
||||
_ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.")
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
using Artemis.Web.Workshop.Entities;
|
||||
|
||||
namespace Artemis.WebClient.Workshop.UploadHandlers;
|
||||
|
||||
public class EntryUploadResult
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public Release? Release { get; set; }
|
||||
|
||||
public static EntryUploadResult FromFailure(string? message)
|
||||
{
|
||||
return new EntryUploadResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public static EntryUploadResult FromSuccess(Release release)
|
||||
{
|
||||
return new EntryUploadResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
Release = release
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
|
||||
namespace Artemis.WebClient.Workshop.UploadHandlers;
|
||||
|
||||
public interface IEntryUploadHandler
|
||||
{
|
||||
Task<EntryUploadResult> CreateReleaseAsync(Guid entryId, object file, CancellationToken cancellationToken);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using RGB.NET.Layout;
|
||||
|
||||
namespace Artemis.WebClient.Workshop.UploadHandlers.Implementations;
|
||||
|
||||
public class LayoutEntryUploadHandler : IEntryUploadHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<EntryUploadResult> CreateReleaseAsync(Guid entryId, object file, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.Web.Workshop.Entities;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Artemis.WebClient.Workshop.UploadHandlers.Implementations;
|
||||
|
||||
public class ProfileEntryUploadHandler : IEntryUploadHandler
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IProfileService _profileService;
|
||||
|
||||
public ProfileEntryUploadHandler(IHttpClientFactory httpClientFactory, IProfileService profileService)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_profileService = profileService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<EntryUploadResult> CreateReleaseAsync(Guid entryId, object file, CancellationToken cancellationToken)
|
||||
{
|
||||
if (file is not ProfileConfiguration profileConfiguration)
|
||||
throw new InvalidOperationException("Can only create releases for profile configurations");
|
||||
|
||||
ProfileConfigurationExportModel export = _profileService.ExportProfile(profileConfiguration);
|
||||
string json = JsonConvert.SerializeObject(export, IProfileService.ExportSettings);
|
||||
|
||||
using MemoryStream archiveStream = new();
|
||||
|
||||
// Create a ZIP archive with a single entry on the archive stream
|
||||
using (ZipArchive archive = new(archiveStream, ZipArchiveMode.Create, true))
|
||||
{
|
||||
ZipArchiveEntry entry = archive.CreateEntry("profile.json");
|
||||
await using (Stream entryStream = entry.Open())
|
||||
{
|
||||
await entryStream.WriteAsync(Encoding.Default.GetBytes(json), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the archive
|
||||
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
|
||||
|
||||
// Construct the request
|
||||
archiveStream.Seek(0, SeekOrigin.Begin);
|
||||
MultipartFormDataContent content = new();
|
||||
StreamContent streamContent = new(archiveStream);
|
||||
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
|
||||
content.Add(streamContent, "file", "file.zip");
|
||||
|
||||
// Submit
|
||||
HttpResponseMessage response = await client.PostAsync("releases/upload/" + entryId, content, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return EntryUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}");
|
||||
|
||||
Release? release = JsonConvert.DeserializeObject<Release>(await response.Content.ReadAsStringAsync(cancellationToken));
|
||||
return release != null ? EntryUploadResult.FromSuccess(release) : EntryUploadResult.FromFailure("Failed to deserialize response");
|
||||
}
|
||||
}
|
||||
@ -4,4 +4,5 @@ public static class WorkshopConstants
|
||||
{
|
||||
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
|
||||
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
|
||||
public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient";
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user