1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Workshop - Implemented workshop installation, layout selection and removal

This commit is contained in:
RobertBeekman 2024-01-14 12:46:51 +01:00
parent dad6a56238
commit 28edabae89
23 changed files with 226 additions and 168 deletions

View File

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

View File

@ -398,14 +398,6 @@ public class ArtemisDevice : CorePropertyChanged
} }
} }
/// <summary>
/// Invokes the <see cref="DeviceUpdated" /> event
/// </summary>
protected virtual void OnDeviceUpdated()
{
DeviceUpdated?.Invoke(this, EventArgs.Empty);
}
/// <summary> /// <summary>
/// Applies the provided layout to the device /// Applies the provided layout to the device
/// </summary> /// </summary>
@ -418,7 +410,7 @@ public class ArtemisDevice : CorePropertyChanged
/// A boolean indicating whether to remove excess LEDs present in the device but missing /// A boolean indicating whether to remove excess LEDs present in the device but missing
/// in the layout /// in the layout
/// </param> /// </param>
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) 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"); 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(); CalculateRenderProperties();
} }
/// <summary>
/// Invokes the <see cref="DeviceUpdated" /> event
/// </summary>
protected virtual void OnDeviceUpdated()
{
DeviceUpdated?.Invoke(this, EventArgs.Empty);
}
private void ClearLayout() private void ClearLayout()
{ {
if (Layout == null) if (Layout == null)

View File

@ -1,17 +1,8 @@
using Artemis.Core.Services; namespace Artemis.Core.Providers;
using RGB.NET.Layout;
namespace Artemis.Core.Providers;
public class CustomPathLayoutProvider : ILayoutProvider public class CustomPathLayoutProvider : ILayoutProvider
{ {
public static string LayoutType = "CustomPath"; public static string LayoutType = "CustomPath";
private readonly IDeviceService _deviceService;
public CustomPathLayoutProvider(IDeviceService deviceService)
{
_deviceService = deviceService;
}
/// <inheritdoc /> /// <inheritdoc />
public ArtemisLayout? GetDeviceLayout(ArtemisDevice device) public ArtemisLayout? GetDeviceLayout(ArtemisDevice device)
@ -34,7 +25,7 @@ public class CustomPathLayoutProvider : ILayoutProvider
} }
/// <summary> /// <summary>
/// Configures the provided device to use this layout provider. /// Configures the provided device to use this layout provider.
/// </summary> /// </summary>
/// <param name="device">The device to apply the provider to.</param> /// <param name="device">The device to apply the provider to.</param>
/// <param name="path">The path to the custom layout.</param> /// <param name="path">The path to the custom layout.</param>
@ -42,7 +33,5 @@ public class CustomPathLayoutProvider : ILayoutProvider
{ {
device.LayoutSelection.Type = LayoutType; device.LayoutSelection.Type = LayoutType;
device.LayoutSelection.Parameter = path; device.LayoutSelection.Parameter = path;
_deviceService.SaveDevice(device);
_deviceService.LoadDeviceLayout(device);
} }
} }

View File

@ -1,17 +1,9 @@
using Artemis.Core.Services; namespace Artemis.Core.Providers;
namespace Artemis.Core.Providers;
public class DefaultLayoutProvider : ILayoutProvider public class DefaultLayoutProvider : ILayoutProvider
{ {
public static string LayoutType = "Default"; public static string LayoutType = "Default";
private readonly IDeviceService _deviceService;
public DefaultLayoutProvider(IDeviceService deviceService)
{
_deviceService = deviceService;
}
/// <inheritdoc /> /// <inheritdoc />
public ArtemisLayout? GetDeviceLayout(ArtemisDevice device) public ArtemisLayout? GetDeviceLayout(ArtemisDevice device)
{ {
@ -30,22 +22,20 @@ public class DefaultLayoutProvider : ILayoutProvider
else else
device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported);
} }
/// <inheritdoc /> /// <inheritdoc />
public bool IsMatch(ArtemisDevice device) public bool IsMatch(ArtemisDevice device)
{ {
return device.LayoutSelection.Type == LayoutType; return device.LayoutSelection.Type == LayoutType;
} }
/// <summary> /// <summary>
/// Configures the provided device to use this layout provider. /// Configures the provided device to use this layout provider.
/// </summary> /// </summary>
/// <param name="device">The device to apply the provider to.</param> /// <param name="device">The device to apply the provider to.</param>
public void ConfigureDevice(ArtemisDevice device) public void ConfigureDevice(ArtemisDevice device)
{ {
device.LayoutSelection.Type = LayoutType; device.LayoutSelection.Type = LayoutType;
device.LayoutSelection.Parameter = null; device.LayoutSelection.Parameter = null;
_deviceService.SaveDevice(device);
_deviceService.LoadDeviceLayout(device);
} }
} }

View File

@ -1,15 +1,15 @@
namespace Artemis.Core.Providers; namespace Artemis.Core.Providers;
/// <summary> /// <summary>
/// Represents a class that can provide Artemis layouts for devices. /// Represents a class that can provide Artemis layouts for devices.
/// </summary> /// </summary>
public interface ILayoutProvider public interface ILayoutProvider
{ {
/// <summary> /// <summary>
/// If available, loads an Artemis layout for the provided device. /// If available, loads an Artemis layout for the provided device.
/// </summary> /// </summary>
/// <param name="device">The device to load the layout for.</param> /// <param name="device">The device to load the layout for.</param>
/// <returns>The resulting layout if one was available; otherwise <see langword="null"/>.</returns> /// <returns>The resulting layout if one was available; otherwise <see langword="null" />.</returns>
ArtemisLayout? GetDeviceLayout(ArtemisDevice device); ArtemisLayout? GetDeviceLayout(ArtemisDevice device);
void ApplyLayout(ArtemisDevice device, ArtemisLayout layout); void ApplyLayout(ArtemisDevice device, ArtemisLayout layout);

View File

@ -1,17 +1,9 @@
using Artemis.Core.Services; namespace Artemis.Core.Providers;
namespace Artemis.Core.Providers;
public class NoneLayoutProvider : ILayoutProvider public class NoneLayoutProvider : ILayoutProvider
{ {
private readonly IDeviceService _deviceService;
public static string LayoutType = "None"; public static string LayoutType = "None";
public NoneLayoutProvider(IDeviceService deviceService)
{
_deviceService = deviceService;
}
/// <inheritdoc /> /// <inheritdoc />
public ArtemisLayout? GetDeviceLayout(ArtemisDevice device) public ArtemisLayout? GetDeviceLayout(ArtemisDevice device)
{ {
@ -23,22 +15,20 @@ public class NoneLayoutProvider : ILayoutProvider
{ {
device.ApplyLayout(null, false, false); device.ApplyLayout(null, false, false);
} }
/// <inheritdoc /> /// <inheritdoc />
public bool IsMatch(ArtemisDevice device) public bool IsMatch(ArtemisDevice device)
{ {
return device.LayoutSelection.Type == LayoutType; return device.LayoutSelection.Type == LayoutType;
} }
/// <summary> /// <summary>
/// Configures the provided device to use this layout provider. /// Configures the provided device to use this layout provider.
/// </summary> /// </summary>
/// <param name="device">The device to apply the provider to.</param> /// <param name="device">The device to apply the provider to.</param>
public void ConfigureDevice(ArtemisDevice device) public void ConfigureDevice(ArtemisDevice device)
{ {
device.LayoutSelection.Type = LayoutType; device.LayoutSelection.Type = LayoutType;
device.LayoutSelection.Parameter = null; device.LayoutSelection.Parameter = null;
_deviceService.SaveDevice(device);
_deviceService.LoadDeviceLayout(device);
} }
} }

View File

@ -20,7 +20,7 @@ internal class DeviceService : IDeviceService
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;
private readonly IDeviceRepository _deviceRepository; private readonly IDeviceRepository _deviceRepository;
private readonly Lazy<IRenderService> _renderService; private readonly Lazy<IRenderService> _renderService;
private readonly LazyEnumerable<ILayoutProvider> _layoutProviders; private readonly Func<List<ILayoutProvider>> _getLayoutProviders;
private readonly List<ArtemisDevice> _enabledDevices = new(); private readonly List<ArtemisDevice> _enabledDevices = new();
private readonly List<ArtemisDevice> _devices = new(); private readonly List<ArtemisDevice> _devices = new();
@ -28,13 +28,13 @@ internal class DeviceService : IDeviceService
IPluginManagementService pluginManagementService, IPluginManagementService pluginManagementService,
IDeviceRepository deviceRepository, IDeviceRepository deviceRepository,
Lazy<IRenderService> renderService, Lazy<IRenderService> renderService,
LazyEnumerable<ILayoutProvider> layoutProviders) Func<List<ILayoutProvider>> getLayoutProviders)
{ {
_logger = logger; _logger = logger;
_pluginManagementService = pluginManagementService; _pluginManagementService = pluginManagementService;
_deviceRepository = deviceRepository; _deviceRepository = deviceRepository;
_renderService = renderService; _renderService = renderService;
_layoutProviders = layoutProviders; _getLayoutProviders = getLayoutProviders;
EnabledDevices = new ReadOnlyCollection<ArtemisDevice>(_enabledDevices); EnabledDevices = new ReadOnlyCollection<ArtemisDevice>(_enabledDevices);
Devices = new ReadOnlyCollection<ArtemisDevice>(_devices); Devices = new ReadOnlyCollection<ArtemisDevice>(_devices);
@ -167,7 +167,7 @@ internal class DeviceService : IDeviceService
/// <inheritdoc /> /// <inheritdoc />
public void LoadDeviceLayout(ArtemisDevice device) public void LoadDeviceLayout(ArtemisDevice device)
{ {
ILayoutProvider? provider = _layoutProviders.FirstOrDefault(p => p.IsMatch(device)); ILayoutProvider? provider = _getLayoutProviders().FirstOrDefault(p => p.IsMatch(device));
if (provider == null) if (provider == null)
_logger.Warning("Could not find a layout provider for type {LayoutType} of device {Device}", device.LayoutSelection.Type, device); _logger.Warning("Could not find a layout provider for type {LayoutType} of device {Device}", device.LayoutSelection.Type, device);

View File

@ -57,7 +57,7 @@ internal class PluginManagementService : IPluginManagementService
// Remove the old directory if it exists // Remove the old directory if it exists
if (Directory.Exists(pluginDirectory.FullName)) if (Directory.Exists(pluginDirectory.FullName))
pluginDirectory.DeleteRecursively(); pluginDirectory.Delete(true);
// Extract everything in the same archive directory to the unique plugin directory // Extract everything in the same archive directory to the unique plugin directory
Utilities.CreateAccessibleDirectory(pluginDirectory.FullName); Utilities.CreateAccessibleDirectory(pluginDirectory.FullName);

View File

@ -1,6 +1,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Providers; using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Builders;
@ -11,11 +12,12 @@ public class CustomLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
{ {
private readonly CustomPathLayoutProvider _layoutProvider; private readonly CustomPathLayoutProvider _layoutProvider;
public CustomLayoutViewModel(IWindowService windowService, INotificationService notificationService, CustomPathLayoutProvider layoutProvider) public CustomLayoutViewModel(IWindowService windowService, INotificationService notificationService, IDeviceService deviceService, CustomPathLayoutProvider layoutProvider)
{ {
_layoutProvider = layoutProvider; _layoutProvider = layoutProvider;
_windowService = windowService; _windowService = windowService;
_notificationService = notificationService; _notificationService = notificationService;
_deviceService = deviceService;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -32,11 +34,13 @@ public class CustomLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly IDeviceService _deviceService;
public void ClearCustomLayout() public void ClearCustomLayout()
{ {
_layoutProvider.ConfigureDevice(Device, null); _layoutProvider.ConfigureDevice(Device, null);
Save();
_notificationService.CreateNotification() _notificationService.CreateNotification()
.WithMessage("Cleared imported layout.") .WithMessage("Cleared imported layout.")
.WithSeverity(NotificationSeverity.Informational); .WithSeverity(NotificationSeverity.Informational);
@ -52,7 +56,8 @@ public class CustomLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
if (files?.Length > 0) if (files?.Length > 0)
{ {
_layoutProvider.ConfigureDevice(Device, files[0]); _layoutProvider.ConfigureDevice(Device, files[0]);
Save();
_notificationService.CreateNotification() _notificationService.CreateNotification()
.WithTitle("Imported layout") .WithTitle("Imported layout")
.WithMessage($"File loaded from {files[0]}") .WithMessage($"File loaded from {files[0]}")
@ -64,5 +69,12 @@ public class CustomLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
public void Apply() public void Apply()
{ {
_layoutProvider.ConfigureDevice(Device, null); _layoutProvider.ConfigureDevice(Device, null);
Save();
}
private void Save()
{
_deviceService.SaveDevice(Device);
_deviceService.LoadDeviceLayout(Device);
} }
} }

View File

@ -1,5 +1,6 @@
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Providers; using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.UI.Shared; using Artemis.UI.Shared;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders; namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
@ -7,10 +8,12 @@ namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public class DefaultLayoutViewModel : ViewModelBase, ILayoutProviderViewModel public class DefaultLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
{ {
private readonly DefaultLayoutProvider _layoutProvider; private readonly DefaultLayoutProvider _layoutProvider;
private readonly IDeviceService _deviceService;
public DefaultLayoutViewModel(DefaultLayoutProvider layoutProvider) public DefaultLayoutViewModel(DefaultLayoutProvider layoutProvider, IDeviceService deviceService)
{ {
_layoutProvider = layoutProvider; _layoutProvider = layoutProvider;
_deviceService = deviceService;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -28,5 +31,12 @@ public class DefaultLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
public void Apply() public void Apply()
{ {
_layoutProvider.ConfigureDevice(Device); _layoutProvider.ConfigureDevice(Device);
Save();
}
private void Save()
{
_deviceService.SaveDevice(Device);
_deviceService.LoadDeviceLayout(Device);
} }
} }

View File

@ -1,5 +1,6 @@
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Providers; using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.UI.Shared; using Artemis.UI.Shared;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders; namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
@ -7,10 +8,12 @@ namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public class NoneLayoutViewModel : ViewModelBase, ILayoutProviderViewModel public class NoneLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
{ {
private readonly NoneLayoutProvider _layoutProvider; private readonly NoneLayoutProvider _layoutProvider;
private readonly IDeviceService _deviceService;
public NoneLayoutViewModel(NoneLayoutProvider layoutProvider) public NoneLayoutViewModel(NoneLayoutProvider layoutProvider, IDeviceService deviceService)
{ {
_layoutProvider = layoutProvider; _layoutProvider = layoutProvider;
_deviceService = deviceService;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -28,5 +31,12 @@ public class NoneLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
public void Apply() public void Apply()
{ {
_layoutProvider.ConfigureDevice(Device); _layoutProvider.ConfigureDevice(Device);
Save();
}
private void Save()
{
_deviceService.SaveDevice(Device);
_deviceService.LoadDeviceLayout(Device);
} }
} }

View File

@ -2,13 +2,45 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 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" 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">
<StackPanel ClipToBounds="False"> <StackPanel ClipToBounds="False">
<Border Classes="card-separator" /> <Border Classes="card-separator" />
<StackPanel> <Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<TextBlock Text="Current layout" /> <StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Classes="subtitle" FontSize="12" Text="Loading the layout from a workshop entry" TextWrapping="Wrap" /> <TextBlock Text="Current layout" />
</StackPanel> <TextBlock Classes="subtitle" FontSize="12" Text="Loading the layout from a workshop entry" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
<StackPanel.Styles>
<Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate x:DataType="services:InstalledEntry">
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Styles>
<ComboBox Classes="layoutProvider"
Width="350"
SelectedItem="{CompiledBinding SelectedEntry}"
ItemsSource="{CompiledBinding Entries}"
PlaceholderText="Select an installed layout">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="services:InstalledEntry">
<StackPanel>
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
<TextBlock Classes="subtitle" Text="{CompiledBinding Author}" TextWrapping="Wrap" MaxWidth="350" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Grid>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View File

@ -1,8 +1,9 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders; namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public partial class WorkshopLayoutView : UserControl public partial class WorkshopLayoutView : ReactiveUserControl<WorkshopLayoutViewModel>
{ {
public WorkshopLayoutView() public WorkshopLayoutView()
{ {

View File

@ -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.Providers;
using Artemis.Core.Services;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Providers; using Artemis.WebClient.Workshop.Providers;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders; 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 WorkshopLayoutProvider _layoutProvider;
private readonly IDeviceService _deviceService;
public WorkshopLayoutViewModel(WorkshopLayoutProvider layoutProvider) public WorkshopLayoutViewModel(WorkshopLayoutProvider layoutProvider, IWorkshopService workshopService, IDeviceService deviceService)
{ {
_layoutProvider = layoutProvider; _layoutProvider = layoutProvider;
_deviceService = deviceService;
Entries = new ObservableCollection<InstalledEntry>(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));
} }
/// <inheritdoc /> /// <inheritdoc />
@ -19,6 +35,8 @@ public class WorkshopLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
public ArtemisDevice Device { get; set; } = null!; public ArtemisDevice Device { get; set; } = null!;
public ObservableCollection<InstalledEntry> Entries { get; }
/// <inheritdoc /> /// <inheritdoc />
public string Name => "Workshop"; public string Name => "Workshop";
@ -28,6 +46,21 @@ public class WorkshopLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
/// <inheritdoc /> /// <inheritdoc />
public void Apply() 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);
} }
} }

View File

@ -45,7 +45,7 @@ public class EntryReleasesViewModel : ViewModelBase
return; return;
IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType); IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType);
EntryInstallResult result = await installationHandler.InstallAsync(Entry, Entry.LatestRelease.Id, new Progress<StreamProgress>(), cancellationToken); EntryInstallResult result = await installationHandler.InstallAsync(Entry, Entry.LatestRelease, new Progress<StreamProgress>(), cancellationToken);
if (result.IsSuccess) if (result.IsSuccess)
_notificationService.CreateNotification().WithTitle($"{Entry.EntryType.Humanize(LetterCasing.Sentence)} installed").WithSeverity(NotificationSeverity.Success).Show(); _notificationService.CreateNotification().WithTitle($"{Entry.EntryType.Humanize(LetterCasing.Sentence)} installed").WithSeverity(NotificationSeverity.Success).Show();
else else

View File

@ -5,6 +5,6 @@ namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
public interface IEntryInstallationHandler public interface IEntryInstallationHandler
{ {
Task<EntryInstallResult> InstallAsync(IGetEntryById_Entry entry, long releaseId, Progress<StreamProgress> progress, CancellationToken cancellationToken); Task<EntryInstallResult> InstallAsync(IEntryDetails entry, IRelease release, Progress<StreamProgress> progress, CancellationToken cancellationToken);
Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken); Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken);
} }

View File

@ -1,10 +1,10 @@
using System.Data; using System.IO.Compression;
using System.IO.Compression;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Providers;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Shared.Extensions; using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.Providers;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers; namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
@ -14,15 +14,17 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IWorkshopService _workshopService; private readonly IWorkshopService _workshopService;
private readonly IDeviceService _deviceService; 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; _httpClientFactory = httpClientFactory;
_workshopService = workshopService; _workshopService = workshopService;
_deviceService = deviceService; _deviceService = deviceService;
_defaultLayoutProvider = defaultLayoutProvider;
} }
public async Task<EntryInstallResult> InstallAsync(IGetEntryById_Entry entry, long releaseId, Progress<StreamProgress> progress, CancellationToken cancellationToken) public async Task<EntryInstallResult> InstallAsync(IEntryDetails entry, IRelease release, Progress<StreamProgress> progress, CancellationToken cancellationToken)
{ {
using MemoryStream stream = new(); using MemoryStream stream = new();
@ -30,7 +32,7 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler
try try
{ {
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); 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) catch (Exception e)
{ {
@ -38,8 +40,8 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler
} }
// Ensure there is an installed entry // Ensure there is an installed entry
InstalledEntry installedEntry = _workshopService.GetInstalledEntry(entry) ?? _workshopService.CreateInstalledEntry(entry); InstalledEntry installedEntry = _workshopService.GetInstalledEntry(entry) ?? new InstalledEntry(entry, release);
DirectoryInfo entryDirectory = installedEntry.GetDirectory(); 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 the folder already exists, remove it so that if the layout now contains less files, old things dont stick around
if (entryDirectory.Exists) if (entryDirectory.Exists)
@ -53,7 +55,11 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler
ArtemisLayout layout = new(Path.Combine(entryDirectory.FullName, "layout.xml")); ArtemisLayout layout = new(Path.Combine(entryDirectory.FullName, "layout.xml"));
if (layout.IsValid) if (layout.IsValid)
{
installedEntry.ApplyRelease(release);
_workshopService.SaveInstalledEntry(installedEntry);
return EntryInstallResult.FromSuccess(layout); return EntryInstallResult.FromSuccess(layout);
}
// If the layout ended up being invalid yoink it out again, shoooo // If the layout ended up being invalid yoink it out again, shoooo
entryDirectory.Delete(true); entryDirectory.Delete(true);
@ -61,21 +67,26 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler
return EntryInstallResult.FromFailure("Layout failed to load because it is invalid"); return EntryInstallResult.FromFailure("Layout failed to load because it is invalid");
} }
public async Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) public Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken)
{ {
// Remove the layout from any devices currently using it // Remove the layout from any devices currently using it
foreach (ArtemisDevice device in _deviceService.Devices) 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 // Remove from filesystem
DirectoryInfo directory = installedEntry.GetDirectory(true); DirectoryInfo directory = installedEntry.GetDirectory();
if (directory.Exists) if (directory.Exists)
directory.Delete(); directory.Delete(true);
// Remove entry // Remove entry
_workshopService.RemoveInstalledEntry(installedEntry); _workshopService.RemoveInstalledEntry(installedEntry);
return Task.FromResult(EntryUninstallResult.FromSuccess());
return EntryUninstallResult.FromSuccess();
} }
} }

View File

@ -19,7 +19,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
_workshopService = workshopService; _workshopService = workshopService;
} }
public async Task<EntryInstallResult> InstallAsync(IGetEntryById_Entry entry, long releaseId, Progress<StreamProgress> progress, CancellationToken cancellationToken) public async Task<EntryInstallResult> InstallAsync(IEntryDetails entry, IRelease release, Progress<StreamProgress> progress, CancellationToken cancellationToken)
{ {
using MemoryStream stream = new(); using MemoryStream stream = new();
@ -27,7 +27,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
try try
{ {
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); 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) catch (Exception e)
{ {
@ -45,13 +45,13 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
installedEntry.SetMetadata("ProfileId", overwritten.ProfileId); installedEntry.SetMetadata("ProfileId", overwritten.ProfileId);
// Update the release and return the profile configuration // Update the release and return the profile configuration
UpdateRelease(releaseId, installedEntry); UpdateRelease(installedEntry, release);
return EntryInstallResult.FromSuccess(overwritten); return EntryInstallResult.FromSuccess(overwritten);
} }
} }
// Ensure there is an installed entry // Ensure there is an installed entry
installedEntry ??= _workshopService.CreateInstalledEntry(entry); installedEntry ??= new InstalledEntry(entry, release);
// Add the profile as a fresh import // Add the profile as a fresh import
ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == "Workshop") ?? _profileService.CreateProfileCategory("Workshop", true); 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); installedEntry.SetMetadata("ProfileId", imported.ProfileId);
// Update the release and return the profile configuration // Update the release and return the profile configuration
UpdateRelease(releaseId, installedEntry); UpdateRelease(installedEntry, release);
return EntryInstallResult.FromSuccess(imported); return EntryInstallResult.FromSuccess(imported);
} }
@ -89,11 +89,9 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
}, cancellationToken); }, cancellationToken);
} }
private void UpdateRelease(long releaseId, InstalledEntry installedEntry) private void UpdateRelease(InstalledEntry installedEntry, IRelease release)
{ {
installedEntry.ReleaseId = releaseId; installedEntry.ApplyRelease(release);
installedEntry.ReleaseVersion = "TODO";
installedEntry.InstalledAt = DateTimeOffset.UtcNow;
_workshopService.SaveInstalledEntry(installedEntry); _workshopService.SaveInstalledEntry(installedEntry);
} }
} }

View File

@ -1,6 +1,5 @@
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Providers; using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
namespace Artemis.WebClient.Workshop.Providers; namespace Artemis.WebClient.Workshop.Providers;
@ -8,34 +7,31 @@ namespace Artemis.WebClient.Workshop.Providers;
public class WorkshopLayoutProvider : ILayoutProvider public class WorkshopLayoutProvider : ILayoutProvider
{ {
public static string LayoutType = "Workshop"; public static string LayoutType = "Workshop";
private readonly IDeviceService _deviceService;
private readonly IWorkshopService _workshopService; private readonly IWorkshopService _workshopService;
public WorkshopLayoutProvider(IDeviceService deviceService, IWorkshopService workshopService) public WorkshopLayoutProvider(IWorkshopService workshopService)
{ {
_deviceService = deviceService;
_workshopService = workshopService; _workshopService = workshopService;
} }
/// <inheritdoc /> /// <inheritdoc />
public ArtemisLayout? GetDeviceLayout(ArtemisDevice device) 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) if (layoutEntry == null)
return 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)) if (!File.Exists(layoutPath))
return null; return null;
return new ArtemisLayout(layoutPath); return new ArtemisLayout(layoutPath);
} }
/// <inheritdoc /> /// <inheritdoc />
public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout) public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout)
{ {
throw new NotImplementedException(); device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -44,20 +40,17 @@ public class WorkshopLayoutProvider : ILayoutProvider
return device.LayoutSelection.Type == LayoutType; return device.LayoutSelection.Type == LayoutType;
} }
private bool MatchesDevice(InstalledEntry entry, ArtemisDevice device)
{
return entry.TryGetMetadata("DeviceId", out HashSet<string>? deviceIds) && deviceIds.Contains(device.Identifier);
}
/// <summary> /// <summary>
/// Configures the provided device to use this layout provider. /// Configures the provided device to use this layout provider.
/// </summary> /// </summary>
/// <param name="device">The device to apply the provider to.</param> /// <param name="device">The device to apply the provider to.</param>
public void ConfigureDevice(ArtemisDevice device) /// <param name="entry">The workshop entry to use as a layout.</param>
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.Type = LayoutType;
device.LayoutSelection.Parameter = null; device.LayoutSelection.Parameter = entry?.EntryId.ToString();
_deviceService.SaveDevice(device);
_deviceService.LoadDeviceLayout(device);
} }
} }

View File

@ -41,13 +41,17 @@ fragment entryDetails on Entry {
...category ...category
} }
latestRelease { latestRelease {
id ...release
version
downloadSize
md5Hash
createdAt
} }
images { images {
...image ...image
} }
} }
fragment release on Release {
id
version
downloadSize
md5Hash
createdAt
}

View File

@ -14,7 +14,7 @@ public class InstalledEntry
Load(); Load();
} }
public InstalledEntry(IGetEntryById_Entry entry) public InstalledEntry(IEntryDetails entry, IRelease release)
{ {
Entity = new EntryEntity(); Entity = new EntryEntity();
@ -23,6 +23,9 @@ public class InstalledEntry
Author = entry.Author; Author = entry.Author;
Name = entry.Name; Name = entry.Name;
InstalledAt = DateTimeOffset.Now;
ReleaseId = release.Id;
ReleaseVersion = release.Version;
} }
public long EntryId { get; set; } public long EntryId { get; set; }
@ -110,14 +113,30 @@ public class InstalledEntry
/// <summary> /// <summary>
/// Returns the directory info of the entry, where any files would be stored if applicable. /// Returns the directory info of the entry, where any files would be stored if applicable.
/// </summary> /// </summary>
/// <param name="rootDirectory">A value indicating whether or not to return the root directory of the entry, and not the version.</param> /// <returns>The directory info of the directory.</returns>
/// <returns>The directory info of the entry, where any files would be stored if applicable.</returns> public DirectoryInfo GetDirectory()
public DirectoryInfo GetDirectory(bool rootDirectory = false)
{ {
if (rootDirectory) return new DirectoryInfo(Path.Combine(Constants.WorkshopFolder, $"{EntryId}-{StringUtilities.UrlFriendly(Name)}"));
return new DirectoryInfo(Path.Combine(Constants.WorkshopFolder, EntryId.ToString())); }
string safeVersion = Path.GetInvalidFileNameChars().Aggregate(ReleaseVersion, (current, c) => current.Replace(c, '-')); /// <summary>
return new DirectoryInfo(Path.Combine(Constants.WorkshopFolder, EntryId.ToString(), safeVersion)); /// Returns the directory info of a release of this entry, where any files would be stored if applicable.
/// </summary>
/// <param name="release">The release to use, if none provided the current release is used.</param>
/// <returns>The directory info of the directory.</returns>
public DirectoryInfo GetReleaseDirectory(IRelease? release = null)
{
return new DirectoryInfo(Path.Combine(GetDirectory().FullName, StringUtilities.UrlFriendly(release?.Version ?? ReleaseVersion)));
}
/// <summary>
/// Applies the provided release to the installed entry.
/// </summary>
/// <param name="release">The release to apply.</param>
public void ApplyRelease(IRelease release)
{
ReleaseId = release.Id;
ReleaseVersion = release.Version;
InstalledAt = DateTimeOffset.UtcNow;
} }
} }

View File

@ -1,4 +1,3 @@
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers; using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
namespace Artemis.WebClient.Workshop.Services; namespace Artemis.WebClient.Workshop.Services;
@ -13,11 +12,9 @@ public interface IWorkshopService
Task NavigateToEntry(long entryId, EntryType entryType); Task NavigateToEntry(long entryId, EntryType entryType);
List<InstalledEntry> GetInstalledEntries(); List<InstalledEntry> GetInstalledEntries();
InstalledEntry? GetInstalledEntry(IGetEntryById_Entry entry); InstalledEntry? GetInstalledEntry(IEntryDetails entry);
InstalledEntry CreateInstalledEntry(IGetEntryById_Entry entry);
void RemoveInstalledEntry(InstalledEntry installedEntry); void RemoveInstalledEntry(InstalledEntry installedEntry);
void SaveInstalledEntry(InstalledEntry entry); void SaveInstalledEntry(InstalledEntry entry);
public record WorkshopStatus(bool IsReachable, string Message); public record WorkshopStatus(bool IsReachable, string Message);
} }

View File

@ -133,7 +133,7 @@ public class WorkshopService : IWorkshopService
} }
/// <inheritdoc /> /// <inheritdoc />
public InstalledEntry? GetInstalledEntry(IGetEntryById_Entry entry) public InstalledEntry? GetInstalledEntry(IEntryDetails entry)
{ {
EntryEntity? entity = _entryRepository.GetByEntryId(entry.Id); EntryEntity? entity = _entryRepository.GetByEntryId(entry.Id);
if (entity == null) if (entity == null)
@ -143,9 +143,9 @@ public class WorkshopService : IWorkshopService
} }
/// <inheritdoc /> /// <inheritdoc />
public InstalledEntry CreateInstalledEntry(IGetEntryById_Entry entry) public void AddOrUpdateInstalledEntry(InstalledEntry entry, IRelease release)
{ {
return new InstalledEntry(entry); throw new NotImplementedException();
} }
/// <inheritdoc /> /// <inheritdoc />