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

Merge branch 'development'

This commit is contained in:
Robert 2021-06-03 11:10:19 +02:00
commit 4152c290d2
22 changed files with 419 additions and 109 deletions

View File

@ -17,6 +17,7 @@ namespace Artemis.Core
private DataModel? _dynamicDataModel; private DataModel? _dynamicDataModel;
private Type? _dynamicDataModelType; private Type? _dynamicDataModelType;
private DataModelPropertyAttribute? _dynamicDataModelAttribute; private DataModelPropertyAttribute? _dynamicDataModelAttribute;
private PropertyInfo? _property;
internal DataModelPathSegment(DataModelPath dataModelPath, string identifier, string path) internal DataModelPathSegment(DataModelPath dataModelPath, string identifier, string path)
{ {
@ -99,7 +100,7 @@ namespace Artemis.Core
return null; return null;
// If this is not the first segment in a path, the property is located on the previous segment // If this is not the first segment in a path, the property is located on the previous segment
return Previous?.GetPropertyType()?.GetProperty(Identifier); return Previous?.GetPropertyType()?.GetProperties().FirstOrDefault(p => p.Name == Identifier);
} }
/// <summary> /// <summary>
@ -209,14 +210,18 @@ namespace Artemis.Core
accessorExpression = expression; accessorExpression = expression;
// A static segment just needs to access the property or filed // A static segment just needs to access the property or filed
else if (Type == DataModelPathSegmentType.Static) else if (Type == DataModelPathSegmentType.Static)
accessorExpression = Expression.PropertyOrField(expression, Identifier); {
accessorExpression = _property != null
? Expression.Property(expression, _property)
: Expression.PropertyOrField(expression, Identifier);
}
// A dynamic segment calls the generic method DataModel.DynamicChild<T> and provides the identifier as an argument // A dynamic segment calls the generic method DataModel.DynamicChild<T> and provides the identifier as an argument
else else
{ {
accessorExpression = Expression.Call( accessorExpression = Expression.Call(
expression, expression,
nameof(DataModel.GetDynamicChildValue), nameof(DataModel.GetDynamicChildValue),
_dynamicDataModelType != null ? new[] { _dynamicDataModelType } : null, _dynamicDataModelType != null ? new[] {_dynamicDataModelType} : null,
Expression.Constant(Identifier) Expression.Constant(Identifier)
); );
} }
@ -243,8 +248,11 @@ namespace Artemis.Core
private void DetermineStaticType(Type previousType) private void DetermineStaticType(Type previousType)
{ {
PropertyInfo? property = previousType.GetProperty(Identifier, BindingFlags.Public | BindingFlags.Instance); // Situations in which AmbiguousMatchException occurs ...
Type = property == null ? DataModelPathSegmentType.Invalid : DataModelPathSegmentType.Static; //
// ...derived type declares a property that hides an inherited property with the same name, by using the new modifier
_property = previousType.GetProperties(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(p => p.Name == Identifier);
Type = _property == null ? DataModelPathSegmentType.Invalid : DataModelPathSegmentType.Static;
} }
#region IDisposable #region IDisposable

View File

@ -33,6 +33,8 @@ namespace Artemis.Core
BlueScale = 1; BlueScale = 1;
IsEnabled = true; IsEnabled = true;
LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(new Dictionary<LedId, ArtemisLed>());
Leds = new ReadOnlyCollection<ArtemisLed>(new List<ArtemisLed>());
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>();
@ -51,6 +53,8 @@ namespace Artemis.Core
RgbDevice = rgbDevice; RgbDevice = rgbDevice;
DeviceProvider = deviceProvider; DeviceProvider = deviceProvider;
LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(new Dictionary<LedId, ArtemisLed>());
Leds = new ReadOnlyCollection<ArtemisLed>(new List<ArtemisLed>());
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>();
@ -258,6 +262,19 @@ namespace Artemis.Core
} }
} }
/// <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>
@ -532,6 +549,11 @@ namespace Artemis.Core
else else
LogicalLayout = DeviceEntity.LogicalLayout; LogicalLayout = DeviceEntity.LogicalLayout;
} }
public void ClearLayout()
{
// TODO
}
} }
/// <summary> /// <summary>

View File

@ -22,11 +22,6 @@ namespace Artemis.Core
/// </summary> /// </summary>
public abstract string Description { get; } public abstract string Description { get; }
/// <summary>
/// [NYI] Gets a boolean indicating whether installing or uninstalling this prerequisite requires admin privileges
/// </summary>
public abstract bool RequiresElevation { get; }
/// <summary> /// <summary>
/// Gets a list of actions to execute when <see cref="Install" /> is called /// Gets a list of actions to execute when <see cref="Install" /> is called
/// </summary> /// </summary>

View File

@ -18,7 +18,7 @@ namespace Artemis.Core
/// <param name="fileName">The target file to execute</param> /// <param name="fileName">The target file to execute</param>
/// <param name="arguments">A set of command-line arguments to use when starting the application</param> /// <param name="arguments">A set of command-line arguments to use when starting the application</param>
/// <param name="waitForExit">A boolean indicating whether the action should wait for the process to exit</param> /// <param name="waitForExit">A boolean indicating whether the action should wait for the process to exit</param>
/// <param name="elevate">A boolean indicating whether the file should run with administrator privileges (does not require <see cref="PluginPrerequisite.RequiresElevation"/>)</param> /// <param name="elevate">A boolean indicating whether the file should run with administrator privileges</param>
public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true, bool elevate = false) : base(name) public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true, bool elevate = false) : base(name)
{ {
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
@ -72,7 +72,7 @@ namespace Artemis.Core
} }
} }
private static Task<int> RunProcessAsync(string fileName, string? arguments, bool elevate) internal static Task<int> RunProcessAsync(string fileName, string? arguments, bool elevate)
{ {
TaskCompletionSource<int> tcs = new(); TaskCompletionSource<int> tcs = new();

View File

@ -0,0 +1,81 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
{
/// <summary>
/// Represents a plugin prerequisite action runs inline powershell
/// </summary>
public class RunInlinePowerShellAction : PluginPrerequisiteAction
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="code">The inline code to run</param>
/// <param name="elevate">A boolean indicating whether the file should run with administrator privileges</param>
/// <param name="arguments">
/// Optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </param>
public RunInlinePowerShellAction(string name, string code, bool elevate = false, string? arguments = null) : base(name)
{
Code = code;
Elevate = elevate;
Arguments = arguments;
ProgressIndeterminate = true;
}
/// <summary>
/// Gets the inline code to run
/// </summary>
public string Code { get; }
/// <summary>
/// Gets a boolean indicating whether the file should run with administrator privileges
/// </summary>
public bool Elevate { get; }
/// <summary>
/// Gets optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </summary>
public string? Arguments { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
string file = Path.GetTempFileName().Replace(".tmp", ".ps1");
try
{
string code =
@"try
{
" + Code + @"
Start-Sleep 1
}
catch
{
Write-Error $_.Exception.ToString()
pause
}";
await File.WriteAllTextAsync(file, code, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
Status = "Running PowerShell script and waiting for exit..";
ShowProgressBar = true;
ProgressIndeterminate = true;
int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {file} {Arguments}", Elevate);
Status = $"PowerShell exited with code {result}";
}
finally
{
File.Delete(file);
}
}
}
}

View File

@ -0,0 +1,64 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
{
/// <summary>
/// Represents a plugin prerequisite action that runs a PowerShell script
/// <para>Note: To run an inline script instead, use <see cref="RunInlinePowerShellAction" /></para>
/// </summary>
public class RunPowerShellAction : PluginPrerequisiteAction
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="scriptPath">The full path of the script to run</param>
/// <param name="elevate">A boolean indicating whether the file should run with administrator privileges</param>
/// <param name="arguments">
/// Optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </param>
public RunPowerShellAction(string name, string scriptPath, bool elevate = false, string? arguments = null) : base(name)
{
ScriptPath = scriptPath;
Elevate = elevate;
Arguments = arguments;
ProgressIndeterminate = true;
}
/// <summary>
/// Gets the inline full path of the script to run
/// </summary>
public string ScriptPath { get; }
/// <summary>
/// Gets a boolean indicating whether the file should run with administrator privileges
/// </summary>
public bool Elevate { get; }
/// <summary>
/// Gets optional arguments to pass to your script, you are responsible for proper quoting etc.
/// <para>Arguments are available in PowerShell as <c>$args[0], $args[1]</c> etc.</para>
/// </summary>
public string? Arguments { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
if (!ScriptPath.EndsWith(".ps1"))
throw new ArtemisPluginException($"Script at path {ScriptPath} must have the .ps1 extension or PowerShell will refuse to run it");
if (!File.Exists(ScriptPath))
throw new ArtemisCoreException($"Script not found at path {ScriptPath}");
Status = "Running PowerShell script and waiting for exit..";
ShowProgressBar = true;
ProgressIndeterminate = true;
int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {ScriptPath} {Arguments}", Elevate);
Status = $"PowerShell exited with code {result}";
}
}
}

View File

@ -135,11 +135,16 @@ namespace Artemis.Core.Services
DeviceProvider GetDeviceProviderByDevice(IRGBDevice device); DeviceProvider GetDeviceProviderByDevice(IRGBDevice device);
/// <summary> /// <summary>
/// Queues an action for the provided plugin for the next time Artemis starts, before plugins are loaded /// Queues the provided plugin to be deleted the next time Artemis starts, before plugins are loaded
/// </summary> /// </summary>
/// <param name="plugin">The plugin to queue the action for</param> /// <param name="plugin">The plugin to delete</param>
/// <param name="pluginAction">The action to take</param> void QueuePluginDeletion(Plugin plugin);
void QueuePluginAction(Plugin plugin, PluginManagementAction pluginAction);
/// <summary>
/// Removes the provided plugin for the deletion queue it was added to via <see cref="QueuePluginDeletion" />
/// </summary>
/// <param name="plugin">The plugin to dequeue</param>
void DequeuePluginDeletion(Plugin plugin);
/// <summary> /// <summary>
/// Occurs when built-in plugins are being loaded /// Occurs when built-in plugins are being loaded

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using Artemis.Core.DeviceProviders; using Artemis.Core.DeviceProviders;
using Artemis.Core.Ninject; using Artemis.Core.Ninject;
using Artemis.Storage.Entities.General;
using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Plugins;
using Artemis.Storage.Repositories.Interfaces; using Artemis.Storage.Repositories.Interfaces;
using McMaster.NETCore.Plugins; using McMaster.NETCore.Plugins;
@ -26,17 +27,19 @@ namespace Artemis.Core.Services
private readonly IKernel _kernel; private readonly IKernel _kernel;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IPluginRepository _pluginRepository; private readonly IPluginRepository _pluginRepository;
private readonly IQueuedActionRepository _queuedActionRepository;
private readonly List<Plugin> _plugins; private readonly List<Plugin> _plugins;
private bool _isElevated; private bool _isElevated;
public PluginManagementService(IKernel kernel, ILogger logger, IPluginRepository pluginRepository) public PluginManagementService(IKernel kernel, ILogger logger, IPluginRepository pluginRepository, IQueuedActionRepository queuedActionRepository)
{ {
_kernel = kernel; _kernel = kernel;
_logger = logger; _logger = logger;
_pluginRepository = pluginRepository; _pluginRepository = pluginRepository;
_queuedActionRepository = queuedActionRepository;
_plugins = new List<Plugin>(); _plugins = new List<Plugin>();
ProcessQueuedActions(); ProcessPluginDeletionQueue();
} }
private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory) private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory)
@ -664,27 +667,51 @@ namespace Artemis.Core.Services
#region Queued actions #region Queued actions
public void QueuePluginAction(Plugin plugin, PluginManagementAction pluginAction) public void QueuePluginDeletion(Plugin plugin)
{ {
List<PluginQueuedActionEntity> existing = _pluginRepository.GetQueuedActions(plugin.Guid); _queuedActionRepository.Add(new QueuedActionEntity
if (existing.Any(e => pluginAction == PluginManagementAction.Delete && e is PluginQueuedDeleteEntity)) {
return; Type = "DeletePlugin",
CreatedAt = DateTimeOffset.Now,
if (pluginAction == PluginManagementAction.Delete) Parameters = new Dictionary<string, object>()
_pluginRepository.AddQueuedAction(new PluginQueuedDeleteEntity {PluginGuid = plugin.Guid, Directory = plugin.Directory.FullName}); {
{"pluginGuid", plugin.Guid.ToString()},
{"plugin", plugin.ToString()},
{"directory", plugin.Directory.FullName}
}
});
} }
private void ProcessQueuedActions() public void DequeuePluginDeletion(Plugin plugin)
{ {
foreach (PluginQueuedActionEntity pluginQueuedActionEntity in _pluginRepository.GetQueuedActions()) QueuedActionEntity? queuedActionEntity = _queuedActionRepository.GetByType("DeletePlugin").FirstOrDefault(q => q.Parameters["pluginGuid"].Equals(plugin.Guid.ToString()));
{ if (queuedActionEntity != null)
if (pluginQueuedActionEntity is PluginQueuedDeleteEntity deleteAction) _queuedActionRepository.Remove(queuedActionEntity);
{ }
if (Directory.Exists(deleteAction.Directory))
Directory.Delete(deleteAction.Directory, true);
}
_pluginRepository.RemoveQueuedAction(pluginQueuedActionEntity); private void ProcessPluginDeletionQueue()
{
foreach (QueuedActionEntity queuedActionEntity in _queuedActionRepository.GetByType("DeletePlugin"))
{
string? directory = queuedActionEntity.Parameters["directory"].ToString();
try
{
if (Directory.Exists(directory))
{
_logger.Information("Queued plugin deletion - deleting folder - {plugin}", queuedActionEntity.Parameters["plugin"]);
Directory.Delete(directory!, true);
}
else
{
_logger.Information("Queued plugin deletion - folder already deleted - {plugin}", queuedActionEntity.Parameters["plugin"]);
}
_queuedActionRepository.Remove(queuedActionEntity);
}
catch (Exception e)
{
_logger.Warning(e, "Queued plugin deletion failed - {plugin}", queuedActionEntity.Parameters["plugin"]);
}
} }
} }

View File

@ -354,19 +354,26 @@ namespace Artemis.Core.Services
} }
// Finally fall back to a default layout // Finally fall back to a default layout
layout = ArtemisLayout.GetDefaultLayout(device); if (!device.DisableDefaultLayout)
if (layout != null) layout = ArtemisLayout.GetDefaultLayout(device);
ApplyDeviceLayout(device, layout); ApplyDeviceLayout(device, layout);
return layout; return layout;
} }
public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout) public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout)
{ {
if (layout == null)
{
if (device.Layout != null)
device.ClearLayout();
return;
}
if (layout.Source == LayoutSource.Default) if (layout.Source == LayoutSource.Default)
device.ApplyLayout(layout, false, false); device.ApplyLayout(layout, false, false);
else else
device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported);
UpdateLedGroup(); UpdateLedGroup();
} }

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace Artemis.Storage.Entities.General
{
public class QueuedActionEntity
{
public QueuedActionEntity()
{
Parameters = new Dictionary<string, object>();
}
public Guid Id { get; set; }
public string Type { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public Dictionary<string, object> Parameters { get; set; }
}
}

View File

@ -13,21 +13,4 @@ namespace Artemis.Storage.Entities.Plugins
public string Name { get; set; } public string Name { get; set; }
public string Value { get; set; } public string Value { get; set; }
} }
/// <summary>
/// Represents a queued action for a plugin
/// </summary>
public abstract class PluginQueuedActionEntity
{
public Guid Id { get; set; }
public Guid PluginGuid { get; set; }
}
/// <summary>
/// Represents a queued delete action for a plugin
/// </summary>
public class PluginQueuedDeleteEntity : PluginQueuedActionEntity
{
public string Directory { get; set; }
}
} }

View File

@ -22,6 +22,7 @@ namespace Artemis.Storage.Entities.Surface
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 CustomLayoutPath { get; set; }

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Plugins;
namespace Artemis.Storage.Repositories.Interfaces namespace Artemis.Storage.Repositories.Interfaces
@ -9,16 +8,11 @@ namespace Artemis.Storage.Repositories.Interfaces
void AddPlugin(PluginEntity pluginEntity); void AddPlugin(PluginEntity pluginEntity);
PluginEntity GetPluginByGuid(Guid pluginGuid); PluginEntity GetPluginByGuid(Guid pluginGuid);
void SavePlugin(PluginEntity pluginEntity); void SavePlugin(PluginEntity pluginEntity);
void AddSetting(PluginSettingEntity pluginSettingEntity); void AddSetting(PluginSettingEntity pluginSettingEntity);
PluginSettingEntity GetSettingByGuid(Guid pluginGuid); PluginSettingEntity GetSettingByGuid(Guid pluginGuid);
PluginSettingEntity GetSettingByNameAndGuid(string name, Guid pluginGuid); PluginSettingEntity GetSettingByNameAndGuid(string name, Guid pluginGuid);
void SaveSetting(PluginSettingEntity pluginSettingEntity); void SaveSetting(PluginSettingEntity pluginSettingEntity);
void RemoveSettings(Guid pluginGuid); void RemoveSettings(Guid pluginGuid);
void AddQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity);
List<PluginQueuedActionEntity> GetQueuedActions();
List<PluginQueuedActionEntity> GetQueuedActions(Guid pluginGuid);
void RemoveQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity);
} }
} }

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Artemis.Storage.Entities.General;
namespace Artemis.Storage.Repositories.Interfaces
{
public interface IQueuedActionRepository : IRepository
{
void Add(QueuedActionEntity queuedActionEntity);
void Remove(QueuedActionEntity queuedActionEntity);
List<QueuedActionEntity> GetAll();
List<QueuedActionEntity> GetByType(string type);
}
}

View File

@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using Artemis.Storage.Entities.Module; using Artemis.Storage.Entities.Module;
using Artemis.Storage.Repositories.Interfaces; using Artemis.Storage.Repositories.Interfaces;
using LiteDB; using LiteDB;

View File

@ -15,7 +15,6 @@ namespace Artemis.Storage.Repositories
_repository = repository; _repository = repository;
_repository.Database.GetCollection<PluginSettingEntity>().EnsureIndex(s => new {s.Name, s.PluginGuid}, true); _repository.Database.GetCollection<PluginSettingEntity>().EnsureIndex(s => new {s.Name, s.PluginGuid}, true);
_repository.Database.GetCollection<PluginQueuedActionEntity>().EnsureIndex(s => s.PluginGuid);
} }
public void AddPlugin(PluginEntity pluginEntity) public void AddPlugin(PluginEntity pluginEntity)
@ -59,24 +58,5 @@ namespace Artemis.Storage.Repositories
{ {
_repository.DeleteMany<PluginSettingEntity>(s => s.PluginGuid == pluginGuid); _repository.DeleteMany<PluginSettingEntity>(s => s.PluginGuid == pluginGuid);
} }
public List<PluginQueuedActionEntity> GetQueuedActions()
{
return _repository.Query<PluginQueuedActionEntity>().ToList();
}
public List<PluginQueuedActionEntity> GetQueuedActions(Guid pluginGuid)
{
return _repository.Query<PluginQueuedActionEntity>().Where(q => q.PluginGuid == pluginGuid).ToList();
}
public void AddQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity)
{
_repository.Upsert(pluginQueuedActionEntity);
}
public void RemoveQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity)
{
_repository.Delete<PluginQueuedActionEntity>(pluginQueuedActionEntity.Id);
}
} }
} }

View File

@ -0,0 +1,46 @@
using System.Collections.Generic;
using Artemis.Storage.Entities.General;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
namespace Artemis.Storage.Repositories
{
public class QueuedActionRepository : IQueuedActionRepository
{
private readonly LiteRepository _repository;
public QueuedActionRepository(LiteRepository repository)
{
_repository = repository;
_repository.Database.GetCollection<QueuedActionEntity>().EnsureIndex(s => s.Type);
}
#region Implementation of IQueuedActionRepository
/// <inheritdoc />
public void Add(QueuedActionEntity queuedActionEntity)
{
_repository.Insert(queuedActionEntity);
}
/// <inheritdoc />
public void Remove(QueuedActionEntity queuedActionEntity)
{
_repository.Delete<QueuedActionEntity>(queuedActionEntity.Id);
}
/// <inheritdoc />
public List<QueuedActionEntity> GetAll()
{
return _repository.Query<QueuedActionEntity>().ToList();
}
/// <inheritdoc />
public List<QueuedActionEntity> GetByType(string type)
{
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).ToList();
}
#endregion
}
}

View File

@ -0,0 +1,41 @@
using System.Windows.Documents;
using System.Windows.Navigation;
using Artemis.Core;
using Microsoft.Xaml.Behaviors;
namespace Artemis.UI.Shared
{
/// <summary>
/// Represents a behavior that opens the URI of the hyperlink in the browser when requested
/// </summary>
public class OpenInBrowser : Behavior<Hyperlink>
{
private Hyperlink? _hyperLink;
/// <inheritdoc />
protected override void OnAttached()
{
base.OnAttached();
_hyperLink = AssociatedObject;
if (_hyperLink == null)
return;
_hyperLink.RequestNavigate += HyperLinkOnRequestNavigate;
}
/// <inheritdoc />
protected override void OnDetaching()
{
if (_hyperLink == null) return;
_hyperLink.RequestNavigate -= HyperLinkOnRequestNavigate;
base.OnDetaching();
}
private void HyperLinkOnRequestNavigate(object sender, RequestNavigateEventArgs e)
{
Utilities.OpenUrl(e.Uri.AbsoluteUri);
}
}
}

View File

@ -14,5 +14,11 @@
SelectedValuePath="Value" SelectedValuePath="Value"
DisplayMemberPath="Description" DisplayMemberPath="Description"
SelectedValue="{Binding Path=InputValue}" SelectedValue="{Binding Path=InputValue}"
IsDropDownOpen="True"/> IsDropDownOpen="True">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
</UserControl> </UserControl>

View File

@ -13,18 +13,13 @@ namespace Artemis.UI.Screens.Plugins
public class PluginPrerequisiteViewModel : Conductor<PluginPrerequisiteActionViewModel>.Collection.OneActive public class PluginPrerequisiteViewModel : Conductor<PluginPrerequisiteActionViewModel>.Collection.OneActive
{ {
private readonly bool _uninstall; private readonly bool _uninstall;
private readonly ICoreService _coreService;
private readonly IDialogService _dialogService;
private bool _installing; private bool _installing;
private bool _uninstalling; private bool _uninstalling;
private bool _isMet; private bool _isMet;
public PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall, ICoreService coreService, IDialogService dialogService) public PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall)
{ {
_uninstall = uninstall; _uninstall = uninstall;
_coreService = coreService;
_dialogService = dialogService;
PluginPrerequisite = pluginPrerequisite; PluginPrerequisite = pluginPrerequisite;
} }
@ -65,12 +60,6 @@ namespace Artemis.UI.Screens.Plugins
if (Busy) if (Busy)
return; return;
if (PluginPrerequisite.RequiresElevation && !_coreService.IsElevated)
{
await _dialogService.ShowConfirmDialog("Install plugin prerequisite", "This plugin prerequisite admin rights to install (restart & elevate NYI)");
return;
}
Installing = true; Installing = true;
try try
{ {
@ -88,12 +77,6 @@ namespace Artemis.UI.Screens.Plugins
if (Busy) if (Busy)
return; return;
if (PluginPrerequisite.RequiresElevation && !_coreService.IsElevated)
{
await _dialogService.ShowConfirmDialog("Install plugin prerequisite", "This plugin prerequisite admin rights to install (restart & elevate NYI)");
return;
}
Uninstalling = true; Uninstalling = true;
try try
{ {

View File

@ -7,6 +7,7 @@
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet" xmlns:s="https://github.com/canton7/Stylet"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:DevicePropertiesTabViewModel}"> d:DataContext="{d:DesignInstance local:DevicePropertiesTabViewModel}">
@ -29,7 +30,7 @@
Foreground="{DynamicResource MaterialDesignBodyLight}" Foreground="{DynamicResource MaterialDesignBodyLight}"
TextWrapping="Wrap" TextWrapping="Wrap"
TextAlignment="Justify"> TextAlignment="Justify">
Artemis uses categories to determine where the layers of imported profiles are applied to. <LineBreak/> Artemis uses categories to determine where the layers of imported profiles are applied to. <LineBreak />
You can hover over a category for a more detailed description. You can hover over a category for a more detailed description.
</TextBlock> </TextBlock>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
@ -192,15 +193,25 @@
<!-- Layout --> <!-- Layout -->
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}"> <TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}">
Custom layout Layout
</TextBlock> </TextBlock>
<TextBlock Style="{StaticResource MaterialDesignCaptionTextBlock}" <TextBlock Style="{StaticResource MaterialDesignCaptionTextBlock}"
Foreground="{DynamicResource MaterialDesignBodyLight}" Foreground="{DynamicResource MaterialDesignBodyLight}"
TextWrapping="Wrap" TextWrapping="Wrap"
TextAlignment="Justify"> TextAlignment="Justify">
Select a custom layout below if you want to change the appearance and/or LEDs of this device. The device layout is used to determine the position of LEDs and to create the visual representation of the device you see on the left side of this window.
</TextBlock> </TextBlock>
<CheckBox Margin="0 0 0 5" Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding UseDefaultLayout}">
<CheckBox.Content>
<TextBlock Margin="0 -5 0 0">
Use default layout if needed
<materialDesign:PackIcon Kind="HelpCircle"
ToolTip="If there is no built-in layout and no custom layout, with this enabled Artemis will fall back to the default layout of this device type." />
</TextBlock>
</CheckBox.Content>
</CheckBox>
<TextBox Style="{StaticResource MaterialDesignFilledTextBox}" <TextBox Style="{StaticResource MaterialDesignFilledTextBox}"
Text="{Binding Device.CustomLayoutPath}" Text="{Binding Device.CustomLayoutPath}"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -210,10 +221,25 @@
<materialDesign:HintAssist.Hint> <materialDesign:HintAssist.Hint>
<StackPanel Orientation="Horizontal" Margin="-2 0 0 0"> <StackPanel Orientation="Horizontal" Margin="-2 0 0 0">
<materialDesign:PackIcon Kind="Xml" Width="20" /> <materialDesign:PackIcon Kind="Xml" Width="20" />
<TextBlock>Layout path</TextBlock> <TextBlock>Custom layout path</TextBlock>
</StackPanel> </StackPanel>
</materialDesign:HintAssist.Hint> </materialDesign:HintAssist.Hint>
</TextBox> </TextBox>
<TextBlock Style="{StaticResource MaterialDesignCaptionTextBlock}"
Foreground="{DynamicResource MaterialDesignBodyLight}"
TextWrapping="Wrap"
TextAlignment="Justify">
Select a custom layout below if you want to change the appearance and/or LEDs of this device.
For info on how to create layouts, check out
<Hyperlink Style="{StaticResource ArtemisHyperlink}"
NavigateUri="https://wiki.artemis-rgb.com/en/guides/developer/layouts">
this wiki article
<b:Interaction.Behaviors>
<shared:OpenInBrowser />
</b:Interaction.Behaviors>
</Hyperlink>
.
</TextBlock>
</StackPanel> </StackPanel>
<!-- Buttons --> <!-- Buttons -->

View File

@ -134,6 +134,16 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs
set => SetCategory(DeviceCategory.Peripherals, value); set => SetCategory(DeviceCategory.Peripherals, value);
} }
public bool UseDefaultLayout
{
get => !Device.DisableDefaultLayout;
set
{
Device.DisableDefaultLayout = !value;
NotifyOfPropertyChange(nameof(UseDefaultLayout));
}
}
public void ApplyScaling() public void ApplyScaling()
{ {
Device.RedScale = RedScale / 100f; Device.RedScale = RedScale / 100f;
@ -255,7 +265,7 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs
private void DeviceOnPropertyChanged(object sender, PropertyChangedEventArgs e) private void DeviceOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == nameof(Device.CustomLayoutPath)) if (e.PropertyName == nameof(Device.CustomLayoutPath) || e.PropertyName == nameof(Device.DisableDefaultLayout))
_rgbService.ApplyBestDeviceLayout(Device); _rgbService.ApplyBestDeviceLayout(Device);
} }