From ad4da3032d44d58fa4a1e9b7cbb9457011ebe763 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 13 Aug 2023 21:11:05 +0200 Subject: [PATCH] Workshop - Added profile uploading Submission wizard - Final upload steps --- src/Artemis.UI/Assets/Animations/success.json | 1 + src/Artemis.UI/Extensions/Bitmap.cs | 6 +- ...TypeView.axaml => EntryTypeStepView.axaml} | 4 +- ...ew.axaml.cs => EntryTypeStepView.axaml.cs} | 4 +- ...ViewModel.cs => EntryTypeStepViewModel.cs} | 4 +- .../Steps/LoginStepViewModel.cs | 2 +- .../ProfileAdaptionHintsStepViewModel.cs | 2 +- .../Profile/ProfileSelectionStepViewModel.cs | 2 +- ...iew.axaml => SpecificationsStepView.axaml} | 4 +- ...aml.cs => SpecificationsStepView.axaml.cs} | 4 +- ...odel.cs => SpecificationsStepViewModel.cs} | 144 +++++++++--------- .../Steps/SubmitStepView.axaml | 71 +++++++++ .../Steps/SubmitStepView.axaml.cs | 19 +++ .../Steps/SubmitStepViewModel.cs | 73 +++++++++ .../Steps/UploadStepView.axaml | 31 ++++ .../Steps/UploadStepView.axaml.cs | 19 +++ .../Steps/UploadStepViewModel.cs | 101 ++++++++++++ .../Steps/ValidateEmailStepViewModel.cs | 2 +- .../Steps/WelcomeStepView.axaml | 4 +- .../Steps/WelcomeStepViewModel.cs | 2 +- .../SubmissionWizardView.axaml | 2 +- .../DryIoc/ContainerExtensions.cs | 13 ++ .../Entities/Release.cs | 22 +++ .../Extensions/ClientBuilderExtensions.cs | 18 +++ .../Queries/CreateEntry.graphql | 5 + .../AuthenticationDelegatingHandler.cs | 22 +++ .../EntryUploadHandlerFactory.cs | 24 +++ .../UploadHandlers/EntryUploadResult.cs | 28 ++++ .../UploadHandlers/IEntryUploadHandler.cs | 7 + .../LayoutEntryUploadHandler.cs | 12 ++ .../ProfileEntryUploadHandler.cs | 61 ++++++++ .../WorkshopConstants.cs | 1 + 32 files changed, 621 insertions(+), 93 deletions(-) create mode 100644 src/Artemis.UI/Assets/Animations/success.json rename src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/{EntryTypeView.axaml => EntryTypeStepView.axaml} (96%) rename src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/{EntryTypeView.axaml.cs => EntryTypeStepView.axaml.cs} (54%) rename src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/{EntryTypeViewModel.cs => EntryTypeStepViewModel.cs} (92%) rename src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/{EntrySpecificationsStepView.axaml => SpecificationsStepView.axaml} (98%) rename src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/{EntrySpecificationsStepView.axaml.cs => SpecificationsStepView.axaml.cs} (67%) rename src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/{EntrySpecificationsStepViewModel.cs => SpecificationsStepViewModel.cs} (72%) create mode 100644 src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepViewModel.cs create mode 100644 src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs create mode 100644 src/Artemis.WebClient.Workshop/Entities/Release.cs create mode 100644 src/Artemis.WebClient.Workshop/Extensions/ClientBuilderExtensions.cs create mode 100644 src/Artemis.WebClient.Workshop/Queries/CreateEntry.graphql create mode 100644 src/Artemis.WebClient.Workshop/Services/AuthenticationDelegatingHandler.cs create mode 100644 src/Artemis.WebClient.Workshop/UploadHandlers/EntryUploadHandlerFactory.cs create mode 100644 src/Artemis.WebClient.Workshop/UploadHandlers/EntryUploadResult.cs create mode 100644 src/Artemis.WebClient.Workshop/UploadHandlers/IEntryUploadHandler.cs create mode 100644 src/Artemis.WebClient.Workshop/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs create mode 100644 src/Artemis.WebClient.Workshop/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs 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