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

Devices - Reworking layout loading

This commit is contained in:
RobertBeekman 2024-01-09 21:27:51 +01:00
parent 9393bf2b68
commit e8590abd61
57 changed files with 824 additions and 349 deletions

View File

@ -62,6 +62,11 @@ public static class Constants
/// The full path to the Artemis user layouts folder /// The full path to the Artemis user layouts folder
/// </summary> /// </summary>
public static readonly string LayoutsFolder = Path.Combine(DataFolder, "User Layouts"); public static readonly string LayoutsFolder = Path.Combine(DataFolder, "User Layouts");
/// <summary>
/// The full path to the Artemis user layouts folder
/// </summary>
public static readonly string WorkshopFolder = Path.Combine(DataFolder, "workshop");
/// <summary> /// <summary>
/// The current API version for plugins /// The current API version for plugins
@ -155,4 +160,5 @@ public static class Constants
/// Gets the graphics context to be used for rendering by SkiaSharp. /// Gets the graphics context to be used for rendering by SkiaSharp.
/// </summary> /// </summary>
public static IManagedGraphicsContext? ManagedGraphicsContext { get; internal set; } public static IManagedGraphicsContext? ManagedGraphicsContext { get; internal set; }
} }

View File

@ -2,6 +2,7 @@ using System;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Artemis.Core.DryIoc.Factories; using Artemis.Core.DryIoc.Factories;
using Artemis.Core.Providers;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.Storage; using Artemis.Storage;
using Artemis.Storage.Migrations.Interfaces; using Artemis.Storage.Migrations.Interfaces;
@ -35,7 +36,8 @@ public static class ContainerExtensions
// Bind migrations // Bind migrations
container.RegisterMany(storageAssembly, type => type.IsAssignableTo<IStorageMigration>(), Reuse.Singleton, nonPublicServiceTypes: true); container.RegisterMany(storageAssembly, type => type.IsAssignableTo<IStorageMigration>(), Reuse.Singleton, nonPublicServiceTypes: true);
container.RegisterMany(coreAssembly, type => type.IsAssignableTo<ILayoutProvider>(), Reuse.Singleton);
container.Register<IPluginSettingsFactory, PluginSettingsFactory>(Reuse.Singleton); container.Register<IPluginSettingsFactory, PluginSettingsFactory>(Reuse.Singleton);
container.Register(Made.Of(_ => ServiceInfo.Of<IPluginSettingsFactory>(), f => f.CreatePluginSettings(Arg.Index<Type>(0)), r => r.Parent.ImplementationType)); container.Register(Made.Of(_ => ServiceInfo.Of<IPluginSettingsFactory>(), f => f.CreatePluginSettings(Arg.Index<Type>(0)), r => r.Parent.ImplementationType));
container.Register<ILoggerFactory, LoggerFactory>(Reuse.Singleton); container.Register<ILoggerFactory, LoggerFactory>(Reuse.Singleton);

View File

@ -2,9 +2,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Artemis.Core.DeviceProviders; using Artemis.Core.DeviceProviders;
using Artemis.Core.Providers;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.Storage.Entities.Surface; using Artemis.Storage.Entities.Surface;
using RGB.NET.Core; using RGB.NET.Core;
@ -46,6 +46,7 @@ public class ArtemisDevice : CorePropertyChanged
InputIdentifiers = new List<ArtemisDeviceInputIdentifier>(); InputIdentifiers = new List<ArtemisDeviceInputIdentifier>();
InputMappings = new Dictionary<ArtemisLed, ArtemisLed>(); InputMappings = new Dictionary<ArtemisLed, ArtemisLed>();
Categories = new HashSet<DeviceCategory>(); Categories = new HashSet<DeviceCategory>();
LayoutSelection = new LayoutSelection {Type = DefaultLayoutProvider.LayoutType};
RgbDevice.ColorCorrections.Clear(); RgbDevice.ColorCorrections.Clear();
RgbDevice.ColorCorrections.Add(new ScaleColorCorrection(this)); RgbDevice.ColorCorrections.Add(new ScaleColorCorrection(this));
@ -74,6 +75,7 @@ public class ArtemisDevice : CorePropertyChanged
InputIdentifiers = new List<ArtemisDeviceInputIdentifier>(); InputIdentifiers = new List<ArtemisDeviceInputIdentifier>();
InputMappings = new Dictionary<ArtemisLed, ArtemisLed>(); InputMappings = new Dictionary<ArtemisLed, ArtemisLed>();
Categories = new HashSet<DeviceCategory>(); Categories = new HashSet<DeviceCategory>();
LayoutSelection = new LayoutSelection {Type = DefaultLayoutProvider.LayoutType};
foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers)
InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier));
@ -153,6 +155,8 @@ public class ArtemisDevice : CorePropertyChanged
/// </summary> /// </summary>
public HashSet<DeviceCategory> Categories { get; } public HashSet<DeviceCategory> Categories { get; }
public LayoutSelection LayoutSelection { get; }
/// <summary> /// <summary>
/// Gets or sets the X-position of the device /// Gets or sets the X-position of the device
/// </summary> /// </summary>
@ -293,19 +297,6 @@ public class ArtemisDevice : CorePropertyChanged
} }
} }
/// <summary>
/// Gets or sets a boolean indicating whether falling back to default layouts is enabled or not
/// </summary>
public bool DisableDefaultLayout
{
get => DeviceEntity.DisableDefaultLayout;
set
{
DeviceEntity.DisableDefaultLayout = value;
OnPropertyChanged(nameof(DisableDefaultLayout));
}
}
/// <summary> /// <summary>
/// Gets or sets the logical layout of the device e.g. DE, UK or US. /// Gets or sets the logical layout of the device e.g. DE, UK or US.
/// <para>Only applicable to keyboards</para> /// <para>Only applicable to keyboards</para>
@ -320,20 +311,6 @@ public class ArtemisDevice : CorePropertyChanged
} }
} }
/// <summary>
/// Gets or sets the path of the custom layout to load when calling <see cref="GetBestDeviceLayout" />
/// for this device
/// </summary>
public string? CustomLayoutPath
{
get => DeviceEntity.CustomLayoutPath;
set
{
DeviceEntity.CustomLayoutPath = value;
OnPropertyChanged(nameof(CustomLayoutPath));
}
}
/// <summary> /// <summary>
/// Gets the layout of the device expanded with Artemis-specific data /// Gets the layout of the device expanded with Artemis-specific data
/// </summary> /// </summary>
@ -381,40 +358,6 @@ public class ArtemisDevice : CorePropertyChanged
return artemisLed; return artemisLed;
} }
/// <summary>
/// Returns the most preferred device layout for this device.
/// </summary>
/// <returns>The most preferred device layout for this device.</returns>
public ArtemisLayout? GetBestDeviceLayout()
{
ArtemisLayout? layout;
// Configured layout path takes precedence over all other options
if (CustomLayoutPath != null)
{
layout = new ArtemisLayout(CustomLayoutPath, LayoutSource.Configured);
if (layout.IsValid)
return layout;
}
// Look for a layout provided by the user
layout = DeviceProvider.LoadUserLayout(this);
if (layout.IsValid)
return layout;
if (DisableDefaultLayout)
return null;
// Look for a layout provided by the plugin
layout = DeviceProvider.LoadLayout(this);
if (layout.IsValid)
return layout;
// Finally fall back to a default layout
layout = ArtemisLayout.GetDefaultLayout(this);
return layout;
}
/// <summary> /// <summary>
/// Occurs when the underlying RGB.NET device was updated /// Occurs when the underlying RGB.NET device was updated
/// </summary> /// </summary>
@ -481,7 +424,7 @@ public class ArtemisDevice : CorePropertyChanged
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");
if (layout != null && layout.IsValid && removeExcessiveLeds && !DeviceProvider.RemoveExcessiveLedsSupported) if (layout != null && layout.IsValid && removeExcessiveLeds && !DeviceProvider.RemoveExcessiveLedsSupported)
throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} set to true because the device provider does not support it"); throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} set to true because the device provider does not support it");
// Always clear the current layout // Always clear the current layout
ClearLayout(); ClearLayout();
@ -534,6 +477,9 @@ public class ArtemisDevice : CorePropertyChanged
DeviceEntity.Categories.Clear(); DeviceEntity.Categories.Clear();
foreach (DeviceCategory deviceCategory in Categories) foreach (DeviceCategory deviceCategory in Categories)
DeviceEntity.Categories.Add((int) deviceCategory); DeviceEntity.Categories.Add((int) deviceCategory);
DeviceEntity.LayoutType = LayoutSelection.Type;
DeviceEntity.LayoutParameter = LayoutSelection.Parameter;
} }
internal void Load() internal void Load()
@ -548,6 +494,9 @@ public class ArtemisDevice : CorePropertyChanged
if (!Categories.Any()) if (!Categories.Any())
ApplyDefaultCategories(); ApplyDefaultCategories();
LayoutSelection.Type = DeviceEntity.LayoutType;
LayoutSelection.Parameter = DeviceEntity.LayoutParameter;
LoadInputMappings(); LoadInputMappings();
} }
@ -573,7 +522,7 @@ public class ArtemisDevice : CorePropertyChanged
{ {
Leds = RgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly(); Leds = RgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly();
LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(Leds.ToDictionary(l => l.RgbLed.Id, l => l)); LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(Leds.ToDictionary(l => l.RgbLed.Id, l => l));
if (loadInputMappings) if (loadInputMappings)
LoadInputMappings(); LoadInputMappings();
} }

View File

@ -12,17 +12,18 @@ namespace Artemis.Core;
/// </summary> /// </summary>
public class ArtemisLayout public class ArtemisLayout
{ {
private static readonly string DefaultLayoutPath = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis");
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="ArtemisLayout" /> class /// Creates a new instance of the <see cref="ArtemisLayout" /> class
/// </summary> /// </summary>
/// <param name="filePath">The path of the layout XML file</param> /// <param name="filePath">The path of the layout XML file</param>
/// <param name="source">The source from where this layout is being loaded</param> public ArtemisLayout(string filePath)
public ArtemisLayout(string filePath, LayoutSource source)
{ {
FilePath = filePath; FilePath = filePath;
Source = source;
Leds = new List<ArtemisLedLayout>(); Leds = new List<ArtemisLedLayout>();
IsDefaultLayout = filePath.StartsWith(DefaultLayoutPath);
LoadLayout(); LoadLayout();
} }
@ -31,11 +32,6 @@ public class ArtemisLayout
/// </summary> /// </summary>
public string FilePath { get; } public string FilePath { get; }
/// <summary>
/// Gets the source from where this layout was loaded
/// </summary>
public LayoutSource Source { get; }
/// <summary> /// <summary>
/// Gets a boolean indicating whether a valid layout was loaded /// Gets a boolean indicating whether a valid layout was loaded
/// </summary> /// </summary>
@ -61,6 +57,8 @@ public class ArtemisLayout
/// </summary> /// </summary>
public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!; public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!;
public bool IsDefaultLayout { get; private set; }
/// <summary> /// <summary>
/// Applies the layout to the provided device /// Applies the layout to the provided device
/// </summary> /// </summary>
@ -121,29 +119,28 @@ public class ArtemisLayout
internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device) internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device)
{ {
string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis");
if (device.DeviceType == RGBDeviceType.Keyboard) if (device.DeviceType == RGBDeviceType.Keyboard)
{ {
// XL layout is defined by its programmable macro keys // XL layout is defined by its programmable macro keys
if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_Programmable1 && l.RgbLed.Id <= LedId.Keyboard_Programmable32)) if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_Programmable1 && l.RgbLed.Id <= LedId.Keyboard_Programmable32))
{ {
if (device.PhysicalLayout == KeyboardLayoutType.ANSI) if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ANSI.xml"), LayoutSource.Default); return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis XL keyboard-ANSI.xml"));
return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ISO.xml"), LayoutSource.Default); return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis XL keyboard-ISO.xml"));
} }
// L layout is defined by its numpad // L layout is defined by its numpad
if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_NumLock && l.RgbLed.Id <= LedId.Keyboard_NumPeriodAndDelete)) if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_NumLock && l.RgbLed.Id <= LedId.Keyboard_NumPeriodAndDelete))
{ {
if (device.PhysicalLayout == KeyboardLayoutType.ANSI) if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis L keyboard-ANSI.xml"), LayoutSource.Default); return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis L keyboard-ANSI.xml"));
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis L keyboard-ISO.xml"), LayoutSource.Default); return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis L keyboard-ISO.xml"));
} }
// No numpad will result in TKL // No numpad will result in TKL
if (device.PhysicalLayout == KeyboardLayoutType.ANSI) if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis TKL keyboard-ANSI.xml"), LayoutSource.Default); return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis TKL keyboard-ANSI.xml"));
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis TKL keyboard-ISO.xml"), LayoutSource.Default); return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis TKL keyboard-ISO.xml"));
} }
// if (device.DeviceType == RGBDeviceType.Mouse) // if (device.DeviceType == RGBDeviceType.Mouse)
@ -151,21 +148,21 @@ public class ArtemisLayout
// if (device.Leds.Count == 1) // if (device.Leds.Count == 1)
// { // {
// if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo))
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse logo.xml"), LayoutSource.Default); // return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "1 LED mouse logo.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse.xml"), LayoutSource.Default); // return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "1 LED mouse.xml"), LayoutSource.Default);
// } // }
// if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo))
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse logo.xml"), LayoutSource.Default); // return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "4 LED mouse logo.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse.xml"), LayoutSource.Default); // return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "4 LED mouse.xml"), LayoutSource.Default);
// } // }
if (device.DeviceType == RGBDeviceType.Headset) if (device.DeviceType == RGBDeviceType.Headset)
{ {
if (device.Leds.Count == 1) if (device.Leds.Count == 1)
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 1 LED headset.xml"), LayoutSource.Default); return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Headset", "Artemis 1 LED headset.xml"));
if (device.Leds.Count == 2) if (device.Leds.Count == 2)
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 2 LED headset.xml"), LayoutSource.Default); return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Headset", "Artemis 2 LED headset.xml"));
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 4 LED headset.xml"), LayoutSource.Default); return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Headset", "Artemis 4 LED headset.xml"));
} }
return null; return null;
@ -206,30 +203,10 @@ public class ArtemisLayout
else else
Image = null; Image = null;
} }
}
/// <summary> /// <inheritdoc />
/// Represents a source from where a layout came public override string ToString()
/// </summary> {
public enum LayoutSource return FilePath;
{ }
/// <summary>
/// A layout loaded from config
/// </summary>
Configured,
/// <summary>
/// A layout loaded from the user layout folder
/// </summary>
User,
/// <summary>
/// A layout loaded from the plugin folder
/// </summary>
Plugin,
/// <summary>
/// A default layout loaded as a fallback option
/// </summary>
Default
} }

View File

@ -0,0 +1,28 @@
namespace Artemis.Core;
/// <summary>
/// Represents a reference to a layout for a device.
/// </summary>
public class LayoutSelection : CorePropertyChanged
{
private string? _type;
private string? _parameter;
/// <summary>
/// Gets or sets what kind of layout reference this is.
/// </summary>
public string? Type
{
get => _type;
set => SetAndNotify(ref _type, value);
}
/// <summary>
/// Gets or sets the parameter of the layout reference, such as a file path of workshop entry ID.
/// </summary>
public string? Parameter
{
get => _parameter;
set => SetAndNotify(ref _parameter, value);
}
}

View File

@ -62,7 +62,7 @@ public abstract class DeviceProvider : PluginFeature
device.DeviceType.ToString(), device.DeviceType.ToString(),
GetDeviceLayoutName(device) GetDeviceLayoutName(device)
); );
return new ArtemisLayout(filePath, LayoutSource.Plugin); return new ArtemisLayout(filePath);
} }
/// <summary> /// <summary>
@ -79,7 +79,7 @@ public abstract class DeviceProvider : PluginFeature
device.DeviceType.ToString(), device.DeviceType.ToString(),
GetDeviceLayoutName(device) GetDeviceLayoutName(device)
); );
return new ArtemisLayout(filePath, LayoutSource.User); return new ArtemisLayout(filePath);
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,28 @@
using RGB.NET.Layout;
namespace Artemis.Core.Providers;
public class CustomPathLayoutProvider : ILayoutProvider
{
public static string LayoutType = "CustomPath";
/// <inheritdoc />
public ArtemisLayout? GetDeviceLayout(ArtemisDevice device)
{
if (device.LayoutSelection.Parameter == null)
return null;
return new ArtemisLayout(device.LayoutSelection.Parameter);
}
/// <inheritdoc />
public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout)
{
device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported);
}
/// <inheritdoc />
public bool IsMatch(ArtemisDevice device)
{
return device.LayoutSelection.Type == LayoutType;
}
}

View File

@ -0,0 +1,31 @@
namespace Artemis.Core.Providers;
public class DefaultLayoutProvider : ILayoutProvider
{
public static string LayoutType = "Default";
/// <inheritdoc />
public ArtemisLayout? GetDeviceLayout(ArtemisDevice device)
{
// Look for a layout provided by the plugin
ArtemisLayout layout = device.DeviceProvider.LoadLayout(device);
// Finally fall back to a default layout
return layout.IsValid ? layout : ArtemisLayout.GetDefaultLayout(device);
}
/// <inheritdoc />
public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout)
{
if (layout.IsDefaultLayout)
device.ApplyLayout(layout, false, false);
else
device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported);
}
/// <inheritdoc />
public bool IsMatch(ArtemisDevice device)
{
return device.LayoutSelection.Type == LayoutType;
}
}

View File

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

View File

@ -0,0 +1,24 @@
namespace Artemis.Core.Providers;
public class NoneLayoutProvider : ILayoutProvider
{
public static string LayoutType = "None";
/// <inheritdoc />
public ArtemisLayout? GetDeviceLayout(ArtemisDevice device)
{
return null;
}
/// <inheritdoc />
public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout)
{
device.ApplyLayout(null, false, false);
}
/// <inheritdoc />
public bool IsMatch(ArtemisDevice device)
{
return device.LayoutSelection.Type == LayoutType;
}
}

View File

@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core.DeviceProviders; using Artemis.Core.DeviceProviders;
using Artemis.Core.Providers;
using Artemis.Core.Services.Models; using Artemis.Core.Services.Models;
using Artemis.Storage.Entities.Surface; using Artemis.Storage.Entities.Surface;
using Artemis.Storage.Repositories.Interfaces; using Artemis.Storage.Repositories.Interfaces;
@ -18,15 +19,21 @@ 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 Func<IEnumerable<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();
public DeviceService(ILogger logger, IPluginManagementService pluginManagementService, IDeviceRepository deviceRepository, Lazy<IRenderService> renderService) public DeviceService(ILogger logger,
IPluginManagementService pluginManagementService,
IDeviceRepository deviceRepository,
Lazy<IRenderService> renderService,
Func<IEnumerable<ILayoutProvider>> getLayoutProviders)
{ {
_logger = logger; _logger = logger;
_pluginManagementService = pluginManagementService; _pluginManagementService = pluginManagementService;
_deviceRepository = deviceRepository; _deviceRepository = deviceRepository;
_renderService = renderService; _renderService = renderService;
_getLayoutProviders = getLayoutProviders;
EnabledDevices = new ReadOnlyCollection<ArtemisDevice>(_enabledDevices); EnabledDevices = new ReadOnlyCollection<ArtemisDevice>(_enabledDevices);
Devices = new ReadOnlyCollection<ArtemisDevice>(_devices); Devices = new ReadOnlyCollection<ArtemisDevice>(_devices);
@ -157,12 +164,23 @@ internal class DeviceService : IDeviceService
} }
/// <inheritdoc /> /// <inheritdoc />
public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout) public void LoadDeviceLayout(ArtemisDevice device)
{ {
if (layout == null || layout.Source == LayoutSource.Default) ILayoutProvider? provider = _getLayoutProviders().FirstOrDefault(p => p.IsMatch(device));
device.ApplyLayout(layout, false, false); if (provider == null)
_logger.Warning("Could not find a layout provider for type {LayoutType} of device {Device}", device.LayoutSelection.Type, device);
ArtemisLayout? layout = provider?.GetDeviceLayout(device);
if (layout != null && !layout.IsValid)
{
_logger.Warning("Got an invalid layout {Layout} from {LayoutProvider}", layout, provider!.GetType().FullName);
layout = null;
}
if (layout == null)
device.ApplyLayout(null, false, false);
else else
device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); provider!.ApplyLayout(device, layout);
UpdateLeds(); UpdateLeds();
} }
@ -230,7 +248,7 @@ internal class DeviceService : IDeviceService
device = new ArtemisDevice(rgbDevice, deviceProvider); device = new ArtemisDevice(rgbDevice, deviceProvider);
} }
ApplyDeviceLayout(device, device.GetBestDeviceLayout()); LoadDeviceLayout(device);
return device; return device;
} }

View File

@ -43,11 +43,10 @@ public interface IDeviceService : IArtemisService
void AutoArrangeDevices(); void AutoArrangeDevices();
/// <summary> /// <summary>
/// Apples the provided <see cref="ArtemisLayout" /> to the provided <see cref="ArtemisDevice" /> /// Apples the best available to the provided <see cref="ArtemisDevice" />
/// </summary> /// </summary>
/// <param name="device"></param> /// <param name="device"></param>
/// <param name="layout"></param> void LoadDeviceLayout(ArtemisDevice device);
void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout);
/// <summary> /// <summary>
/// Enables the provided device /// Enables the provided device

View File

@ -22,11 +22,11 @@ public class DeviceEntity
public float GreenScale { get; set; } public float GreenScale { get; set; }
public float BlueScale { get; set; } public float BlueScale { get; set; }
public bool IsEnabled { get; set; } public bool IsEnabled { get; set; }
public bool DisableDefaultLayout { get; set; }
public int PhysicalLayout { get; set; } public int PhysicalLayout { get; set; }
public string LogicalLayout { get; set; } public string LogicalLayout { get; set; }
public string CustomLayoutPath { get; set; } public string LayoutType { get; set; }
public string LayoutParameter { get; set; }
public List<DeviceInputIdentifierEntity> InputIdentifiers { get; set; } public List<DeviceInputIdentifierEntity> InputIdentifiers { get; set; }
public List<InputMappingEntity> InputMappings { get; set; } public List<InputMappingEntity> InputMappings { get; set; }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace Artemis.Storage.Entities.Workshop; namespace Artemis.Storage.Entities.Workshop;
@ -16,6 +17,6 @@ public class EntryEntity
public long ReleaseId { get; set; } public long ReleaseId { get; set; }
public string ReleaseVersion { get; set; } public string ReleaseVersion { get; set; }
public DateTimeOffset InstalledAt { get; set; } public DateTimeOffset InstalledAt { get; set; }
public string LocalReference { get; set; } public Dictionary<string,object> Metadata { get; set; }
} }

View File

@ -58,19 +58,4 @@
<HintPath>..\..\..\RGB.NET\bin\net7.0\RGB.NET.Layout.dll</HintPath> <HintPath>..\..\..\RGB.NET\bin\net7.0\RGB.NET.Layout.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Update="Screens\Workshop\Entries\List\EntryListInputView.axaml.cs">
<DependentUpon>EntryListInputView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Entries\List\EntryListItemView.axaml.cs">
<DependentUpon>EntryListItemView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Entries\Details\EntrySpecificationsView.axaml.cs">
<DependentUpon>EntrySpecificationsView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Project> </Project>

View File

@ -1,6 +1,7 @@
using System.Reflection; using System.Reflection;
using Artemis.UI.DryIoc.Factories; using Artemis.UI.DryIoc.Factories;
using Artemis.UI.DryIoc.InstanceProviders; using Artemis.UI.DryIoc.InstanceProviders;
using Artemis.UI.Screens.Device.Layout.LayoutProviders;
using Artemis.UI.Screens.VisualScripting; using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Services.Interfaces; using Artemis.UI.Services.Interfaces;
using Artemis.UI.Services.Updating; using Artemis.UI.Services.Updating;
@ -26,6 +27,7 @@ public static class ContainerExtensions
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<ViewModelBase>(), setup: Setup.With(preventDisposal: true)); container.RegisterMany(thisAssembly, type => type.IsAssignableTo<ViewModelBase>(), setup: Setup.With(preventDisposal: true));
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IToolViewModel>() && type.IsInterface, setup: Setup.With(preventDisposal: true)); container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IToolViewModel>() && type.IsInterface, setup: Setup.With(preventDisposal: true));
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<ILayoutProviderViewModel>() && type.IsInterface, setup: Setup.With(preventDisposal: true));
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IVmFactory>() && type != typeof(PropertyVmFactory)); container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IVmFactory>() && type != typeof(PropertyVmFactory));
container.Register<NodeScriptWindowViewModelBase, NodeScriptWindowViewModel>(Reuse.Singleton); container.Register<NodeScriptWindowViewModelBase, NodeScriptWindowViewModel>(Reuse.Singleton);

View File

@ -1,13 +1,14 @@
using System; using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive; using System.Reactive;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.LayerBrushes; using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects; using Artemis.Core.LayerEffects;
using Artemis.Core.ScriptingProviders; using Artemis.Core.ScriptingProviders;
using Artemis.UI.Routing;
using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Device;
using Artemis.UI.Screens.Device.General;
using Artemis.UI.Screens.Device.InputMappings;
using Artemis.UI.Screens.Device.Layout;
using Artemis.UI.Screens.Device.Leds;
using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.Plugins.Features; using Artemis.UI.Screens.Plugins.Features;
using Artemis.UI.Screens.Plugins.Prerequisites; using Artemis.UI.Screens.Plugins.Prerequisites;
@ -27,12 +28,8 @@ using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Screens.SurfaceEditor; using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.VisualScripting; using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Screens.VisualScripting.Pins;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Updating; using Artemis.WebClient.Updating;
using DryIoc; using DryIoc;
using DynamicData;
using Material.Icons;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.DryIoc.Factories; namespace Artemis.UI.DryIoc.Factories;
@ -51,48 +48,49 @@ public interface IDeviceVmFactory : IVmFactory
InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds); InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds);
DeviceGeneralTabViewModel DeviceGeneralTabViewModel(ArtemisDevice device); DeviceGeneralTabViewModel DeviceGeneralTabViewModel(ArtemisDevice device);
} }
public class DeviceFactory : IDeviceVmFactory public class DeviceFactory : IDeviceVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
public DeviceFactory(IContainer container) public DeviceFactory(IContainer container)
{ {
_container = container; _container = container;
} }
public DevicePropertiesViewModel DevicePropertiesViewModel(ArtemisDevice device) public DevicePropertiesViewModel DevicePropertiesViewModel(ArtemisDevice device)
{ {
return _container.Resolve<DevicePropertiesViewModel>(new object[] { device }); return _container.Resolve<DevicePropertiesViewModel>(new object[] {device});
} }
public DeviceSettingsViewModel DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel) public DeviceSettingsViewModel DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel)
{ {
return _container.Resolve<DeviceSettingsViewModel>(new object[] { device, devicesTabViewModel }); return _container.Resolve<DeviceSettingsViewModel>(new object[] {device, devicesTabViewModel});
} }
public DeviceDetectInputViewModel DeviceDetectInputViewModel(ArtemisDevice device) public DeviceDetectInputViewModel DeviceDetectInputViewModel(ArtemisDevice device)
{ {
return _container.Resolve<DeviceDetectInputViewModel>(new object[] { device }); return _container.Resolve<DeviceDetectInputViewModel>(new object[] {device});
} }
public DeviceLayoutTabViewModel DeviceLayoutTabViewModel(ArtemisDevice device) public DeviceLayoutTabViewModel DeviceLayoutTabViewModel(ArtemisDevice device)
{ {
return _container.Resolve<DeviceLayoutTabViewModel>(new object[] { device }); return _container.Resolve<DeviceLayoutTabViewModel>(new object[] {device});
} }
public DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds) public DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds)
{ {
return _container.Resolve<DeviceLedsTabViewModel>(new object[] { device, selectedLeds }); return _container.Resolve<DeviceLedsTabViewModel>(new object[] {device, selectedLeds});
} }
public InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds) public InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds)
{ {
return _container.Resolve<InputMappingsTabViewModel>(new object[] { device, selectedLeds }); return _container.Resolve<InputMappingsTabViewModel>(new object[] {device, selectedLeds});
} }
public DeviceGeneralTabViewModel DeviceGeneralTabViewModel(ArtemisDevice device) public DeviceGeneralTabViewModel DeviceGeneralTabViewModel(ArtemisDevice device)
{ {
return _container.Resolve<DeviceGeneralTabViewModel>(new object[] { device }); return _container.Resolve<DeviceGeneralTabViewModel>(new object[] {device});
} }
} }
@ -102,28 +100,29 @@ public interface ISettingsVmFactory : IVmFactory
PluginViewModel PluginViewModel(Plugin plugin, ReactiveCommand<Unit, Unit>? reload); PluginViewModel PluginViewModel(Plugin plugin, ReactiveCommand<Unit, Unit>? reload);
PluginFeatureViewModel PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield); PluginFeatureViewModel PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield);
} }
public class SettingsVmFactory : ISettingsVmFactory public class SettingsVmFactory : ISettingsVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
public SettingsVmFactory(IContainer container) public SettingsVmFactory(IContainer container)
{ {
_container = container; _container = container;
} }
public PluginSettingsViewModel PluginSettingsViewModel(Plugin plugin) public PluginSettingsViewModel PluginSettingsViewModel(Plugin plugin)
{ {
return _container.Resolve<PluginSettingsViewModel>(new object[] { plugin }); return _container.Resolve<PluginSettingsViewModel>(new object[] {plugin});
} }
public PluginViewModel PluginViewModel(Plugin plugin, ReactiveCommand<Unit, Unit>? reload) public PluginViewModel PluginViewModel(Plugin plugin, ReactiveCommand<Unit, Unit>? reload)
{ {
return _container.Resolve<PluginViewModel>(new object?[] { plugin, reload }); return _container.Resolve<PluginViewModel>(new object?[] {plugin, reload});
} }
public PluginFeatureViewModel PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield) public PluginFeatureViewModel PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield)
{ {
return _container.Resolve<PluginFeatureViewModel>(new object[] { pluginFeatureInfo, showShield }); return _container.Resolve<PluginFeatureViewModel>(new object[] {pluginFeatureInfo, showShield});
} }
} }
@ -132,23 +131,24 @@ public interface ISidebarVmFactory : IVmFactory
SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory);
SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration);
} }
public class SidebarVmFactory : ISidebarVmFactory public class SidebarVmFactory : ISidebarVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
public SidebarVmFactory(IContainer container) public SidebarVmFactory(IContainer container)
{ {
_container = container; _container = container;
} }
public SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory) public SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory)
{ {
return _container.Resolve<SidebarCategoryViewModel>(new object[] { profileCategory }); return _container.Resolve<SidebarCategoryViewModel>(new object[] {profileCategory});
} }
public SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration) public SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration)
{ {
return _container.Resolve<SidebarProfileConfigurationViewModel>(new object[] { profileConfiguration }); return _container.Resolve<SidebarProfileConfigurationViewModel>(new object[] {profileConfiguration});
} }
} }
@ -157,6 +157,7 @@ public interface ISurfaceVmFactory : IVmFactory
SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel); SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel);
ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel); ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel);
} }
public class SurfaceVmFactory : ISurfaceVmFactory public class SurfaceVmFactory : ISurfaceVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
@ -168,12 +169,12 @@ public class SurfaceVmFactory : ISurfaceVmFactory
public SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel) public SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel)
{ {
return _container.Resolve<SurfaceDeviceViewModel>(new object[] { device, surfaceEditorViewModel }); return _container.Resolve<SurfaceDeviceViewModel>(new object[] {device, surfaceEditorViewModel});
} }
public ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel) public ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel)
{ {
return _container.Resolve<ListDeviceViewModel>(new object[] { device, surfaceEditorViewModel }); return _container.Resolve<ListDeviceViewModel>(new object[] {device, surfaceEditorViewModel});
} }
} }
@ -181,6 +182,7 @@ public interface IPrerequisitesVmFactory : IVmFactory
{ {
PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall); PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall);
} }
public class PrerequisitesVmFactory : IPrerequisitesVmFactory public class PrerequisitesVmFactory : IPrerequisitesVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
@ -192,7 +194,7 @@ public class PrerequisitesVmFactory : IPrerequisitesVmFactory
public PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall) public PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall)
{ {
return _container.Resolve<PluginPrerequisiteViewModel>(new object[] { pluginPrerequisite, uninstall }); return _container.Resolve<PluginPrerequisiteViewModel>(new object[] {pluginPrerequisite, uninstall});
} }
} }
@ -204,6 +206,7 @@ public interface IProfileEditorVmFactory : IVmFactory
LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer); LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer);
LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer); LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer);
} }
public class ProfileEditorVmFactory : IProfileEditorVmFactory public class ProfileEditorVmFactory : IProfileEditorVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
@ -215,27 +218,27 @@ public class ProfileEditorVmFactory : IProfileEditorVmFactory
public FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder) public FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder)
{ {
return _container.Resolve<FolderTreeItemViewModel>(new object?[] { parent, folder }); return _container.Resolve<FolderTreeItemViewModel>(new object?[] {parent, folder});
} }
public LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer) public LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer)
{ {
return _container.Resolve<LayerShapeVisualizerViewModel>(new object[] { layer }); return _container.Resolve<LayerShapeVisualizerViewModel>(new object[] {layer});
} }
public LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer) public LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer)
{ {
return _container.Resolve<LayerTreeItemViewModel>(new object?[] { parent, layer }); return _container.Resolve<LayerTreeItemViewModel>(new object?[] {parent, layer});
} }
public LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer) public LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer)
{ {
return _container.Resolve<LayerVisualizerViewModel>(new object[] { layer }); return _container.Resolve<LayerVisualizerViewModel>(new object[] {layer});
} }
public ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen) public ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen)
{ {
return _container.Resolve<ProfileEditorViewModel>(new object[] { hostScreen }); return _container.Resolve<ProfileEditorViewModel>(new object[] {hostScreen});
} }
} }
@ -251,6 +254,7 @@ public interface ILayerPropertyVmFactory : IVmFactory
TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels); TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels);
TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel); TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
} }
public class LayerPropertyVmFactory : ILayerPropertyVmFactory public class LayerPropertyVmFactory : ILayerPropertyVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
@ -262,37 +266,37 @@ public class LayerPropertyVmFactory : ILayerPropertyVmFactory
public PropertyViewModel PropertyViewModel(ILayerProperty layerProperty) public PropertyViewModel PropertyViewModel(ILayerProperty layerProperty)
{ {
return _container.Resolve<PropertyViewModel>(new object[] { layerProperty }); return _container.Resolve<PropertyViewModel>(new object[] {layerProperty});
} }
public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup) public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup)
{ {
return _container.Resolve<PropertyGroupViewModel>(new object[] { layerPropertyGroup }); return _container.Resolve<PropertyGroupViewModel>(new object[] {layerPropertyGroup});
} }
public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush layerBrush) public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush layerBrush)
{ {
return _container.Resolve<PropertyGroupViewModel>(new object[] { layerPropertyGroup, layerBrush }); return _container.Resolve<PropertyGroupViewModel>(new object[] {layerPropertyGroup, layerBrush});
} }
public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect) public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect)
{ {
return _container.Resolve<PropertyGroupViewModel>(new object[] { layerPropertyGroup, layerEffect }); return _container.Resolve<PropertyGroupViewModel>(new object[] {layerPropertyGroup, layerEffect});
} }
public TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel) public TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel)
{ {
return _container.Resolve<TreeGroupViewModel>(new object[] { propertyGroupViewModel }); return _container.Resolve<TreeGroupViewModel>(new object[] {propertyGroupViewModel});
} }
public TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels) public TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels)
{ {
return _container.Resolve<TimelineViewModel>(new object[] { propertyGroupViewModels }); return _container.Resolve<TimelineViewModel>(new object[] {propertyGroupViewModels});
} }
public TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel) public TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel)
{ {
return _container.Resolve<TimelineGroupViewModel>(new object[] { propertyGroupViewModel }); return _container.Resolve<TimelineGroupViewModel>(new object[] {propertyGroupViewModel});
} }
} }
@ -300,6 +304,7 @@ public interface IDataBindingVmFactory : IVmFactory
{ {
DataBindingViewModel DataBindingViewModel(); DataBindingViewModel DataBindingViewModel();
} }
public class DataBindingVmFactory : IDataBindingVmFactory public class DataBindingVmFactory : IDataBindingVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
@ -333,6 +338,7 @@ public interface INodeVmFactory : IVmFactory
InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel); InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel);
OutputPinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel); OutputPinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel);
} }
public class NodeVmFactory : INodeVmFactory public class NodeVmFactory : INodeVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
@ -344,47 +350,47 @@ public class NodeVmFactory : INodeVmFactory
public NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript, bool isPreview) public NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript, bool isPreview)
{ {
return _container.Resolve<NodeScriptViewModel>(new object[] { nodeScript, isPreview }); return _container.Resolve<NodeScriptViewModel>(new object[] {nodeScript, isPreview});
} }
public NodePickerViewModel NodePickerViewModel(NodeScript nodeScript) public NodePickerViewModel NodePickerViewModel(NodeScript nodeScript)
{ {
return _container.Resolve<NodePickerViewModel>(new object[] { nodeScript }); return _container.Resolve<NodePickerViewModel>(new object[] {nodeScript});
} }
public NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node) public NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node)
{ {
return _container.Resolve<NodeViewModel>(new object[] { nodeScriptViewModel, node }); return _container.Resolve<NodeViewModel>(new object[] {nodeScriptViewModel, node});
} }
public CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to) public CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to)
{ {
return _container.Resolve<CableViewModel>(new object[] { nodeScriptViewModel, from, to }); return _container.Resolve<CableViewModel>(new object[] {nodeScriptViewModel, from, to});
} }
public DragCableViewModel DragCableViewModel(PinViewModel pinViewModel) public DragCableViewModel DragCableViewModel(PinViewModel pinViewModel)
{ {
return _container.Resolve<DragCableViewModel>(new object[] { pinViewModel }); return _container.Resolve<DragCableViewModel>(new object[] {pinViewModel});
} }
public InputPinViewModel InputPinViewModel(IPin inputPin, NodeScriptViewModel nodeScriptViewModel) public InputPinViewModel InputPinViewModel(IPin inputPin, NodeScriptViewModel nodeScriptViewModel)
{ {
return _container.Resolve<InputPinViewModel>(new object[] { inputPin, nodeScriptViewModel }); return _container.Resolve<InputPinViewModel>(new object[] {inputPin, nodeScriptViewModel});
} }
public OutputPinViewModel OutputPinViewModel(IPin outputPin, NodeScriptViewModel nodeScriptViewModel) public OutputPinViewModel OutputPinViewModel(IPin outputPin, NodeScriptViewModel nodeScriptViewModel)
{ {
return _container.Resolve<OutputPinViewModel>(new object[] { outputPin, nodeScriptViewModel }); return _container.Resolve<OutputPinViewModel>(new object[] {outputPin, nodeScriptViewModel});
} }
public InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel) public InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel)
{ {
return _container.Resolve<InputPinCollectionViewModel>(new object[] { inputPinCollection, nodeScriptViewModel }); return _container.Resolve<InputPinCollectionViewModel>(new object[] {inputPinCollection, nodeScriptViewModel});
} }
public OutputPinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel) public OutputPinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel)
{ {
return _container.Resolve<OutputPinCollectionViewModel>(new object[] { outputPinCollection, nodeScriptViewModel }); return _container.Resolve<OutputPinCollectionViewModel>(new object[] {outputPinCollection, nodeScriptViewModel});
} }
} }
@ -395,6 +401,7 @@ public interface IConditionVmFactory : IVmFactory
StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition); StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition);
EventConditionViewModel EventConditionViewModel(EventCondition eventCondition); EventConditionViewModel EventConditionViewModel(EventCondition eventCondition);
} }
public class ConditionVmFactory : IConditionVmFactory public class ConditionVmFactory : IConditionVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
@ -406,22 +413,22 @@ public class ConditionVmFactory : IConditionVmFactory
public AlwaysOnConditionViewModel AlwaysOnConditionViewModel(AlwaysOnCondition alwaysOnCondition) public AlwaysOnConditionViewModel AlwaysOnConditionViewModel(AlwaysOnCondition alwaysOnCondition)
{ {
return _container.Resolve<AlwaysOnConditionViewModel>(new object[] { alwaysOnCondition }); return _container.Resolve<AlwaysOnConditionViewModel>(new object[] {alwaysOnCondition});
} }
public PlayOnceConditionViewModel PlayOnceConditionViewModel(PlayOnceCondition playOnceCondition) public PlayOnceConditionViewModel PlayOnceConditionViewModel(PlayOnceCondition playOnceCondition)
{ {
return _container.Resolve<PlayOnceConditionViewModel>(new object[] { playOnceCondition }); return _container.Resolve<PlayOnceConditionViewModel>(new object[] {playOnceCondition});
} }
public StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition) public StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition)
{ {
return _container.Resolve<StaticConditionViewModel>(new object[] { staticCondition }); return _container.Resolve<StaticConditionViewModel>(new object[] {staticCondition});
} }
public EventConditionViewModel EventConditionViewModel(EventCondition eventCondition) public EventConditionViewModel EventConditionViewModel(EventCondition eventCondition)
{ {
return _container.Resolve<EventConditionViewModel>(new object[] { eventCondition }); return _container.Resolve<EventConditionViewModel>(new object[] {eventCondition});
} }
} }
@ -432,6 +439,7 @@ public interface ILayerHintVmFactory : IVmFactory
KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(Layer layer, KeyboardSectionAdaptionHint adaptionHint); KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(Layer layer, KeyboardSectionAdaptionHint adaptionHint);
SingleLedAdaptionHintViewModel SingleLedAdaptionHintViewModel(Layer layer, SingleLedAdaptionHint adaptionHint); SingleLedAdaptionHintViewModel SingleLedAdaptionHintViewModel(Layer layer, SingleLedAdaptionHint adaptionHint);
} }
public class LayerHintVmFactory : ILayerHintVmFactory public class LayerHintVmFactory : ILayerHintVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
@ -443,22 +451,22 @@ public class LayerHintVmFactory : ILayerHintVmFactory
public CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(Layer layer, CategoryAdaptionHint adaptionHint) public CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(Layer layer, CategoryAdaptionHint adaptionHint)
{ {
return _container.Resolve<CategoryAdaptionHintViewModel>(new object[] { layer, adaptionHint }); return _container.Resolve<CategoryAdaptionHintViewModel>(new object[] {layer, adaptionHint});
} }
public DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(Layer layer, DeviceAdaptionHint adaptionHint) public DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(Layer layer, DeviceAdaptionHint adaptionHint)
{ {
return _container.Resolve<DeviceAdaptionHintViewModel>(new object[] { layer, adaptionHint }); return _container.Resolve<DeviceAdaptionHintViewModel>(new object[] {layer, adaptionHint});
} }
public KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(Layer layer, KeyboardSectionAdaptionHint adaptionHint) public KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(Layer layer, KeyboardSectionAdaptionHint adaptionHint)
{ {
return _container.Resolve<KeyboardSectionAdaptionHintViewModel>(new object[] { layer, adaptionHint }); return _container.Resolve<KeyboardSectionAdaptionHintViewModel>(new object[] {layer, adaptionHint});
} }
public SingleLedAdaptionHintViewModel SingleLedAdaptionHintViewModel(Layer layer, SingleLedAdaptionHint adaptionHint) public SingleLedAdaptionHintViewModel SingleLedAdaptionHintViewModel(Layer layer, SingleLedAdaptionHint adaptionHint)
{ {
return _container.Resolve<SingleLedAdaptionHintViewModel>(new object[] { layer, adaptionHint }); return _container.Resolve<SingleLedAdaptionHintViewModel>(new object[] {layer, adaptionHint});
} }
} }
@ -467,6 +475,7 @@ public interface IScriptVmFactory : IVmFactory
ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration); ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration);
ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration); ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration);
} }
public class ScriptVmFactory : IScriptVmFactory public class ScriptVmFactory : IScriptVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
@ -478,12 +487,12 @@ public class ScriptVmFactory : IScriptVmFactory
public ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration) public ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration)
{ {
return _container.Resolve<ScriptConfigurationViewModel>(new object[] { scriptConfiguration }); return _container.Resolve<ScriptConfigurationViewModel>(new object[] {scriptConfiguration});
} }
public ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration) public ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration)
{ {
return _container.Resolve<ScriptConfigurationViewModel>(new object[] { profile, scriptConfiguration }); return _container.Resolve<ScriptConfigurationViewModel>(new object[] {profile, scriptConfiguration});
} }
} }
@ -491,6 +500,7 @@ public interface IReleaseVmFactory : IVmFactory
{ {
ReleaseViewModel ReleaseListViewModel(IGetReleases_PublishedReleases_Nodes release); ReleaseViewModel ReleaseListViewModel(IGetReleases_PublishedReleases_Nodes release);
} }
public class ReleaseVmFactory : IReleaseVmFactory public class ReleaseVmFactory : IReleaseVmFactory
{ {
private readonly IContainer _container; private readonly IContainer _container;
@ -499,9 +509,9 @@ public class ReleaseVmFactory : IReleaseVmFactory
{ {
_container = container; _container = container;
} }
public ReleaseViewModel ReleaseListViewModel(IGetReleases_PublishedReleases_Nodes release) public ReleaseViewModel ReleaseListViewModel(IGetReleases_PublishedReleases_Nodes release)
{ {
return _container.Resolve<ReleaseViewModel>(new object[] { release }); return _container.Resolve<ReleaseViewModel>(new object[] {release});
} }
} }

View File

@ -5,9 +5,10 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:device="clr-namespace:Artemis.UI.Screens.Device" xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:general="clr-namespace:Artemis.UI.Screens.Device.General"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650"
x:Class="Artemis.UI.Screens.Device.DeviceGeneralTabView" x:Class="Artemis.UI.Screens.Device.General.DeviceGeneralTabView"
x:DataType="device:DeviceGeneralTabViewModel"> x:DataType="general:DeviceGeneralTabViewModel">
<UserControl.Resources> <UserControl.Resources>
<converters:SKColorToColorConverter x:Key="SKColorToColorConverter" /> <converters:SKColorToColorConverter x:Key="SKColorToColorConverter" />
</UserControl.Resources> </UserControl.Resources>

View File

@ -1,7 +1,6 @@
using Avalonia.Controls;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.General;
public partial class DeviceGeneralTabView : ReactiveUserControl<DeviceGeneralTabViewModel> public partial class DeviceGeneralTabView : ReactiveUserControl<DeviceGeneralTabViewModel>
{ {
public DeviceGeneralTabView() public DeviceGeneralTabView()

View File

@ -4,6 +4,7 @@ using System.Reactive.Disposables;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Screens.Device.Layout;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
@ -11,7 +12,7 @@ using ReactiveUI;
using RGB.NET.Core; using RGB.NET.Core;
using SkiaSharp; using SkiaSharp;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.General;
public partial class DeviceGeneralTabViewModel : ActivatableViewModelBase public partial class DeviceGeneralTabViewModel : ActivatableViewModelBase
{ {
@ -56,7 +57,7 @@ public partial class DeviceGeneralTabViewModel : ActivatableViewModelBase
_initialGreenScale = Device.GreenScale; _initialGreenScale = Device.GreenScale;
_initialBlueScale = Device.BlueScale; _initialBlueScale = Device.BlueScale;
this.WhenAnyValue(x => x.RedScale, x => x.GreenScale, x => x.BlueScale).Subscribe(_ => ApplyScaling()); this.WhenAnyValue<DeviceGeneralTabViewModel, float, float, float>(x => x.RedScale, x => x.GreenScale, x => x.BlueScale).Subscribe(_ => ApplyScaling());
this.WhenActivated(d => this.WhenActivated(d =>
{ {
@ -127,12 +128,12 @@ public partial class DeviceGeneralTabViewModel : ActivatableViewModelBase
return; return;
if (!Device.DeviceProvider.CanDetectPhysicalLayout && !await DevicePhysicalLayoutDialogViewModel.SelectPhysicalLayout(_windowService, Device)) if (!Device.DeviceProvider.CanDetectPhysicalLayout && !await DevicePhysicalLayoutDialogViewModel.SelectPhysicalLayout(_windowService, Device))
return; return;
if (!Device.DeviceProvider.CanDetectLogicalLayout && !await DeviceLogicalLayoutDialogViewModel.SelectLogicalLayout(_windowService, Device)) if (!Device.DeviceProvider.CanDetectLogicalLayout && !await Layout.DeviceLogicalLayoutDialogViewModel.SelectLogicalLayout(_windowService, Device))
return; return;
await Task.Delay(400); await Task.Delay(400);
_deviceService.SaveDevice(Device); _deviceService.SaveDevice(Device);
_deviceService.ApplyDeviceLayout(Device, Device.GetBestDeviceLayout()); _deviceService.LoadDeviceLayout(Device);
} }
private void Apply() private void Apply()

View File

@ -4,9 +4,10 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:device="clr-namespace:Artemis.UI.Screens.Device" xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:inputMappings="clr-namespace:Artemis.UI.Screens.Device.InputMappings"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.InputMappingsTabView" x:Class="Artemis.UI.Screens.Device.InputMappings.InputMappingsTabView"
x:DataType="device:InputMappingsTabViewModel"> x:DataType="inputMappings:InputMappingsTabViewModel">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"> <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<Grid RowDefinitions="Auto,Auto,*"> <Grid RowDefinitions="Auto,Auto,*">
<StackPanel Grid.Row="0"> <StackPanel Grid.Row="0">

View File

@ -1,7 +1,6 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.InputMappings;
public partial class InputMappingsTabView : ReactiveUserControl<InputMappingsTabViewModel> public partial class InputMappingsTabView : ReactiveUserControl<InputMappingsTabViewModel>
{ {

View File

@ -6,13 +6,12 @@ using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Exceptions; using Artemis.UI.Exceptions;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using HidSharp.Reports.Units;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using ReactiveUI; using ReactiveUI;
using RGB.NET.Core; using RGB.NET.Core;
using Unit = System.Reactive.Unit; using Unit = System.Reactive.Unit;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.InputMappings;
public partial class InputMappingsTabViewModel : ActivatableViewModelBase public partial class InputMappingsTabViewModel : ActivatableViewModelBase
{ {

View File

@ -5,9 +5,12 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:device="clr-namespace:Artemis.UI.Screens.Device" xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:providers="clr-namespace:Artemis.Core.Providers;assembly=Artemis.Core"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Device.Layout"
xmlns:layoutProviders="clr-namespace:Artemis.UI.Screens.Device.Layout.LayoutProviders"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Device.DeviceLayoutTabView" x:Class="Artemis.UI.Screens.Device.Layout.DeviceLayoutTabView"
x:DataType="device:DeviceLayoutTabViewModel"> x:DataType="layout:DeviceLayoutTabViewModel">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"> <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<Border Classes="card" Margin="5"> <Border Classes="card" Margin="5">
<Grid RowDefinitions="*,Auto"> <Grid RowDefinitions="*,Auto">
@ -28,6 +31,7 @@
</Button> </Button>
</StackPanel> </StackPanel>
</Grid> </Grid>
<Border Classes="card-separator" /> <Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto"> <Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0"> <StackPanel Grid.Row="1" Grid.Column="0">
@ -45,28 +49,43 @@
</Button> </Button>
</StackPanel> </StackPanel>
</Grid> </Grid>
<Border Classes="card-separator" /> <Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto"> <Grid RowDefinitions="*,*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0"> <StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Disable default layout" /> <TextBlock Text="Layout provider" />
<TextBlock Classes="subtitle" FontSize="12" Text="With this checked, Artemis will not load a layout for this device unless you specifically provide one." /> <TextBlock Classes="subtitle" FontSize="12" Text="Choose between different ways to load a layout for this device." />
</StackPanel> </StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center"> <StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
<CheckBox HorizontalAlignment="Right" Margin="0,0,-10,0" IsChecked="{CompiledBinding Device.DisableDefaultLayout}"/> <StackPanel.Styles>
</StackPanel> <Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
</Grid> <Setter Property="ContentTemplate">
<Border Classes="card-separator" /> <Setter.Value>
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto"> <DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
<StackPanel Grid.Row="1" Grid.Column="0"> <TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
<TextBlock Text="Custom layout path" /> </DataTemplate>
<TextBlock Classes="subtitle" FontSize="12" Text="{CompiledBinding CustomLayoutPath, TargetNullValue=None}"/> </Setter.Value>
</StackPanel> </Setter>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal"> </Style>
<Button Content="Clear" Command="{CompiledBinding ClearCustomLayout}" IsEnabled="{CompiledBinding HasCustomLayout}" /> </StackPanel.Styles>
<!-- 5 pixels of margin between the buttons --> <ComboBox Classes="layoutProvider"
<Button Margin="5,0,0,0" Content="Browse" Command="{CompiledBinding BrowseCustomLayout}" /> Width="150"
SelectedItem="{CompiledBinding SelectedLayoutProvider}"
ItemsSource="{CompiledBinding LayoutProviders}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
<StackPanel>
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
<TextBlock Classes="subtitle" Text="{CompiledBinding Description}" TextWrapping="Wrap" MaxWidth="350" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel> </StackPanel>
</Grid> </Grid>
<ContentControl Content="{CompiledBinding SelectedLayoutProvider}" ClipToBounds="False"></ContentControl>
<Border Classes="card-separator" /> <Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto"> <Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0"> <StackPanel Grid.Row="1" Grid.Column="0">

View File

@ -2,7 +2,7 @@
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.Layout;
public partial class DeviceLayoutTabView : ReactiveUserControl<DeviceLayoutTabViewModel> public partial class DeviceLayoutTabView : ReactiveUserControl<DeviceLayoutTabViewModel>
{ {

View File

@ -1,81 +1,52 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reactive.Disposables;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Serialization; using System.Xml.Serialization;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.UI.Screens.Device.Layout.LayoutProviders;
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;
using Avalonia.Controls; using PropertyChanged.SourceGenerator;
using ReactiveUI;
using RGB.NET.Layout; using RGB.NET.Layout;
using SkiaSharp;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.Layout;
public class DeviceLayoutTabViewModel : ActivatableViewModelBase public partial class DeviceLayoutTabViewModel : ActivatableViewModelBase
{ {
[Notify] private ILayoutProviderViewModel? _selectedLayoutProvider;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly IDeviceService _deviceService;
public DeviceLayoutTabViewModel(IWindowService windowService, INotificationService notificationService, IDeviceService deviceService, ArtemisDevice device) public DeviceLayoutTabViewModel(IWindowService windowService, INotificationService notificationService, ArtemisDevice device, List<ILayoutProviderViewModel> layoutProviders)
{ {
_windowService = windowService; _windowService = windowService;
_notificationService = notificationService; _notificationService = notificationService;
_deviceService = deviceService;
Device = device; Device = device;
DisplayName = "Layout"; DisplayName = "Layout";
DefaultLayoutPath = Device.DeviceProvider.LoadLayout(Device).FilePath; DefaultLayoutPath = Device.DeviceProvider.LoadLayout(Device).FilePath;
this.WhenActivated(d => LayoutProviders = new ObservableCollection<ILayoutProviderViewModel>(layoutProviders);
foreach (ILayoutProviderViewModel layoutProviderViewModel in layoutProviders)
{ {
Device.PropertyChanged += DeviceOnPropertyChanged; layoutProviderViewModel.Device = Device;
Disposable.Create(() => Device.PropertyChanged -= DeviceOnPropertyChanged).DisposeWith(d); if (layoutProviderViewModel.IsMatch(Device))
}); SelectedLayoutProvider = layoutProviderViewModel;
}
} }
public ArtemisDevice Device { get; } public ArtemisDevice Device { get; }
public ObservableCollection<ILayoutProviderViewModel> LayoutProviders { get; set; }
public string DefaultLayoutPath { get; } public string DefaultLayoutPath { get; }
public string? ImagePath => Device.Layout?.Image?.LocalPath; public string? ImagePath => Device.Layout?.Image?.LocalPath;
public string? CustomLayoutPath => Device.CustomLayoutPath;
public bool HasCustomLayout => Device.CustomLayoutPath != null;
public void ClearCustomLayout()
{
Device.CustomLayoutPath = null;
_notificationService.CreateNotification()
.WithMessage("Cleared imported layout.")
.WithSeverity(NotificationSeverity.Informational);
}
public async Task BrowseCustomLayout()
{
string[]? files = await _windowService.CreateOpenFileDialog()
.WithTitle("Select device layout file")
.HavingFilter(f => f.WithName("Layout files").WithExtension("xml"))
.ShowAsync();
if (files?.Length > 0)
{
Device.CustomLayoutPath = files[0];
_notificationService.CreateNotification()
.WithTitle("Imported layout")
.WithMessage($"File loaded from {files[0]}")
.WithSeverity(NotificationSeverity.Informational);
}
}
public async Task ExportLayout() public async Task ExportLayout()
{ {
string fileName = Device.DeviceProvider.GetDeviceLayoutName(Device); string fileName = Device.DeviceProvider.GetDeviceLayoutName(Device);
@ -145,19 +116,4 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase
.WithSeverity(NotificationSeverity.Informational) .WithSeverity(NotificationSeverity.Informational)
.Show(); .Show();
} }
private void DeviceOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(Device.CustomLayoutPath) or nameof(Device.DisableDefaultLayout))
{
Task.Run(() =>
{
_deviceService.ApplyDeviceLayout(Device, Device.GetBestDeviceLayout());
_deviceService.SaveDevice(Device);
});
this.RaisePropertyChanged(nameof(CustomLayoutPath));
this.RaisePropertyChanged(nameof(HasCustomLayout));
}
}
} }

View File

@ -4,9 +4,10 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:device="clr-namespace:Artemis.UI.Screens.Device" xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:globalization="clr-namespace:System.Globalization;assembly=System.Runtime" xmlns:globalization="clr-namespace:System.Globalization;assembly=System.Runtime"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Device.Layout"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.DeviceLogicalLayoutDialogView" x:Class="Artemis.UI.Screens.Device.Layout.DeviceLogicalLayoutDialogView"
x:DataType="device:DeviceLogicalLayoutDialogViewModel"> x:DataType="layout:DeviceLogicalLayoutDialogViewModel">
<StackPanel> <StackPanel>
<TextBlock TextWrapping="Wrap">Artemis couldn't automatically determine the logical layout of your</TextBlock> <TextBlock TextWrapping="Wrap">Artemis couldn't automatically determine the logical layout of your</TextBlock>
<TextBlock TextWrapping="Wrap" Text="{CompiledBinding Device.RgbDevice.DeviceInfo.DeviceName, Mode=OneWay}" /> <TextBlock TextWrapping="Wrap" Text="{CompiledBinding Device.RgbDevice.DeviceInfo.DeviceName, Mode=OneWay}" />

View File

@ -1,12 +1,10 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading; using Avalonia.Threading;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.Layout;
public partial class DeviceLogicalLayoutDialogView : ReactiveUserControl<DeviceLogicalLayoutDialogViewModel> public partial class DeviceLogicalLayoutDialogView : ReactiveUserControl<DeviceLogicalLayoutDialogViewModel>
{ {

View File

@ -12,7 +12,7 @@ using PropertyChanged.SourceGenerator;
using ReactiveUI; using ReactiveUI;
using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton; using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.Layout;
public partial class DeviceLogicalLayoutDialogViewModel : ContentDialogViewModelBase public partial class DeviceLogicalLayoutDialogViewModel : ContentDialogViewModelBase
{ {
@ -24,7 +24,7 @@ public partial class DeviceLogicalLayoutDialogViewModel : ContentDialogViewModel
public DeviceLogicalLayoutDialogViewModel(ArtemisDevice device) public DeviceLogicalLayoutDialogViewModel(ArtemisDevice device)
{ {
Device = device; Device = device;
ApplyLogicalLayout = ReactiveCommand.Create(ExecuteApplyLogicalLayout, this.WhenAnyValue(vm => vm.SelectedRegion).Select(r => r != null)); ApplyLogicalLayout = ReactiveCommand.Create(ExecuteApplyLogicalLayout, this.WhenAnyValue<DeviceLogicalLayoutDialogViewModel, RegionInfo>(vm => vm.SelectedRegion).Select(r => r != null));
Regions = new ObservableCollection<RegionInfo>(CultureInfo.GetCultures(CultureTypes.SpecificCultures) Regions = new ObservableCollection<RegionInfo>(CultureInfo.GetCultures(CultureTypes.SpecificCultures)
.Where(c => c.LCID != LOCALE_INVARIANT && .Where(c => c.LCID != LOCALE_INVARIANT &&
c.LCID != LOCALE_NEUTRAL && c.LCID != LOCALE_NEUTRAL &&

View File

@ -3,9 +3,10 @@
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:device="clr-namespace:Artemis.UI.Screens.Device" xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Device.Layout"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.DevicePhysicalLayoutDialogView" x:Class="Artemis.UI.Screens.Device.Layout.DevicePhysicalLayoutDialogView"
x:DataType="device:DevicePhysicalLayoutDialogViewModel"> x:DataType="layout:DevicePhysicalLayoutDialogViewModel">
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*">
<StackPanel Grid.Row="0"> <StackPanel Grid.Row="0">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">

View File

@ -1,7 +1,6 @@
using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.Layout;
public partial class DevicePhysicalLayoutDialogView : ReactiveUserControl<DevicePhysicalLayoutDialogViewModel> public partial class DevicePhysicalLayoutDialogView : ReactiveUserControl<DevicePhysicalLayoutDialogViewModel>
{ {

View File

@ -7,7 +7,7 @@ using Artemis.UI.Shared.Services;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.Layout;
public class DevicePhysicalLayoutDialogViewModel : ContentDialogViewModelBase public class DevicePhysicalLayoutDialogViewModel : ContentDialogViewModelBase
{ {

View File

@ -0,0 +1,25 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
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.CustomLayoutView"
x:DataType="layoutProviders:CustomLayoutViewModel"
ClipToBounds="False">
<StackPanel ClipToBounds="False">
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Custom layout path" />
<TextBlock Classes="subtitle" FontSize="12" Text="{CompiledBinding Device.LayoutSelection.Parameter, TargetNullValue=None}" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal">
<Button Content="Clear" Command="{CompiledBinding ClearCustomLayout}" IsEnabled="{CompiledBinding !!Device.LayoutSelection.Parameter}" />
<!-- 5 pixels of margin between the buttons -->
<Button Margin="5,0,0,0" Content="Browse" Command="{CompiledBinding BrowseCustomLayout}" />
</StackPanel>
</Grid>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public partial class CustomLayoutView : UserControl
{
public CustomLayoutView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,71 @@
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;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public class CustomLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
{
private readonly CustomPathLayoutProvider _layoutProvider;
private readonly IDeviceService _deviceService;
public CustomLayoutViewModel(IWindowService windowService, INotificationService notificationService, CustomPathLayoutProvider layoutProvider, IDeviceService deviceService)
{
_layoutProvider = layoutProvider;
_deviceService = deviceService;
_windowService = windowService;
_notificationService = notificationService;
}
/// <inheritdoc />
public string Name => "Custom";
/// <inheritdoc />
public string Description => "Select a layout file from a folder on your computer";
/// <inheritdoc />
public bool IsMatch(ArtemisDevice device)
{
return _layoutProvider.IsMatch(device);
}
public ArtemisDevice Device { get; set; } = null!;
private readonly IWindowService _windowService;
private readonly INotificationService _notificationService;
public void ClearCustomLayout()
{
Device.LayoutSelection.Type = CustomPathLayoutProvider.LayoutType;
Device.LayoutSelection.Parameter = null;
_notificationService.CreateNotification()
.WithMessage("Cleared imported layout.")
.WithSeverity(NotificationSeverity.Informational);
_deviceService.SaveDevice(Device);
}
public async Task BrowseCustomLayout()
{
string[]? files = await _windowService.CreateOpenFileDialog()
.WithTitle("Select device layout file")
.HavingFilter(f => f.WithName("Layout files").WithExtension("xml"))
.ShowAsync();
if (files?.Length > 0)
{
Device.LayoutSelection.Type = CustomPathLayoutProvider.LayoutType;
Device.LayoutSelection.Parameter = files[0];
_notificationService.CreateNotification()
.WithTitle("Imported layout")
.WithMessage($"File loaded from {files[0]}")
.WithSeverity(NotificationSeverity.Informational);
}
_deviceService.SaveDevice(Device);
}
}

View File

@ -0,0 +1,10 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.Layout.LayoutProviders.DefaultLayoutView">
<TextBlock Classes="subtitle" FontSize="12">
Loading the default layout from the plugin or User Layouts folder.
</TextBlock>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public partial class DefaultLayoutView : UserControl
{
public DefaultLayoutView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,29 @@
using Artemis.Core;
using Artemis.Core.Providers;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public class DefaultLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
{
private readonly DefaultLayoutProvider _layoutProvider;
public DefaultLayoutViewModel(DefaultLayoutProvider layoutProvider)
{
_layoutProvider = layoutProvider;
}
public ArtemisDevice Device { get; set; } = null!;
/// <inheritdoc />
public string Name => "Default";
/// <inheritdoc />
public string Description => "Attempts to load a layout from the default paths";
/// <inheritdoc />
public bool IsMatch(ArtemisDevice device)
{
return _layoutProvider.IsMatch(device);
}
}

View File

@ -0,0 +1,14 @@
using Artemis.Core;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public interface ILayoutProviderViewModel
{
string Name { get; }
string Description { get; }
bool IsMatch(ArtemisDevice device);
ArtemisDevice Device { get; set; }
}

View File

@ -0,0 +1,10 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.Layout.LayoutProviders.NoneLayoutView">
<TextBlock Classes="subtitle" FontSize="12">
Not loading any layout, leaving LEDs to be positioned by the device provider.
</TextBlock>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public partial class NoneLayoutView : UserControl
{
public NoneLayoutView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,29 @@
using Artemis.Core;
using Artemis.Core.Providers;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public class NoneLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
{
private readonly NoneLayoutProvider _layoutProvider;
public NoneLayoutViewModel(NoneLayoutProvider layoutProvider)
{
_layoutProvider = layoutProvider;
}
public ArtemisDevice Device { get; set; } = null!;
/// <inheritdoc />
public string Name => "None";
/// <inheritdoc />
public string Description => "Do not load any layout";
/// <inheritdoc />
public bool IsMatch(ArtemisDevice device)
{
return _layoutProvider.IsMatch(device);
}
}

View File

@ -0,0 +1,11 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.Layout.LayoutProviders.WorkshopLayoutView">
<TextBlock Classes="subtitle" FontSize="12">
Here you'll do workshop things
</TextBlock>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public partial class WorkshopLayoutView : UserControl
{
public WorkshopLayoutView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,29 @@
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.WebClient.Workshop.Providers;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public class WorkshopLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
{
private readonly WorkshopLayoutProvider _layoutProvider;
public WorkshopLayoutViewModel(WorkshopLayoutProvider layoutProvider)
{
_layoutProvider = layoutProvider;
}
public ArtemisDevice Device { get; set; } = null!;
/// <inheritdoc />
public string Name => "Workshop";
/// <inheritdoc />
public string Description => "Load a layout from the workshop";
/// <inheritdoc />
public bool IsMatch(ArtemisDevice device)
{
return _layoutProvider.IsMatch(device);
}
}

View File

@ -2,7 +2,7 @@
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Shared; using Artemis.UI.Shared;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.Leds;
public class DeviceLedsTabLedViewModel : ViewModelBase public class DeviceLedsTabLedViewModel : ViewModelBase
{ {

View File

@ -5,9 +5,10 @@
xmlns:device="clr-namespace:Artemis.UI.Screens.Device" xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:shared="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" xmlns:shared="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:leds="clr-namespace:Artemis.UI.Screens.Device.Leds"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.DeviceLedsTabView" x:Class="Artemis.UI.Screens.Device.Leds.DeviceLedsTabView"
x:DataType="device:DeviceLedsTabViewModel"> x:DataType="leds:DeviceLedsTabViewModel">
<UserControl.Resources> <UserControl.Resources>
<converters:UriToFileNameConverter x:Key="UriToFileNameConverter" /> <converters:UriToFileNameConverter x:Key="UriToFileNameConverter" />
<converters:LedIdToStringConverter x:Key="LedIdToStringConverter" /> <converters:LedIdToStringConverter x:Key="LedIdToStringConverter" />

View File

@ -1,7 +1,6 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.Leds;
public partial class DeviceLedsTabView : ReactiveUserControl<DeviceLedsTabViewModel> public partial class DeviceLedsTabView : ReactiveUserControl<DeviceLedsTabViewModel>
{ {

View File

@ -8,7 +8,7 @@ using Artemis.UI.Shared;
using DynamicData.Binding; using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.Device; namespace Artemis.UI.Screens.Device.Leds;
public class DeviceLedsTabViewModel : ActivatableViewModelBase public class DeviceLedsTabViewModel : ActivatableViewModelBase
{ {

View File

@ -46,7 +46,7 @@ public partial class InstalledTabItemViewModel : ViewModelBase
private async Task ExecuteViewLocal(CancellationToken cancellationToken) private async Task ExecuteViewLocal(CancellationToken cancellationToken)
{ {
if (InstalledEntry.EntryType == EntryType.Profile && Guid.TryParse(InstalledEntry.LocalReference, out Guid profileId)) if (InstalledEntry.EntryType == EntryType.Profile && InstalledEntry.TryGetMetadata("ProfileId", out Guid profileId))
{ {
await _router.Navigate($"profile-editor/{profileId}"); await _router.Navigate($"profile-editor/{profileId}");
} }

View File

@ -63,7 +63,7 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel
try try
{ {
ArtemisLayout layout = new(selected[0], LayoutSource.User); ArtemisLayout layout = new(selected[0]);
if (!layout.IsValid) if (!layout.IsValid)
{ {
await _windowService.ShowConfirmContentDialog("Failed to load layout", "The selected file does not appear to be a valid RGB.NET layout file.", "Close", null); await _windowService.ShowConfirmContentDialog("Failed to load layout", "The selected file does not appear to be a valid RGB.NET layout file.", "Close", null);

View File

@ -5,11 +5,13 @@ using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Device;
using Artemis.UI.Screens.Device.Layout;
using Artemis.UI.Services.Interfaces; using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Services.MainWindow;
using Avalonia.Threading; using Avalonia.Threading;
using RGB.NET.Core; using RGB.NET.Core;
using DeviceLogicalLayoutDialogViewModel = Artemis.UI.Screens.Device.Layout.DeviceLogicalLayoutDialogViewModel;
using KeyboardLayoutType = Artemis.Core.KeyboardLayoutType; using KeyboardLayoutType = Artemis.Core.KeyboardLayoutType;
namespace Artemis.UI.Services; namespace Artemis.UI.Services;
@ -61,7 +63,7 @@ public class DeviceLayoutService : IDeviceLayoutService
await Task.Delay(400); await Task.Delay(400);
_deviceService.SaveDevice(artemisDevice); _deviceService.SaveDevice(artemisDevice);
_deviceService.ApplyDeviceLayout(artemisDevice, artemisDevice.GetBestDeviceLayout()); _deviceService.LoadDeviceLayout(artemisDevice);
} }
private bool DeviceNeedsLayout(ArtemisDevice d) private bool DeviceNeedsLayout(ArtemisDevice d)

View File

@ -1,7 +1,9 @@
using System.Reflection; using System.Reflection;
using Artemis.Core.Providers;
using Artemis.WebClient.Workshop.Extensions; using Artemis.WebClient.Workshop.Extensions;
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers; using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers; using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Providers;
using Artemis.WebClient.Workshop.Repositories; using Artemis.WebClient.Workshop.Repositories;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
using Artemis.WebClient.Workshop.State; using Artemis.WebClient.Workshop.State;
@ -46,6 +48,7 @@ public static class ContainerExtensions
container.Register<IAuthenticationRepository, AuthenticationRepository>(Reuse.Singleton); container.Register<IAuthenticationRepository, AuthenticationRepository>(Reuse.Singleton);
container.Register<IAuthenticationService, AuthenticationService>(Reuse.Singleton); container.Register<IAuthenticationService, AuthenticationService>(Reuse.Singleton);
container.Register<IWorkshopService, WorkshopService>(Reuse.Singleton); container.Register<IWorkshopService, WorkshopService>(Reuse.Singleton);
container.Register<ILayoutProvider, WorkshopLayoutProvider>(Reuse.Singleton);
container.Register<EntryUploadHandlerFactory>(Reuse.Transient); container.Register<EntryUploadHandlerFactory>(Reuse.Transient);
container.RegisterMany(workshopAssembly, type => type.IsAssignableTo<IEntryUploadHandler>(), Reuse.Transient); container.RegisterMany(workshopAssembly, type => type.IsAssignableTo<IEntryUploadHandler>(), Reuse.Transient);

View File

@ -1,4 +1,9 @@
using Artemis.UI.Shared.Extensions; using System.Data;
using System.IO.Compression;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
@ -8,17 +13,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;
public LayoutEntryInstallationHandler(IHttpClientFactory httpClientFactory, IWorkshopService workshopService) public LayoutEntryInstallationHandler(IHttpClientFactory httpClientFactory, IWorkshopService workshopService, IDeviceService deviceService)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_workshopService = workshopService; _workshopService = workshopService;
_deviceService = deviceService;
} }
public async Task<EntryInstallResult> InstallAsync(IGetEntryById_Entry entry, long releaseId, Progress<StreamProgress> progress, CancellationToken cancellationToken) public async Task<EntryInstallResult> InstallAsync(IGetEntryById_Entry entry, long releaseId, Progress<StreamProgress> progress, CancellationToken cancellationToken)
{ {
throw new NotImplementedException();
using MemoryStream stream = new(); using MemoryStream stream = new();
// Download the provided release // Download the provided release
@ -31,15 +36,46 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler
{ {
return EntryInstallResult.FromFailure(e.Message); return EntryInstallResult.FromFailure(e.Message);
} }
// return EntryInstallResult.FromSuccess(); // Ensure there is an installed entry
InstalledEntry installedEntry = _workshopService.GetInstalledEntry(entry) ?? _workshopService.CreateInstalledEntry(entry);
DirectoryInfo entryDirectory = installedEntry.GetDirectory();
// If the folder already exists, remove it so that if the layout now contains less files, old things dont stick around
if (entryDirectory.Exists)
entryDirectory.Delete(true);
entryDirectory.Create();
// Extract the archive, we could go through the hoops of keeping track of progress but this should be so quick it doesn't matter
stream.Seek(0, SeekOrigin.Begin);
using ZipArchive archive = new(stream);
archive.ExtractToDirectory(entryDirectory.FullName);
ArtemisLayout layout = new(Path.Combine(entryDirectory.FullName, "layout.xml"));
if (layout.IsValid)
return EntryInstallResult.FromSuccess(layout);
// If the layout ended up being invalid yoink it out again, shoooo
entryDirectory.Delete(true);
_workshopService.RemoveInstalledEntry(installedEntry);
return EntryInstallResult.FromFailure("Layout failed to load because it is invalid");
} }
public async Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) public async Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken)
{ {
throw new NotImplementedException(); // Remove the layout from any devices currently using it
foreach (ArtemisDevice device in _deviceService.Devices)
if (!Guid.TryParse(installedEntry.LocalReference, out Guid profileId)) {
return EntryUninstallResult.FromFailure("Local reference does not contain a GUID"); }
// Remove from filesystem
DirectoryInfo directory = installedEntry.GetDirectory(true);
if (directory.Exists)
directory.Delete();
// Remove entry
_workshopService.RemoveInstalledEntry(installedEntry);
return EntryUninstallResult.FromSuccess();
} }
} }

View File

@ -36,13 +36,13 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
// Find existing installation to potentially replace the profile // Find existing installation to potentially replace the profile
InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry); InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry);
if (installedEntry != null && Guid.TryParse(installedEntry.LocalReference, out Guid profileId)) if (installedEntry != null && installedEntry.TryGetMetadata("ProfileId", out Guid profileId))
{ {
ProfileConfiguration? existing = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == profileId); ProfileConfiguration? existing = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == profileId);
if (existing != null) if (existing != null)
{ {
ProfileConfiguration overwritten = await _profileService.OverwriteProfile(stream, existing); ProfileConfiguration overwritten = await _profileService.OverwriteProfile(stream, existing);
installedEntry.LocalReference = overwritten.ProfileId.ToString(); 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(releaseId, installedEntry);
@ -56,8 +56,8 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
// 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);
ProfileConfiguration imported = await _profileService.ImportProfile(stream, category, true, true, null); ProfileConfiguration imported = await _profileService.ImportProfile(stream, category, true, true, null);
installedEntry.LocalReference = imported.ProfileId.ToString(); 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(releaseId, installedEntry);
return EntryInstallResult.FromSuccess(imported); return EntryInstallResult.FromSuccess(imported);
@ -65,7 +65,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
public async Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) public async Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken)
{ {
if (!Guid.TryParse(installedEntry.LocalReference, out Guid profileId)) if (!installedEntry.TryGetMetadata("ProfileId", out Guid profileId))
return EntryUninstallResult.FromFailure("Local reference does not contain a GUID"); return EntryUninstallResult.FromFailure("Local reference does not contain a GUID");
return await Task.Run(() => return await Task.Run(() =>

View File

@ -0,0 +1,48 @@
using Artemis.Core;
using Artemis.Core.Providers;
using Artemis.WebClient.Workshop.Services;
namespace Artemis.WebClient.Workshop.Providers;
public class WorkshopLayoutProvider : ILayoutProvider
{
public static string LayoutType = "Workshop";
private readonly IWorkshopService _workshopService;
public WorkshopLayoutProvider(IWorkshopService workshopService)
{
_workshopService = workshopService;
}
/// <inheritdoc />
public ArtemisLayout? GetDeviceLayout(ArtemisDevice device)
{
InstalledEntry? layoutEntry = _workshopService.GetInstalledEntries().FirstOrDefault(e => e.EntryType == EntryType.Layout && MatchesDevice(e, device));
if (layoutEntry == null)
return null;
string layoutPath = Path.Combine(Constants.WorkshopFolder, layoutEntry.EntryId.ToString(), "layout.xml");
if (!File.Exists(layoutPath))
return null;
return new ArtemisLayout(layoutPath);
}
/// <inheritdoc />
public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public bool IsMatch(ArtemisDevice device)
{
return device.LayoutSelection.Type == LayoutType;
}
private bool MatchesDevice(InstalledEntry entry, ArtemisDevice device)
{
return entry.TryGetMetadata("DeviceId", out HashSet<string>? deviceIds) && deviceIds.Contains(device.Identifier);
}
}

View File

@ -1,13 +1,16 @@
using Artemis.Storage.Entities.Workshop; using System.Diagnostics.CodeAnalysis;
using Artemis.Core;
using Artemis.Storage.Entities.Workshop;
namespace Artemis.WebClient.Workshop.Services; namespace Artemis.WebClient.Workshop.Services;
public class InstalledEntry public class InstalledEntry
{ {
private Dictionary<string, object> _metadata = new();
internal InstalledEntry(EntryEntity entity) internal InstalledEntry(EntryEntity entity)
{ {
Entity = entity; Entity = entity;
Load(); Load();
} }
@ -32,8 +35,6 @@ public class InstalledEntry
public string ReleaseVersion { get; set; } = string.Empty; public string ReleaseVersion { get; set; } = string.Empty;
public DateTimeOffset InstalledAt { get; set; } public DateTimeOffset InstalledAt { get; set; }
public string? LocalReference { get; set; }
internal EntryEntity Entity { get; } internal EntryEntity Entity { get; }
internal void Load() internal void Load()
@ -48,7 +49,7 @@ public class InstalledEntry
ReleaseVersion = Entity.ReleaseVersion; ReleaseVersion = Entity.ReleaseVersion;
InstalledAt = Entity.InstalledAt; InstalledAt = Entity.InstalledAt;
LocalReference = Entity.LocalReference; _metadata = new Dictionary<string, object>(Entity.Metadata);
} }
internal void Save() internal void Save()
@ -63,6 +64,60 @@ public class InstalledEntry
Entity.ReleaseVersion = ReleaseVersion; Entity.ReleaseVersion = ReleaseVersion;
Entity.InstalledAt = InstalledAt; Entity.InstalledAt = InstalledAt;
Entity.LocalReference = LocalReference; Entity.Metadata = new Dictionary<string, object>(_metadata);
}
/// <summary>
/// Gets the metadata value associated with the specified key.
/// </summary>
/// <param name="key">The key of the value to get.</param>
/// <param name="value">When this method returns, contains the value associated with the specified key, if the key is found and of type <typeparamref name="T"/>;
/// otherwise, the default value for the type of the value parameter. This parameter is passed uninitialized.</param>
/// <typeparam name="T">The type of the value.</typeparam>
/// <returns><see langword="true"/> if the metadata contains an element with the specified key; otherwise, <see langword="false"/>.</returns>
public bool TryGetMetadata<T>(string key, [NotNullWhen(true)] out T? value)
{
if (!_metadata.TryGetValue(key, out object? objectValue) || objectValue is not T result)
{
value = default;
return false;
}
value = result;
return true;
}
/// <summary>
/// Sets metadata with the provided key to the provided value.
/// </summary>
/// <param name="key">The key of the value to set</param>
/// <param name="value">The value to set.</param>
public void SetMetadata(string key, object value)
{
_metadata.Add(key, value);
}
/// <summary>
/// Removes metadata with the provided key.
/// </summary>
/// <param name="key">The key of the metadata to remove</param>
/// <returns><see langword="true"/> if the element is successfully found and removed; otherwise, <see langword="false"/>.</returns>
public bool RemoveMetadata(string key)
{
return _metadata.Remove(key);
}
/// <summary>
/// Returns the directory info of the entry, where any files would be stored if applicable.
/// </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 entry, where any files would be stored if applicable.</returns>
public DirectoryInfo GetDirectory(bool rootDirectory = false)
{
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));
} }
} }