From 6b4ed48d05e2ce14cbdf22946f01c5a824621074 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 30 Mar 2024 17:55:51 +0100 Subject: [PATCH] Workshop - Reworked installation management --- .../Routable/RoutableScreenOfTParam.cs | 30 ++++- .../Routing/Router/Router.cs | 27 ++++- src/Artemis.UI/Artemis.UI.csproj | 1 + src/Artemis.UI/Routing/Routes.cs | 2 + .../Entries/Details/EntryInfoView.axaml | 5 +- .../Entries/Details/EntryInfoView.axaml.cs | 3 +- .../Entries/Details/EntryInfoViewModel.cs | 33 +++++- .../Entries/List/EntryListItemView.axaml | 4 +- .../EntryReleases/EntryReleaseItemView.axaml | 34 ++++++ .../EntryReleaseItemView.axaml.cs | 11 ++ .../EntryReleaseItemViewModel.cs | 41 +++++++ .../EntryReleases/EntryReleaseView.axaml | 19 ++-- .../EntryReleases/EntryReleaseViewModel.cs | 30 ++++- .../EntryReleases/EntryReleasesView.axaml | 30 +---- .../EntryReleases/EntryReleasesViewModel.cs | 16 +-- .../Workshop/Layout/LayoutDetailsViewModel.cs | 37 ------- .../Workshop/Layout/LayoutManageView.axaml | 46 ++++++++ .../Workshop/Layout/LayoutManageView.axaml.cs | 14 +++ .../Workshop/Layout/LayoutManageViewModel.cs | 103 ++++++++++++++++++ .../Plugins/Dialogs/PluginDialogView.axaml | 19 ---- .../Plugins/Dialogs/PluginDialogView.axaml.cs | 11 -- .../Plugins/Dialogs/PluginDialogViewModel.cs | 23 ---- .../Plugins/PluginDetailsViewModel.cs | 42 ------- .../Workshop/Plugins/PluginManageView.axaml | 33 ++++++ .../Plugins/PluginManageView.axaml.cs | 14 +++ .../Workshop/Plugins/PluginManageViewModel.cs | 77 +++++++++++++ .../Services/Interfaces/IWorkshopService.cs | 2 + .../Services/WorkshopService.cs | 4 + 28 files changed, 514 insertions(+), 197 deletions(-) create mode 100644 src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemViewModel.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs delete mode 100644 src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml delete mode 100644 src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml.cs delete mode 100644 src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogViewModel.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Plugins/PluginManageViewModel.cs diff --git a/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs b/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs index bbe71fe2a..52e8a5e48 100644 --- a/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs +++ b/src/Artemis.UI.Shared/Routing/Routable/RoutableScreenOfTParam.cs @@ -13,6 +13,11 @@ namespace Artemis.UI.Shared.Routing; /// The type of parameters the screen expects. It must have a parameterless constructor. public abstract class RoutableScreen : RoutableScreen, IRoutableScreen where TParam : new() { + /// + /// Gets or sets the parameter source of the screen. + /// + protected ParameterSource ParameterSource { get; set; } = ParameterSource.Segment; + /// /// Called while navigating to this screen. /// @@ -26,15 +31,16 @@ public abstract class RoutableScreen : RoutableScreen, IRoutableScreen w { return Task.CompletedTask; } - + async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken) { Func activator = GetParameterActivator(); - if (args.SegmentParameters.Length != _parameterPropertyCount) - throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {args.SegmentParameters.Length}."); + object[] routeParameters = ParameterSource == ParameterSource.Segment ? args.SegmentParameters : args.RouteParameters; + if (routeParameters.Length != _parameterPropertyCount) + throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {routeParameters.Length}."); - TParam parameters = activator(args.SegmentParameters); + TParam parameters = activator(routeParameters); await OnNavigating(args, cancellationToken); await OnNavigating(parameters, args, cancellationToken); } @@ -97,4 +103,20 @@ public abstract class RoutableScreen : RoutableScreen, IRoutableScreen w } #endregion +} + +/// +/// Enum representing the source of parameters in the RoutableScreen class. +/// +public enum ParameterSource +{ + /// + /// Represents the source where parameters are obtained from the segment of the route. + /// + Segment, + + /// + /// Represents the source where parameters are obtained from the entire route. + /// + Route } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Routing/Router/Router.cs b/src/Artemis.UI.Shared/Routing/Router/Router.cs index 3f1263a29..269d51fce 100644 --- a/src/Artemis.UI.Shared/Routing/Router/Router.cs +++ b/src/Artemis.UI.Shared/Routing/Router/Router.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; using Artemis.Core; @@ -72,7 +73,13 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable /// public async Task Navigate(string path, RouterNavigationOptions? options = null) { - path = path.ToLower().Trim(' ', '/', '\\'); + if (path.StartsWith('/') && _currentRouteSubject.Value != null) + path = _currentRouteSubject.Value + path; + if (path.StartsWith("../") && _currentRouteSubject.Value != null) + path = NavigateUp(_currentRouteSubject.Value, path); + else + path = path.ToLower().Trim(' ', '/', '\\'); + options ??= new RouterNavigationOptions(); // Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself @@ -216,6 +223,24 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable _logger.Debug("Router disposed, should that be? Stacktrace: \r\n{StackTrace}", Environment.StackTrace); } + + + private string NavigateUp(string current, string path) + { + string[] pathParts = current.Split('/'); + string[] navigateParts = path.Split('/'); + int upCount = navigateParts.TakeWhile(part => part == "..").Count(); + + if (upCount >= pathParts.Length) + { + throw new InvalidOperationException("Cannot navigate up beyond the root"); + } + + IEnumerable remainingCurrentPathParts = pathParts.Take(pathParts.Length - upCount); + IEnumerable remainingNavigatePathParts = navigateParts.Skip(upCount); + + return string.Join("/", remainingCurrentPathParts.Concat(remainingNavigatePathParts)); + } private void MainWindowServiceOnMainWindowOpened(object? sender, EventArgs e) { diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index c52465960..1714435f1 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -65,5 +65,6 @@ + \ No newline at end of file diff --git a/src/Artemis.UI/Routing/Routes.cs b/src/Artemis.UI/Routing/Routes.cs index e64c17868..cac359e3d 100644 --- a/src/Artemis.UI/Routing/Routes.cs +++ b/src/Artemis.UI/Routing/Routes.cs @@ -30,6 +30,7 @@ namespace Artemis.UI.Routing new RouteRegistration("entries", [ new RouteRegistration("plugins", [ new RouteRegistration("details/{entryId:long}", [ + new RouteRegistration("manage"), new RouteRegistration("releases/{releaseId:long}") ]) ]), @@ -40,6 +41,7 @@ namespace Artemis.UI.Routing ]), new RouteRegistration("layouts", [ new RouteRegistration("details/{entryId:long}", [ + new RouteRegistration("manage"), new RouteRegistration("releases/{releaseId:long}") ]) ]) diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml index 2d0ecbffa..52f1b8dd3 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml @@ -32,7 +32,6 @@ - Updated + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs index 5c6ec0a0c..2d9a96dc7 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs @@ -1,10 +1,11 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Workshop.Entries.Details; -public partial class EntryInfoView : UserControl +public partial class EntryInfoView : ReactiveUserControl { public EntryInfoView() { diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs index cd37606ea..8fc0f1b33 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs @@ -1,29 +1,54 @@ using System; using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Shared; +using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; +using ReactiveUI; namespace Artemis.UI.Screens.Workshop.Entries.Details; -public class EntryInfoViewModel : ViewModelBase +public partial class EntryInfoViewModel : ActivatableViewModelBase { + private readonly IRouter _router; private readonly INotificationService _notificationService; - public IEntryDetails Entry { get; } - public DateTimeOffset? UpdatedAt { get; } + [Notify] private bool _canBeManaged; - public EntryInfoViewModel(IEntryDetails entry, INotificationService notificationService) + public EntryInfoViewModel(IEntryDetails entry, IRouter router, INotificationService notificationService, IWorkshopService workshopService) { + _router = router; _notificationService = notificationService; Entry = entry; UpdatedAt = Entry.Releases.Any() ? Entry.Releases.Max(r => r.CreatedAt) : Entry.CreatedAt; + CanBeManaged = Entry.EntryType != EntryType.Profile && workshopService.GetInstalledEntry(entry.Id) != null; + + this.WhenActivated(d => + { + Observable.FromEventPattern(x => workshopService.OnInstalledEntrySaved += x, x => workshopService.OnInstalledEntrySaved -= x) + .StartWith([]) + .Subscribe(_ => CanBeManaged = Entry.EntryType != EntryType.Profile && workshopService.GetInstalledEntry(entry.Id) != null) + .DisposeWith(d); + }); } + public IEntryDetails Entry { get; } + public DateTimeOffset? UpdatedAt { get; } + public async Task CopyShareLink() { await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}"); _notificationService.CreateNotification().WithTitle("Copied share link to clipboard.").Show(); } + + public async Task GoToManage() + { + await _router.Navigate("/manage"); + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml index 91932033a..712d7fc68 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml @@ -79,11 +79,11 @@ - + installed - + update available diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml new file mode 100644 index 000000000..1a328d64a --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + Created + + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml.cs new file mode 100644 index 000000000..ffc325a79 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.EntryReleases; + +public partial class EntryReleaseItemView : ReactiveUserControl +{ + public EntryReleaseItemView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemViewModel.cs new file mode 100644 index 000000000..bafa65507 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseItemViewModel.cs @@ -0,0 +1,41 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.UI.Shared; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.EntryReleases; + +public partial class EntryReleaseItemViewModel : ActivatableViewModelBase +{ + private readonly IWorkshopService _workshopService; + private readonly IEntryDetails _entry; + [Notify] private bool _isCurrentVersion; + + public EntryReleaseItemViewModel(IWorkshopService workshopService, IEntryDetails entry, IRelease release) + { + _workshopService = workshopService; + _entry = entry; + + Release = release; + UpdateIsCurrentVersion(); + + this.WhenActivated(d => + { + Observable.FromEventPattern(x => _workshopService.OnInstalledEntrySaved += x, x => _workshopService.OnInstalledEntrySaved -= x) + .Subscribe(_ => UpdateIsCurrentVersion()) + .DisposeWith(d); + }); + } + + public IRelease Release { get; } + + private void UpdateIsCurrentVersion() + { + IsCurrentVersion = _workshopService.GetInstalledEntry(_entry.Id)?.ReleaseId == Release.Id; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml index 6bdededd8..3185f1a05 100644 --- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseView.axaml @@ -46,7 +46,7 @@ Release info - + - - + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs index a44cc6e8d..32b1de42a 100644 --- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseViewModel.cs @@ -8,6 +8,7 @@ using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop.Handlers.InstallationHandlers; +using Artemis.WebClient.Workshop.Services; using PropertyChanged.SourceGenerator; using StrawberryShake; @@ -19,21 +20,25 @@ public partial class EntryReleaseViewModel : RoutableScreen _progress = new(); [Notify] private IGetReleaseById_Release? _release; [Notify] private float _installProgress; [Notify] private bool _installationInProgress; + [Notify] private bool _isCurrentVersion; private CancellationTokenSource? _cts; - public EntryReleaseViewModel(IWorkshopClient client, IRouter router, INotificationService notificationService, IWindowService windowService, EntryInstallationHandlerFactory factory) + public EntryReleaseViewModel(IWorkshopClient client, IRouter router, INotificationService notificationService, IWindowService windowService, IWorkshopService workshopService, + EntryInstallationHandlerFactory factory) { _client = client; _router = router; _notificationService = notificationService; _windowService = windowService; + _workshopService = workshopService; _factory = factory; _progress.ProgressChanged += (_, f) => InstallProgress = f.ProgressPercentage; } @@ -56,18 +61,32 @@ public partial class EntryReleaseViewModel : RoutableScreen result = await _client.GetReleaseById.ExecuteAsync(parameters.ReleaseId, cancellationToken); Release = result.Data?.Release; + IsCurrentVersion = Release != null && _workshopService.GetInstalledEntry(Release.Entry.Id)?.ReleaseId == Release.Id; } #region Overrides of RoutableScreen diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml index cf62f0e5b..0e0346d83 100644 --- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesView.axaml @@ -2,38 +2,12 @@ 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:converters="clr-namespace:Artemis.UI.Converters" - xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" - xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:workshop="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop" xmlns:entryReleases="clr-namespace:Artemis.UI.Screens.Workshop.EntryReleases" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Workshop.EntryReleases.EntryReleasesView" - x:DataType="entryReleases:EntryReleasesViewModel"> - - - - - - + x:DataType="entryReleases:EntryReleasesViewModel"> Releases - - - - - - - - - - Created - - - - - - - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs index c21b841dc..6b2b344f8 100644 --- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleasesViewModel.cs @@ -16,38 +16,34 @@ namespace Artemis.UI.Screens.Workshop.EntryReleases; public partial class EntryReleasesViewModel : ActivatableViewModelBase { private readonly IRouter _router; - [Notify] private IRelease? _selectedRelease; + [Notify] private EntryReleaseItemViewModel? _selectedRelease; - public EntryReleasesViewModel(IEntryDetails entry, IRouter router) + public EntryReleasesViewModel(IEntryDetails entry, IRouter router, Func getEntryReleaseItemViewModel) { _router = router; Entry = entry; - Releases = Entry.Releases.OrderByDescending(r => r.CreatedAt).Take(5).Cast().ToList(); + Releases = Entry.Releases.OrderByDescending(r => r.CreatedAt).Take(5).Select(r => getEntryReleaseItemViewModel(r)).ToList(); NavigateToRelease = ReactiveCommand.CreateFromTask(ExecuteNavigateToRelease); this.WhenActivated(d => { router.CurrentPath.Subscribe(p => SelectedRelease = p != null && p.Contains("releases") && float.TryParse(p.Split('/').Last(), out float releaseId) - ? Releases.FirstOrDefault(r => r.Id == releaseId) + ? Releases.FirstOrDefault(r => r.Release.Id == releaseId) : null) .DisposeWith(d); this.WhenAnyValue(vm => vm.SelectedRelease) .WhereNotNull() - .Subscribe(s => ExecuteNavigateToRelease(s)) + .Subscribe(s => ExecuteNavigateToRelease(s.Release)) .DisposeWith(d); }); } public IEntryDetails Entry { get; } - public List Releases { get; } - + public List Releases { get; } public ReactiveCommand NavigateToRelease { get; } - public Func> OnInstallationStarted { get; set; } - public Func? OnInstallationFinished { get; set; } - private async Task ExecuteNavigateToRelease(IRelease release) { switch (Entry.EntryType) diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs index c03896bf8..f4d797310 100644 --- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs @@ -1,20 +1,11 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Artemis.Core; -using Artemis.Core.Services; using Artemis.UI.Screens.Workshop.Entries.Details; using Artemis.UI.Screens.Workshop.EntryReleases; -using Artemis.UI.Screens.Workshop.Layout.Dialogs; using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Shared.Routing; -using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; -using Artemis.WebClient.Workshop.Models; -using Artemis.WebClient.Workshop.Services; using PropertyChanged.SourceGenerator; using StrawberryShake; @@ -23,8 +14,6 @@ namespace Artemis.UI.Screens.Workshop.Layout; public partial class LayoutDetailsViewModel : RoutableHostScreen { private readonly IWorkshopClient _client; - private readonly IDeviceService _deviceService; - private readonly IWindowService _windowService; private readonly Func _getEntryInfoViewModel; private readonly Func _getEntryReleasesViewModel; private readonly Func _getEntryImagesViewModel; @@ -34,16 +23,12 @@ public partial class LayoutDetailsViewModel : RoutableHostScreen getEntryInfoViewModel, Func getEntryReleasesViewModel, Func getEntryImagesViewModel) { _client = client; - _deviceService = deviceService; - _windowService = windowService; _getEntryInfoViewModel = getEntryInfoViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel; @@ -70,28 +55,6 @@ public partial class LayoutDetailsViewModel : RoutableHostScreen devices = _deviceService.Devices.Where(d => d.RgbDevice.DeviceInfo.DeviceType == layout.RgbLayout.Type).ToList(); - - // If any are found, offer to apply - if (devices.Any()) - { - await _windowService.CreateContentDialog() - .WithTitle("Apply layout to devices") - .WithViewModel(out DeviceSelectionDialogViewModel vm, devices, installedEntry) - .WithCloseButtonText(null) - .HavingPrimaryButton(b => b.WithText("Continue").WithCommand(vm.Apply)) - .ShowAsync(); - } - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml new file mode 100644 index 000000000..af6b25fc2 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml @@ -0,0 +1,46 @@ + + + + + + Manage layout + + + + + + This layout is made for devices of type + . + Unfortunately, none were detected. + + + + Select the devices on which you would like to apply the downloaded layout. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml.cs new file mode 100644 index 000000000..bc26260c9 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Layout; + +public partial class LayoutManageView : ReactiveUserControl +{ + public LayoutManageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs new file mode 100644 index 000000000..9ce907d57 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutManageViewModel.cs @@ -0,0 +1,103 @@ +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.DryIoc.Factories; +using Artemis.UI.Screens.SurfaceEditor; +using Artemis.UI.Screens.Workshop.Parameters; +using Artemis.UI.Shared.Routing; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Providers; +using Artemis.WebClient.Workshop.Services; +using Avalonia.Threading; +using PropertyChanged.SourceGenerator; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Layout; + +public partial class LayoutManageViewModel : RoutableScreen +{ + private readonly ISurfaceVmFactory _surfaceVmFactory; + private readonly IRouter _router; + private readonly IWorkshopService _workshopService; + private readonly IDeviceService _deviceService; + private readonly WorkshopLayoutProvider _layoutProvider; + private readonly IWindowService _windowService; + [Notify] private ArtemisLayout? _layout; + [Notify] private InstalledEntry? _entry; + [Notify] private ObservableCollection? _devices; + + public LayoutManageViewModel(ISurfaceVmFactory surfaceVmFactory, + IRouter router, + IWorkshopService workshopService, + IDeviceService deviceService, + WorkshopLayoutProvider layoutProvider, + IWindowService windowService) + { + _surfaceVmFactory = surfaceVmFactory; + _router = router; + _workshopService = workshopService; + _deviceService = deviceService; + _layoutProvider = layoutProvider; + _windowService = windowService; + Apply = ReactiveCommand.Create(ExecuteApply); + ParameterSource = ParameterSource.Route; + } + + public ReactiveCommand Apply { get; } + + public async Task Close() + { + await _router.GoUp(); + } + + public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + { + InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(parameters.EntryId); + if (installedEntry == null) + { + // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens + Dispatcher.UIThread.InvokeAsync(async () => + { + await _windowService.ShowConfirmContentDialog("Entry not found", "The entry you're trying to manage could not be found.", "Go back", null); + await Close(); + }); + return; + } + + Layout = new ArtemisLayout(Path.Combine(installedEntry.GetReleaseDirectory().FullName, "layout.xml")); + if (!Layout.IsValid) + { + // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens + Dispatcher.UIThread.InvokeAsync(async () => + { + await _windowService.ShowConfirmContentDialog("Invalid layout", "The layout of the entry you're trying to manage is invalid.", "Go back", null); + await Close(); + }); + return; + } + + Entry = installedEntry; + Devices = new ObservableCollection(_deviceService.Devices + .Where(d => d.RgbDevice.DeviceInfo.DeviceType == Layout.RgbLayout.Type) + .Select(_surfaceVmFactory.ListDeviceViewModel)); + } + + private void ExecuteApply() + { + if (Devices == null) + return; + + foreach (ListDeviceViewModel listDeviceViewModel in Devices.Where(d => d.IsSelected)) + { + _layoutProvider.ConfigureDevice(listDeviceViewModel.Device, Entry); + _deviceService.SaveDevice(listDeviceViewModel.Device); + _deviceService.LoadDeviceLayout(listDeviceViewModel.Device); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml b/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml deleted file mode 100644 index 3c0336a37..000000000 --- a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - Plugin features - - - - - diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml.cs deleted file mode 100644 index 19329553e..000000000 --- a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.ReactiveUI; - -namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs; - -public partial class PluginDialogView : ReactiveUserControl -{ - public PluginDialogView() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogViewModel.cs deleted file mode 100644 index 863417326..000000000 --- a/src/Artemis.UI/Screens/Workshop/Plugins/Dialogs/PluginDialogViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive.Linq; -using Artemis.Core; -using Artemis.UI.DryIoc.Factories; -using Artemis.UI.Screens.Plugins; -using Artemis.UI.Screens.Plugins.Features; -using Artemis.UI.Shared; -using ReactiveUI; - -namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs; - -public class PluginDialogViewModel : ContentDialogViewModelBase -{ - public PluginDialogViewModel(Plugin plugin, ISettingsVmFactory settingsVmFactory) - { - PluginViewModel = settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => {}, Observable.Empty())); - PluginFeatures = new ObservableCollection(plugin.Features.Select(f => settingsVmFactory.PluginFeatureViewModel(f, false))); - } - - public PluginViewModel PluginViewModel { get; } - public ObservableCollection PluginFeatures { get; } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs index a1b6e0939..cfab9166c 100644 --- a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs @@ -1,20 +1,13 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Artemis.Core; -using Artemis.Core.Services; using Artemis.UI.Screens.Workshop.Entries.Details; using Artemis.UI.Screens.Workshop.Entries.List; using Artemis.UI.Screens.Workshop.EntryReleases; using Artemis.UI.Screens.Workshop.Parameters; -using Artemis.UI.Screens.Workshop.Plugins.Dialogs; using Artemis.UI.Shared.Routing; -using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; -using Artemis.WebClient.Workshop.Models; using PropertyChanged.SourceGenerator; using StrawberryShake; @@ -23,8 +16,6 @@ namespace Artemis.UI.Screens.Workshop.Plugins; public partial class PluginDetailsViewModel : RoutableHostScreen { private readonly IWorkshopClient _client; - private readonly IWindowService _windowService; - private readonly IPluginManagementService _pluginManagementService; private readonly Func _getEntryInfoViewModel; private readonly Func _getEntryReleasesViewModel; private readonly Func _getEntryImagesViewModel; @@ -35,16 +26,12 @@ public partial class PluginDetailsViewModel : RoutableHostScreen? _dependants; public PluginDetailsViewModel(IWorkshopClient client, - IWindowService windowService, - IPluginManagementService pluginManagementService, PluginDescriptionViewModel pluginDescriptionViewModel, Func getEntryInfoViewModel, Func getEntryReleasesViewModel, Func getEntryImagesViewModel) { _client = client; - _windowService = windowService; - _pluginManagementService = pluginManagementService; _getEntryInfoViewModel = getEntryInfoViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel; @@ -72,35 +59,6 @@ public partial class PluginDetailsViewModel : RoutableHostScreen OnInstallationStarted(IEntryDetails entryDetails, IRelease release) - { - bool confirm = await _windowService.ShowConfirmContentDialog( - "Installing plugin", - $"You are about to install version {release.Version} of {entryDetails.Name}. \r\n\r\n" + - "Plugins are NOT verified by Artemis and could harm your PC, if you have doubts about a plugin please ask on Discord!", - "I trust this plugin, install it" - ); - - return !confirm; - } - - private async Task OnInstallationFinished(InstalledEntry installedEntry) - { - if (!installedEntry.TryGetMetadata("PluginId", out Guid pluginId)) - return; - Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId); - if (plugin == null) - return; - - await _windowService.CreateContentDialog().WithTitle("Manage plugin").WithViewModel(out PluginDialogViewModel _, plugin).WithFullScreen().ShowAsync(); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml new file mode 100644 index 000000000..c6fe8c059 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml @@ -0,0 +1,33 @@ + + + + + + Manage plugin + + + + + + + + + + Plugin features + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml.cs new file mode 100644 index 000000000..d5136c54e --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Plugins; + +public partial class PluginManageView : ReactiveUserControl +{ + public PluginManageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageViewModel.cs new file mode 100644 index 000000000..9bd558217 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginManageViewModel.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.DryIoc.Factories; +using Artemis.UI.Screens.Plugins; +using Artemis.UI.Screens.Plugins.Features; +using Artemis.UI.Screens.Workshop.Parameters; +using Artemis.UI.Shared.Routing; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using Avalonia.Threading; +using PropertyChanged.SourceGenerator; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Plugins; + +public partial class PluginManageViewModel : RoutableScreen +{ + private readonly ISettingsVmFactory _settingsVmFactory; + private readonly IRouter _router; + private readonly IWorkshopService _workshopService; + private readonly IPluginManagementService _pluginManagementService; + private readonly IWindowService _windowService; + [Notify] private PluginViewModel? _pluginViewModel; + [Notify] private ObservableCollection? _pluginFeatures; + + public PluginManageViewModel(ISettingsVmFactory settingsVmFactory, IRouter router, IWorkshopService workshopService, IPluginManagementService pluginManagementService, IWindowService windowService) + { + _settingsVmFactory = settingsVmFactory; + _router = router; + _workshopService = workshopService; + _pluginManagementService = pluginManagementService; + _windowService = windowService; + ParameterSource = ParameterSource.Route; + } + + public async Task Close() + { + await _router.GoUp(); + } + + /// + public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + { + InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(parameters.EntryId); + if (installedEntry == null || !installedEntry.TryGetMetadata("PluginId", out Guid pluginId)) + { + // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens + Dispatcher.UIThread.InvokeAsync(async () => + { + await _windowService.ShowConfirmContentDialog("Invalid plugin", "The plugin you're trying to manage is invalid or doesn't exist", "Go back", null); + await Close(); + }); + return; + } + + Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId); + if (plugin == null) + { + // TODO: Fix cancelling without this workaround, currently navigation is stopped but the page still opens + Dispatcher.UIThread.InvokeAsync(async () => + { + await _windowService.ShowConfirmContentDialog("Invalid plugin", "The plugin you're trying to manage is invalid or doesn't exist", "Go back", null); + await Close(); + }); + return; + } + + PluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { })); + PluginFeatures = new ObservableCollection(plugin.Features.Select(f => _settingsVmFactory.PluginFeatureViewModel(f, false))); + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs index ea61b3b6d..304f4ef05 100644 --- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs @@ -20,4 +20,6 @@ public interface IWorkshopService void Initialize(); public record WorkshopStatus(bool IsReachable, string Message); + + event EventHandler? OnInstalledEntrySaved; } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs index 95b90ccc5..715a5a1bb 100644 --- a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs @@ -172,6 +172,8 @@ public class WorkshopService : IWorkshopService { entry.Save(); _entryRepository.Save(entry.Entity); + + OnInstalledEntrySaved?.Invoke(this, entry); } /// @@ -231,4 +233,6 @@ public class WorkshopService : IWorkshopService _logger.Warning(e, "Failed to remove orphaned workshop entry at {Directory}", directory); } } + + public event EventHandler? OnInstalledEntrySaved; } \ No newline at end of file