From 28edabae89b3ad5e450fb8dfc1bfc4700d65382a Mon Sep 17 00:00:00 2001 From: RobertBeekman Date: Sun, 14 Jan 2024 12:46:51 +0100 Subject: [PATCH] Workshop - Implemented workshop installation, layout selection and removal --- .../Extensions/DirectoryInfoExtensions.cs | 31 -------------- .../Models/Surface/ArtemisDevice.cs | 18 ++++---- .../Providers/CustomPathLayoutProvider.cs | 15 +------ .../Providers/DefaultLayoutProvider.cs | 18 ++------ .../Providers/Interfaces/ILayoutProvider.cs | 6 +-- .../Providers/NoneLayoutProvider.cs | 18 ++------ src/Artemis.Core/Services/DeviceService.cs | 8 ++-- .../Services/PluginManagementService.cs | 2 +- .../LayoutProviders/CustomLayoutViewModel.cs | 18 ++++++-- .../LayoutProviders/DefaultLayoutViewModel.cs | 12 +++++- .../LayoutProviders/NoneLayoutViewModel.cs | 12 +++++- .../LayoutProviders/WorkshopLayoutView.axaml | 42 ++++++++++++++++--- .../WorkshopLayoutView.axaml.cs | 3 +- .../WorkshopLayoutViewModel.cs | 41 ++++++++++++++++-- .../Entries/Details/EntryReleasesViewModel.cs | 2 +- .../IEntryInstallationHandler.cs | 2 +- .../LayoutEntryInstallationHandler.cs | 37 ++++++++++------ .../ProfileEntryInstallationHandler.cs | 16 ++++--- .../Providers/WorkshopLayoutProvider.cs | 29 +++++-------- .../Queries/Fragments.graphql | 16 ++++--- .../Services/InstalledEntry.cs | 37 ++++++++++++---- .../Services/Interfaces/IWorkshopService.cs | 5 +-- .../Services/WorkshopService.cs | 6 +-- 23 files changed, 226 insertions(+), 168 deletions(-) delete mode 100644 src/Artemis.Core/Extensions/DirectoryInfoExtensions.cs diff --git a/src/Artemis.Core/Extensions/DirectoryInfoExtensions.cs b/src/Artemis.Core/Extensions/DirectoryInfoExtensions.cs deleted file mode 100644 index 5a6864273..000000000 --- a/src/Artemis.Core/Extensions/DirectoryInfoExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.IO; - -namespace Artemis.Core; - -internal static class DirectoryInfoExtensions -{ - public static void CopyFilesRecursively(this DirectoryInfo source, DirectoryInfo target) - { - foreach (DirectoryInfo dir in source.GetDirectories()) - CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name)); - foreach (FileInfo file in source.GetFiles()) - file.CopyTo(Path.Combine(target.FullName, file.Name)); - } - - public static void DeleteRecursively(this DirectoryInfo baseDir) - { - if (!baseDir.Exists) - return; - - foreach (DirectoryInfo dir in baseDir.EnumerateDirectories()) - DeleteRecursively(dir); - FileInfo[] files = baseDir.GetFiles(); - foreach (FileInfo file in files) - { - file.IsReadOnly = false; - file.Delete(); - } - - baseDir.Delete(); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index 34a10d0fc..6a42aacc7 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -398,14 +398,6 @@ public class ArtemisDevice : CorePropertyChanged } } - /// - /// Invokes the event - /// - protected virtual void OnDeviceUpdated() - { - DeviceUpdated?.Invoke(this, EventArgs.Empty); - } - /// /// Applies the provided layout to the device /// @@ -418,7 +410,7 @@ public class ArtemisDevice : CorePropertyChanged /// A boolean indicating whether to remove excess LEDs present in the device but missing /// in the layout /// - internal void ApplyLayout(ArtemisLayout? layout, bool createMissingLeds, bool removeExcessiveLeds) + public void ApplyLayout(ArtemisLayout? layout, bool createMissingLeds, bool removeExcessiveLeds) { if (layout != null && layout.IsValid && createMissingLeds && !DeviceProvider.CreateMissingLedsSupported) throw new ArtemisCoreException($"Cannot apply layout with {nameof(createMissingLeds)} set to true because the device provider does not support it"); @@ -445,6 +437,14 @@ public class ArtemisDevice : CorePropertyChanged CalculateRenderProperties(); } + /// + /// Invokes the event + /// + protected virtual void OnDeviceUpdated() + { + DeviceUpdated?.Invoke(this, EventArgs.Empty); + } + private void ClearLayout() { if (Layout == null) diff --git a/src/Artemis.Core/Providers/CustomPathLayoutProvider.cs b/src/Artemis.Core/Providers/CustomPathLayoutProvider.cs index 61f47119a..bd2ba228b 100644 --- a/src/Artemis.Core/Providers/CustomPathLayoutProvider.cs +++ b/src/Artemis.Core/Providers/CustomPathLayoutProvider.cs @@ -1,17 +1,8 @@ -using Artemis.Core.Services; -using RGB.NET.Layout; - -namespace Artemis.Core.Providers; +namespace Artemis.Core.Providers; public class CustomPathLayoutProvider : ILayoutProvider { public static string LayoutType = "CustomPath"; - private readonly IDeviceService _deviceService; - - public CustomPathLayoutProvider(IDeviceService deviceService) - { - _deviceService = deviceService; - } /// public ArtemisLayout? GetDeviceLayout(ArtemisDevice device) @@ -34,7 +25,7 @@ public class CustomPathLayoutProvider : ILayoutProvider } /// - /// Configures the provided device to use this layout provider. + /// Configures the provided device to use this layout provider. /// /// The device to apply the provider to. /// The path to the custom layout. @@ -42,7 +33,5 @@ public class CustomPathLayoutProvider : ILayoutProvider { device.LayoutSelection.Type = LayoutType; device.LayoutSelection.Parameter = path; - _deviceService.SaveDevice(device); - _deviceService.LoadDeviceLayout(device); } } \ No newline at end of file diff --git a/src/Artemis.Core/Providers/DefaultLayoutProvider.cs b/src/Artemis.Core/Providers/DefaultLayoutProvider.cs index 3ab899bed..8c709ab27 100644 --- a/src/Artemis.Core/Providers/DefaultLayoutProvider.cs +++ b/src/Artemis.Core/Providers/DefaultLayoutProvider.cs @@ -1,17 +1,9 @@ -using Artemis.Core.Services; - -namespace Artemis.Core.Providers; +namespace Artemis.Core.Providers; public class DefaultLayoutProvider : ILayoutProvider { public static string LayoutType = "Default"; - private readonly IDeviceService _deviceService; - public DefaultLayoutProvider(IDeviceService deviceService) - { - _deviceService = deviceService; - } - /// public ArtemisLayout? GetDeviceLayout(ArtemisDevice device) { @@ -30,22 +22,20 @@ public class DefaultLayoutProvider : ILayoutProvider else device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); } - + /// public bool IsMatch(ArtemisDevice device) { return device.LayoutSelection.Type == LayoutType; } - + /// - /// Configures the provided device to use this layout provider. + /// Configures the provided device to use this layout provider. /// /// The device to apply the provider to. public void ConfigureDevice(ArtemisDevice device) { device.LayoutSelection.Type = LayoutType; device.LayoutSelection.Parameter = null; - _deviceService.SaveDevice(device); - _deviceService.LoadDeviceLayout(device); } } \ No newline at end of file diff --git a/src/Artemis.Core/Providers/Interfaces/ILayoutProvider.cs b/src/Artemis.Core/Providers/Interfaces/ILayoutProvider.cs index fe8d421c2..3cccfe39d 100644 --- a/src/Artemis.Core/Providers/Interfaces/ILayoutProvider.cs +++ b/src/Artemis.Core/Providers/Interfaces/ILayoutProvider.cs @@ -1,15 +1,15 @@ namespace Artemis.Core.Providers; /// -/// Represents a class that can provide Artemis layouts for devices. +/// Represents a class that can provide Artemis layouts for devices. /// public interface ILayoutProvider { /// - /// If available, loads an Artemis layout for the provided device. + /// If available, loads an Artemis layout for the provided device. /// /// The device to load the layout for. - /// The resulting layout if one was available; otherwise . + /// The resulting layout if one was available; otherwise . ArtemisLayout? GetDeviceLayout(ArtemisDevice device); void ApplyLayout(ArtemisDevice device, ArtemisLayout layout); diff --git a/src/Artemis.Core/Providers/NoneLayoutProvider.cs b/src/Artemis.Core/Providers/NoneLayoutProvider.cs index 35de69ee6..59c13391b 100644 --- a/src/Artemis.Core/Providers/NoneLayoutProvider.cs +++ b/src/Artemis.Core/Providers/NoneLayoutProvider.cs @@ -1,17 +1,9 @@ -using Artemis.Core.Services; - -namespace Artemis.Core.Providers; +namespace Artemis.Core.Providers; public class NoneLayoutProvider : ILayoutProvider { - private readonly IDeviceService _deviceService; public static string LayoutType = "None"; - public NoneLayoutProvider(IDeviceService deviceService) - { - _deviceService = deviceService; - } - /// public ArtemisLayout? GetDeviceLayout(ArtemisDevice device) { @@ -23,22 +15,20 @@ public class NoneLayoutProvider : ILayoutProvider { device.ApplyLayout(null, false, false); } - + /// public bool IsMatch(ArtemisDevice device) { return device.LayoutSelection.Type == LayoutType; } - + /// - /// Configures the provided device to use this layout provider. + /// Configures the provided device to use this layout provider. /// /// The device to apply the provider to. public void ConfigureDevice(ArtemisDevice device) { device.LayoutSelection.Type = LayoutType; device.LayoutSelection.Parameter = null; - _deviceService.SaveDevice(device); - _deviceService.LoadDeviceLayout(device); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/DeviceService.cs b/src/Artemis.Core/Services/DeviceService.cs index 3a17d0bdc..afc949bba 100644 --- a/src/Artemis.Core/Services/DeviceService.cs +++ b/src/Artemis.Core/Services/DeviceService.cs @@ -20,7 +20,7 @@ internal class DeviceService : IDeviceService private readonly IPluginManagementService _pluginManagementService; private readonly IDeviceRepository _deviceRepository; private readonly Lazy _renderService; - private readonly LazyEnumerable _layoutProviders; + private readonly Func> _getLayoutProviders; private readonly List _enabledDevices = new(); private readonly List _devices = new(); @@ -28,13 +28,13 @@ internal class DeviceService : IDeviceService IPluginManagementService pluginManagementService, IDeviceRepository deviceRepository, Lazy renderService, - LazyEnumerable layoutProviders) + Func> getLayoutProviders) { _logger = logger; _pluginManagementService = pluginManagementService; _deviceRepository = deviceRepository; _renderService = renderService; - _layoutProviders = layoutProviders; + _getLayoutProviders = getLayoutProviders; EnabledDevices = new ReadOnlyCollection(_enabledDevices); Devices = new ReadOnlyCollection(_devices); @@ -167,7 +167,7 @@ internal class DeviceService : IDeviceService /// public void LoadDeviceLayout(ArtemisDevice device) { - ILayoutProvider? provider = _layoutProviders.FirstOrDefault(p => p.IsMatch(device)); + ILayoutProvider? provider = _getLayoutProviders().FirstOrDefault(p => p.IsMatch(device)); if (provider == null) _logger.Warning("Could not find a layout provider for type {LayoutType} of device {Device}", device.LayoutSelection.Type, device); diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 639aa014f..8194cef44 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -57,7 +57,7 @@ internal class PluginManagementService : IPluginManagementService // Remove the old directory if it exists if (Directory.Exists(pluginDirectory.FullName)) - pluginDirectory.DeleteRecursively(); + pluginDirectory.Delete(true); // Extract everything in the same archive directory to the unique plugin directory Utilities.CreateAccessibleDirectory(pluginDirectory.FullName); diff --git a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/CustomLayoutViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/CustomLayoutViewModel.cs index 624fa376e..86b73c8a8 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/CustomLayoutViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/CustomLayoutViewModel.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Providers; +using Artemis.Core.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Builders; @@ -11,11 +12,12 @@ public class CustomLayoutViewModel : ViewModelBase, ILayoutProviderViewModel { private readonly CustomPathLayoutProvider _layoutProvider; - public CustomLayoutViewModel(IWindowService windowService, INotificationService notificationService, CustomPathLayoutProvider layoutProvider) + public CustomLayoutViewModel(IWindowService windowService, INotificationService notificationService, IDeviceService deviceService, CustomPathLayoutProvider layoutProvider) { _layoutProvider = layoutProvider; _windowService = windowService; _notificationService = notificationService; + _deviceService = deviceService; } /// @@ -32,11 +34,13 @@ public class CustomLayoutViewModel : ViewModelBase, ILayoutProviderViewModel private readonly IWindowService _windowService; private readonly INotificationService _notificationService; + private readonly IDeviceService _deviceService; public void ClearCustomLayout() { _layoutProvider.ConfigureDevice(Device, null); - + Save(); + _notificationService.CreateNotification() .WithMessage("Cleared imported layout.") .WithSeverity(NotificationSeverity.Informational); @@ -52,7 +56,8 @@ public class CustomLayoutViewModel : ViewModelBase, ILayoutProviderViewModel if (files?.Length > 0) { _layoutProvider.ConfigureDevice(Device, files[0]); - + Save(); + _notificationService.CreateNotification() .WithTitle("Imported layout") .WithMessage($"File loaded from {files[0]}") @@ -64,5 +69,12 @@ public class CustomLayoutViewModel : ViewModelBase, ILayoutProviderViewModel public void Apply() { _layoutProvider.ConfigureDevice(Device, null); + Save(); + } + + private void Save() + { + _deviceService.SaveDevice(Device); + _deviceService.LoadDeviceLayout(Device); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/DefaultLayoutViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/DefaultLayoutViewModel.cs index 78d4b9b0f..b4c2e5547 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/DefaultLayoutViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/DefaultLayoutViewModel.cs @@ -1,5 +1,6 @@ using Artemis.Core; using Artemis.Core.Providers; +using Artemis.Core.Services; using Artemis.UI.Shared; namespace Artemis.UI.Screens.Device.Layout.LayoutProviders; @@ -7,10 +8,12 @@ namespace Artemis.UI.Screens.Device.Layout.LayoutProviders; public class DefaultLayoutViewModel : ViewModelBase, ILayoutProviderViewModel { private readonly DefaultLayoutProvider _layoutProvider; + private readonly IDeviceService _deviceService; - public DefaultLayoutViewModel(DefaultLayoutProvider layoutProvider) + public DefaultLayoutViewModel(DefaultLayoutProvider layoutProvider, IDeviceService deviceService) { _layoutProvider = layoutProvider; + _deviceService = deviceService; } /// @@ -28,5 +31,12 @@ public class DefaultLayoutViewModel : ViewModelBase, ILayoutProviderViewModel public void Apply() { _layoutProvider.ConfigureDevice(Device); + Save(); + } + + private void Save() + { + _deviceService.SaveDevice(Device); + _deviceService.LoadDeviceLayout(Device); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/NoneLayoutViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/NoneLayoutViewModel.cs index 466c13f31..ab3ae1214 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/NoneLayoutViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/NoneLayoutViewModel.cs @@ -1,5 +1,6 @@ using Artemis.Core; using Artemis.Core.Providers; +using Artemis.Core.Services; using Artemis.UI.Shared; namespace Artemis.UI.Screens.Device.Layout.LayoutProviders; @@ -7,10 +8,12 @@ namespace Artemis.UI.Screens.Device.Layout.LayoutProviders; public class NoneLayoutViewModel : ViewModelBase, ILayoutProviderViewModel { private readonly NoneLayoutProvider _layoutProvider; + private readonly IDeviceService _deviceService; - public NoneLayoutViewModel(NoneLayoutProvider layoutProvider) + public NoneLayoutViewModel(NoneLayoutProvider layoutProvider, IDeviceService deviceService) { _layoutProvider = layoutProvider; + _deviceService = deviceService; } /// @@ -28,5 +31,12 @@ public class NoneLayoutViewModel : ViewModelBase, ILayoutProviderViewModel public void Apply() { _layoutProvider.ConfigureDevice(Device); + Save(); + } + + private void Save() + { + _deviceService.SaveDevice(Device); + _deviceService.LoadDeviceLayout(Device); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml index 50de1a677..75bbd544d 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml +++ b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml @@ -2,13 +2,45 @@ 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:services="clr-namespace:Artemis.WebClient.Workshop.Services;assembly=Artemis.WebClient.Workshop" + xmlns:layoutProviders="clr-namespace:Artemis.UI.Screens.Device.Layout.LayoutProviders" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Device.Layout.LayoutProviders.WorkshopLayoutView"> + x:Class="Artemis.UI.Screens.Device.Layout.LayoutProviders.WorkshopLayoutView" + x:DataType="layoutProviders:WorkshopLayoutViewModel"> - - - - + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml.cs b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml.cs index fc27fb9aa..f0f9766c1 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml.cs @@ -1,8 +1,9 @@ using Avalonia.Controls; +using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Device.Layout.LayoutProviders; -public partial class WorkshopLayoutView : UserControl +public partial class WorkshopLayoutView : ReactiveUserControl { public WorkshopLayoutView() { diff --git a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs index 948f8fe29..e3c3f9f05 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs @@ -1,17 +1,33 @@ -using Artemis.Core; +using System.Collections.ObjectModel; +using System; +using System.Linq; +using System.Reactive.Disposables; +using Artemis.Core; using Artemis.Core.Providers; +using Artemis.Core.Services; using Artemis.UI.Shared; +using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop.Providers; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; +using ReactiveUI; namespace Artemis.UI.Screens.Device.Layout.LayoutProviders; -public class WorkshopLayoutViewModel : ViewModelBase, ILayoutProviderViewModel +public partial class WorkshopLayoutViewModel : ActivatableViewModelBase, ILayoutProviderViewModel { + [Notify] private InstalledEntry? _selectedEntry; private readonly WorkshopLayoutProvider _layoutProvider; + private readonly IDeviceService _deviceService; - public WorkshopLayoutViewModel(WorkshopLayoutProvider layoutProvider) + public WorkshopLayoutViewModel(WorkshopLayoutProvider layoutProvider, IWorkshopService workshopService, IDeviceService deviceService) { _layoutProvider = layoutProvider; + _deviceService = deviceService; + Entries = new ObservableCollection(workshopService.GetInstalledEntries().Where(e => e.EntryType == EntryType.Layout)); + + this.WhenAnyValue(vm => vm.SelectedEntry).Subscribe(ApplyEntry); + this.WhenActivated((CompositeDisposable _) => SelectedEntry = Entries.FirstOrDefault(e => e.EntryId.ToString() == Device.LayoutSelection.Parameter)); } /// @@ -19,6 +35,8 @@ public class WorkshopLayoutViewModel : ViewModelBase, ILayoutProviderViewModel public ArtemisDevice Device { get; set; } = null!; + public ObservableCollection Entries { get; } + /// public string Name => "Workshop"; @@ -28,6 +46,21 @@ public class WorkshopLayoutViewModel : ViewModelBase, ILayoutProviderViewModel /// public void Apply() { - _layoutProvider.ConfigureDevice(Device); + _layoutProvider.ConfigureDevice(Device, null); + Save(); + } + + private void ApplyEntry(InstalledEntry? entry) + { + if (entry == null || Device.LayoutSelection.Parameter == entry.EntryId.ToString()) + return; + _layoutProvider.ConfigureDevice(Device, entry); + Save(); + } + + private void Save() + { + _deviceService.SaveDevice(Device); + _deviceService.LoadDeviceLayout(Device); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs index 11608288a..02cbef240 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs @@ -45,7 +45,7 @@ public class EntryReleasesViewModel : ViewModelBase return; IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType); - EntryInstallResult result = await installationHandler.InstallAsync(Entry, Entry.LatestRelease.Id, new Progress(), cancellationToken); + EntryInstallResult result = await installationHandler.InstallAsync(Entry, Entry.LatestRelease, new Progress(), cancellationToken); if (result.IsSuccess) _notificationService.CreateNotification().WithTitle($"{Entry.EntryType.Humanize(LetterCasing.Sentence)} installed").WithSeverity(NotificationSeverity.Success).Show(); else diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs index d8e7ba951..0e4e2d4f4 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/IEntryInstallationHandler.cs @@ -5,6 +5,6 @@ namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers; public interface IEntryInstallationHandler { - Task InstallAsync(IGetEntryById_Entry entry, long releaseId, Progress progress, CancellationToken cancellationToken); + Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken); Task UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs index 13a2e2318..a0adb4d0c 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs @@ -1,10 +1,10 @@ -using System.Data; -using System.IO.Compression; +using System.IO.Compression; using Artemis.Core; +using Artemis.Core.Providers; using Artemis.Core.Services; using Artemis.UI.Shared.Extensions; -using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Utilities; +using Artemis.WebClient.Workshop.Providers; using Artemis.WebClient.Workshop.Services; namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers; @@ -14,15 +14,17 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler private readonly IHttpClientFactory _httpClientFactory; private readonly IWorkshopService _workshopService; private readonly IDeviceService _deviceService; + private readonly DefaultLayoutProvider _defaultLayoutProvider; - public LayoutEntryInstallationHandler(IHttpClientFactory httpClientFactory, IWorkshopService workshopService, IDeviceService deviceService) + public LayoutEntryInstallationHandler(IHttpClientFactory httpClientFactory, IWorkshopService workshopService, IDeviceService deviceService, DefaultLayoutProvider defaultLayoutProvider) { _httpClientFactory = httpClientFactory; _workshopService = workshopService; _deviceService = deviceService; + _defaultLayoutProvider = defaultLayoutProvider; } - public async Task InstallAsync(IGetEntryById_Entry entry, long releaseId, Progress progress, CancellationToken cancellationToken) + public async Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken) { using MemoryStream stream = new(); @@ -30,7 +32,7 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler try { HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); - await client.DownloadDataAsync($"releases/download/{releaseId}", stream, progress, cancellationToken); + await client.DownloadDataAsync($"releases/download/{release.Id}", stream, progress, cancellationToken); } catch (Exception e) { @@ -38,8 +40,8 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler } // Ensure there is an installed entry - InstalledEntry installedEntry = _workshopService.GetInstalledEntry(entry) ?? _workshopService.CreateInstalledEntry(entry); - DirectoryInfo entryDirectory = installedEntry.GetDirectory(); + InstalledEntry installedEntry = _workshopService.GetInstalledEntry(entry) ?? new InstalledEntry(entry, release); + DirectoryInfo entryDirectory = installedEntry.GetReleaseDirectory(release); // If the folder already exists, remove it so that if the layout now contains less files, old things dont stick around if (entryDirectory.Exists) @@ -53,7 +55,11 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler ArtemisLayout layout = new(Path.Combine(entryDirectory.FullName, "layout.xml")); if (layout.IsValid) + { + installedEntry.ApplyRelease(release); + _workshopService.SaveInstalledEntry(installedEntry); return EntryInstallResult.FromSuccess(layout); + } // If the layout ended up being invalid yoink it out again, shoooo entryDirectory.Delete(true); @@ -61,21 +67,26 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler return EntryInstallResult.FromFailure("Layout failed to load because it is invalid"); } - public async Task UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) + public Task UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) { // Remove the layout from any devices currently using it foreach (ArtemisDevice device in _deviceService.Devices) { + if (device.LayoutSelection.Type == WorkshopLayoutProvider.LayoutType && device.LayoutSelection.Parameter == installedEntry.EntryId.ToString()) + { + _defaultLayoutProvider.ConfigureDevice(device); + _deviceService.SaveDevice(device); + _deviceService.LoadDeviceLayout(device); + } } // Remove from filesystem - DirectoryInfo directory = installedEntry.GetDirectory(true); + DirectoryInfo directory = installedEntry.GetDirectory(); if (directory.Exists) - directory.Delete(); + directory.Delete(true); // Remove entry _workshopService.RemoveInstalledEntry(installedEntry); - - return EntryUninstallResult.FromSuccess(); + return Task.FromResult(EntryUninstallResult.FromSuccess()); } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs index b322e40cd..c44956700 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs @@ -19,7 +19,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler _workshopService = workshopService; } - public async Task InstallAsync(IGetEntryById_Entry entry, long releaseId, Progress progress, CancellationToken cancellationToken) + public async Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken) { using MemoryStream stream = new(); @@ -27,7 +27,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler try { HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); - await client.DownloadDataAsync($"releases/download/{releaseId}", stream, progress, cancellationToken); + await client.DownloadDataAsync($"releases/download/{release}", stream, progress, cancellationToken); } catch (Exception e) { @@ -45,13 +45,13 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler installedEntry.SetMetadata("ProfileId", overwritten.ProfileId); // Update the release and return the profile configuration - UpdateRelease(releaseId, installedEntry); + UpdateRelease(installedEntry, release); return EntryInstallResult.FromSuccess(overwritten); } } // Ensure there is an installed entry - installedEntry ??= _workshopService.CreateInstalledEntry(entry); + installedEntry ??= new InstalledEntry(entry, release); // Add the profile as a fresh import ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == "Workshop") ?? _profileService.CreateProfileCategory("Workshop", true); @@ -59,7 +59,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler installedEntry.SetMetadata("ProfileId", imported.ProfileId); // Update the release and return the profile configuration - UpdateRelease(releaseId, installedEntry); + UpdateRelease(installedEntry, release); return EntryInstallResult.FromSuccess(imported); } @@ -89,11 +89,9 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler }, cancellationToken); } - private void UpdateRelease(long releaseId, InstalledEntry installedEntry) + private void UpdateRelease(InstalledEntry installedEntry, IRelease release) { - installedEntry.ReleaseId = releaseId; - installedEntry.ReleaseVersion = "TODO"; - installedEntry.InstalledAt = DateTimeOffset.UtcNow; + installedEntry.ApplyRelease(release); _workshopService.SaveInstalledEntry(installedEntry); } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Providers/WorkshopLayoutProvider.cs b/src/Artemis.WebClient.Workshop/Providers/WorkshopLayoutProvider.cs index 6544ff437..4d028f4a1 100644 --- a/src/Artemis.WebClient.Workshop/Providers/WorkshopLayoutProvider.cs +++ b/src/Artemis.WebClient.Workshop/Providers/WorkshopLayoutProvider.cs @@ -1,6 +1,5 @@ using Artemis.Core; using Artemis.Core.Providers; -using Artemis.Core.Services; using Artemis.WebClient.Workshop.Services; namespace Artemis.WebClient.Workshop.Providers; @@ -8,34 +7,31 @@ namespace Artemis.WebClient.Workshop.Providers; public class WorkshopLayoutProvider : ILayoutProvider { public static string LayoutType = "Workshop"; - - private readonly IDeviceService _deviceService; private readonly IWorkshopService _workshopService; - public WorkshopLayoutProvider(IDeviceService deviceService, IWorkshopService workshopService) + public WorkshopLayoutProvider(IWorkshopService workshopService) { - _deviceService = deviceService; _workshopService = workshopService; } /// public ArtemisLayout? GetDeviceLayout(ArtemisDevice device) { - InstalledEntry? layoutEntry = _workshopService.GetInstalledEntries().FirstOrDefault(e => e.EntryType == EntryType.Layout && MatchesDevice(e, device)); + InstalledEntry? layoutEntry = _workshopService.GetInstalledEntries().FirstOrDefault(e => e.EntryId.ToString() == device.LayoutSelection.Parameter); if (layoutEntry == null) return null; - string layoutPath = Path.Combine(Constants.WorkshopFolder, layoutEntry.EntryId.ToString(), "layout.xml"); + string layoutPath = Path.Combine(layoutEntry.GetReleaseDirectory().FullName, "layout.xml"); if (!File.Exists(layoutPath)) return null; - + return new ArtemisLayout(layoutPath); } /// public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout) { - throw new NotImplementedException(); + device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); } /// @@ -44,20 +40,17 @@ public class WorkshopLayoutProvider : ILayoutProvider return device.LayoutSelection.Type == LayoutType; } - private bool MatchesDevice(InstalledEntry entry, ArtemisDevice device) - { - return entry.TryGetMetadata("DeviceId", out HashSet? deviceIds) && deviceIds.Contains(device.Identifier); - } - /// /// Configures the provided device to use this layout provider. /// /// The device to apply the provider to. - public void ConfigureDevice(ArtemisDevice device) + /// The workshop entry to use as a layout. + public void ConfigureDevice(ArtemisDevice device, InstalledEntry? entry) { + if (entry != null && entry.EntryType != EntryType.Layout) + throw new InvalidOperationException($"Cannot use a workshop entry of type {entry.EntryType} as a layout"); + device.LayoutSelection.Type = LayoutType; - device.LayoutSelection.Parameter = null; - _deviceService.SaveDevice(device); - _deviceService.LoadDeviceLayout(device); + device.LayoutSelection.Parameter = entry?.EntryId.ToString(); } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql index 9dc7949ea..a66c3a660 100644 --- a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql @@ -41,13 +41,17 @@ fragment entryDetails on Entry { ...category } latestRelease { - id - version - downloadSize - md5Hash - createdAt + ...release } images { ...image } -} \ No newline at end of file +} + +fragment release on Release { + id + version + downloadSize + md5Hash + createdAt +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/InstalledEntry.cs b/src/Artemis.WebClient.Workshop/Services/InstalledEntry.cs index 1ac3069e2..6373a8d2a 100644 --- a/src/Artemis.WebClient.Workshop/Services/InstalledEntry.cs +++ b/src/Artemis.WebClient.Workshop/Services/InstalledEntry.cs @@ -14,7 +14,7 @@ public class InstalledEntry Load(); } - public InstalledEntry(IGetEntryById_Entry entry) + public InstalledEntry(IEntryDetails entry, IRelease release) { Entity = new EntryEntity(); @@ -23,6 +23,9 @@ public class InstalledEntry Author = entry.Author; Name = entry.Name; + InstalledAt = DateTimeOffset.Now; + ReleaseId = release.Id; + ReleaseVersion = release.Version; } public long EntryId { get; set; } @@ -110,14 +113,30 @@ public class InstalledEntry /// /// Returns the directory info of the entry, where any files would be stored if applicable. /// - /// A value indicating whether or not to return the root directory of the entry, and not the version. - /// The directory info of the entry, where any files would be stored if applicable. - public DirectoryInfo GetDirectory(bool rootDirectory = false) + /// The directory info of the directory. + public DirectoryInfo GetDirectory() { - if (rootDirectory) - return new DirectoryInfo(Path.Combine(Constants.WorkshopFolder, EntryId.ToString())); - - string safeVersion = Path.GetInvalidFileNameChars().Aggregate(ReleaseVersion, (current, c) => current.Replace(c, '-')); - return new DirectoryInfo(Path.Combine(Constants.WorkshopFolder, EntryId.ToString(), safeVersion)); + return new DirectoryInfo(Path.Combine(Constants.WorkshopFolder, $"{EntryId}-{StringUtilities.UrlFriendly(Name)}")); + } + + /// + /// Returns the directory info of a release of this entry, where any files would be stored if applicable. + /// + /// The release to use, if none provided the current release is used. + /// The directory info of the directory. + public DirectoryInfo GetReleaseDirectory(IRelease? release = null) + { + return new DirectoryInfo(Path.Combine(GetDirectory().FullName, StringUtilities.UrlFriendly(release?.Version ?? ReleaseVersion))); + } + + /// + /// Applies the provided release to the installed entry. + /// + /// The release to apply. + public void ApplyRelease(IRelease release) + { + ReleaseId = release.Id; + ReleaseVersion = release.Version; + InstalledAt = DateTimeOffset.UtcNow; } } \ 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 f1041f2d0..3b36c6b2d 100644 --- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs @@ -1,4 +1,3 @@ -using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Workshop.Handlers.UploadHandlers; namespace Artemis.WebClient.Workshop.Services; @@ -13,11 +12,9 @@ public interface IWorkshopService Task NavigateToEntry(long entryId, EntryType entryType); List GetInstalledEntries(); - InstalledEntry? GetInstalledEntry(IGetEntryById_Entry entry); - InstalledEntry CreateInstalledEntry(IGetEntryById_Entry entry); + InstalledEntry? GetInstalledEntry(IEntryDetails entry); void RemoveInstalledEntry(InstalledEntry installedEntry); void SaveInstalledEntry(InstalledEntry entry); - public record WorkshopStatus(bool IsReachable, string Message); } \ 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 3257c5c0e..920d29f8e 100644 --- a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs @@ -133,7 +133,7 @@ public class WorkshopService : IWorkshopService } /// - public InstalledEntry? GetInstalledEntry(IGetEntryById_Entry entry) + public InstalledEntry? GetInstalledEntry(IEntryDetails entry) { EntryEntity? entity = _entryRepository.GetByEntryId(entry.Id); if (entity == null) @@ -143,9 +143,9 @@ public class WorkshopService : IWorkshopService } /// - public InstalledEntry CreateInstalledEntry(IGetEntryById_Entry entry) + public void AddOrUpdateInstalledEntry(InstalledEntry entry, IRelease release) { - return new InstalledEntry(entry); + throw new NotImplementedException(); } ///