From d9df443970f6ddfe19c3f4f15bbfe9d2913a2e1f Mon Sep 17 00:00:00 2001 From: RobertBeekman Date: Wed, 14 Feb 2024 23:04:19 +0100 Subject: [PATCH] Workshop - Added plugin upload support --- .../Services/PluginManagementService.cs | 2 +- src/Artemis.UI/Artemis.UI.csproj | 20 +++++ .../Entries/Tabs/LayoutListViewModel.cs | 1 + .../Models/SubmissionWizardState.cs | 5 +- .../Steps/EntryTypeStepView.axaml | 9 ++ .../Plugin/PluginSelectionStepView.axaml | 46 ++++++++++ .../Plugin/PluginSelectionStepView.axaml.cs | 11 +++ .../Plugin/PluginSelectionStepViewModel.cs | 83 +++++++++++++++++++ .../Steps/SpecificationsStepViewModel.cs | 22 ++--- .../EntryUploadHandlerFactory.cs | 1 + .../Implementations/PluginEntrySource.cs | 15 ++++ .../PluginEntryUploadHandler.cs | 40 +++++++++ 12 files changed, 240 insertions(+), 15 deletions(-) create mode 100644 src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs create mode 100644 src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntrySource.cs create mode 100644 src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 8194cef44..03a4a1269 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -597,7 +597,7 @@ internal class PluginManagementService : IPluginManagementService using StreamReader reader = new(metaDataFileEntry.Open()); PluginInfo pluginInfo = CoreJson.DeserializeObject(reader.ReadToEnd())!; if (!pluginInfo.Main.EndsWith(".dll")) - throw new ArtemisPluginException("Main entry in plugin.json must point to a .dll file" + fileName); + throw new ArtemisPluginException("Main entry in plugin.json must point to a .dll file"); Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid); if (existing != null) diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 0048cffb7..3f5b1f056 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -35,4 +35,24 @@ + + + + PluginSelectionStepView.axaml + Code + + + ProfileAdaptionHintsStepView.axaml + Code + + + ProfileSelectionStepView.axaml + Code + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs index 6cab02f27..ed2fa328b 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs @@ -27,6 +27,7 @@ public class LayoutListViewModel : List.EntryListViewModel And = new[] { new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Layout}}, + new EntryFilterInput(){LatestReleaseId = new LongOperationFilterInput {Gt = 0}}, base.GetFilter() } }; diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs index 89d2f4fa9..e06f14ce9 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout; +using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; @@ -56,7 +57,9 @@ public class SubmissionWizardState : IDisposable public void StartForCurrentEntry() { - if (EntryType == EntryType.Profile) + if (EntryType == EntryType.Plugin) + ChangeScreen(); + else if (EntryType == EntryType.Profile) ChangeScreen(); else if (EntryType == EntryType.Layout) ChangeScreen(); diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepView.axaml index c1720ccfa..c33d2b2db 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepView.axaml +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeStepView.axaml @@ -44,5 +44,14 @@ + + + + Plugin + A plugin that adds new features to Artemis. + + + diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepView.axaml new file mode 100644 index 000000000..06598cd93 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepView.axaml @@ -0,0 +1,46 @@ + + + + + + + + + + Plugin selection + + + Please select the plugin you want to share, a preview will be shown below. + + + + + + Path + + + Name + + + Description + + + Main entry point + + + Version + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepView.axaml.cs new file mode 100644 index 000000000..79866d0e5 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin; + +public partial class PluginSelectionStepView : ReactiveUserControl +{ + public PluginSelectionStepView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs new file mode 100644 index 000000000..a44a7e05b --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs @@ -0,0 +1,83 @@ +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Workshop.Handlers.UploadHandlers; +using PropertyChanged.SourceGenerator; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin; + +public partial class PluginSelectionStepViewModel : SubmissionViewModel +{ + private readonly IWindowService _windowService; + [Notify] private PluginInfo? _selectedPlugin; + [Notify] private string? _path; + + /// + public PluginSelectionStepViewModel(IWindowService windowService) + { + _windowService = windowService; + + GoBack = ReactiveCommand.Create(() => State.ChangeScreen()); + Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.SelectedPlugin).Select(p => p != null)); + Browse = ReactiveCommand.CreateFromTask(ExecuteBrowse); + + this.WhenActivated((CompositeDisposable _) => + { + ShowGoBack = State.EntryId == null; + if (State.EntrySource is PluginEntrySource pluginEntrySource) + { + SelectedPlugin = pluginEntrySource.PluginInfo; + Path = pluginEntrySource.Path; + } + }); + } + + public ReactiveCommand Browse { get; } + + private async Task ExecuteBrowse() + { + string[]? files = await _windowService.CreateOpenFileDialog().HavingFilter(f => f.WithExtension("zip").WithName("ZIP files")).ShowAsync(); + if (files == null) + return; + + // Find the metadata file in the zip + using ZipArchive archive = ZipFile.OpenRead(files[0]); + ZipArchiveEntry? metaDataFileEntry = archive.Entries.FirstOrDefault(e => e.Name == "plugin.json"); + if (metaDataFileEntry == null) + throw new ArtemisPluginException("Couldn't find a plugin.json in " + files[0]); + + using StreamReader reader = new(metaDataFileEntry.Open()); + PluginInfo pluginInfo = CoreJson.DeserializeObject(reader.ReadToEnd())!; + if (!pluginInfo.Main.EndsWith(".dll")) + throw new ArtemisPluginException("Main entry in plugin.json must point to a .dll file"); + + SelectedPlugin = pluginInfo; + Path = files[0]; + } + + private void ExecuteContinue() + { + if (SelectedPlugin == null || Path == null) + return; + + State.EntrySource = new PluginEntrySource(SelectedPlugin, Path); + + if (string.IsNullOrWhiteSpace(State.Name)) + State.Name = SelectedPlugin.Name; + if (string.IsNullOrWhiteSpace(State.Summary)) + State.Summary = SelectedPlugin.Description ?? ""; + + if (State.EntryId == null) + State.ChangeScreen(); + else + State.ChangeScreen(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs index bf9ea0f6e..678aea918 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs @@ -6,6 +6,7 @@ using System.Reactive.Disposables; using Artemis.UI.Extensions; using Artemis.UI.Screens.Workshop.Entries; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout; +using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; using Artemis.WebClient.Workshop; using DynamicData; @@ -37,19 +38,14 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel // Apply what's there so far ApplyToState(); - switch (State.EntryType) - { - case EntryType.Layout: - State.ChangeScreen(); - break; - case EntryType.Plugin: - break; - case EntryType.Profile: - State.ChangeScreen(); - break; - default: - throw new ArgumentOutOfRangeException(); - } + if (State.EntryType == EntryType.Layout) + State.ChangeScreen(); + else if (State.EntryType == EntryType.Plugin) + State.ChangeScreen(); + else if (State.EntryType == EntryType.Profile) + State.ChangeScreen(); + else + throw new ArgumentOutOfRangeException(); } private void ExecuteContinue() diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/EntryUploadHandlerFactory.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/EntryUploadHandlerFactory.cs index 809c1c16b..8bc2621ea 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/EntryUploadHandlerFactory.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/EntryUploadHandlerFactory.cs @@ -15,6 +15,7 @@ public class EntryUploadHandlerFactory { return entryType switch { + EntryType.Plugin => _container.Resolve(), EntryType.Profile => _container.Resolve(), EntryType.Layout => _container.Resolve(), _ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.") diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntrySource.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntrySource.cs new file mode 100644 index 000000000..553a7f394 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntrySource.cs @@ -0,0 +1,15 @@ +using Artemis.Core; + +namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers; + +public class PluginEntrySource : IEntrySource +{ + public PluginEntrySource(PluginInfo pluginInfo, string path) + { + PluginInfo = pluginInfo; + Path = path; + } + + public PluginInfo PluginInfo { get; set; } + public string Path { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs new file mode 100644 index 000000000..50fa0a283 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs @@ -0,0 +1,40 @@ +using System.Net.Http.Headers; +using Artemis.WebClient.Workshop.Entities; +using Newtonsoft.Json; + +namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers; + +public class PluginEntryUploadHandler : IEntryUploadHandler +{ + private readonly IHttpClientFactory _httpClientFactory; + + public PluginEntryUploadHandler(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + /// + public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken) + { + if (entrySource is not PluginEntrySource source) + throw new InvalidOperationException("Can only create releases for plugins"); + + // Submit the archive + HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); + + // Construct the request + await using FileStream fileStream = File.Open(source.Path, FileMode.Open); + MultipartFormDataContent content = new(); + StreamContent streamContent = new(fileStream); + 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