Workshop - Added profile uploading

Submission wizard - Final upload steps
This commit is contained in:
Robert committed 2023-08-13 21:11:05 +02:00
1 parent e52faf1c9f
commit ad4da3032d
32 files changed
+619 -91

No files matched your search

@@ -0,0 +1 @@
{"v":"5.0.1","fr":29.9700012207031,"ip":0,"op":45.0000018328876,"w":512,"h":512,"nm":"Comp 1","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.572],"y":[0.556]},"o":{"x":[0.167],"y":[0.167]},"n":["0p572_0p556_0p167_0p167"],"t":7,"s":[100],"e":[92.154]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.641],"y":[0.056]},"n":["0p833_0p833_0p641_0p056"],"t":13,"s":[92.154],"e":[30]},{"t":17.0000006924242}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-230,4],[214,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.23137254902,0.741176470588,0.36862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":70,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.588],"y":[-51709.363]},"o":{"x":[0.167],"y":[0.167]},"n":["0p588_-51709p363_0p167_0p167"],"t":7,"s":[0],"e":[0]},{"i":{"x":[0.696],"y":[0.999]},"o":{"x":[0.509],"y":[0.003]},"n":["0p696_0p999_0p509_0p003"],"t":10,"s":[0],"e":[100]},{"t":16.0000006516934}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.566],"y":[0.999]},"o":{"x":[0.457],"y":[0.063]},"n":["0p566_0p999_0p457_0p063"],"t":7,"s":[0],"e":[100]},{"t":16.0000006516934}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0,0],"y":[0.997,0.997]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0_0p997_0p167_0p167","0_0p997_0p167_0p167"],"t":24,"s":[40,40],"e":[90,90]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.574,0.574],"y":[-0.004,-0.004]},"n":["0p833_0p833_0p574_-0p004","0p833_0p833_0p574_-0p004"],"t":27,"s":[90,90],"e":[18.394,18.394]},{"t":38.0000015477717}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":24,"s":[-181.074,-5.414],"e":[200,-5.414],"to":[34.0465698242188,0],"ti":[-26.72825050354,0]},{"t":38.0000015477717}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":24,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":25,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":29,"s":[100],"e":[0]},{"t":38.0000015477717}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.572],"y":[0.556]},"o":{"x":[0.167],"y":[0.167]},"n":["0p572_0p556_0p167_0p167"],"t":10,"s":[100],"e":[92.154]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.641],"y":[0.056]},"n":["0p833_0p833_0p641_0p056"],"t":16,"s":[92.154],"e":[30]},{"t":20.0000008146167}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-230,4],[214,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.23137254902,0.741176470588,0.36862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":70,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np"Line truncated
+5 -1
View File
@@ -14,7 +14,11 @@ public class BitmapExtensions
public static Bitmap LoadAndResize(Stream stream, int size) 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); return Resize(source, size);
} }
@@ -6,8 +6,8 @@
xmlns:workshop="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop" xmlns:workshop="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.EntryTypeView" x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.EntryTypeStepView"
x:DataType="steps:EntryTypeViewModel"> x:DataType="steps:EntryTypeStepViewModel">
<UserControl.Resources> <UserControl.Resources>
<converters:EnumToBooleanConverter x:Key="EnumBoolConverter" /> <converters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
</UserControl.Resources> </UserControl.Resources>
@@ -2,9 +2,9 @@ using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public partial class EntryTypeView : ReactiveUserControl<EntryTypeViewModel> public partial class EntryTypeStepView : ReactiveUserControl<EntryTypeStepViewModel>
{ {
public EntryTypeView() public EntryTypeStepView()
{ {
InitializeComponent(); InitializeComponent();
} }
@@ -6,12 +6,12 @@ using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class EntryTypeViewModel : SubmissionViewModel public class EntryTypeStepViewModel : SubmissionViewModel
{ {
private EntryType? _selectedEntryType; private EntryType? _selectedEntryType;
/// <inheritdoc /> /// <inheritdoc />
public EntryTypeViewModel() public EntryTypeStepViewModel()
{ {
GoBack = ReactiveCommand.Create(() => State.ChangeScreen<WelcomeStepViewModel>()); GoBack = ReactiveCommand.Create(() => State.ChangeScreen<WelcomeStepViewModel>());
Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.SelectedEntryType).Select(e => e != null)); 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); Claim? emailVerified = _authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.EmailVerified);
if (emailVerified?.Value == "true") if (emailVerified?.Value == "true")
State.ChangeScreen<EntryTypeViewModel>(); State.ChangeScreen<EntryTypeStepViewModel>();
else else
State.ChangeScreen<ValidateEmailStepViewModel>(); State.ChangeScreen<ValidateEmailStepViewModel>();
} }
@@ -62,6 +62,6 @@ public class ProfileAdaptionHintsStepViewModel : SubmissionViewModel
if (Layers.Any(l => l.AdaptionHintCount == 0)) if (Layers.Any(l => l.AdaptionHintCount == 0))
return; return;
State.ChangeScreen<EntrySpecificationsStepViewModel>(); State.ChangeScreen<SpecificationsStepViewModel>();
} }
} }
@@ -43,7 +43,7 @@ public class ProfileSelectionStepViewModel : SubmissionViewModel
ProfilePreview = profilePreviewViewModel; 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)); Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.SelectedProfile).Select(p => p != null));
this.WhenAnyValue(vm => vm.SelectedProfile).Subscribe(p => Update(p)); this.WhenAnyValue(vm => vm.SelectedProfile).Subscribe(p => Update(p));
@@ -7,8 +7,8 @@
xmlns:categories="clr-namespace:Artemis.UI.Screens.Workshop.Categories" xmlns:categories="clr-namespace:Artemis.UI.Screens.Workshop.Categories"
xmlns:tagsInput="clr-namespace:Artemis.UI.Shared.TagsInput;assembly=Artemis.UI.Shared" xmlns:tagsInput="clr-namespace:Artemis.UI.Shared.TagsInput;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="970" d:DesignHeight="625" mc:Ignorable="d" d:DesignWidth="970" d:DesignHeight="625"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.EntrySpecificationsStepView" x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.SpecificationsStepView"
x:DataType="steps:EntrySpecificationsStepViewModel"> x:DataType="steps:SpecificationsStepViewModel">
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*">
<StackPanel Grid.Row="0"> <StackPanel Grid.Row="0">
<StackPanel.Styles> <StackPanel.Styles>
@@ -5,9 +5,9 @@ using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public partial class EntrySpecificationsStepView : ReactiveUserControl<EntrySpecificationsStepViewModel> public partial class SpecificationsStepView : ReactiveUserControl<SpecificationsStepViewModel>
{ {
public EntrySpecificationsStepView() public SpecificationsStepView()
{ {
InitializeComponent(); InitializeComponent();
} }
@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Drawing;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reactive; using System.Reactive;
@@ -13,7 +12,6 @@ using Artemis.UI.Screens.Workshop.Categories;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Extensions;
using Avalonia.Threading; using Avalonia.Threading;
using DynamicData; using DynamicData;
using DynamicData.Aggregation; using DynamicData.Aggregation;
@@ -26,18 +24,17 @@ using Bitmap = Avalonia.Media.Imaging.Bitmap;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class EntrySpecificationsStepViewModel : SubmissionViewModel public class SpecificationsStepViewModel : SubmissionViewModel
{ {
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<bool>? _categoriesValid; private ObservableAsPropertyHelper<bool>? _categoriesValid;
private ObservableAsPropertyHelper<bool>? _iconValid; private ObservableAsPropertyHelper<bool>? _iconValid;
private string _description = string.Empty; private string _description = string.Empty;
private Bitmap? _iconBitmap;
private bool _isDirty;
private string _name = string.Empty; private string _name = string.Empty;
private string _summary = string.Empty; private string _summary = string.Empty;
private Bitmap? _iconBitmap;
public EntrySpecificationsStepViewModel(IWorkshopClient workshopClient, IWindowService windowService) public SpecificationsStepViewModel(IWorkshopClient workshopClient, IWindowService windowService)
{ {
_windowService = windowService; _windowService = windowService;
GoBack = ReactiveCommand.Create(ExecuteGoBack); GoBack = ReactiveCommand.Create(ExecuteGoBack);
@@ -45,47 +42,18 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
SelectIcon = ReactiveCommand.CreateFromTask(ExecuteSelectIcon); SelectIcon = ReactiveCommand.CreateFromTask(ExecuteSelectIcon);
ClearIcon = ReactiveCommand.Create(ExecuteClearIcon); 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 => this.WhenActivated(d =>
{ {
DisplayName = $"{State.EntryType} Information"; DisplayName = $"{State.EntryType} Information";
// Basic fields // Load categories
Name = State.Name; Observable.FromAsync(workshopClient.GetCategories.ExecuteAsync).Subscribe(PopulateCategories).DisposeWith(d);
Summary = State.Summary;
Description = State.Description;
// Categories // Apply the state
foreach (CategoryViewModel categoryViewModel in Categories) ApplyFromState();
categoryViewModel.IsSelected = State.Categories.Contains(categoryViewModel.Id);
// Tags
Tags.Clear();
Tags.AddRange(State.Tags);
// Icon
if (State.Icon != null)
{
State.Icon.Seek(0, SeekOrigin.Begin);
IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128);
}
IsDirty = false;
this.ClearValidationRules(); this.ClearValidationRules();
Disposable.Create(ExecuteClearIcon).DisposeWith(d);
Disposable.Create(() =>
{
IconBitmap?.Dispose();
IconBitmap = null;
}).DisposeWith(d);
}); });
} }
@@ -94,7 +62,7 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
public ReactiveCommand<Unit, Unit> SelectIcon { get; } public ReactiveCommand<Unit, Unit> SelectIcon { get; }
public ReactiveCommand<Unit, Unit> ClearIcon { 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 ObservableCollection<string> Tags { get; } = new();
public bool CategoriesValid => _categoriesValid?.Value ?? true; public bool CategoriesValid => _categoriesValid?.Value ?? true;
public bool IconValid => _iconValid?.Value ?? true; public bool IconValid => _iconValid?.Value ?? true;
@@ -117,12 +85,6 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
set => RaiseAndSetIfChanged(ref _description, value); set => RaiseAndSetIfChanged(ref _description, value);
} }
public bool IsDirty
{
get => _isDirty;
set => RaiseAndSetIfChanged(ref _isDirty, value);
}
public Bitmap? IconBitmap public Bitmap? IconBitmap
{ {
get => _iconBitmap; get => _iconBitmap;
@@ -131,6 +93,9 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
private void ExecuteGoBack() private void ExecuteGoBack()
{ {
// Apply what's there so far
ApplyToState();
switch (State.EntryType) switch (State.EntryType)
{ {
case EntryType.Layout: case EntryType.Layout:
@@ -147,35 +112,20 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
private void ExecuteContinue() private void ExecuteContinue()
{ {
if (!IsDirty) if (!ValidationContext.Validations.Any())
{ {
SetupDataValidation();
IsDirty = true;
// The ValidationContext seems to update asynchronously, so stop and schedule a retry // The ValidationContext seems to update asynchronously, so stop and schedule a retry
SetupDataValidation();
Dispatcher.UIThread.Post(ExecuteContinue); Dispatcher.UIThread.Post(ExecuteContinue);
return; return;
} }
ApplyToState();
if (!ValidationContext.GetIsValid()) if (!ValidationContext.GetIsValid())
return; return;
State.Name = Name; State.ChangeScreen<SubmitStepViewModel>();
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;
}
} }
private async Task ExecuteSelectIcon() private async Task ExecuteSelectIcon()
@@ -197,6 +147,13 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
IconBitmap = null; 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() private void SetupDataValidation()
{ {
// Hopefully this can be avoided in the future // Hopefully this can be avoided in the future
@@ -207,14 +164,53 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
// These don't use inputs that support validation messages, do so manually // 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 iconRule = this.ValidationRule(vm => vm.IconBitmap, s => s != null, "Icon required");
ValidationHelper categoriesRule = this.ValidationRule(vm => vm.Categories, Categories.ToObservableChangeSet() ValidationHelper categoriesRule = this.ValidationRule(vm => vm.Categories, Categories.ToObservableChangeSet().AutoRefresh(c => c.IsSelected).Filter(c => c.IsSelected).IsNotEmpty(),
.AutoRefresh(c => c.IsSelected)
.Filter(c => c.IsSelected)
.IsEmpty()
.CombineLatest(this.WhenAnyValue(vm => vm.IsDirty), (empty, dirty) => !dirty || !empty),
"At least one category must be selected" "At least one category must be selected"
); );
_iconValid = iconRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.IconValid); _iconValid = iconRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.IconValid);
_categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid); _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() private void ExecuteContinue()
{ {
State.ChangeScreen<EntryTypeViewModel>(); State.ChangeScreen<EntryTypeStepViewModel>();
} }
private async Task ExecuteRefresh(CancellationToken ct) private async Task ExecuteRefresh(CancellationToken ct)
@@ -3,7 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:steps="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps" 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:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.WelcomeStepView"
x:DataType="steps:WelcomeStepViewModel"> x:DataType="steps:WelcomeStepViewModel">
<StackPanel Margin="0 50 0 0" > <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. Here we'll take you, step by step, through the process of uploading your submission to the workshop.
</TextBlock> </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> </StackPanel>
</UserControl> </UserControl>
@@ -38,7 +38,7 @@ public class WelcomeStepViewModel : SubmissionViewModel
else else
{ {
if (_authenticationService.Claims.Any(c => c.Type == JwtClaimTypes.EmailVerified && c.Value == "true")) if (_authenticationService.Claims.Any(c => c.Type == JwtClaimTypes.EmailVerified && c.Value == "true"))
State.ChangeScreen<EntryTypeViewModel>(); State.ChangeScreen<EntryTypeStepViewModel>();
else else
State.ChangeScreen<ValidateEmailStepViewModel>(); State.ChangeScreen<ValidateEmailStepViewModel>();
} }
@@ -12,7 +12,7 @@
Icon="/Assets/Images/Logo/application.ico" Icon="/Assets/Images/Logo/application.ico"
Title="Artemis | Workshop submission wizard" Title="Artemis | Workshop submission wizard"
Width="1000" Width="1000"
Height="735" Height="950"
WindowStartupLocation="CenterOwner"> WindowStartupLocation="CenterOwner">
<Grid Margin="15" RowDefinitions="Auto,*,Auto"> <Grid Margin="15" RowDefinitions="Auto,*,Auto">
<Grid RowDefinitions="*,*" ColumnDefinitions="Auto,*,Auto" Margin="0 0 0 15"> <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.Repositories;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
using Artemis.WebClient.Workshop.State;
using Artemis.WebClient.Workshop.UploadHandlers;
using DryIoc; using DryIoc;
using DryIoc.Microsoft.DependencyInjection; using DryIoc.Microsoft.DependencyInjection;
using IdentityModel.Client; using IdentityModel.Client;
@@ -18,11 +22,17 @@ public static class ContainerExtensions
/// <param name="container">The builder building the current container</param> /// <param name="container">The builder building the current container</param>
public static void RegisterWorkshopClient(this IContainer container) public static void RegisterWorkshopClient(this IContainer container)
{ {
Assembly[] workshopAssembly = {typeof(WorkshopConstants).Assembly};
ServiceCollection serviceCollection = new(); ServiceCollection serviceCollection = new();
serviceCollection serviceCollection
.AddHttpClient() .AddHttpClient()
.AddWorkshopClient() .AddWorkshopClient()
.AddHttpMessageHandler<WorkshopClientStoreAccessor, AuthenticationDelegatingHandler>()
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL + "/graphql")); .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 => serviceCollection.AddSingleton<IDiscoveryCache>(r =>
{ {
@@ -34,5 +44,8 @@ public static class ContainerExtensions
container.Register<IAuthenticationRepository, AuthenticationRepository>(Reuse.Singleton); container.Register<IAuthenticationRepository, AuthenticationRepository>(Reuse.Singleton);
container.Register<IAuthenticationService, AuthenticationService>(Reuse.Singleton); container.Register<IAuthenticationService, AuthenticationService>(Reuse.Singleton);
container.Register<EntryUploadHandlerFactory>(Reuse.Transient);
container.RegisterMany(workshopAssembly, type => type.IsAssignableTo<IEntryUploadHandler>(), Reuse.Transient);
} }
} }
@@ -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 AUTHORITY_URL = "https://identity.artemis-rgb.com";
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com"; public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient";
} }