From d9df443970f6ddfe19c3f4f15bbfe9d2913a2e1f Mon Sep 17 00:00:00 2001 From: RobertBeekman Date: Wed, 14 Feb 2024 23:04:19 +0100 Subject: [PATCH 1/4] 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 From 21b8112de58deb72d8c2d886671a516446b9f4da Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 15 Feb 2024 22:57:33 +0100 Subject: [PATCH 2/4] Workshop - Implemented plugin browsing, installation and removal --- src/Artemis.UI/Artemis.UI.csproj | 20 --- src/Artemis.UI/ArtemisBootstrapper.cs | 1 + src/Artemis.UI/Routing/Routes.cs | 3 + src/Artemis.UI/Screens/Root/RootViewModel.cs | 5 + .../Screens/Sidebar/SidebarViewModel.cs | 1 + .../Workshop/Entries/EntriesViewModel.cs | 3 +- .../Entries/List/EntryListItemViewModel.cs | 1 + .../Entries/Tabs/PluginListView.axaml | 65 ++++++++++ .../Entries/Tabs/PluginListView.axaml.cs | 14 ++ .../Entries/Tabs/PluginListViewModel.cs | 34 +++++ .../Entries/Tabs/ProfileListViewModel.cs | 2 +- .../Workshop/Home/WorkshopHomeView.axaml | 8 ++ .../Workshop/Plugin/PluginDetailsView.axaml | 30 +++++ .../Plugin/PluginDetailsView.axaml.cs | 14 ++ .../Workshop/Plugin/PluginDetailsViewModel.cs | 59 +++++++++ .../EntryInstallationHandlerFactory.cs | 1 + .../EntryUninstallResult.cs | 5 +- .../PluginEntryInstallationHandler.cs | 122 ++++++++++++++++++ .../Services/Interfaces/IWorkshopService.cs | 1 + .../Services/WorkshopService.cs | 48 ++++++- .../WorkshopConstants.cs | 8 +- 21 files changed, 410 insertions(+), 35 deletions(-) create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Plugin/PluginDetailsView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Plugin/PluginDetailsView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Plugin/PluginDetailsViewModel.cs create mode 100644 src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 3f5b1f056..0048cffb7 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -35,24 +35,4 @@ - - - - PluginSelectionStepView.axaml - Code - - - ProfileAdaptionHintsStepView.axaml - Code - - - ProfileSelectionStepView.axaml - Code - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/ArtemisBootstrapper.cs b/src/Artemis.UI/ArtemisBootstrapper.cs index be365cd9f..f869bb8e3 100644 --- a/src/Artemis.UI/ArtemisBootstrapper.cs +++ b/src/Artemis.UI/ArtemisBootstrapper.cs @@ -13,6 +13,7 @@ using Artemis.UI.Shared.Services; using Artemis.VisualScripting.DryIoc; using Artemis.WebClient.Updating.DryIoc; using Artemis.WebClient.Workshop.DryIoc; +using Artemis.WebClient.Workshop.Services; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; diff --git a/src/Artemis.UI/Routing/Routes.cs b/src/Artemis.UI/Routing/Routes.cs index 1d4862f44..8e26f8d3b 100644 --- a/src/Artemis.UI/Routing/Routes.cs +++ b/src/Artemis.UI/Routing/Routes.cs @@ -12,6 +12,7 @@ using Artemis.UI.Screens.Workshop.Home; using Artemis.UI.Screens.Workshop.Layout; using Artemis.UI.Screens.Workshop.Library; using Artemis.UI.Screens.Workshop.Library.Tabs; +using Artemis.UI.Screens.Workshop.Plugin; using Artemis.UI.Screens.Workshop.Profile; using Artemis.UI.Shared.Routing; @@ -32,6 +33,8 @@ public static class Routes { Children = new List { + new RouteRegistration("plugins/{page:int}"), + new RouteRegistration("plugins/details/{entryId:long}"), new RouteRegistration("profiles/{page:int}"), new RouteRegistration("profiles/details/{entryId:long}"), new RouteRegistration("layouts/{page:int}"), diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index e1fb3eeb9..ce83f0742 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -13,6 +13,7 @@ using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.MainWindow; +using Artemis.WebClient.Workshop.Services; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -41,6 +42,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv IMainWindowService mainWindowService, IDebugService debugService, IUpdateService updateService, + IWorkshopService workshopService, SidebarViewModel sidebarViewModel, DefaultTitleBarViewModel defaultTitleBarViewModel) { @@ -76,6 +78,9 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv { if (_updateService.Initialize()) return; + + // Before initializing the core and files become in use, clean up orphaned files + workshopService.RemoveOrphanedFiles(); coreService.Initialize(); registrationService.RegisterBuiltInDataModelDisplays(); diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs index ae9de0d2c..bb2d56f95 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs @@ -43,6 +43,7 @@ public partial class SidebarViewModel : ActivatableViewModelBase { new(MaterialIconKind.FolderVideo, "Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"), new(MaterialIconKind.KeyboardVariant, "Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts"), + new(MaterialIconKind.Plugin, "Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"), new(MaterialIconKind.Bookshelf, "Library", "workshop/library"), }), diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs index 4ee34fee4..904348bc6 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs @@ -25,7 +25,8 @@ public partial class EntriesViewModel : RoutableHostScreen Tabs = new ObservableCollection { new("Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"), - new("Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts") + new("Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts"), + new("Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"), }; this.WhenActivated(d => diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs index be57fd898..757396c72 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs @@ -34,6 +34,7 @@ public class EntryListItemViewModel : ActivatableViewModelBase await _router.Navigate($"workshop/entries/profiles/details/{Entry.Id}"); break; case EntryType.Plugin: + await _router.Navigate($"workshop/entries/plugins/details/{Entry.Id}"); break; default: throw new ArgumentOutOfRangeException(); diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml new file mode 100644 index 000000000..e52c30641 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml @@ -0,0 +1,65 @@ + + + + + + + + + + + Categories + + + + + + + + + + + + + + + + + + + + + + + + + + + Looks like your current filters gave no results + + Modify or clear your filters to view other plugins + + + + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs new file mode 100644 index 000000000..2e32eea93 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Entries.Tabs; + +public partial class PluginListView : ReactiveUserControl +{ + public PluginListView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs new file mode 100644 index 000000000..206697490 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs @@ -0,0 +1,34 @@ +using System; +using Artemis.UI.Screens.Workshop.Categories; +using Artemis.UI.Screens.Workshop.Entries.List; +using Artemis.UI.Shared.Routing; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Workshop; + +namespace Artemis.UI.Screens.Workshop.Entries.Tabs; + +public class PluginListViewModel : EntryListViewModel +{ + public PluginListViewModel(IWorkshopClient workshopClient, + IRouter router, + CategoriesViewModel categoriesViewModel, + EntryListInputViewModel entryListInputViewModel, + INotificationService notificationService, + Func getEntryListViewModel) + : base("workshop/entries/plugins", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) + { + entryListInputViewModel.SearchWatermark = "Search plugins"; + } + + protected override EntryFilterInput GetFilter() + { + return new EntryFilterInput + { + And = new[] + { + new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Plugin}}, + base.GetFilter() + } + }; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs index c09a587a3..f251ee10d 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs @@ -12,7 +12,7 @@ public class ProfileListViewModel : List.EntryListViewModel public ProfileListViewModel(IWorkshopClient workshopClient, IRouter router, CategoriesViewModel categoriesViewModel, - List.EntryListInputViewModel entryListInputViewModel, + EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, Func getEntryListViewModel) : base("workshop/entries/profiles", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml index 433f87c50..4bc7bd2a4 100644 --- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml @@ -57,6 +57,14 @@ + + - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -46,7 +51,7 @@ - + @@ -66,8 +71,8 @@ + + Description + Created at + Expires at + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/AccountTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/AccountTabViewModel.cs index 0e361deb3..afab57079 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/AccountTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/AccountTabViewModel.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Linq; +using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading; @@ -12,6 +15,7 @@ using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop.Handlers.UploadHandlers; +using Artemis.WebClient.Workshop.Models; using Artemis.WebClient.Workshop.Services; using IdentityModel; using PropertyChanged.SourceGenerator; @@ -29,6 +33,7 @@ public partial class AccountTabViewModel : RoutableScreen [Notify(Setter.Private)] private string? _name; [Notify(Setter.Private)] private string? _email; [Notify(Setter.Private)] private string? _avatarUrl; + [Notify(Setter.Private)] private ObservableCollection _personalAccessTokens = new(); public AccountTabViewModel(IWindowService windowService, IAuthenticationService authenticationService, IUserManagementService userManagementService) { @@ -36,10 +41,11 @@ public partial class AccountTabViewModel : RoutableScreen _authenticationService = authenticationService; _userManagementService = userManagementService; _authenticationService.AutoLogin(true); - + DisplayName = "Account"; IsLoggedIn = _authenticationService.IsLoggedIn; - + DeleteToken = ReactiveCommand.CreateFromTask(ExecuteDeleteToken); + this.WhenActivated(d => { _canChangePassword = _authenticationService.GetClaim(JwtClaimTypes.AuthenticationMethod).Select(c => c?.Value == "pwd").ToProperty(this, vm => vm.CanChangePassword); @@ -50,12 +56,13 @@ public partial class AccountTabViewModel : RoutableScreen public bool CanChangePassword => _canChangePassword?.Value ?? false; public IObservable IsLoggedIn { get; } + public ReactiveCommand DeleteToken { get; } public async Task Login() { await _windowService.CreateContentDialog().WithViewModel(out WorkshopLoginViewModel _).WithTitle("Account login").ShowAsync(); } - + public async Task ChangeAvatar() { string[]? result = await _windowService.CreateOpenFileDialog().HavingFilter(f => f.WithBitmaps()).ShowAsync(); @@ -85,7 +92,7 @@ public partial class AccountTabViewModel : RoutableScreen .HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit)) .ShowAsync(); } - + public async Task ChangePasswordAddress() { await _windowService.CreateContentDialog().WithTitle("Change password") @@ -94,7 +101,7 @@ public partial class AccountTabViewModel : RoutableScreen .HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit)) .ShowAsync(); } - + public async Task RemoveAccount() { await _windowService.CreateContentDialog().WithTitle("Remove account") @@ -104,11 +111,36 @@ public partial class AccountTabViewModel : RoutableScreen .ShowAsync(); } - private void LoadCurrentUser() + public async Task GenerateToken() + { + await _windowService.CreateContentDialog().WithTitle("Create Personal Access Token") + .WithViewModel(out CreatePersonalAccessTokenViewModel vm) + .WithCloseButtonText("Cancel") + .HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit)) + .ShowAsync(); + + List personalAccessTokens = await _userManagementService.GetPersonAccessTokens(CancellationToken.None); + PersonalAccessTokens = new ObservableCollection(personalAccessTokens); + } + + private async Task ExecuteDeleteToken(PersonalAccessToken token) + { + bool confirm = await _windowService.ShowConfirmContentDialog("Delete Personal Access Token", "Are you sure you want to delete this token? Any services using it will stop working"); + if (!confirm) + return; + + await _userManagementService.DeletePersonAccessToken(token, CancellationToken.None); + PersonalAccessTokens.Remove(token); + } + + private async Task LoadCurrentUser() { string? userId = _authenticationService.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; Name = _authenticationService.Claims.FirstOrDefault(c => c.Type == "name")?.Value; Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value; AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{userId}"; + + List personalAccessTokens = await _userManagementService.GetPersonAccessTokens(CancellationToken.None); + PersonalAccessTokens = new ObservableCollection(personalAccessTokens); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs index bb2d56f95..3b7de1c0c 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs @@ -43,7 +43,7 @@ public partial class SidebarViewModel : ActivatableViewModelBase { new(MaterialIconKind.FolderVideo, "Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"), new(MaterialIconKind.KeyboardVariant, "Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts"), - new(MaterialIconKind.Plugin, "Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"), + new(MaterialIconKind.Connection, "Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"), new(MaterialIconKind.Bookshelf, "Library", "workshop/library"), }), diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs index 311164269..21be13b28 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs @@ -9,7 +9,6 @@ using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop.Handlers.InstallationHandlers; using Artemis.WebClient.Workshop.Models; -using Artemis.WebClient.Workshop.Services; using Humanizer; using ReactiveUI; @@ -29,11 +28,13 @@ public class EntryReleasesViewModel : ViewModelBase Entry = entry; DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease); + OnInstallationStarted = Confirm; } public IGetEntryById_Entry Entry { get; } public ReactiveCommand DownloadLatestRelease { get; } + public Func> OnInstallationStarted { get; set; } public Func? OnInstallationFinished { get; set; } private async Task ExecuteDownloadLatestRelease(CancellationToken cancellationToken) @@ -41,11 +42,7 @@ public class EntryReleasesViewModel : ViewModelBase if (Entry.LatestRelease == null) return; - bool confirm = await _windowService.ShowConfirmContentDialog( - "Install latest release", - $"Are you sure you want to download and install version {Entry.LatestRelease.Version} of {Entry.Name}?" - ); - if (!confirm) + if (await OnInstallationStarted(Entry)) return; IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType); @@ -64,4 +61,14 @@ public class EntryReleasesViewModel : ViewModelBase .WithSeverity(NotificationSeverity.Error).Show(); } } + + private async Task Confirm(IEntryDetails entryDetails) + { + bool confirm = await _windowService.ShowConfirmContentDialog( + "Install latest release", + $"Are you sure you want to download and install version {entryDetails.LatestRelease?.Version} of {entryDetails.Name}?" + ); + + return !confirm; + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml index 4bc7bd2a4..c2ed7ebb8 100644 --- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml @@ -59,7 +59,7 @@ + @@ -109,16 +109,26 @@ - + + + Tokens be used to communicate with Artemis APIs without using a username and password + + + + Description Created at Expires at + + + You have no active personal access tokens. + - + - @@ -130,8 +140,8 @@ diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml index 00e7eae8f..4f5994982 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml @@ -20,7 +20,7 @@ HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Command="{CompiledBinding ViewWorkshopPage}"> - + - + + Installed - +