1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 21:38:38 +00:00

Workshop - Added plugin upload support

This commit is contained in:
RobertBeekman 2024-02-14 23:04:19 +01:00
parent 38a1318170
commit d9df443970
12 changed files with 240 additions and 15 deletions

View File

@ -597,7 +597,7 @@ internal class PluginManagementService : IPluginManagementService
using StreamReader reader = new(metaDataFileEntry.Open());
PluginInfo pluginInfo = CoreJson.DeserializeObject<PluginInfo>(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)

View File

@ -35,4 +35,24 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<Compile Update="Screens\Workshop\SubmissionWizard\Steps\Plugin\PluginSelectionStepView.axaml.cs">
<DependentUpon>PluginSelectionStepView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\SubmissionWizard\Steps\Plugin\PluginAdaptionHintsStepView.axaml.cs">
<DependentUpon>ProfileAdaptionHintsStepView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\SubmissionWizard\Steps\Plugin\PluginSelectionStepView.axaml.cs">
<DependentUpon>ProfileSelectionStepView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Screens\Workshop\SubmissionWizard\Steps\Plugin\PluginAdaptionHintsStepView.axaml" />
<AdditionalFiles Include="Screens\Workshop\SubmissionWizard\Steps\Plugin\PluginSelectionStepView.axaml" />
</ItemGroup>
</Project>

View File

@ -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()
}
};

View File

@ -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<PluginSelectionStepViewModel>();
else if (EntryType == EntryType.Profile)
ChangeScreen<ProfileSelectionStepViewModel>();
else if (EntryType == EntryType.Layout)
ChangeScreen<LayoutSelectionStepViewModel>();

View File

@ -44,5 +44,14 @@
</StackPanel>
</RadioButton.Content>
</RadioButton>
<RadioButton GroupName="EntryType"
IsChecked="{CompiledBinding SelectedEntryType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static workshop:EntryType.Plugin}}">
<RadioButton.Content>
<StackPanel>
<TextBlock>Plugin</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">A plugin that adds new features to Artemis.</TextBlock>
</StackPanel>
</RadioButton.Content>
</RadioButton>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,46 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:plugin="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin.PluginSelectionStepView"
x:DataType="plugin:PluginSelectionStepViewModel">
<Grid RowDefinitions="Auto,Auto">
<StackPanel>
<StackPanel.Styles>
<Styles>
<Style Selector="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
</Styles>
</StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}" TextWrapping="Wrap">
Plugin selection
</TextBlock>
<TextBlock TextWrapping="Wrap">
Please select the plugin you want to share, a preview will be shown below.
</TextBlock>
<Button Command="{CompiledBinding Browse}" Margin="0 20">Browse file</Button>
</StackPanel>
<Border Grid.Row="1" Classes="card" ClipToBounds="True" IsVisible="{CompiledBinding SelectedPlugin, Converter={x:Static ObjectConverters.IsNotNull}}">
<Grid RowDefinitions="30,30,30,30,Auto" ColumnDefinitions="200,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" FontWeight="SemiBold">Path</TextBlock>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{CompiledBinding Path}"></TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0" FontWeight="SemiBold">Name</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{CompiledBinding SelectedPlugin.Name}"></TextBlock>
<TextBlock Grid.Row="2" Grid.Column="0" FontWeight="SemiBold">Description</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{CompiledBinding SelectedPlugin.Description}"></TextBlock>
<TextBlock Grid.Row="3" Grid.Column="0" FontWeight="SemiBold">Main entry point</TextBlock>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{CompiledBinding SelectedPlugin.Main}"></TextBlock>
<TextBlock Grid.Row="4" Grid.Column="0" FontWeight="SemiBold">Version</TextBlock>
<TextBlock Grid.Row="4" Grid.Column="1" Text="{CompiledBinding SelectedPlugin.Version}"></TextBlock>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin;
public partial class PluginSelectionStepView : ReactiveUserControl<PluginSelectionStepViewModel>
{
public PluginSelectionStepView()
{
InitializeComponent();
}
}

View File

@ -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;
/// <inheritdoc />
public PluginSelectionStepViewModel(IWindowService windowService)
{
_windowService = windowService;
GoBack = ReactiveCommand.Create(() => State.ChangeScreen<EntryTypeStepViewModel>());
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<Unit, Unit> 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<PluginInfo>(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<SpecificationsStepViewModel>();
else
State.ChangeScreen<UploadStepViewModel>();
}
}

View File

@ -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<LayoutInfoStepViewModel>();
break;
case EntryType.Plugin:
break;
case EntryType.Profile:
State.ChangeScreen<ProfileAdaptionHintsStepViewModel>();
break;
default:
throw new ArgumentOutOfRangeException();
}
if (State.EntryType == EntryType.Layout)
State.ChangeScreen<LayoutInfoStepViewModel>();
else if (State.EntryType == EntryType.Plugin)
State.ChangeScreen<PluginSelectionStepViewModel>();
else if (State.EntryType == EntryType.Profile)
State.ChangeScreen<ProfileAdaptionHintsStepViewModel>();
else
throw new ArgumentOutOfRangeException();
}
private void ExecuteContinue()

View File

@ -15,6 +15,7 @@ public class EntryUploadHandlerFactory
{
return entryType switch
{
EntryType.Plugin => _container.Resolve<PluginEntryUploadHandler>(),
EntryType.Profile => _container.Resolve<ProfileEntryUploadHandler>(),
EntryType.Layout => _container.Resolve<LayoutEntryUploadHandler>(),
_ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.")

View File

@ -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; }
}

View File

@ -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;
}
/// <inheritdoc />
public async Task<EntryUploadResult> 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<Release>(await response.Content.ReadAsStringAsync(cancellationToken));
return release != null ? EntryUploadResult.FromSuccess(release) : EntryUploadResult.FromFailure("Failed to deserialize response");
}
}