diff --git a/src/Artemis.UI/Assets/Animations/success.json b/src/Artemis.UI/Assets/Animations/success.json
new file mode 100644
index 000000000..f274c145e
--- /dev/null
+++ b/src/Artemis.UI/Assets/Animations/success.json
@@ -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":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":10,"s":[0],"e":[0]},{"i":{"x":[0.696],"y":[0.999]},"o":{"x":[0.509],"y":[0.003]},"n":["0p696_0p999_0p509_0p003"],"t":13,"s":[0],"e":[100]},{"t":19.0000007738859}],"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":10,"s":[0],"e":[100]},{"t":19.0000007738859}],"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}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"trait","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[263.334,471.109,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[15,15,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"trait","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[51.641,253.275,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[15,15,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"trait","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[266.322,44.315,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[15,15,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"trait","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[469.91,258.792,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[15,15,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"firefly","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-18.097,"ix":10},"p":{"a":0,"k":[400.635,189.708,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[20,20,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"firefly","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-72.471,"ix":10},"p":{"a":0,"k":[359.413,150.912,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[20,20,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"firefly","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45.707,"ix":10},"p":{"a":0,"k":[396.894,150.961,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[30,30,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"trait 2","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-135.205,"ix":10},"p":{"a":0,"k":[410.865,406.53,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[-19.512,19.512,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"trait 2","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45.606,"ix":10},"p":{"a":0,"k":[105.535,402.598,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[-19.512,19.512,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"trait 2","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-135.205,"ix":10},"p":{"a":0,"k":[104.864,111.71,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[19.512,19.512,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"trait 2","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45.606,"ix":10},"p":{"a":0,"k":[416.722,113.206,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[19.512,19.512,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[236.888,240.258,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[69.59,69.59,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-76.426,37.999],[12.056,114.074],[169.991,-68.635]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":35,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-7,11],"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":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[-2.986]},"o":{"x":[0.167],"y":[0]},"n":["0p833_-2p986_0p167_0"],"t":0,"s":[0],"e":[0]},{"i":{"x":[0],"y":[0.973]},"o":{"x":[0.167],"y":[0.042]},"n":["0_0p973_0p167_0p042"],"t":14.791,"s":[0],"e":[32]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.828],"y":[0.011]},"n":["0p833_0p833_0p828_0p011"],"t":19.791,"s":[32],"e":[100]},{"t":24.7912510097683}],"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},{"ddd":0,"ind":13,"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":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.976,0.976],"y":[0.968,0.968]},"o":{"x":[0.654,0.654],"y":[0.007,0.007]},"n":["0p976_0p968_0p654_0p007","0p976_0p968_0p654_0p007"],"t":0,"s":[0,0],"e":[401.025,401.025]},{"i":{"x":[0.468,0.468],"y":[1.057,1.057]},"o":{"x":[0.346,0.346],"y":[-4.83,-4.83]},"n":["0p468_1p057_0p346_-4p83","0p468_1p057_0p346_-4p83"],"t":7,"s":[401.025,401.025],"e":[372.7,372.7]},{"i":{"x":[0.375,0.375],"y":[1.543,1.543]},"o":{"x":[0.364,0.364],"y":[0.031,0.031]},"n":["0p375_1p543_0p364_0p031","0p375_1p543_0p364_0p031"],"t":12,"s":[372.7,372.7],"e":[401.025,401.025]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.327,0.327],"y":[-8.038,-8.038]},"n":["0p833_1_0p327_-8p038","0p833_1_0p327_-8p038"],"t":16,"s":[401.025,401.025],"e":[401.025,401.025]},{"t":20.0000008146167}],"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":[0.229886716955,0.739552696078,0.369435897528,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":0,"k":[5.992,3.49],"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":"Ellipse 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}]}
\ No newline at end of file
diff --git a/src/Artemis.UI/Extensions/Bitmap.cs b/src/Artemis.UI/Extensions/Bitmap.cs
index ce8cba4d2..8c14ee4da 100644
--- a/src/Artemis.UI/Extensions/Bitmap.cs
+++ b/src/Artemis.UI/Extensions/Bitmap.cs
@@ -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);
}
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepView.axaml
similarity index 96%
rename from src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeView.axaml
rename to src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepView.axaml
index d94509c6e..c1720ccfa 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepView.axaml
@@ -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">
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepView.axaml.cs
similarity index 54%
rename from src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeView.axaml.cs
rename to src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepView.axaml.cs
index 3e4df145c..087084ca5 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepView.axaml.cs
@@ -2,9 +2,9 @@ using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
-public partial class EntryTypeView : ReactiveUserControl
+public partial class EntryTypeStepView : ReactiveUserControl
{
- public EntryTypeView()
+ public EntryTypeStepView()
{
InitializeComponent();
}
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepViewModel.cs
similarity index 92%
rename from src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeViewModel.cs
rename to src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepViewModel.cs
index 9d3c9ea5d..cce78ea9d 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepViewModel.cs
@@ -6,12 +6,12 @@ using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
-public class EntryTypeViewModel : SubmissionViewModel
+public class EntryTypeStepViewModel : SubmissionViewModel
{
private EntryType? _selectedEntryType;
///
- public EntryTypeViewModel()
+ public EntryTypeStepViewModel()
{
GoBack = ReactiveCommand.Create(() => State.ChangeScreen());
Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.SelectedEntryType).Select(e => e != null));
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/LoginStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/LoginStepViewModel.cs
index dfad8f7af..83b7d3ca0 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/LoginStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/LoginStepViewModel.cs
@@ -43,7 +43,7 @@ public class LoginStepViewModel : SubmissionViewModel
Claim? emailVerified = _authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.EmailVerified);
if (emailVerified?.Value == "true")
- State.ChangeScreen();
+ State.ChangeScreen();
else
State.ChangeScreen();
}
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs
index 757d6f0c0..768ca5405 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs
@@ -62,6 +62,6 @@ public class ProfileAdaptionHintsStepViewModel : SubmissionViewModel
if (Layers.Any(l => l.AdaptionHintCount == 0))
return;
- State.ChangeScreen();
+ State.ChangeScreen();
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs
index 219fd6910..3cc2b409a 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs
@@ -43,7 +43,7 @@ public class ProfileSelectionStepViewModel : SubmissionViewModel
ProfilePreview = profilePreviewViewModel;
- GoBack = ReactiveCommand.Create(() => State.ChangeScreen());
+ GoBack = ReactiveCommand.Create(() => State.ChangeScreen());
Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.SelectedProfile).Select(p => p != null));
this.WhenAnyValue(vm => vm.SelectedProfile).Subscribe(p => Update(p));
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntrySpecificationsStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepView.axaml
similarity index 98%
rename from src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntrySpecificationsStepView.axaml
rename to src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepView.axaml
index fdf3c6f38..197f2ef06 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntrySpecificationsStepView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepView.axaml
@@ -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">
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntrySpecificationsStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepView.axaml.cs
similarity index 67%
rename from src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntrySpecificationsStepView.axaml.cs
rename to src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepView.axaml.cs
index b57435eb7..f7d256121 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntrySpecificationsStepView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepView.axaml.cs
@@ -5,9 +5,9 @@ using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
-public partial class EntrySpecificationsStepView : ReactiveUserControl
+public partial class SpecificationsStepView : ReactiveUserControl
{
- public EntrySpecificationsStepView()
+ public SpecificationsStepView()
{
InitializeComponent();
}
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntrySpecificationsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs
similarity index 72%
rename from src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntrySpecificationsStepViewModel.cs
rename to src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs
index a95f335cf..fcacc0d58 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntrySpecificationsStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs
@@ -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? _categoriesValid;
private ObservableAsPropertyHelper? _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 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 SelectIcon { get; }
public ReactiveCommand ClearIcon { get; }
- public ReadOnlyObservableCollection Categories { get; }
+ public ObservableCollection Categories { get; } = new();
public ObservableCollection 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(Tags);
-
- State.Icon?.Dispose();
- if (IconBitmap != null)
- {
- State.Icon = new MemoryStream();
- IconBitmap.Save(State.Icon);
- }
- else
- {
- State.Icon = null;
- }
+
+ State.ChangeScreen();
}
private async Task ExecuteSelectIcon()
@@ -197,6 +147,13 @@ public class EntrySpecificationsStepViewModel : SubmissionViewModel
IconBitmap = null;
}
+ private void PopulateCategories(IOperationResult 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(Tags);
+
+ // Icon
+ State.Icon?.Dispose();
+ if (IconBitmap != null)
+ {
+ State.Icon = new MemoryStream();
+ IconBitmap.Save(State.Icon);
+ }
+ else
+ {
+ State.Icon = null;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepView.axaml
new file mode 100644
index 000000000..381f0ffb9
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepView.axaml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+ Ready to submit?
+
+
+ We have all the information we need, are you ready to submit the following to the workshop?
+
+
+
+
+
+
+
+
+
+
+
+
+
+ by
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepView.axaml.cs
new file mode 100644
index 000000000..49f0af7ff
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepView.axaml.cs
@@ -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
+{
+ public SubmitStepView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepViewModel.cs
new file mode 100644
index 000000000..0752ccc0d
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepViewModel.cs
@@ -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? _categories;
+ private Bitmap? _iconBitmap;
+
+ ///
+ public SubmitStepViewModel(IAuthenticationService authenticationService, IWorkshopClient workshopClient)
+ {
+ CurrentUser = authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Name)?.Value;
+ GoBack = ReactiveCommand.Create(() => State.ChangeScreen());
+ Continue = ReactiveCommand.Create(() => State.ChangeScreen());
+
+ 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? Categories
+ {
+ get => _categories;
+ set => RaiseAndSetIfChanged(ref _categories, value);
+ }
+
+ public override ReactiveCommand Continue { get; }
+
+ public override ReactiveCommand GoBack { get; }
+
+ private void PopulateCategories(IOperationResult result)
+ {
+ if (result.Data == null)
+ Categories = null;
+ else
+ {
+ Categories = new ReadOnlyObservableCollection(
+ new ObservableCollection(result.Data.Categories.Where(c => State.Categories.Contains(c.Id)).Select(c => new CategoryViewModel(c)))
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml
new file mode 100644
index 000000000..bd215cce5
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+ Uploading your submission...
+
+
+ Wooo, the final step, that was pretty easy, right!?
+
+
+
+
+ All done! Hit finish to view your submission.
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml.cs
new file mode 100644
index 000000000..7dc094d0c
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml.cs
@@ -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
+{
+ public UploadStepView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs
new file mode 100644
index 000000000..8cec0dba1
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs
@@ -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;
+
+ ///
+ 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));
+ }
+
+ ///
+ public override ReactiveCommand Continue { get; }
+
+ ///
+ public override ReactiveCommand GoBack { get; } = null!;
+
+ public bool Finished
+ {
+ get => _finished;
+ set => RaiseAndSetIfChanged(ref _finished, value);
+ }
+
+ public async Task ExecuteUpload(CancellationToken cancellationToken)
+ {
+ IOperationResult 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();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ValidateEmailStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ValidateEmailStepViewModel.cs
index a6576ee88..3778c2673 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ValidateEmailStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ValidateEmailStepViewModel.cs
@@ -73,7 +73,7 @@ public class ValidateEmailStepViewModel : SubmissionViewModel
private void ExecuteContinue()
{
- State.ChangeScreen();
+ State.ChangeScreen();
}
private async Task ExecuteRefresh(CancellationToken ct)
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepView.axaml
index 7bf5c4dd8..9f9538c38 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepView.axaml
@@ -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">
@@ -14,6 +14,6 @@
Here we'll take you, step by step, through the process of uploading your submission to the workshop.
-
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepViewModel.cs
index caa55733c..3160fbb1e 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepViewModel.cs
@@ -38,7 +38,7 @@ public class WelcomeStepViewModel : SubmissionViewModel
else
{
if (_authenticationService.Claims.Any(c => c.Type == JwtClaimTypes.EmailVerified && c.Value == "true"))
- State.ChangeScreen();
+ State.ChangeScreen();
else
State.ChangeScreen();
}
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml
index 055860af1..e313f0f9f 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml
@@ -12,7 +12,7 @@
Icon="/Assets/Images/Logo/application.ico"
Title="Artemis | Workshop submission wizard"
Width="1000"
- Height="735"
+ Height="950"
WindowStartupLocation="CenterOwner">
diff --git a/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs b/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs
index bdcdbede6..46c4060fc 100644
--- a/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs
+++ b/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs
@@ -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
/// The builder building the current container
public static void RegisterWorkshopClient(this IContainer container)
{
+ Assembly[] workshopAssembly = {typeof(WorkshopConstants).Assembly};
+
ServiceCollection serviceCollection = new();
serviceCollection
.AddHttpClient()
.AddWorkshopClient()
+ .AddHttpMessageHandler()
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL + "/graphql"));
+ serviceCollection.AddHttpClient(WorkshopConstants.WORKSHOP_CLIENT_NAME)
+ .AddHttpMessageHandler()
+ .ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL));
serviceCollection.AddSingleton(r =>
{
@@ -34,5 +44,8 @@ public static class ContainerExtensions
container.Register(Reuse.Singleton);
container.Register(Reuse.Singleton);
+
+ container.Register(Reuse.Transient);
+ container.RegisterMany(workshopAssembly, type => type.IsAssignableTo(), Reuse.Transient);
}
}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Entities/Release.cs b/src/Artemis.WebClient.Workshop/Entities/Release.cs
new file mode 100644
index 000000000..e2c2d1146
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/Entities/Release.cs
@@ -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; }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Extensions/ClientBuilderExtensions.cs b/src/Artemis.WebClient.Workshop/Extensions/ClientBuilderExtensions.cs
new file mode 100644
index 000000000..6db06795f
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/Extensions/ClientBuilderExtensions.cs
@@ -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 AddHttpMessageHandler(this IClientBuilder builder) where THandler : DelegatingHandler where T : IStoreAccessor
+ {
+ builder.Services.Configure(
+ builder.ClientName,
+ options => options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(b.Services.GetRequiredService()))
+ );
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Queries/CreateEntry.graphql b/src/Artemis.WebClient.Workshop/Queries/CreateEntry.graphql
new file mode 100644
index 000000000..7191bc704
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/Queries/CreateEntry.graphql
@@ -0,0 +1,5 @@
+mutation AddEntry ($input: CreateEntryInput!) {
+ addEntry(input: $input) {
+ id
+ }
+}
diff --git a/src/Artemis.WebClient.Workshop/Services/AuthenticationDelegatingHandler.cs b/src/Artemis.WebClient.Workshop/Services/AuthenticationDelegatingHandler.cs
new file mode 100644
index 000000000..3adda3145
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/Services/AuthenticationDelegatingHandler.cs
@@ -0,0 +1,22 @@
+using System.Net.Http.Headers;
+
+namespace Artemis.WebClient.Workshop.Services;
+
+public class AuthenticationDelegatingHandler : DelegatingHandler
+{
+ private readonly IAuthenticationService _authenticationService;
+
+ ///
+ public AuthenticationDelegatingHandler(IAuthenticationService authenticationService)
+ {
+ _authenticationService = authenticationService;
+ }
+
+ protected override async Task 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);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/UploadHandlers/EntryUploadHandlerFactory.cs b/src/Artemis.WebClient.Workshop/UploadHandlers/EntryUploadHandlerFactory.cs
new file mode 100644
index 000000000..ea0155e4f
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/UploadHandlers/EntryUploadHandlerFactory.cs
@@ -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(),
+ EntryType.Layout => _container.Resolve(),
+ _ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.")
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/UploadHandlers/EntryUploadResult.cs b/src/Artemis.WebClient.Workshop/UploadHandlers/EntryUploadResult.cs
new file mode 100644
index 000000000..040b99413
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/UploadHandlers/EntryUploadResult.cs
@@ -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
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/UploadHandlers/IEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/UploadHandlers/IEntryUploadHandler.cs
new file mode 100644
index 000000000..a80787acb
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/UploadHandlers/IEntryUploadHandler.cs
@@ -0,0 +1,7 @@
+
+namespace Artemis.WebClient.Workshop.UploadHandlers;
+
+public interface IEntryUploadHandler
+{
+ Task CreateReleaseAsync(Guid entryId, object file, CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs
new file mode 100644
index 000000000..1b3318940
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs
@@ -0,0 +1,12 @@
+using RGB.NET.Layout;
+
+namespace Artemis.WebClient.Workshop.UploadHandlers.Implementations;
+
+public class LayoutEntryUploadHandler : IEntryUploadHandler
+{
+ ///
+ public async Task CreateReleaseAsync(Guid entryId, object file, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs
new file mode 100644
index 000000000..4582e4f9b
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs
@@ -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;
+ }
+
+ ///
+ public async Task 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(await response.Content.ReadAsStringAsync(cancellationToken));
+ return release != null ? EntryUploadResult.FromSuccess(release) : EntryUploadResult.FromFailure("Failed to deserialize response");
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs
index b514ae862..192a92b56 100644
--- a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs
+++ b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs
@@ -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";
}
\ No newline at end of file