diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index 0c62553d4..df21cdfd2 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -35,15 +35,13 @@ + - - - diff --git a/src/Artemis.Core/DryIoc/ContainerExtensions.cs b/src/Artemis.Core/DryIoc/ContainerExtensions.cs new file mode 100644 index 000000000..4f418d453 --- /dev/null +++ b/src/Artemis.Core/DryIoc/ContainerExtensions.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Reflection; +using Artemis.Core.DryIoc.Factories; +using Artemis.Core.Services; +using Artemis.Storage; +using Artemis.Storage.Migrations.Interfaces; +using Artemis.Storage.Repositories.Interfaces; +using DryIoc; + +namespace Artemis.Core.DryIoc; + +/// +/// Provides an extension method to register services onto a DryIoc . +/// +public static class CoreContainerExtensions +{ + /// + /// Registers core services into the container. + /// + /// The builder building the current container + public static void RegisterCore(this IContainer container) + { + Assembly[] coreAssembly = {typeof(IArtemisService).Assembly}; + Assembly[] storageAssembly = {typeof(IRepository).Assembly}; + + // Bind all services as singletons + container.RegisterMany(coreAssembly, type => type.IsAssignableTo(), Reuse.Singleton); + container.RegisterMany(coreAssembly, type => type.IsAssignableTo(), Reuse.Singleton, setup: Setup.With(condition: HasAccessToProtectedService)); + + // Bind storage + container.RegisterDelegate(() => StorageManager.CreateRepository(Constants.DataFolder), Reuse.Singleton); + container.Register(Reuse.Singleton); + container.RegisterMany(storageAssembly, type => type.IsAssignableTo(), Reuse.Singleton); + + // Bind migrations + container.RegisterMany(storageAssembly, type => type.IsAssignableTo(), Reuse.Singleton, nonPublicServiceTypes: true); + + container.Register(Reuse.Singleton); + container.Register(made: Made.Of(_ => ServiceInfo.Of(), f => f.CreatePluginSettings(Arg.Index(0)), r => r.Parent.ImplementationType)); + container.Register(Reuse.Singleton); + container.Register(made: Made.Of(_ => ServiceInfo.Of(), f => f.CreateLogger(Arg.Index(0)), r => r.Parent.ImplementationType)); + } + + /// + /// Registers plugin services into the container, this is typically a child container. + /// + /// The builder building the current container + /// The plugin to register + public static void RegisterPlugin(this IContainer container, Plugin plugin) + { + container.RegisterInstance(plugin, setup: Setup.With(preventDisposal: true)); + + // Bind plugin service interfaces, DryIoc expects at least one match when calling RegisterMany so ensure there is something to register first + if (plugin.Assembly != null && plugin.Assembly.GetTypes().Any(t => t.IsAssignableTo())) + container.RegisterMany(new[] {plugin.Assembly}, type => type.IsAssignableTo(), Reuse.Singleton, ifAlreadyRegistered: IfAlreadyRegistered.Keep); + } + + private static bool HasAccessToProtectedService(Request request) + { + // Plugin assembly locations may not be set for some reason, that case it's also not allowed >:( + return request.Parent.ImplementationType != null && + !string.IsNullOrWhiteSpace(request.Parent.ImplementationType.Assembly.Location) && + !request.Parent.ImplementationType.Assembly.Location.StartsWith(Constants.PluginsFolder); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/LoggerProvider.cs b/src/Artemis.Core/DryIoc/Factories/LoggerFactory.cs similarity index 64% rename from src/Artemis.Core/Ninject/LoggerProvider.cs rename to src/Artemis.Core/DryIoc/Factories/LoggerFactory.cs index 5759557c6..ba5e90db7 100644 --- a/src/Artemis.Core/Ninject/LoggerProvider.cs +++ b/src/Artemis.Core/DryIoc/Factories/LoggerFactory.cs @@ -1,17 +1,16 @@ -using System; +using System; using System.IO; -using Ninject.Activation; using Serilog; using Serilog.Core; using Serilog.Events; -namespace Artemis.Core.Ninject; +namespace Artemis.Core.DryIoc.Factories; -internal class LoggerProvider : Provider +internal class LoggerFactory : ILoggerFactory { internal static readonly LoggingLevelSwitch LoggingLevelSwitch = new(LogEventLevel.Verbose); - private static readonly ILogger Logger = new LoggerConfiguration() + internal static readonly ILogger Logger = new LoggerConfiguration() .Enrich.FromLogContext() .WriteTo.File(Path.Combine(Constants.LogsFolder, "Artemis log-.log"), rollingInterval: RollingInterval.Day, @@ -24,12 +23,10 @@ internal class LoggerProvider : Provider .MinimumLevel.ControlledBy(LoggingLevelSwitch) .CreateLogger(); - protected override ILogger CreateInstance(IContext context) + /// + public ILogger CreateLogger(Type type) { - Type? requestingType = context.Request.ParentContext?.Plan?.Type; - if (requestingType != null) - return Logger.ForContext(requestingType); - return Logger; + return Logger.ForContext(type); } } @@ -40,4 +37,9 @@ internal class ArtemisSink : ILogEventSink { LogStore.Emit(logEvent); } +} + +internal interface ILoggerFactory +{ + ILogger CreateLogger(Type type); } \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/PluginSettingsProvider.cs b/src/Artemis.Core/DryIoc/Factories/PluginSettingsFactory.cs similarity index 51% rename from src/Artemis.Core/Ninject/PluginSettingsProvider.cs rename to src/Artemis.Core/DryIoc/Factories/PluginSettingsFactory.cs index 3fb83fda8..7684b2b43 100644 --- a/src/Artemis.Core/Ninject/PluginSettingsProvider.cs +++ b/src/Artemis.Core/DryIoc/Factories/PluginSettingsFactory.cs @@ -1,35 +1,26 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Artemis.Core.Services; using Artemis.Storage.Repositories.Interfaces; -using Ninject.Activation; -namespace Artemis.Core.Ninject; +namespace Artemis.Core.DryIoc.Factories; -// TODO: Investigate if this can't just be set as a constant on the plugin child kernel -internal class PluginSettingsProvider : Provider +internal class PluginSettingsFactory : IPluginSettingsFactory { private static readonly List PluginSettings = new(); private readonly IPluginManagementService _pluginManagementService; private readonly IPluginRepository _pluginRepository; - public PluginSettingsProvider(IPluginRepository pluginRepository, IPluginManagementService pluginManagementService) + public PluginSettingsFactory(IPluginRepository pluginRepository, IPluginManagementService pluginManagementService) { _pluginRepository = pluginRepository; _pluginManagementService = pluginManagementService; } - protected override PluginSettings CreateInstance(IContext context) + public PluginSettings CreatePluginSettings(Type type) { - IRequest parentRequest = context.Request.ParentRequest; - if (parentRequest == null) - throw new ArtemisCoreException("PluginSettings couldn't be injected, failed to get the injection parent request"); - - // First try by PluginInfo parameter - Plugin? plugin = parentRequest.Parameters.FirstOrDefault(p => p.Name == "Plugin")?.GetValue(context, null) as Plugin; - // Fall back to assembly based detection - if (plugin == null) - plugin = _pluginManagementService.GetPluginByAssembly(parentRequest.Service.Assembly); + Plugin? plugin = _pluginManagementService.GetPluginByAssembly(type.Assembly); if (plugin == null) throw new ArtemisCoreException("PluginSettings can only be injected with the PluginInfo parameter provided " + @@ -46,4 +37,9 @@ internal class PluginSettingsProvider : Provider return settings; } } +} + +internal interface IPluginSettingsFactory +{ + PluginSettings CreatePluginSettings(Type type); } \ No newline at end of file diff --git a/src/Artemis.Core/Events/UpdateEventArgs.cs b/src/Artemis.Core/Events/UpdateEventArgs.cs new file mode 100644 index 000000000..d513bae5e --- /dev/null +++ b/src/Artemis.Core/Events/UpdateEventArgs.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Core; + +/// +/// Provides data about application update events +/// +public class UpdateEventArgs : EventArgs +{ + internal UpdateEventArgs(bool silent) + { + Silent = silent; + } + + /// + /// Gets a boolean indicating whether to silently update or not. + /// + public bool Silent { get; } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs index 3615fbfbc..8550a9b10 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs @@ -241,7 +241,16 @@ public class ColorGradient : IList, IList, INotifyCollectionC return _stops[^1].Color; //find the first stop after the position - int stop2Index = _stops.FindIndex(s => s.Position >= position); + int stop2Index = 0; + + for (int i = 0; i < _stops.Count; i++) + { + if (_stops[i].Position >= position) + { + stop2Index = i; + break; + } + } //if the position is before the first stop, return that color if (stop2Index == 0) return _stops[0].Color; diff --git a/src/Artemis.Core/Models/Profile/ProfileCategory.cs b/src/Artemis.Core/Models/Profile/ProfileCategory.cs index 603bfdf08..13a3e2f4b 100644 --- a/src/Artemis.Core/Models/Profile/ProfileCategory.cs +++ b/src/Artemis.Core/Models/Profile/ProfileCategory.cs @@ -10,6 +10,11 @@ namespace Artemis.Core; /// public class ProfileCategory : CorePropertyChanged, IStorageModel { + /// + /// Represents an empty profile category. + /// + public static readonly ProfileCategory Empty = new("Empty", -1); + private readonly List _profileConfigurations = new(); private bool _isCollapsed; private bool _isSuspended; diff --git a/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs b/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs index 51d0ce1ef..ac2c9ed24 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs @@ -16,7 +16,10 @@ public class Hotkey : CorePropertyChanged, IStorageModel Entity = new ProfileConfigurationHotkeyEntity(); } - internal Hotkey(ProfileConfigurationHotkeyEntity entity) + /// + /// Creates a new instance of based on the provided entity + /// + public Hotkey(ProfileConfigurationHotkeyEntity entity) { Entity = entity; Load(); @@ -32,7 +35,10 @@ public class Hotkey : CorePropertyChanged, IStorageModel /// public KeyboardModifierKey? Modifiers { get; set; } - internal ProfileConfigurationHotkeyEntity Entity { get; } + /// + /// Gets the entity used to store this hotkey + /// + public ProfileConfigurationHotkeyEntity Entity { get; } /// /// Determines whether the provided match the hotkey diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index b349acfd1..06a2090d3 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -11,6 +11,11 @@ namespace Artemis.Core; /// public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable { + /// + /// Represents an empty profile. + /// + public static readonly ProfileConfiguration Empty = new(ProfileCategory.Empty, "Empty", "Empty"); + private ActivationBehaviour _activationBehaviour; private bool _activationConditionMet; private ProfileCategory _category; diff --git a/src/Artemis.Core/Ninject/CoreModule.cs b/src/Artemis.Core/Ninject/CoreModule.cs deleted file mode 100644 index 14d1c08dc..000000000 --- a/src/Artemis.Core/Ninject/CoreModule.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Artemis.Core.Services; -using Artemis.Storage; -using Artemis.Storage.Migrations.Interfaces; -using Artemis.Storage.Repositories.Interfaces; -using LiteDB; -using Ninject.Activation; -using Ninject.Extensions.Conventions; -using Ninject.Modules; -using Ninject.Planning.Bindings.Resolvers; -using Serilog; - -namespace Artemis.Core.Ninject; - -/// -/// The main of the Artemis Core that binds all services -/// -public class CoreModule : NinjectModule -{ - /// - public override void Load() - { - if (Kernel == null) - throw new ArtemisCoreException("Failed to bind Ninject Core module, kernel is null."); - - Kernel.Components.Remove(); - - // Bind all services as singletons - Kernel.Bind(x => - { - x.FromThisAssembly() - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.InSingletonScope()); - }); - - // Bind all protected services as singletons - Kernel.Bind(x => - { - x.FromThisAssembly() - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.When(HasAccessToProtectedService).InSingletonScope()); - }); - - Kernel.Bind().ToMethod(_ => StorageManager.CreateRepository(Constants.DataFolder)).InSingletonScope(); - Kernel.Bind().ToSelf().InSingletonScope(); - - // Bind all migrations as singletons - Kernel.Bind(x => - { - x.FromAssemblyContaining() - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.InSingletonScope()); - }); - - // Bind all repositories as singletons - Kernel.Bind(x => - { - x.FromAssemblyContaining() - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.InSingletonScope()); - }); - - Kernel.Bind().ToProvider(); - Kernel.Bind().ToProvider(); - Kernel.Bind().ToSelf(); - } - - private bool HasAccessToProtectedService(IRequest r) - { - return r.ParentRequest != null && !r.ParentRequest.Service.Assembly.Location.StartsWith(Constants.PluginsFolder); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/PluginModule.cs b/src/Artemis.Core/Ninject/PluginModule.cs deleted file mode 100644 index 612685567..000000000 --- a/src/Artemis.Core/Ninject/PluginModule.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using Artemis.Core.Services; -using Ninject.Extensions.Conventions; -using Ninject.Modules; -using Ninject.Planning.Bindings.Resolvers; - -namespace Artemis.Core.Ninject; - -internal class PluginModule : NinjectModule -{ - public PluginModule(Plugin plugin) - { - Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); - } - - public Plugin Plugin { get; } - - public override void Load() - { - if (Kernel == null) - throw new ArtemisCoreException("Failed to bind plugin child module, kernel is null."); - - Kernel.Components.Remove(); - - Kernel.Bind().ToConstant(Plugin).InTransientScope(); - - // Bind plugin service interfaces - Kernel.Bind(x => - { - x.From(Plugin.Assembly) - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.InSingletonScope()); - }); - - // Plugin developers may not use an interface so bind the plugin services to themselves - // Sadly if they do both, the kernel will treat the interface and the base type as two different singletons - // perhaps we can avoid that, but I'm not sure how - Kernel.Bind(x => - { - x.From(Plugin.Assembly) - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindToSelf() - .Configure(c => c.InSingletonScope()); - }); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/SettingsServiceProvider.cs b/src/Artemis.Core/Ninject/SettingsServiceProvider.cs deleted file mode 100644 index a7c022ae6..000000000 --- a/src/Artemis.Core/Ninject/SettingsServiceProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Artemis.Core.Services; -using Ninject; -using Ninject.Activation; - -namespace Artemis.Core.Ninject; - -internal class SettingsServiceProvider : Provider -{ - private readonly SettingsService _instance; - - public SettingsServiceProvider(IKernel kernel) - { - // This is not lazy, but the core is always going to be using this anyway - _instance = kernel.Get(); - } - - protected override ISettingsService CreateInstance(IContext context) - { - IRequest parentRequest = context.Request.ParentRequest; - if (parentRequest == null || typeof(PluginFeature).IsAssignableFrom(parentRequest.Service)) - throw new ArtemisPluginException($"SettingsService can not be injected into a plugin. Inject {nameof(PluginSettings)} instead."); - - return _instance; - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs index 67d5ce8d4..0c51a5cf4 100644 --- a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs +++ b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs @@ -1,9 +1,7 @@ using System; using System.IO; using System.Linq; -using Ninject; using RGB.NET.Core; -using Serilog; namespace Artemis.Core.DeviceProviders; @@ -26,14 +24,7 @@ public abstract class DeviceProvider : PluginFeature /// The RGB.NET device provider backing this Artemis device provider /// public IRGBDeviceProvider RgbDeviceProvider { get; } - - /// - /// TODO: Make internal while still injecting. - /// A logger used by the device provider internally, ignore this - /// - [Inject] - public ILogger? Logger { get; set; } - + /// /// A boolean indicating whether this device provider detects the physical layout of connected keyboards. /// diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs index 4279b2810..3e245f9c4 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs @@ -1,6 +1,5 @@ using System; using Artemis.Storage.Entities.Profile; -using Ninject; namespace Artemis.Core.LayerBrushes; @@ -63,7 +62,7 @@ public class LayerBrushDescriptor if (layer == null) throw new ArgumentNullException(nameof(layer)); - BaseLayerBrush brush = (BaseLayerBrush) Provider.Plugin.Kernel!.Get(LayerBrushType); + BaseLayerBrush brush = (BaseLayerBrush) Provider.Plugin.Resolve(LayerBrushType); brush.Layer = layer; brush.Descriptor = this; brush.LayerBrushEntity = entity ?? new LayerBrushEntity {ProviderId = Provider.Id, BrushType = LayerBrushType.FullName}; diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs index 196461c45..93ea5e0ed 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs @@ -1,7 +1,6 @@ using System; using Artemis.Core.LayerEffects.Placeholder; using Artemis.Storage.Entities.Profile; -using Ninject; namespace Artemis.Core.LayerEffects; @@ -80,7 +79,7 @@ public class LayerEffectDescriptor if (LayerEffectType == null) throw new ArtemisCoreException("Cannot create an instance of a layer effect because this descriptor is not a placeholder but is still missing its LayerEffectType"); - BaseLayerEffect effect = (BaseLayerEffect) Provider.Plugin.Kernel!.Get(LayerEffectType); + BaseLayerEffect effect = (BaseLayerEffect) Provider.Plugin.Resolve(LayerEffectType); effect.ProfileElement = renderElement; effect.Descriptor = this; if (entity != null) diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index 4c0d2cfb9..45b469146 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -7,8 +7,8 @@ using System.Linq; using System.Reflection; using Artemis.Core.DeviceProviders; using Artemis.Storage.Entities.Plugins; +using DryIoc; using McMaster.NETCore.Plugins; -using Ninject; namespace Artemis.Core; @@ -88,9 +88,9 @@ public class Plugin : CorePropertyChanged, IDisposable public PluginBootstrapper? Bootstrapper { get; internal set; } /// - /// The Ninject kernel of the plugin + /// Gets the IOC container of the plugin, only use this for advanced IOC operations, otherwise see and /// - public IKernel? Kernel { get; internal set; } + public IContainer? Container { get; internal set; } /// /// The PluginLoader backing this plugin @@ -165,16 +165,66 @@ public class Plugin : CorePropertyChanged, IDisposable } /// - /// Gets an instance of the specified service using the plugins dependency injection container. - /// Note: To use parameters reference Ninject and use directly. + /// Gets an instance of the specified service using the plugins dependency injection container. /// + /// Arguments to supply to the service. /// The service to resolve. /// An instance of the service. - public T Get() + /// + public T Resolve(params object?[] arguments) { - if (Kernel == null) - throw new ArtemisPluginException("Cannot use Get before the plugin finished loading"); - return Kernel.Get(); + if (Container == null) + throw new ArtemisPluginException("Cannot use Resolve before the plugin finished loading"); + return Container.Resolve(args: arguments); + } + + /// + /// Gets an instance of the specified service using the plugins dependency injection container. + /// + /// The type of service to resolve. + /// Arguments to supply to the service. + /// An instance of the service. + /// + public object Resolve(Type type, params object?[] arguments) + { + if (Container == null) + throw new ArtemisPluginException("Cannot use Resolve before the plugin finished loading"); + return Container.Resolve(type, args: arguments); + } + + /// + /// Registers service of type implemented by type. + /// + /// The scope in which the service should live, if you are not sure leave it on singleton. + /// The service to register. + /// The implementation of the service to register. + public void Register(PluginServiceScope scope = PluginServiceScope.Singleton) where TImplementation : TService + { + IReuse reuse = scope switch + { + PluginServiceScope.Transient => Reuse.Transient, + PluginServiceScope.Singleton => Reuse.Singleton, + PluginServiceScope.Scoped => Reuse.Scoped, + _ => throw new ArgumentOutOfRangeException(nameof(scope), scope, null) + }; + Container.Register(reuse); + } + + /// + /// Registers implementation type with itself as service type. + /// + /// The scope in which the service should live, if you are not sure leave it on singleton. + /// The implementation of the service to register. + public void Register(PluginServiceScope scope = PluginServiceScope.Singleton) + { + IReuse reuse = scope switch + { + PluginServiceScope.Transient => Reuse.Transient, + PluginServiceScope.Singleton => Reuse.Singleton, + PluginServiceScope.Scoped => Reuse.Scoped, + _ => throw new ArgumentOutOfRangeException(nameof(scope), scope, null) + }; + Container.Register(reuse); } /// @@ -218,7 +268,7 @@ public class Plugin : CorePropertyChanged, IDisposable feature.Instance?.Dispose(); SetEnabled(false); - Kernel?.Dispose(); + Container?.Dispose(); PluginLoader?.Dispose(); GC.Collect(); @@ -340,4 +390,26 @@ public class Plugin : CorePropertyChanged, IDisposable Dispose(true); GC.SuppressFinalize(this); } +} + +/// +/// Represents a scope in which a plugin service is injected by the IOC container. +/// +public enum PluginServiceScope +{ + /// + /// Services in this scope are never reused, a new instance is injected each time. + /// + Transient, + + /// + /// Services in this scope are reused for as long as the plugin lives, the same instance is injected each time. + /// + Singleton, + + /// + /// Services in this scope are reused within a container scope, this is an advanced setting you shouldn't need. + /// To learn more see the DryIoc docs. + /// + Scoped } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index b1dfbdc7b..ccd9f4c9f 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -1,10 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Artemis.Core.DeviceProviders; -using Artemis.Core.LayerBrushes; -using Artemis.Core.LayerEffects; -using Artemis.Core.Modules; using Artemis.Storage.Entities.Plugins; using Humanizer; using Newtonsoft.Json; diff --git a/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs b/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs index be52ac01f..0795ab261 100644 --- a/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs +++ b/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs @@ -1,9 +1,8 @@ using System; using System.Threading.Tasks; using System.Timers; +using Artemis.Core.DryIoc.Factories; using Artemis.Core.Modules; -using Artemis.Core.Services; -using Ninject; using Serilog; namespace Artemis.Core; @@ -11,7 +10,7 @@ namespace Artemis.Core; /// /// Represents a registration for a timed plugin update /// -public class TimedUpdateRegistration : IDisposable +public sealed class TimedUpdateRegistration : IDisposable { private readonly object _lock = new(); private readonly ILogger _logger; @@ -21,9 +20,7 @@ public class TimedUpdateRegistration : IDisposable internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action action, string? name) { - if (CoreService.Kernel == null) - throw new ArtemisCoreException("Cannot create a TimedUpdateRegistration before initializing the Core"); - _logger = CoreService.Kernel.Get(); + _logger = LoggerFactory.Logger.ForContext(); Feature = feature; Interval = interval; @@ -38,9 +35,7 @@ public class TimedUpdateRegistration : IDisposable internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func asyncAction, string? name) { - if (CoreService.Kernel == null) - throw new ArtemisCoreException("Cannot create a TimedUpdateRegistration before initializing the Core"); - _logger = CoreService.Kernel.Get(); + _logger = LoggerFactory.Logger.ForContext(); Feature = feature; Interval = interval; @@ -125,33 +120,13 @@ public class TimedUpdateRegistration : IDisposable } /// - public sealed override string ToString() + public override string ToString() { if (Interval.TotalSeconds >= 1) return $"{Name} ({Interval.TotalSeconds} sec)"; return $"{Name} ({Interval.TotalMilliseconds} ms)"; } - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - Stop(); - - Feature.Enabled -= FeatureOnEnabled; - Feature.Disabled -= FeatureOnDisabled; - - _disposed = true; - } - } - private void TimerOnElapsed(object? sender, ElapsedEventArgs e) { if (!Feature.IsEnabled) @@ -204,9 +179,12 @@ public class TimedUpdateRegistration : IDisposable /// public void Dispose() { - Dispose(true); - Feature.Profiler.ClearMeasurements(ToString()); + Stop(); - GC.SuppressFinalize(this); + Feature.Enabled -= FeatureOnEnabled; + Feature.Disabled -= FeatureOnDisabled; + + _disposed = true; + Feature.Profiler.ClearMeasurements(ToString()); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 81d8232ab..933d3d3c3 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -3,11 +3,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; -using Artemis.Core.Ninject; +using Artemis.Core.DryIoc.Factories; using Artemis.Core.ScriptingProviders; using Artemis.Storage; +using DryIoc; using HidSharp; -using Ninject; using RGB.NET.Core; using Serilog; using Serilog.Events; @@ -20,7 +20,6 @@ namespace Artemis.Core.Services; /// internal class CoreService : ICoreService { - internal static IKernel? Kernel; private readonly Stopwatch _frameStopWatch; private readonly ILogger _logger; private readonly PluginSetting _loggingLevel; @@ -36,7 +35,7 @@ internal class CoreService : ICoreService private DateTime _lastFrameRateSample; // ReSharper disable UnusedParameter.Local - public CoreService(IKernel kernel, + public CoreService(IContainer container, ILogger logger, StorageMigrationService _1, // injected to ensure migration runs early ISettingsService settingsService, @@ -47,8 +46,7 @@ internal class CoreService : ICoreService IScriptingService scriptingService, IProcessMonitorService _2) { - Kernel = kernel; - Constants.CorePlugin.Kernel = kernel; + Constants.CorePlugin.Container = container; _logger = logger; _pluginManagementService = pluginManagementService; @@ -85,19 +83,19 @@ internal class CoreService : ICoreService if (parts.Length == 2 && Enum.TryParse(typeof(LogEventLevel), parts[1], true, out object? logLevelArgument)) { _logger.Information("Setting logging level to {loggingLevel} from startup argument", (LogEventLevel) logLevelArgument!); - LoggerProvider.LoggingLevelSwitch.MinimumLevel = (LogEventLevel) logLevelArgument; + LoggerFactory.LoggingLevelSwitch.MinimumLevel = (LogEventLevel) logLevelArgument; } else { _logger.Warning("Failed to set log level from startup argument {argument}", argument); _logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value); - LoggerProvider.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value; + LoggerFactory.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value; } } else { _logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value); - LoggerProvider.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value; + LoggerFactory.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value; } } @@ -195,12 +193,6 @@ internal class CoreService : ICoreService public bool ProfileRenderingDisabled { get; set; } public bool IsElevated { get; set; } - public void Dispose() - { - // Dispose services - _pluginManagementService.Dispose(); - } - public bool IsInitialized { get; set; } public void Initialize() diff --git a/src/Artemis.Core/Services/Interfaces/ICoreService.cs b/src/Artemis.Core/Services/Interfaces/ICoreService.cs index 66de54365..5830815ba 100644 --- a/src/Artemis.Core/Services/Interfaces/ICoreService.cs +++ b/src/Artemis.Core/Services/Interfaces/ICoreService.cs @@ -5,7 +5,7 @@ namespace Artemis.Core.Services; /// /// A service that initializes the Core and manages the render loop /// -public interface ICoreService : IArtemisService, IDisposable +public interface ICoreService : IArtemisService { /// /// Gets whether the or not the core has been initialized diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs index f09b20d04..7cae6709b 100644 --- a/src/Artemis.Core/Services/NodeService.cs +++ b/src/Artemis.Core/Services/NodeService.cs @@ -5,8 +5,8 @@ using System.Reflection; using System.Security.Cryptography; using System.Text; using Artemis.Storage.Entities.Profile.Nodes; +using DryIoc; using Newtonsoft.Json; -using Ninject; using SkiaSharp; namespace Artemis.Core.Services; @@ -15,17 +15,17 @@ internal class NodeService : INodeService { #region Constants - private static readonly Type TYPE_NODE = typeof(INode); + private static readonly Type TypeNode = typeof(INode); #endregion - private readonly IKernel _kernel; + private readonly IContainer _container; #region Constructors - public NodeService(IKernel kernel) + public NodeService(IContainer container) { - _kernel = kernel; + _container = container; } #endregion @@ -69,7 +69,7 @@ internal class NodeService : INodeService if (plugin == null) throw new ArgumentNullException(nameof(plugin)); if (nodeType == null) throw new ArgumentNullException(nameof(nodeType)); - if (!TYPE_NODE.IsAssignableFrom(nodeType)) throw new ArgumentException("Node has to be a base type of the Node-Type.", nameof(nodeType)); + if (!TypeNode.IsAssignableFrom(nodeType)) throw new ArgumentException("Node has to be a base type of the Node-Type.", nameof(nodeType)); NodeAttribute? nodeAttribute = nodeType.GetCustomAttribute(); string name = nodeAttribute?.Name ?? nodeType.Name; @@ -106,8 +106,10 @@ internal class NodeService : INodeService private INode CreateNode(INodeScript script, NodeEntity? entity, Type nodeType) { - INode node = _kernel.Get(nodeType) as INode ?? throw new InvalidOperationException($"Node {nodeType} is not an INode"); - + INode node = _container.Resolve(nodeType) as INode ?? throw new InvalidOperationException($"Node {nodeType} is not an INode"); + if (node is Node concreteNode) + concreteNode.Container = _container; + if (entity != null) { node.X = entity.X; diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 1e113ab95..e313ca59d 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -5,19 +5,15 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; -using System.Runtime.Loader; using System.Threading.Tasks; using Artemis.Core.DeviceProviders; -using Artemis.Core.Ninject; +using Artemis.Core.DryIoc; using Artemis.Storage.Entities.General; using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Surface; using Artemis.Storage.Repositories.Interfaces; +using DryIoc; using McMaster.NETCore.Plugins; -using Ninject; -using Ninject.Extensions.ChildKernel; -using Ninject.Parameters; -using Ninject.Planning.Bindings.Resolvers; using RGB.NET.Core; using Serilog; @@ -29,7 +25,7 @@ namespace Artemis.Core.Services; internal class PluginManagementService : IPluginManagementService { private readonly IDeviceRepository _deviceRepository; - private readonly IKernel _kernel; + private readonly IContainer _container; private readonly ILogger _logger; private readonly IPluginRepository _pluginRepository; private readonly List _plugins; @@ -37,9 +33,9 @@ internal class PluginManagementService : IPluginManagementService private bool _disposed; private bool _isElevated; - public PluginManagementService(IKernel kernel, ILogger logger, IPluginRepository pluginRepository, IDeviceRepository deviceRepository, IQueuedActionRepository queuedActionRepository) + public PluginManagementService(IContainer container, ILogger logger, IPluginRepository pluginRepository, IDeviceRepository deviceRepository, IQueuedActionRepository queuedActionRepository) { - _kernel = kernel; + _container = container; _logger = logger; _pluginRepository = pluginRepository; _deviceRepository = deviceRepository; @@ -88,7 +84,20 @@ internal class PluginManagementService : IPluginManagementService foreach (FileInfo zipFile in builtInPluginDirectory.EnumerateFiles("*.zip")) { - // Find the metadata file in the zip + try + { + ExtractBuiltInPlugin(zipFile, pluginDirectory); + } + catch (Exception e) + { + _logger.Error(e, "Failed to copy built-in plugin from {ZipFile}", zipFile.FullName); + } + } + } + + private void ExtractBuiltInPlugin(FileInfo zipFile, DirectoryInfo pluginDirectory) + { + // Find the metadata file in the zip using ZipArchive archive = ZipFile.OpenRead(zipFile.FullName); ZipArchiveEntry? metaDataFileEntry = archive.GetEntry("plugin.json"); if (metaDataFileEntry == null) @@ -139,7 +148,6 @@ internal class PluginManagementService : IPluginManagementService } } } - } } #endregion @@ -198,6 +206,10 @@ internal class PluginManagementService : IPluginManagementService public void Dispose() { + // Disposal happens manually before container disposal but the container doesn't know that so a 2nd call will be made + if (_disposed) + return; + _disposed = true; UnloadPlugins(); } @@ -340,11 +352,11 @@ internal class PluginManagementService : IPluginManagementService configure.IsUnloadable = true; configure.LoadInMemory = true; configure.PreferSharedTypes = true; - + // Resolving failed, try a loaded assembly but ignoring the version configure.DefaultContext.Resolving += (context, assemblyName) => context.Assemblies.FirstOrDefault(a => a.GetName().Name == assemblyName.Name); }); - + try { plugin.Assembly = plugin.PluginLoader.LoadDefaultAssembly(); @@ -406,7 +418,7 @@ internal class PluginManagementService : IPluginManagementService OnPluginLoaded(new PluginEventArgs(plugin)); return plugin; } - + public void EnablePlugin(Plugin plugin, bool saveState, bool ignorePluginLock) { if (!plugin.Info.IsCompatible) @@ -431,10 +443,17 @@ internal class PluginManagementService : IPluginManagementService if (!plugin.Info.ArePrerequisitesMet()) throw new ArtemisPluginPrerequisiteException(plugin.Info, "Cannot enable a plugin whose prerequisites aren't all met"); - // Create the Ninject child kernel and load the module - plugin.Kernel = new ChildKernel(_kernel, new PluginModule(plugin)); - // The kernel used by Core is unforgiving about missing bindings, no need to be so hard on plugin devs - plugin.Kernel.Components.Add(); + // Create a child container for the plugin, be a bit more forgiving about concrete types + plugin.Container = _container.CreateChild(newRules: _container.Rules.WithConcreteTypeDynamicRegistrations()); + try + { + plugin.Container.RegisterPlugin(plugin); + } + catch (Exception e) + { + _logger.Error(e, "Failed to register plugin services for plugin {plugin}, skipping enabling", plugin); + return; + } OnPluginEnabling(new PluginEventArgs(plugin)); @@ -446,11 +465,8 @@ internal class PluginManagementService : IPluginManagementService { try { - plugin.Kernel.Bind(featureInfo.FeatureType).ToSelf().InSingletonScope(); - - // Include Plugin as a parameter for the PluginSettingsProvider - IParameter[] parameters = {new Parameter("Plugin", plugin, false)}; - PluginFeature instance = (PluginFeature) plugin.Kernel.Get(featureInfo.FeatureType, parameters); + plugin.Container.Register(featureInfo.FeatureType, reuse: Reuse.Singleton); + PluginFeature instance = (PluginFeature) plugin.Container.Resolve(featureInfo.FeatureType); // Get the PluginFeature attribute which contains extra info on the feature featureInfo.Instance = instance; @@ -526,8 +542,8 @@ internal class PluginManagementService : IPluginManagementService plugin.SetEnabled(false); - plugin.Kernel?.Dispose(); - plugin.Kernel = null; + plugin.Container?.Dispose(); + plugin.Container = null; GC.Collect(); GC.WaitForPendingFinalizers(); diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index a3471e729..753becc66 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -9,7 +9,7 @@ using Artemis.Core.Services.Models; using Artemis.Core.SkiaSharp; using Artemis.Storage.Entities.Surface; using Artemis.Storage.Repositories.Interfaces; -using Ninject; +using DryIoc; using RGB.NET.Core; using Serilog; @@ -20,31 +20,36 @@ namespace Artemis.Core.Services; /// internal class RgbService : IRgbService { - private readonly IDeviceRepository _deviceRepository; - private readonly List _devices; - private readonly List _enabledDevices; - private readonly IKernel _kernel; private readonly ILogger _logger; private readonly IPluginManagementService _pluginManagementService; + private readonly IDeviceRepository _deviceRepository; + private readonly LazyEnumerable _graphicsContextProviders; + private readonly PluginSetting _preferredGraphicsContext; private readonly PluginSetting _renderScaleSetting; - private readonly ISettingsService _settingsService; private readonly PluginSetting _targetFrameRateSetting; + + private readonly List _devices; + private readonly List _enabledDevices; private readonly SKTextureBrush _textureBrush = new(null) {CalculationMode = RenderMode.Absolute}; private Dictionary _ledMap; private ListLedGroup? _surfaceLedGroup; private SKTexture? _texture; - public RgbService(ILogger logger, IKernel kernel, ISettingsService settingsService, IPluginManagementService pluginManagementService, IDeviceRepository deviceRepository) + public RgbService(ILogger logger, + ISettingsService settingsService, + IPluginManagementService pluginManagementService, + IDeviceRepository deviceRepository, + LazyEnumerable graphicsContextProviders) { _logger = logger; - _kernel = kernel; - _settingsService = settingsService; _pluginManagementService = pluginManagementService; _deviceRepository = deviceRepository; + _graphicsContextProviders = graphicsContextProviders; + _targetFrameRateSetting = settingsService.GetSetting("Core.TargetFrameRate", 30); _renderScaleSetting = settingsService.GetSetting("Core.RenderScale", 0.25); - _preferredGraphicsContext = _settingsService.GetSetting("Core.PreferredGraphicsContext", "Software"); + _preferredGraphicsContext = settingsService.GetSetting("Core.PreferredGraphicsContext", "Software"); Surface = new RGBSurface(); Utilities.RenderScaleMultiplier = (int) (1 / _renderScaleSetting.Value); @@ -226,7 +231,7 @@ internal class RgbService : IRgbService { _logger.Verbose("[AddDeviceProvider] Updating the LED group"); UpdateLedGroup(); - + _logger.Verbose("[AddDeviceProvider] Resuming rendering after adding {DeviceProvider}", deviceProvider.GetType().Name); if (changedRenderPaused) SetRenderPaused(false); @@ -257,7 +262,7 @@ internal class RgbService : IRgbService { _logger.Verbose("[RemoveDeviceProvider] Updating the LED group"); UpdateLedGroup(); - + _logger.Verbose("[RemoveDeviceProvider] Resuming rendering after adding {DeviceProvider}", deviceProvider.GetType().Name); if (changedRenderPaused) SetRenderPaused(false); @@ -372,7 +377,8 @@ internal class RgbService : IRgbService return; } - List providers = _kernel.Get>(); + + List providers = _graphicsContextProviders.ToList(); if (!providers.Any()) { _logger.Warning("No graphics context provider found, defaulting to software rendering"); diff --git a/src/Artemis.Core/Services/ScriptingService.cs b/src/Artemis.Core/Services/ScriptingService.cs index ef9b8a5ad..baeee28e9 100644 --- a/src/Artemis.Core/Services/ScriptingService.cs +++ b/src/Artemis.Core/Services/ScriptingService.cs @@ -2,10 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Reflection; using Artemis.Core.ScriptingProviders; -using Ninject; -using Ninject.Parameters; namespace Artemis.Core.Services; @@ -51,11 +48,7 @@ internal class ScriptingService : IScriptingService if (provider == null) throw new ArtemisCoreException($"Can't create script instance as there is no matching scripting provider found for the script ({scriptConfiguration.ScriptingProviderId})."); - script = (GlobalScript) provider.Plugin.Kernel!.Get( - provider.GlobalScriptType, - CreateScriptConstructorArgument(provider.GlobalScriptType, scriptConfiguration) - ); - + script = (GlobalScript) provider.Plugin.Resolve(provider.GlobalScriptType, scriptConfiguration); script.ScriptingProvider = provider; script.ScriptingService = this; scriptConfiguration.Script = script; @@ -82,12 +75,7 @@ internal class ScriptingService : IScriptingService if (provider == null) throw new ArtemisCoreException($"Can't create script instance as there is no matching scripting provider found for the script ({scriptConfiguration.ScriptingProviderId})."); - script = (ProfileScript) provider.Plugin.Kernel!.Get( - provider.ProfileScriptType, - CreateScriptConstructorArgument(provider.ProfileScriptType, profile), - CreateScriptConstructorArgument(provider.ProfileScriptType, scriptConfiguration) - ); - + script = (ProfileScript) provider.Plugin.Resolve(provider.ProfileScriptType, profile, scriptConfiguration); script.ScriptingProvider = provider; scriptConfiguration.Script = script; provider.InternalScripts.Add(script); @@ -113,21 +101,6 @@ internal class ScriptingService : IScriptingService } } - private ConstructorArgument CreateScriptConstructorArgument(Type scriptType, object value) - { - // Limit to one constructor, there's no need to have more and it complicates things anyway - ConstructorInfo[] constructors = scriptType.GetConstructors(); - if (constructors.Length != 1) - throw new ArtemisCoreException("Scripts must have exactly one constructor"); - - // Find the ScriptConfiguration parameter, it is required by the base constructor so its there for sure - ParameterInfo? configurationParameter = constructors.First().GetParameters().FirstOrDefault(p => value.GetType().IsAssignableFrom(p.ParameterType)); - - if (configurationParameter?.Name == null) - throw new ArtemisCoreException($"Couldn't find a valid constructor argument on {scriptType.Name} with type {value.GetType().Name}"); - return new ConstructorArgument(configurationParameter.Name, value); - } - private void InitializeProfileScripts(Profile profile) { // Initialize the scripts on the profile diff --git a/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs b/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs index 7c8e204d7..1a6375a11 100644 --- a/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs +++ b/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs @@ -1,6 +1,5 @@ using System; using EmbedIO.WebApi; -using Ninject; namespace Artemis.Core.Services; @@ -8,7 +7,7 @@ internal class WebApiControllerRegistration : WebApiControllerRegistration wh { public WebApiControllerRegistration(PluginFeature feature) : base(feature, typeof(T)) { - Factory = () => feature.Plugin.Kernel!.Get(); + Factory = () => feature.Plugin.Resolve(); } public Func Factory { get; set; } diff --git a/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs b/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs index c75f81eed..450ac7435 100644 --- a/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs +++ b/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs @@ -1,6 +1,6 @@ using System; using EmbedIO; -using Ninject; +using DryIoc; namespace Artemis.Core.Services; @@ -27,7 +27,7 @@ internal class WebModuleRegistration if (Create != null) return Create(); if (WebModuleType != null) - return (IWebModule) Feature.Plugin.Kernel!.Get(WebModuleType); + return (IWebModule) Feature.Plugin.Resolve(WebModuleType); throw new ArtemisCoreException("WebModuleRegistration doesn't have a create function nor a web module type :("); } } \ No newline at end of file diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index bbb38c4be..e5590b568 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -50,6 +50,15 @@ public static class Utilities OnRestartRequested(new RestartEventArgs(elevate, delay, extraArgs.ToList())); } + /// + /// Applies a pending update + /// + /// A boolean indicating whether to silently update or not. + public static void ApplyUpdate(bool silent) + { + OnUpdateRequested(new UpdateEventArgs(silent)); + } + /// /// Opens the provided URL in the default web browser /// @@ -96,11 +105,16 @@ public static class Utilities /// Occurs when the core has requested an application shutdown /// public static event EventHandler? ShutdownRequested; - + /// /// Occurs when the core has requested an application restart /// public static event EventHandler? RestartRequested; + + /// + /// Occurs when the core has requested a pending application update to be applied + /// + public static event EventHandler? UpdateRequested; /// /// Opens the provided folder in the user's file explorer @@ -136,6 +150,11 @@ public static class Utilities { ShutdownRequested?.Invoke(null, EventArgs.Empty); } + + private static void OnUpdateRequested(UpdateEventArgs e) + { + UpdateRequested?.Invoke(null, e); + } #region Scaling diff --git a/src/Artemis.Core/VisualScripting/NodeData.cs b/src/Artemis.Core/VisualScripting/NodeData.cs index 1769cb821..5b0c678e2 100644 --- a/src/Artemis.Core/VisualScripting/NodeData.cs +++ b/src/Artemis.Core/VisualScripting/NodeData.cs @@ -1,6 +1,5 @@ using System; using Artemis.Storage.Entities.Profile.Nodes; -using Castle.Core.Internal; namespace Artemis.Core; diff --git a/src/Artemis.Core/VisualScripting/Nodes/Node.cs b/src/Artemis.Core/VisualScripting/Nodes/Node.cs index a6906ab57..5ff7dd254 100644 --- a/src/Artemis.Core/VisualScripting/Nodes/Node.cs +++ b/src/Artemis.Core/VisualScripting/Nodes/Node.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.Events; +using DryIoc; namespace Artemis.Core; @@ -103,6 +104,8 @@ public abstract class Node : BreakableModel, INode /// public override string BrokenDisplayName => Name; + internal IContainer Container { get; set; } = null!; + #endregion #region Construtors diff --git a/src/Artemis.Core/VisualScripting/Nodes/NodeTStorageTViewModel.cs b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorageTViewModel.cs index 1b4109714..98965c154 100644 --- a/src/Artemis.Core/VisualScripting/Nodes/NodeTStorageTViewModel.cs +++ b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorageTViewModel.cs @@ -1,8 +1,5 @@ -using System.Linq; -using System.Reflection; using Artemis.Core; -using Ninject; -using Ninject.Parameters; +using DryIoc; /// /// Represents a kind of node inside a containing storage value of type @@ -22,26 +19,13 @@ public abstract class Node : Node, ICustomViewMo { } - [Inject] - internal IKernel Kernel { get; set; } = null!; - /// /// Called when a view model is required /// /// public virtual TViewModel GetViewModel(NodeScript nodeScript) { - // Limit to one constructor, there's no need to have more and it complicates things anyway - ConstructorInfo[] constructors = typeof(TViewModel).GetConstructors(); - if (constructors.Length != 1) - throw new ArtemisCoreException("Node VMs must have exactly one constructor"); - - // Find the ScriptConfiguration parameter, it is required by the base constructor so its there for sure - ParameterInfo? configurationParameter = constructors.First().GetParameters().FirstOrDefault(p => GetType().IsAssignableFrom(p.ParameterType)); - - if (configurationParameter?.Name == null) - throw new ArtemisCoreException($"Couldn't find a valid constructor argument on {typeof(TViewModel).Name} with type {GetType().Name}"); - return Kernel.Get(new ConstructorArgument(configurationParameter.Name, this), new ConstructorArgument("script", nodeScript)); + return Container.Resolve(args: new object[] {this, nodeScript}); } /// diff --git a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs index 6961b5dfc..688b202ff 100644 --- a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs @@ -9,7 +9,6 @@ public abstract class RenderElementEntity public Guid ParentId { get; set; } public List LayerEffects { get; set; } - public List PropertyEntities { get; set; } public IConditionEntity DisplayCondition { get; set; } public TimelineEntity Timeline { get; set; } diff --git a/src/Artemis.Storage/Entities/Profile/FolderEntity.cs b/src/Artemis.Storage/Entities/Profile/FolderEntity.cs index fe2e5c543..939396b62 100644 --- a/src/Artemis.Storage/Entities/Profile/FolderEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/FolderEntity.cs @@ -9,7 +9,6 @@ public class FolderEntity : RenderElementEntity { public FolderEntity() { - PropertyEntities = new List(); LayerEffects = new List(); } diff --git a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs index 38a6b4750..30fc9ca1b 100644 --- a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs @@ -12,7 +12,6 @@ public class LayerEntity : RenderElementEntity { Leds = new List(); AdaptionHints = new List(); - PropertyEntities = new List(); LayerEffects = new List(); } diff --git a/src/Artemis.Storage/Migrations/M0022TransitionNodes.cs b/src/Artemis.Storage/Migrations/M0022TransitionNodes.cs new file mode 100644 index 000000000..5341371da --- /dev/null +++ b/src/Artemis.Storage/Migrations/M0022TransitionNodes.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using Artemis.Storage.Entities.Profile; +using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; +using Artemis.Storage.Entities.Profile.Nodes; +using Artemis.Storage.Migrations.Interfaces; +using LiteDB; + +namespace Artemis.Storage.Migrations; + +public class M0022TransitionNodes : IStorageMigration +{ + private void MigrateNodeScript(NodeScriptEntity nodeScript) + { + if (nodeScript == null) + return; + + foreach (NodeEntity node in nodeScript.Nodes) + { + if (node.Type == "NumericEasingNode") + node.Type = "NumericTransitionNode"; + else if (node.Type == "ColorGradientEasingNode") + node.Type = "ColorGradientTransitionNode"; + else if (node.Type == "SKColorEasingNode") + node.Type = "SKColorTransitionNode"; + else if (node.Type == "EasingTypeNode") + node.Type = "EasingFunctionNode"; + } + } + + private void MigratePropertyGroup(PropertyGroupEntity propertyGroup) + { + if (propertyGroup == null) + return; + + foreach (PropertyGroupEntity childPropertyGroup in propertyGroup.PropertyGroups) + MigratePropertyGroup(childPropertyGroup); + foreach (PropertyEntity property in propertyGroup.Properties) + MigrateNodeScript(property.DataBinding?.NodeScript); + } + + private void MigrateDisplayCondition(IConditionEntity conditionEntity) + { + if (conditionEntity is EventConditionEntity eventConditionEntity) + MigrateNodeScript(eventConditionEntity.Script); + else if (conditionEntity is StaticConditionEntity staticConditionEntity) + MigrateNodeScript(staticConditionEntity.Script); + } + + public int UserVersion => 22; + + public void Apply(LiteRepository repository) + { + // Migrate profile configuration display conditions + List categories = repository.Query().ToList(); + foreach (ProfileCategoryEntity profileCategoryEntity in categories) + { + foreach (ProfileConfigurationEntity profileConfigurationEntity in profileCategoryEntity.ProfileConfigurations) + MigrateNodeScript(profileConfigurationEntity.ActivationCondition); + repository.Update(profileCategoryEntity); + } + + // Migrate profile display conditions and data bindings + List profiles = repository.Query().ToList(); + foreach (ProfileEntity profileEntity in profiles) + { + foreach (LayerEntity layer in profileEntity.Layers) + { + MigratePropertyGroup(layer.LayerBrush?.PropertyGroup); + MigratePropertyGroup(layer.GeneralPropertyGroup); + MigratePropertyGroup(layer.TransformPropertyGroup); + foreach (LayerEffectEntity layerEffectEntity in layer.LayerEffects) + MigratePropertyGroup(layerEffectEntity?.PropertyGroup); + MigrateDisplayCondition(layer.DisplayCondition); + } + + foreach (FolderEntity folder in profileEntity.Folders) + { + foreach (LayerEffectEntity folderLayerEffect in folder.LayerEffects) + MigratePropertyGroup(folderLayerEffect?.PropertyGroup); + MigrateDisplayCondition(folder.DisplayCondition); + } + + repository.Update(profileEntity); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/StorageManager.cs b/src/Artemis.Storage/StorageManager.cs index 87bc97b76..78fcc3643 100644 --- a/src/Artemis.Storage/StorageManager.cs +++ b/src/Artemis.Storage/StorageManager.cs @@ -30,7 +30,7 @@ public static class StorageManager { FileSystemInfo newest = files.OrderByDescending(fi => fi.CreationTime).First(); FileSystemInfo oldest = files.OrderBy(fi => fi.CreationTime).First(); - if (DateTime.Now - newest.CreationTime < TimeSpan.FromMinutes(10)) + if (DateTime.Now - newest.CreationTime < TimeSpan.FromHours(12)) return; oldest.Delete(); diff --git a/src/Artemis.Storage/StorageMigrationService.cs b/src/Artemis.Storage/StorageMigrationService.cs index 4ae1e6c81..169144e5c 100644 --- a/src/Artemis.Storage/StorageMigrationService.cs +++ b/src/Artemis.Storage/StorageMigrationService.cs @@ -10,10 +10,10 @@ namespace Artemis.Storage; public class StorageMigrationService { private readonly ILogger _logger; - private readonly List _migrations; + private readonly IList _migrations; private readonly LiteRepository _repository; - public StorageMigrationService(ILogger logger, LiteRepository repository, List migrations) + public StorageMigrationService(ILogger logger, LiteRepository repository, IList migrations) { _logger = logger; _repository = repository; diff --git a/src/Artemis.UI.Linux/App.axaml.cs b/src/Artemis.UI.Linux/App.axaml.cs index 0ef14c578..7040f8183 100644 --- a/src/Artemis.UI.Linux/App.axaml.cs +++ b/src/Artemis.UI.Linux/App.axaml.cs @@ -1,11 +1,12 @@ using Artemis.Core.Services; +using Artemis.UI.Linux.DryIoc; using Artemis.UI.Linux.Providers.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Threading; -using Ninject; +using DryIoc; using ReactiveUI; namespace Artemis.UI.Linux; @@ -13,12 +14,12 @@ namespace Artemis.UI.Linux; public class App : Application { private ApplicationStateManager? _applicationStateManager; - private StandardKernel? _kernel; + private IContainer? _container; public override void Initialize() { - _kernel = ArtemisBootstrapper.Bootstrap(this); - Program.CreateLogger(_kernel); + _container = ArtemisBootstrapper.Bootstrap(this, c => c.RegisterProviders()); + Program.CreateLogger(_container); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); @@ -32,15 +33,15 @@ public class App : Application ArtemisBootstrapper.Initialize(); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - _applicationStateManager = new ApplicationStateManager(_kernel!, desktop.Args); + _applicationStateManager = new ApplicationStateManager(_container!, desktop.Args); } private void RegisterProviders() { - IInputService inputService = _kernel.Get(); + IInputService inputService = _container.Resolve(); try { - inputService.AddInputProvider(_kernel.Get()); + inputService.AddInputProvider(_container.Resolve(LinuxInputProvider.Id)); } catch { diff --git a/src/Artemis.UI.Linux/ApplicationStateManager.cs b/src/Artemis.UI.Linux/ApplicationStateManager.cs index 8c0182610..997c19ef3 100644 --- a/src/Artemis.UI.Linux/ApplicationStateManager.cs +++ b/src/Artemis.UI.Linux/ApplicationStateManager.cs @@ -11,7 +11,7 @@ using Artemis.UI.Shared.Services; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; -using Ninject; +using DryIoc; namespace Artemis.UI.Linux; @@ -22,23 +22,23 @@ public class ApplicationStateManager // ReSharper disable once NotAccessedField.Local - Kept in scope to ensure it does not get released private Mutex? _artemisMutex; - public ApplicationStateManager(IKernel kernel, string[] startupArguments) + public ApplicationStateManager(IContainer container, string[] startupArguments) { - _windowService = kernel.Get(); + _windowService = container.Resolve(); StartupArguments = startupArguments; Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; Core.Utilities.RestartRequested += UtilitiesOnRestartRequested; - // On OS shutdown dispose the kernel just so device providers get a chance to clean up + // On OS shutdown dispose the IOC container just so device providers get a chance to clean up if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime) controlledApplicationLifetime.Exit += (_, _) => { RunForcedShutdownIfEnabled(); - // Dispose plugins before disposing the kernel because plugins might access services during dispose - kernel.Get().Dispose(); - kernel.Dispose(); + // Dispose plugins before disposing the IOC container because plugins might access services during dispose + container.Resolve().Dispose(); + container.Dispose(); }; } diff --git a/src/Artemis.UI.Linux/DryIoc/ContainerExtensions.cs b/src/Artemis.UI.Linux/DryIoc/ContainerExtensions.cs new file mode 100644 index 000000000..8647beea5 --- /dev/null +++ b/src/Artemis.UI.Linux/DryIoc/ContainerExtensions.cs @@ -0,0 +1,20 @@ +using Artemis.Core.Services; +using Artemis.UI.Linux.Providers.Input; +using DryIoc; + +namespace Artemis.UI.Linux.DryIoc; + +/// +/// Provides an extension method to register services onto a DryIoc . +/// +public static class UIContainerExtensions +{ + /// + /// Registers providers into the container. + /// + /// The builder building the current container + public static void RegisterProviders(this IContainer container) + { + container.Register(serviceKey: LinuxInputProvider.Id); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Linux/Program.cs b/src/Artemis.UI.Linux/Program.cs index fe8b2b984..e39ba8973 100644 --- a/src/Artemis.UI.Linux/Program.cs +++ b/src/Artemis.UI.Linux/Program.cs @@ -1,7 +1,9 @@ using System; +using Artemis.Core; +using Artemis.Storage; using Avalonia; using Avalonia.ReactiveUI; -using Ninject; +using DryIoc; using Serilog; namespace Artemis.UI.Linux; @@ -18,6 +20,7 @@ internal class Program { try { + StorageManager.CreateBackup(Constants.DataFolder); BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } catch (Exception e) @@ -33,8 +36,8 @@ internal class Program return AppBuilder.Configure().UsePlatformDetect().LogToTrace().UseReactiveUI(); } - public static void CreateLogger(IKernel kernel) + public static void CreateLogger(IContainer container) { - Logger = kernel.Get().ForContext(); + Logger = container.Resolve().ForContext(); } } \ No newline at end of file diff --git a/src/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs b/src/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs index e735a8ed9..62866cb00 100644 --- a/src/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs +++ b/src/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs @@ -26,6 +26,8 @@ public class LinuxInputProvider : InputProvider _readers.Add(reader); } } + + public static Guid Id { get; } = new("72a6fe5c-b11e-4886-bd48-b3ff5d9006c1"); #region Overrides of InputProvider diff --git a/src/Artemis.UI.MacOS/App.axaml.cs b/src/Artemis.UI.MacOS/App.axaml.cs index abbee91f2..f1aed23d3 100644 --- a/src/Artemis.UI.MacOS/App.axaml.cs +++ b/src/Artemis.UI.MacOS/App.axaml.cs @@ -2,19 +2,19 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Threading; -using Ninject; +using DryIoc; using ReactiveUI; namespace Artemis.UI.MacOS; public class App : Application { - private StandardKernel? _kernel; + private IContainer? _container; public override void Initialize() { - _kernel = ArtemisBootstrapper.Bootstrap(this); - Program.CreateLogger(_kernel); + _container = ArtemisBootstrapper.Bootstrap(this); + Program.CreateLogger(_container); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); } diff --git a/src/Artemis.UI.MacOS/Program.cs b/src/Artemis.UI.MacOS/Program.cs index 8434a7831..3343ecafb 100644 --- a/src/Artemis.UI.MacOS/Program.cs +++ b/src/Artemis.UI.MacOS/Program.cs @@ -1,7 +1,9 @@ using System; +using Artemis.Core; +using Artemis.Storage; using Avalonia; using Avalonia.ReactiveUI; -using Ninject; +using DryIoc; using Serilog; namespace Artemis.UI.MacOS; @@ -18,6 +20,7 @@ internal class Program { try { + StorageManager.CreateBackup(Constants.DataFolder); BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } catch (Exception e) @@ -33,8 +36,8 @@ internal class Program return AppBuilder.Configure().UsePlatformDetect().LogToTrace().UseReactiveUI(); } - public static void CreateLogger(IKernel kernel) + public static void CreateLogger(IContainer container) { - Logger = kernel.Get().ForContext(); + Logger = container.Resolve().ForContext(); } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs b/src/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs index 2129430fd..0c50e69e4 100644 --- a/src/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs +++ b/src/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs @@ -8,6 +8,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using FluentAvalonia.Core; using Humanizer; using Material.Icons; @@ -47,7 +48,8 @@ public class HotkeyBox : UserControl Hotkey.Key = (KeyboardKey?) e.Key; Hotkey.Modifiers = (KeyboardModifierKey?) e.KeyModifiers; UpdateDisplayTextBox(); - + HotkeyChanged?.Invoke(this, EventArgs.Empty); + e.Handled = true; } @@ -134,4 +136,13 @@ public class HotkeyBox : UserControl } #endregion + + #region Events + + /// + /// Occurs when the hotkey changes. + /// + public event TypedEventHandler? HotkeyChanged; + + #endregion } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DryIoc/ContainerExtensions.cs b/src/Artemis.UI.Shared/DryIoc/ContainerExtensions.cs new file mode 100644 index 000000000..cb7baef36 --- /dev/null +++ b/src/Artemis.UI.Shared/DryIoc/ContainerExtensions.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using Artemis.UI.Shared.Services; +using DryIoc; + +namespace Artemis.UI.Shared.DryIoc; + +/// +/// Provides an extension method to register services onto a DryIoc . +/// +public static class UIContainerExtensions +{ + /// + /// Registers shared UI services into the container. + /// + /// The builder building the current container + public static void RegisterSharedUI(this IContainer container) + { + Assembly artemisShared = typeof(IArtemisSharedUIService).GetAssembly(); + container.RegisterMany(new[] { artemisShared }, type => type.IsAssignableTo(), Reuse.Singleton); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Ninject/SharedUIModule.cs b/src/Artemis.UI.Shared/Ninject/SharedUIModule.cs deleted file mode 100644 index 5e4b205f1..000000000 --- a/src/Artemis.UI.Shared/Ninject/SharedUIModule.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Artemis.UI.Shared.Services; -using Ninject.Extensions.Conventions; -using Ninject.Modules; - -namespace Artemis.UI.Shared.Ninject; - -/// -/// The main of the Artemis Shared UI toolkit that binds all services -/// -public class SharedUIModule : NinjectModule -{ - /// - public override void Load() - { - if (Kernel == null) - throw new ArgumentNullException("Kernel shouldn't be null here."); - - // Bind all shared UI services as singletons - Kernel.Bind(x => - { - x.FromAssemblyContaining() - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.InSingletonScope()); - }); - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs b/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs deleted file mode 100644 index 0c33b135f..000000000 --- a/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading.Tasks; - -namespace Artemis.UI.Shared.Providers; - -/// -/// Represents a provider for custom cursors. -/// -public interface IUpdateProvider -{ - /// - /// Asynchronously checks whether an update is available. - /// - /// The channel to use when checking updates (i.e. master or development) - /// A task returning if an update is available; otherwise . - Task CheckForUpdate(string channel); - - /// - /// Applies any available updates. - /// - /// The channel to use when checking updates (i.e. master or development) - /// Whether or not to update silently. - Task ApplyUpdate(string channel, bool silent); - - /// - /// Offer to install the update to the user. - /// - /// The channel to use when checking updates (i.e. master or development) - /// A boolean indicating whether the main window is open. - /// A task returning if the user chose to update; otherwise . - Task OfferUpdate(string channel, bool windowOpen); -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs b/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs index 663d37dbb..275345b0e 100644 --- a/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs +++ b/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs @@ -3,9 +3,8 @@ using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using Avalonia.Controls; +using DryIoc; using FluentAvalonia.UI.Controls; -using Ninject; -using Ninject.Parameters; using ReactiveUI; namespace Artemis.UI.Shared.Services.Builders; @@ -16,13 +15,13 @@ namespace Artemis.UI.Shared.Services.Builders; public class ContentDialogBuilder { private readonly ContentDialog _contentDialog; - private readonly IKernel _kernel; + private readonly IContainer _container; private readonly Window _parent; private ContentDialogViewModelBase? _viewModel; - internal ContentDialogBuilder(IKernel kernel, Window parent) + internal ContentDialogBuilder(IContainer container, Window parent) { - _kernel = kernel; + _container = container; _parent = parent; _contentDialog = new ContentDialog { @@ -129,12 +128,11 @@ public class ContentDialogBuilder /// /// The type of the view model to host. /// The resulting view model. - /// Optional parameters to pass to the constructor of the view model, case and order sensitive. + /// Optional parameters to pass to the constructor of the view model. /// The builder that can be used to further build the dialog. - public ContentDialogBuilder WithViewModel(out T viewModel, params (string name, object? value)[] parameters) where T : ContentDialogViewModelBase + public ContentDialogBuilder WithViewModel(out T viewModel, params object[] parameters) where T : ContentDialogViewModelBase { - IParameter[] paramsArray = parameters.Select(kv => new ConstructorArgument(kv.name, kv.value)).Cast().ToArray(); - viewModel = _kernel.Get(paramsArray); + viewModel = _container.Resolve(parameters); viewModel.ContentDialog = _contentDialog; _contentDialog.Content = viewModel; diff --git a/src/Artemis.UI.Shared/Services/DataModelUIService.cs b/src/Artemis.UI.Shared/Services/DataModelUIService.cs index 9b9ef00cb..f7fe353f6 100644 --- a/src/Artemis.UI.Shared/Services/DataModelUIService.cs +++ b/src/Artemis.UI.Shared/Services/DataModelUIService.cs @@ -8,22 +8,21 @@ using Artemis.Core.Services; using Artemis.UI.Shared.DataModelVisualization; using Artemis.UI.Shared.DataModelVisualization.Shared; using Artemis.UI.Shared.DefaultTypes.DataModel.Display; -using Ninject; -using Ninject.Parameters; +using DryIoc; namespace Artemis.UI.Shared.Services; internal class DataModelUIService : IDataModelUIService { private readonly IDataModelService _dataModelService; - private readonly IKernel _kernel; + private readonly IContainer _container; private readonly List _registeredDataModelDisplays; private readonly List _registeredDataModelEditors; - public DataModelUIService(IDataModelService dataModelService, IKernel kernel) + public DataModelUIService(IDataModelService dataModelService, IContainer container) { _dataModelService = dataModelService; - _kernel = kernel; + _container = container; _registeredDataModelEditors = new List(); _registeredDataModelDisplays = new List(); @@ -36,18 +35,8 @@ internal class DataModelUIService : IDataModelUIService // This assumes the type can be converted, that has been checked when the VM was created if (initialValue != null && initialValue.GetType() != registration.SupportedType) initialValue = Convert.ChangeType(initialValue, registration.SupportedType); - - IParameter[] parameters = - { - new ConstructorArgument("targetDescription", description), - new ConstructorArgument("initialValue", initialValue) - }; - - // If this ever happens something is likely wrong with the plugin unload detection - if (registration.Plugin.Kernel == null) - throw new ArtemisSharedUIException("Cannot InstantiateDataModelInputViewModel for a registration by an uninitialized plugin"); - - DataModelInputViewModel viewModel = (DataModelInputViewModel) registration.Plugin.Kernel.Get(registration.ViewModelType, parameters); + + DataModelInputViewModel viewModel = (DataModelInputViewModel) registration.Plugin.Resolve(registration.ViewModelType, description, initialValue); viewModel.CompatibleConversionTypes = registration.CompatibleConversionTypes; return viewModel; } @@ -133,7 +122,7 @@ internal class DataModelUIService : IDataModelUIService return existing; } - _kernel.Bind(viewModelType).ToSelf(); + _container.Register(viewModelType, ifAlreadyRegistered: IfAlreadyRegistered.Replace); // Create the registration DataModelVisualizationRegistration registration = new(this, RegistrationType.Input, plugin, supportedType, viewModelType) @@ -162,7 +151,7 @@ internal class DataModelUIService : IDataModelUIService return existing; } - _kernel.Bind(viewModelType).ToSelf(); + _container.Register(viewModelType); DataModelVisualizationRegistration registration = new(this, RegistrationType.Display, plugin, supportedType, viewModelType); _registeredDataModelDisplays.Add(registration); return registration; @@ -178,7 +167,8 @@ internal class DataModelUIService : IDataModelUIService registration.Unsubscribe(); _registeredDataModelEditors.Remove(registration); - _kernel.Unbind(registration.ViewModelType); + _container.Unregister(registration.ViewModelType); + _container.ClearCache(registration.ViewModelType); } } } @@ -192,7 +182,8 @@ internal class DataModelUIService : IDataModelUIService registration.Unsubscribe(); _registeredDataModelDisplays.Remove(registration); - _kernel.Unbind(registration.ViewModelType); + _container.Unregister(registration.ViewModelType); + _container.ClearCache(registration.ViewModelType); } } } @@ -205,21 +196,11 @@ internal class DataModelUIService : IDataModelUIService DataModelVisualizationRegistration? match = _registeredDataModelDisplays.FirstOrDefault(d => d.SupportedType == propertyType); if (match != null) - { - // If this ever happens something is likely wrong with the plugin unload detection - if (match.Plugin.Kernel == null) - throw new ArtemisSharedUIException("Cannot GetDataModelDisplayViewModel for a registration by an uninitialized plugin"); - - result = (DataModelDisplayViewModel) match.Plugin.Kernel.Get(match.ViewModelType); - } + result = (DataModelDisplayViewModel) match.Plugin.Resolve(match.ViewModelType); else if (!fallBackToDefault) - { result = null; - } else - { - result = _kernel.Get(); - } + result = _container.Resolve(); if (result != null) result.PropertyDescription = description; diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs index b8ae9dc9c..ff4394fde 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs @@ -16,7 +16,7 @@ public interface IWindowService : IArtemisSharedUIService /// /// The type of view model to create /// The created view model - TViewModel ShowWindow(params (string name, object value)[] parameters); + TViewModel ShowWindow(params object[] parameters); /// /// Given a ViewModel, show its corresponding View as a window @@ -37,7 +37,7 @@ public interface IWindowService : IArtemisSharedUIService /// /// The type of view model to create /// The created view model - Task ShowDialogAsync(params (string name, object value)[] parameters); + Task ShowDialogAsync(params object[] parameters); /// /// Given a ViewModel, show its corresponding View as a dialog @@ -60,7 +60,7 @@ public interface IWindowService : IArtemisSharedUIService /// The view model type /// The return type /// A task containing the return value of type - Task ShowDialogAsync(params (string name, object? value)[] parameters) where TViewModel : DialogViewModelBase; + Task ShowDialogAsync(params object[] parameters) where TViewModel : DialogViewModelBase; /// /// Shows a content dialog asking the user to confirm an action diff --git a/src/Artemis.UI.Shared/Services/NodeEditor/Commands/UpdateStorage.cs b/src/Artemis.UI.Shared/Services/NodeEditor/Commands/UpdateStorage.cs index 1c07d14b5..c0b10d093 100644 --- a/src/Artemis.UI.Shared/Services/NodeEditor/Commands/UpdateStorage.cs +++ b/src/Artemis.UI.Shared/Services/NodeEditor/Commands/UpdateStorage.cs @@ -1,5 +1,4 @@ using System; -using Artemis.Core; namespace Artemis.UI.Shared.Services.NodeEditor.Commands; diff --git a/src/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs b/src/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs index ec23d0850..6f91fbdfe 100644 --- a/src/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs +++ b/src/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs @@ -3,19 +3,18 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core; -using Ninject; -using Ninject.Parameters; +using DryIoc; namespace Artemis.UI.Shared.Services.PropertyInput; internal class PropertyInputService : IPropertyInputService { - private readonly IKernel _kernel; + private readonly IContainer _container; private readonly List _registeredPropertyEditors; - public PropertyInputService(IKernel kernel) + public PropertyInputService(IContainer container) { - _kernel = kernel; + _container = container; _registeredPropertyEditors = new List(); RegisteredPropertyEditors = new ReadOnlyCollection(_registeredPropertyEditors); } @@ -55,7 +54,7 @@ internal class PropertyInputService : IPropertyInputService return existing; } - _kernel.Bind(viewModelType).ToSelf(); + _container.Register(viewModelType); PropertyInputRegistration registration = new(this, plugin, supportedType, viewModelType); _registeredPropertyEditors.Add(registration); return registration; @@ -71,7 +70,8 @@ internal class PropertyInputService : IPropertyInputService registration.Unsubscribe(); _registeredPropertyEditors.Remove(registration); - _kernel.Unbind(registration.ViewModelType); + _container.Unregister(registration.ViewModelType); + _container.ClearCache(registration.ViewModelType); } } } @@ -110,10 +110,9 @@ internal class PropertyInputService : IPropertyInputService if (viewModelType == null) return null; - ConstructorArgument parameter = new("layerProperty", layerProperty); // ReSharper disable once InconsistentlySynchronizedField // When you've just spent the last 2 hours trying to figure out a deadlock and reach this line, I'm so, so sorry. I thought this would be fine. - IKernel kernel = registration?.Plugin.Kernel ?? _kernel; - return (PropertyInputViewModel) kernel.Get(viewModelType, parameter); + IContainer container = registration?.Plugin.Container ?? _container; + return (PropertyInputViewModel) container.Resolve(viewModelType, args: new object[] { layerProperty }); } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Window/WindowService.cs b/src/Artemis.UI.Shared/Services/Window/WindowService.cs index 9a5b33206..c30688eef 100644 --- a/src/Artemis.UI.Shared/Services/Window/WindowService.cs +++ b/src/Artemis.UI.Shared/Services/Window/WindowService.cs @@ -6,34 +6,33 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; +using DryIoc; using FluentAvalonia.UI.Controls; -using Ninject; -using Ninject.Parameters; namespace Artemis.UI.Shared.Services; internal class WindowService : IWindowService { - private readonly IKernel _kernel; + private readonly IContainer _container; private bool _exceptionDialogOpen; - public WindowService(IKernel kernel) + public WindowService(IContainer container) { - _kernel = kernel; + _container = container; } - public T ShowWindow(params (string name, object value)[] parameters) + public T ShowWindow(params object[] parameters) { - IParameter[] paramsArray = parameters.Select(kv => new ConstructorArgument(kv.name, kv.value)).Cast().ToArray(); - T viewModel = _kernel.Get(paramsArray)!; + T viewModel = _container.Resolve(parameters); + if (viewModel == null) + throw new ArtemisSharedUIException($"Failed to show window for VM of type {typeof(T).Name}, could not create instance."); + ShowWindow(viewModel); return viewModel; } public Window ShowWindow(object viewModel) { - Window? parent = GetCurrentWindow(); - string name = viewModel.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View"); Type? type = viewModel.GetType().Assembly.GetType(name); @@ -45,18 +44,17 @@ internal class WindowService : IWindowService Window window = (Window) Activator.CreateInstance(type)!; window.DataContext = viewModel; - if (parent != null) - window.Show(parent); - else - window.Show(); + window.Show(); return window; } - public async Task ShowDialogAsync(params (string name, object value)[] parameters) + public async Task ShowDialogAsync(params object[] parameters) { - IParameter[] paramsArray = parameters.Select(kv => new ConstructorArgument(kv.name, kv.value)).Cast().ToArray(); - T viewModel = _kernel.Get(paramsArray)!; + T viewModel = _container.Resolve(parameters); + if (viewModel == null) + throw new ArtemisSharedUIException($"Failed to show window for VM of type {typeof(T).Name}, could not create instance."); + await ShowDialogAsync(viewModel); return viewModel; } @@ -79,10 +77,12 @@ internal class WindowService : IWindowService await window.ShowDialog(parent); } - public async Task ShowDialogAsync(params (string name, object? value)[] parameters) where TViewModel : DialogViewModelBase + public async Task ShowDialogAsync(params object[] parameters) where TViewModel : DialogViewModelBase { - IParameter[] paramsArray = parameters.Select(kv => new ConstructorArgument(kv.name, kv.value)).Cast().ToArray(); - TViewModel viewModel = _kernel.Get(paramsArray)!; + TViewModel viewModel = _container.Resolve(parameters); + if (viewModel == null) + throw new ArtemisSharedUIException($"Failed to show window for VM of type {typeof(TViewModel).Name}, could not create instance."); + return await ShowDialogAsync(viewModel); } @@ -129,7 +129,7 @@ internal class WindowService : IWindowService { try { - await ShowDialogAsync(new ExceptionDialogViewModel(title, exception, _kernel.Get())); + await ShowDialogAsync(new ExceptionDialogViewModel(title, exception, _container.Resolve())); } finally { @@ -143,7 +143,7 @@ internal class WindowService : IWindowService Window? currentWindow = GetCurrentWindow(); if (currentWindow == null) throw new ArtemisSharedUIException("Can't show a content dialog without any windows being shown."); - return new ContentDialogBuilder(_kernel, currentWindow); + return new ContentDialogBuilder(_container, currentWindow); } public OpenFolderDialogBuilder CreateOpenFolderDialog() diff --git a/src/Artemis.UI.Shared/Styles/Condensed.axaml b/src/Artemis.UI.Shared/Styles/Condensed.axaml index 54dd1b525..7b9f44d08 100644 --- a/src/Artemis.UI.Shared/Styles/Condensed.axaml +++ b/src/Artemis.UI.Shared/Styles/Condensed.axaml @@ -39,6 +39,9 @@ + + + @@ -110,4 +113,10 @@ + + \ No newline at end of file diff --git a/src/Artemis.UI.Windows/App.axaml.cs b/src/Artemis.UI.Windows/App.axaml.cs index c41c00aa1..b2a8db3df 100644 --- a/src/Artemis.UI.Windows/App.axaml.cs +++ b/src/Artemis.UI.Windows/App.axaml.cs @@ -7,21 +7,21 @@ using System.Net.Http; using System.Threading; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Windows.Ninject; +using Artemis.UI.Windows.DryIoc; using Artemis.UI.Windows.Providers.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Threading; -using Ninject; +using DryIoc; using ReactiveUI; namespace Artemis.UI.Windows; public class App : Application { - private StandardKernel? _kernel; + private IContainer? _container; private bool _shutDown; public override void Initialize() @@ -33,8 +33,8 @@ public class App : Application Environment.Exit(1); } - _kernel = ArtemisBootstrapper.Bootstrap(this, new WindowsModule()); - Program.CreateLogger(_kernel); + _container = ArtemisBootstrapper.Bootstrap(this, c => c.RegisterProviders()); + Program.CreateLogger(_container); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); } @@ -45,14 +45,14 @@ public class App : Application return; ArtemisBootstrapper.Initialize(); - _applicationStateManager = new ApplicationStateManager(_kernel!, desktop.Args); - RegisterProviders(_kernel!); + _applicationStateManager = new ApplicationStateManager(_container!, desktop.Args); + RegisterProviders(_container!); } - private void RegisterProviders(StandardKernel standardKernel) + private void RegisterProviders(IContainer container) { - IInputService inputService = standardKernel.Get(); - inputService.AddInputProvider(standardKernel.Get()); + IInputService inputService = container.Resolve(); + inputService.AddInputProvider(container.Resolve(serviceKey: WindowsInputProvider.Id)); } private bool FocusExistingInstance() diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index 1630c95ef..2eb3ce47f 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -10,35 +10,36 @@ using Artemis.UI.Windows.Utilities; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; -using Ninject; +using DryIoc; namespace Artemis.UI.Windows; public class ApplicationStateManager { private const int SM_SHUTTINGDOWN = 0x2000; - - public ApplicationStateManager(IKernel kernel, string[] startupArguments) + + public ApplicationStateManager(IContainer container, string[] startupArguments) { StartupArguments = startupArguments; IsElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; Core.Utilities.RestartRequested += UtilitiesOnRestartRequested; + Core.Utilities.UpdateRequested += UtilitiesOnUpdateRequested; - // On Windows shutdown dispose the kernel just so device providers get a chance to clean up + // On Windows shutdown dispose the IOC container just so device providers get a chance to clean up if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime) controlledApplicationLifetime.Exit += (_, _) => { RunForcedShutdownIfEnabled(); - // Dispose plugins before disposing the kernel because plugins might access services during dispose - kernel.Get().Dispose(); - kernel.Dispose(); + // Dispose plugins before disposing the IOC container because plugins might access services during dispose + container.Resolve().Dispose(); + container.Dispose(); }; // Inform the Core about elevation status - kernel.Get().IsElevated = IsElevated; + container.Resolve().IsElevated = IsElevated; } public string[] StartupArguments { get; } @@ -91,6 +92,33 @@ public class ApplicationStateManager Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown()); } + private void UtilitiesOnUpdateRequested(object? sender, UpdateEventArgs e) + { + List argsList = new(StartupArguments); + if (e.Silent) + argsList.Add("--autorun"); + + // Retain startup arguments after update by providing them to the script + string script = $"\"{Path.Combine(Constants.DataFolder, "updating", "pending", "scripts", "update.ps1")}\""; + string source = $"-sourceDirectory \"{Path.Combine(Constants.DataFolder, "updating", "pending")}\""; + string destination = $"-destinationDirectory \"{Constants.ApplicationFolder}\""; + string args = argsList.Any() ? $"-artemisArgs \"{string.Join(',', argsList)}\"" : ""; + + // Run the PowerShell script included in the new version, that way any changes made to the script are used + ProcessStartInfo info = new() + { + Arguments = $"-File {script} {source} {destination} {args}", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + + // Lets try a graceful shutdown, PowerShell will kill if needed + if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime) + Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown()); + } + private void UtilitiesOnShutdownRequested(object? sender, EventArgs e) { // Use PowerShell to kill the process after 8 sec just in case @@ -115,7 +143,7 @@ public class ApplicationStateManager }; Process.Start(info); } - + [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern int GetSystemMetrics(int nIndex); } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj index 74ef13f12..96d6d57fb 100644 --- a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -12,20 +12,10 @@ - - - - - - - - - - - - - - + + + PreserveNewest + application.ico diff --git a/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs b/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs new file mode 100644 index 000000000..4b3bf7fda --- /dev/null +++ b/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs @@ -0,0 +1,26 @@ +using Artemis.Core.Providers; +using Artemis.Core.Services; +using Artemis.UI.Shared.Providers; +using Artemis.UI.Windows.Providers; +using Artemis.UI.Windows.Providers.Input; +using DryIoc; + +namespace Artemis.UI.Windows.DryIoc; + +/// +/// Provides an extension method to register services onto a DryIoc . +/// +public static class UIContainerExtensions +{ + /// + /// Registers providers into the container. + /// + /// The builder building the current container + public static void RegisterProviders(this IContainer container) + { + container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); + container.Register(); + container.Register(serviceKey: WindowsInputProvider.Id); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Ninject/WindowsModule.cs b/src/Artemis.UI.Windows/Ninject/WindowsModule.cs deleted file mode 100644 index e85c5fe83..000000000 --- a/src/Artemis.UI.Windows/Ninject/WindowsModule.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Artemis.Core.Providers; -using Artemis.UI.Shared.Providers; -using Artemis.UI.Windows.Providers; -using Ninject.Modules; - -namespace Artemis.UI.Windows.Ninject; - -public class WindowsModule : NinjectModule -{ - #region Overrides of NinjectModule - - /// - public override void Load() - { - Kernel!.Bind().To().InSingletonScope(); - Kernel!.Bind().To().InSingletonScope(); - Kernel!.Bind().To().InSingletonScope(); - Kernel!.Bind().To(); - } - - #endregion -} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Program.cs b/src/Artemis.UI.Windows/Program.cs index 98c99b14d..37a113fc2 100644 --- a/src/Artemis.UI.Windows/Program.cs +++ b/src/Artemis.UI.Windows/Program.cs @@ -1,7 +1,9 @@ using System; +using Artemis.Core; +using Artemis.Storage; using Avalonia; using Avalonia.ReactiveUI; -using Ninject; +using DryIoc; using Serilog; namespace Artemis.UI.Windows; @@ -18,6 +20,7 @@ internal class Program { try { + StorageManager.CreateBackup(Constants.DataFolder); BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } catch (Exception e) @@ -41,8 +44,8 @@ internal class Program .UseReactiveUI(); } - public static void CreateLogger(IKernel kernel) + public static void CreateLogger(IContainer container) { - Logger = kernel.Get().ForContext(); + Logger = container.Resolve().ForContext(); } } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs b/src/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs index 1091fca54..ef8f168ec 100644 --- a/src/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs +++ b/src/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs @@ -37,6 +37,8 @@ public class WindowsInputProvider : InputProvider RawInputDevice.RegisterDevice(HidUsageAndPage.Mouse, RawInputDeviceFlags.InputSink, _sponge.Handle.Handle); } + public static Guid Id { get; } = new("6737b204-ffb1-4cd9-8776-9fb851db303a"); + #region Overrides of InputProvider diff --git a/src/Artemis.UI.Windows/Providers/UpdateProvider.cs b/src/Artemis.UI.Windows/Providers/UpdateProvider.cs deleted file mode 100644 index 42df93619..000000000 --- a/src/Artemis.UI.Windows/Providers/UpdateProvider.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Artemis.Core; -using Artemis.UI.Exceptions; -using Artemis.UI.Shared.Providers; -using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.MainWindow; -using Artemis.UI.Windows.Models; -using Artemis.UI.Windows.Screens.Update; -using Avalonia.Threading; -using Flurl; -using Flurl.Http; -using Microsoft.Toolkit.Uwp.Notifications; -using Serilog; -using File = System.IO.File; - -namespace Artemis.UI.Windows.Providers; - -public class UpdateProvider : IUpdateProvider, IDisposable -{ - private const string API_URL = "https://dev.azure.com/artemis-rgb/Artemis/_apis/"; - private const string INSTALLER_URL = "https://builds.artemis-rgb.com/binaries/Artemis.Installer.exe"; - - private readonly ILogger _logger; - private readonly IMainWindowService _mainWindowService; - private readonly IWindowService _windowService; - - public UpdateProvider(ILogger logger, IWindowService windowService, IMainWindowService mainWindowService) - { - _logger = logger; - _windowService = windowService; - _mainWindowService = mainWindowService; - - ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated; - } - - public async Task GetBuildInfo(int buildDefinition, string? buildNumber = null) - { - Url request = API_URL.AppendPathSegments("build", "builds") - .SetQueryParam("definitions", buildDefinition) - .SetQueryParam("resultFilter", "succeeded") - .SetQueryParam("branchName", "refs/heads/master") - .SetQueryParam("$top", 1) - .SetQueryParam("api-version", "6.1-preview.6"); - - if (buildNumber != null) - request = request.SetQueryParam("buildNumber", buildNumber); - - try - { - DevOpsBuilds result = await request.GetJsonAsync(); - try - { - return result.Builds.FirstOrDefault(); - } - catch (Exception e) - { - _logger.Warning(e, "GetBuildInfo: Failed to retrieve build info JSON"); - throw; - } - } - catch (FlurlHttpException e) - { - _logger.Warning("GetBuildInfo: Getting build info, request returned {StatusCode}", e.StatusCode); - throw; - } - } - - public async Task GetBuildDifferences(DevOpsBuild a, DevOpsBuild b) - { - return await "https://api.github.com" - .AppendPathSegments("repos", "Artemis-RGB", "Artemis", "compare") - .AppendPathSegment(a.SourceVersion + "..." + b.SourceVersion) - .WithHeader("User-Agent", "Artemis 2") - .WithHeader("Accept", "application/vnd.github.v3+json") - .GetJsonAsync(); - } - - private async void ToastNotificationManagerCompatOnOnActivated(ToastNotificationActivatedEventArgsCompat e) - { - ToastArguments args = ToastArguments.Parse(e.Argument); - string channel = args.Get("channel"); - string action = "view-changes"; - if (args.Contains("action")) - action = args.Get("action"); - - if (action == "install") - await RunInstaller(channel, true); - else if (action == "view-changes") - await Dispatcher.UIThread.InvokeAsync(async () => - { - _mainWindowService.OpenMainWindow(); - await OfferUpdate(channel, true); - }); - } - - private async Task RunInstaller(string channel, bool silent) - { - _logger.Information("ApplyUpdate: Applying update"); - - // Ensure the installer is up-to-date, get installer build info - DevOpsBuild? buildInfo = await GetBuildInfo(6); - string installerPath = Path.Combine(Constants.DataFolder, "installer", "Artemis.Installer.exe"); - - // Always update installer if it is missing ^^ - if (!File.Exists(installerPath)) - { - await UpdateInstaller(); - } - // Compare the creation date of the installer with the build date and update if needed - else - { - if (buildInfo != null && File.GetLastWriteTime(installerPath) < buildInfo.FinishTime) - await UpdateInstaller(); - } - - _logger.Information("ApplyUpdate: Running installer at {InstallerPath}", installerPath); - - try - { - Process.Start(new ProcessStartInfo(installerPath, "-autoupdate") - { - UseShellExecute = true, - Verb = "runas" - }); - } - catch (Win32Exception e) - { - if (e.NativeErrorCode == 0x4c7) - _logger.Warning("ApplyUpdate: Operation was cancelled, user likely clicked No in UAC dialog"); - else - throw; - } - } - - private async Task UpdateInstaller() - { - string installerDirectory = Path.Combine(Constants.DataFolder, "installer"); - string installerPath = Path.Combine(installerDirectory, "Artemis.Installer.exe"); - - _logger.Information("UpdateInstaller: Downloading installer from {DownloadUrl}", INSTALLER_URL); - using HttpClient client = new(); - HttpResponseMessage httpResponseMessage = await client.GetAsync(INSTALLER_URL); - if (!httpResponseMessage.IsSuccessStatusCode) - throw new ArtemisUIException($"Failed to download installer, status code {httpResponseMessage.StatusCode}"); - - _logger.Information("UpdateInstaller: Writing installer file to {InstallerPath}", installerPath); - if (File.Exists(installerPath)) - File.Delete(installerPath); - - Core.Utilities.CreateAccessibleDirectory(installerDirectory); - await using FileStream fs = new(installerPath, FileMode.Create, FileAccess.Write, FileShare.None); - await httpResponseMessage.Content.CopyToAsync(fs); - } - - private void ShowDesktopNotification(string channel) - { - new ToastContentBuilder() - .AddArgument("channel", channel) - .AddText("An update is available") - .AddButton(new ToastButton().SetContent("Install").AddArgument("action", "install").SetBackgroundActivation()) - .AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-changes")) - .Show(); - } - - /// - public void Dispose() - { - ToastNotificationManagerCompat.OnActivated -= ToastNotificationManagerCompatOnOnActivated; - ToastNotificationManagerCompat.Uninstall(); - } - - /// - public async Task CheckForUpdate(string channel) - { - DevOpsBuild? buildInfo = await GetBuildInfo(1); - if (buildInfo == null) - return false; - - double buildNumber = double.Parse(buildInfo.BuildNumber, CultureInfo.InvariantCulture); - string buildNumberDisplay = buildNumber.ToString(CultureInfo.InvariantCulture); - _logger.Information("Latest build is {BuildNumber}, we're running {LocalBuildNumber}", buildNumberDisplay, Constants.BuildInfo.BuildNumberDisplay); - - return buildNumber > Constants.BuildInfo.BuildNumber; - } - - /// - public async Task ApplyUpdate(string channel, bool silent) - { - await RunInstaller(channel, silent); - } - - /// - public async Task OfferUpdate(string channel, bool windowOpen) - { - if (windowOpen) - { - bool update = await _windowService.ShowDialogAsync(("channel", channel)); - if (update) - await RunInstaller(channel, false); - } - else - { - ShowDesktopNotification(channel); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml deleted file mode 100644 index 481709577..000000000 --- a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - A new Artemis update is available! 🥳 - - - - Retrieving changes... - - - - - - - - - - - - - - - - - - - Changelog (auto-generated) - - - - - - - - - - - - - - - - - - We couldn't retrieve any changes - View online - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs deleted file mode 100644 index 9c33b35b8..000000000 --- a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Artemis.UI.Shared; -using Avalonia; -using Avalonia.Markup.Xaml; - -namespace Artemis.UI.Windows.Screens.Update; - -public class UpdateDialogView : ReactiveCoreWindow -{ - public UpdateDialogView() - { - InitializeComponent(); -#if DEBUG - this.AttachDevTools(); -#endif - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs deleted file mode 100644 index a19aecc6a..000000000 --- a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive; -using System.Reactive.Disposables; -using System.Threading.Tasks; -using Artemis.Core; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Providers; -using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.Builders; -using Artemis.UI.Windows.Models; -using Artemis.UI.Windows.Providers; -using Avalonia.Threading; -using DynamicData; -using ReactiveUI; - -namespace Artemis.UI.Windows.Screens.Update; - -public class UpdateDialogViewModel : DialogViewModelBase -{ - // Based on https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&tabs=yaml#skipping-ci-for-individual-commits - private readonly string[] _excludedCommitMessages = - { - "[skip ci]", - "[ci skip]", - "skip-checks: true", - "skip-checks:true", - "[skip azurepipelines]", - "[azurepipelines skip]", - "[skip azpipelines]", - "[azpipelines skip]", - "[skip azp]", - "[azp skip]", - "***NO_CI***" - }; - - private readonly INotificationService _notificationService; - private readonly UpdateProvider _updateProvider; - private bool _hasChanges; - private string? _latestBuild; - - private bool _retrievingChanges; - - public UpdateDialogViewModel(string channel, IUpdateProvider updateProvider, INotificationService notificationService) - { - _updateProvider = (UpdateProvider) updateProvider; - _notificationService = notificationService; - - Channel = channel; - CurrentBuild = Constants.BuildInfo.BuildNumberDisplay; - - this.WhenActivated((CompositeDisposable _) => Dispatcher.UIThread.InvokeAsync(GetBuildChanges)); - Install = ReactiveCommand.Create(() => Close(true)); - AskLater = ReactiveCommand.Create(() => Close(false)); - } - - public ReactiveCommand Install { get; } - public ReactiveCommand AskLater { get; } - - public string Channel { get; } - public string CurrentBuild { get; } - - public ObservableCollection Changes { get; } = new(); - - public bool RetrievingChanges - { - get => _retrievingChanges; - set => RaiseAndSetIfChanged(ref _retrievingChanges, value); - } - - public bool HasChanges - { - get => _hasChanges; - set => RaiseAndSetIfChanged(ref _hasChanges, value); - } - - public string? LatestBuild - { - get => _latestBuild; - set => RaiseAndSetIfChanged(ref _latestBuild, value); - } - - private async Task GetBuildChanges() - { - try - { - RetrievingChanges = true; - Task currentTask = _updateProvider.GetBuildInfo(1, CurrentBuild); - Task latestTask = _updateProvider.GetBuildInfo(1); - - DevOpsBuild? current = await currentTask; - DevOpsBuild? latest = await latestTask; - - LatestBuild = latest?.BuildNumber; - if (current != null && latest != null) - { - GitHubDifference difference = await _updateProvider.GetBuildDifferences(current, latest); - - // Only take commits with one parents (no merges) - Changes.Clear(); - Changes.AddRange(difference.Commits.Where(c => c.Parents.Count == 1) - .SelectMany(c => c.Commit.Message.Split("\n")) - .Select(m => m.Trim()) - .Where(m => !string.IsNullOrWhiteSpace(m) && !_excludedCommitMessages.Contains(m)) - .OrderBy(m => m) - ); - HasChanges = Changes.Any(); - } - } - catch (Exception e) - { - _notificationService.CreateNotification().WithTitle("Failed to retrieve build changes").WithMessage(e.Message).WithSeverity(NotificationSeverity.Error).Show(); - } - finally - { - RetrievingChanges = false; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Scripts/update.ps1 b/src/Artemis.UI.Windows/Scripts/update.ps1 new file mode 100644 index 000000000..4247178dc --- /dev/null +++ b/src/Artemis.UI.Windows/Scripts/update.ps1 @@ -0,0 +1,42 @@ +param ( + [Parameter(Mandatory=$true)][string]$sourceDirectory, + [Parameter(Mandatory=$true)][string]$destinationDirectory, + [Parameter(Mandatory=$false)][string]$artemisArgs +) + +# Wait up to 10 seconds for the process to shut down +for ($i=1; $i -le 10; $i++) { + $process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue + if (!$process) { + break + } + Write-Host "Waiting for Artemis to shut down ($i / 10)" + Start-Sleep -Seconds 1 +} + +# If the process is still running, kill it +$process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue +if ($process) { + Stop-Process -Id $process.Id -Force + Start-Sleep -Seconds 1 +} + +# Check if the destination directory exists +if (!(Test-Path $destinationDirectory)) { + Write-Error "The destination directory does not exist" +} + +# If the destination directory exists, clear it +Get-ChildItem $destinationDirectory | Remove-Item -Recurse -Force + +# Move the contents of the source directory to the destination directory +Get-ChildItem $sourceDirectory | Move-Item -Destination $destinationDirectory + +Start-Sleep -Seconds 1 + +# When finished, run the updated version +if ($artemisArgs) { + Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory -ArgumentList $artemisArgs +} else { + Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory +} \ No newline at end of file diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 04e578c41..086380658 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -11,10 +11,12 @@ + + @@ -22,17 +24,20 @@ + + + - + diff --git a/src/Artemis.UI/ArtemisBootstrapper.cs b/src/Artemis.UI/ArtemisBootstrapper.cs index 901388bf8..c1dd988a9 100644 --- a/src/Artemis.UI/ArtemisBootstrapper.cs +++ b/src/Artemis.UI/ArtemisBootstrapper.cs @@ -3,53 +3,56 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reactive; using Artemis.Core; -using Artemis.Core.Ninject; +using Artemis.Core.DryIoc; +using Artemis.UI.DryIoc; using Artemis.UI.Exceptions; -using Artemis.UI.Ninject; using Artemis.UI.Screens.Root; using Artemis.UI.Shared.DataModelPicker; -using Artemis.UI.Shared.Ninject; +using Artemis.UI.Shared.DryIoc; using Artemis.UI.Shared.Services; -using Artemis.VisualScripting.Ninject; +using Artemis.VisualScripting.DryIoc; +using Artemis.WebClient.Updating.DryIoc; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -using Ninject; -using Ninject.Modules; +using DryIoc; using ReactiveUI; -using Splat.Ninject; +using Splat.DryIoc; namespace Artemis.UI; public static class ArtemisBootstrapper { - private static StandardKernel? _kernel; + private static Container? _container; private static Application? _application; - public static StandardKernel Bootstrap(Application application, params INinjectModule[] modules) + public static IContainer Bootstrap(Application application, Action? configureServices = null) { - if (_application != null || _kernel != null) + if (_application != null || _container != null) throw new ArtemisUIException("UI already bootstrapped"); Utilities.PrepareFirstLaunch(); _application = application; - _kernel = new StandardKernel(); - _kernel.Settings.InjectNonPublic = true; + _container = new Container(rules => rules + .WithMicrosoftDependencyInjectionRules() + .WithConcreteTypeDynamicRegistrations() + .WithoutThrowOnRegisteringDisposableTransient()); - _kernel.Load(); - _kernel.Load(); - _kernel.Load(); - _kernel.Load(); - _kernel.Load(modules); - _kernel.UseNinjectDependencyResolver(); + _container.RegisterCore(); + _container.RegisterUI(); + _container.RegisterSharedUI(); + _container.RegisterUpdatingClient(); + _container.RegisterNoStringEvaluating(); + configureServices?.Invoke(_container); - return _kernel; + _container.UseDryIocDependencyResolver(); + return _container; } public static void Initialize() { - if (_application == null || _kernel == null) + if (_application == null || _container == null) throw new ArtemisUIException("UI not yet bootstrapped"); if (_application.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return; @@ -59,16 +62,16 @@ public static class ArtemisBootstrapper // Don't shut down when the last window closes, we might still be active in the tray desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown; // Create the root view model that drives the UI - RootViewModel rootViewModel = _kernel.Get(); + RootViewModel rootViewModel = _container.Resolve(); // Apply the root view model to the data context of the application so that tray icon commands work _application.DataContext = rootViewModel; RxApp.DefaultExceptionHandler = Observer.Create(DisplayUnhandledException); - DataModelPicker.DataModelUIService = _kernel.Get(); + DataModelPicker.DataModelUIService = _container.Resolve(); } private static void DisplayUnhandledException(Exception exception) { - _kernel?.Get().ShowExceptionDialog("Exception", exception); + _container?.Resolve().ShowExceptionDialog("Exception", exception); } } \ No newline at end of file diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml index 8407e5714..183116c26 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml @@ -27,7 +27,8 @@ Width="200" VerticalAlignment="Center" Items="{CompiledBinding Descriptors}" - SelectedItem="{CompiledBinding SelectedDescriptor}"> + SelectedItem="{CompiledBinding SelectedDescriptor}" + PlaceholderText="Please select a brush"> diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs index e0d3138ce..8f9ae6277 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs @@ -66,7 +66,7 @@ public class BrushPropertyInputViewModel : PropertyInputViewModel _windowService.CreateContentDialog() .WithTitle("Select preset") - .WithViewModel(out LayerBrushPresetViewModel _, ("layerBrush", layer.LayerBrush)) + .WithViewModel(out LayerBrushPresetViewModel _, layer.LayerBrush) .WithDefaultButton(ContentDialogButton.Close) .WithCloseButtonText("Use defaults") .ShowAsync()); diff --git a/src/Artemis.UI/DryIoc/ContainerExtensions.cs b/src/Artemis.UI/DryIoc/ContainerExtensions.cs new file mode 100644 index 000000000..58970b2fd --- /dev/null +++ b/src/Artemis.UI/DryIoc/ContainerExtensions.cs @@ -0,0 +1,44 @@ +using System.Reflection; +using Artemis.UI.DryIoc.Factories; +using Artemis.UI.DryIoc.InstanceProviders; +using Artemis.UI.Screens; +using Artemis.UI.Screens.VisualScripting; +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.ProfileEditor; +using Avalonia.Platform; +using Avalonia.Shared.PlatformSupport; +using DryIoc; + +namespace Artemis.UI.DryIoc; + +/// +/// Provides an extension method to register services onto a DryIoc . +/// +public static class UIContainerExtensions +{ + /// + /// Registers UI services into the container. + /// + /// The builder building the current container + public static void RegisterUI(this IContainer container) + { + Assembly[] thisAssembly = {typeof(UIContainerExtensions).Assembly}; + + container.RegisterInstance(new AssetLoader(), IfAlreadyRegistered.Throw); + container.Register(Reuse.Singleton); + + container.RegisterMany(thisAssembly, type => type.IsAssignableTo()); + container.RegisterMany(thisAssembly, type => type.IsAssignableTo(), ifAlreadyRegistered: IfAlreadyRegistered.Replace); + container.RegisterMany(thisAssembly, type => type.IsAssignableTo() && type.IsInterface); + container.RegisterMany(thisAssembly, type => type.IsAssignableTo() && type != typeof(PropertyVmFactory)); + + container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); + container.Register(); + + container.RegisterMany(thisAssembly, type => type.IsAssignableTo(), Reuse.Singleton); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs b/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs new file mode 100644 index 000000000..ed71b2c6e --- /dev/null +++ b/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs @@ -0,0 +1,477 @@ +using System.Collections.ObjectModel; +using System.Reactive; +using Artemis.Core; +using Artemis.Core.LayerBrushes; +using Artemis.Core.LayerEffects; +using Artemis.Core.ScriptingProviders; +using Artemis.UI.Screens.Device; +using Artemis.UI.Screens.Plugins; +using Artemis.UI.Screens.ProfileEditor; +using Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes; +using Artemis.UI.Screens.ProfileEditor.ProfileTree; +using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints; +using Artemis.UI.Screens.ProfileEditor.Properties; +using Artemis.UI.Screens.ProfileEditor.Properties.DataBinding; +using Artemis.UI.Screens.ProfileEditor.Properties.Timeline; +using Artemis.UI.Screens.ProfileEditor.Properties.Tree; +using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; +using Artemis.UI.Screens.Scripting; +using Artemis.UI.Screens.Settings; +using Artemis.UI.Screens.Sidebar; +using Artemis.UI.Screens.SurfaceEditor; +using Artemis.UI.Screens.VisualScripting; +using Artemis.UI.Screens.VisualScripting.Pins; +using DryIoc; +using ReactiveUI; + +namespace Artemis.UI.DryIoc.Factories; + +public interface IVmFactory +{ +} + +public interface IDeviceVmFactory : IVmFactory +{ + DevicePropertiesViewModel DevicePropertiesViewModel(ArtemisDevice device); + DeviceSettingsViewModel DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel); + DeviceDetectInputViewModel DeviceDetectInputViewModel(ArtemisDevice device); + DevicePropertiesTabViewModel DevicePropertiesTabViewModel(ArtemisDevice device); + DeviceInfoTabViewModel DeviceInfoTabViewModel(ArtemisDevice device); + DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device, ObservableCollection selectedLeds); + InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection selectedLeds); +} +public class DeviceFactory : IDeviceVmFactory +{ + private readonly IContainer _container; + + public DeviceFactory(IContainer container) + { + _container = container; + } + + public DevicePropertiesViewModel DevicePropertiesViewModel(ArtemisDevice device) + { + return _container.Resolve(new object[] { device }); + } + + public DeviceSettingsViewModel DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel) + { + return _container.Resolve(new object[] { device, devicesTabViewModel }); + } + + public DeviceDetectInputViewModel DeviceDetectInputViewModel(ArtemisDevice device) + { + return _container.Resolve(new object[] { device }); + } + + public DevicePropertiesTabViewModel DevicePropertiesTabViewModel(ArtemisDevice device) + { + return _container.Resolve(new object[] { device }); + } + + public DeviceInfoTabViewModel DeviceInfoTabViewModel(ArtemisDevice device) + { + return _container.Resolve(new object[] { device }); + } + + public DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device, ObservableCollection selectedLeds) + { + return _container.Resolve(new object[] { device, selectedLeds }); + } + + public InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection selectedLeds) + { + return _container.Resolve(new object[] { device, selectedLeds }); + } +} + +public interface ISettingsVmFactory : IVmFactory +{ + PluginSettingsViewModel PluginSettingsViewModel(Plugin plugin); + PluginViewModel PluginViewModel(Plugin plugin, ReactiveCommand? reload); + PluginFeatureViewModel PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield); +} +public class SettingsVmFactory : ISettingsVmFactory +{ + private readonly IContainer _container; + + public SettingsVmFactory(IContainer container) + { + _container = container; + } + + public PluginSettingsViewModel PluginSettingsViewModel(Plugin plugin) + { + return _container.Resolve(new object[] { plugin }); + } + + public PluginViewModel PluginViewModel(Plugin plugin, ReactiveCommand? reload) + { + return _container.Resolve(new object?[] { plugin, reload }); + } + + public PluginFeatureViewModel PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield) + { + return _container.Resolve(new object[] { pluginFeatureInfo, showShield }); + } +} + +public interface ISidebarVmFactory : IVmFactory +{ + SidebarViewModel? SidebarViewModel(IScreen hostScreen); + SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); + SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); +} +public class SidebarVmFactory : ISidebarVmFactory +{ + private readonly IContainer _container; + + public SidebarVmFactory(IContainer container) + { + _container = container; + } + + public SidebarViewModel? SidebarViewModel(IScreen hostScreen) + { + return _container.Resolve(new object[] { hostScreen }); + } + + public SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory) + { + return _container.Resolve(new object[] { profileCategory }); + } + + public SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration) + { + return _container.Resolve(new object[] { profileConfiguration }); + } +} + +public interface ISurfaceVmFactory : IVmFactory +{ + SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel); + ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel); +} +public class SurfaceVmFactory : ISurfaceVmFactory +{ + private readonly IContainer _container; + + public SurfaceVmFactory(IContainer container) + { + _container = container; + } + + public SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel) + { + return _container.Resolve(new object[] { device, surfaceEditorViewModel }); + } + + public ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel) + { + return _container.Resolve(new object[] { device, surfaceEditorViewModel }); + } +} + +public interface IPrerequisitesVmFactory : IVmFactory +{ + PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall); +} +public class PrerequisitesVmFactory : IPrerequisitesVmFactory +{ + private readonly IContainer _container; + + public PrerequisitesVmFactory(IContainer container) + { + _container = container; + } + + public PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall) + { + return _container.Resolve(new object[] { pluginPrerequisite, uninstall }); + } +} + +public interface IProfileEditorVmFactory : IVmFactory +{ + ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen); + FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder); + LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer); + LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer); + LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer); +} +public class ProfileEditorVmFactory : IProfileEditorVmFactory +{ + private readonly IContainer _container; + + public ProfileEditorVmFactory(IContainer container) + { + _container = container; + } + + public FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder) + { + return _container.Resolve(new object?[] { parent, folder }); + } + + public LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer) + { + return _container.Resolve(new object[] { layer }); + } + + public LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer) + { + return _container.Resolve(new object?[] { parent, layer }); + } + + public LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer) + { + return _container.Resolve(new object[] { layer }); + } + + public ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen) + { + return _container.Resolve(new object[] { hostScreen }); + } +} + +public interface ILayerPropertyVmFactory : IVmFactory +{ + PropertyViewModel PropertyViewModel(ILayerProperty layerProperty); + PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); + PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush layerBrush); + PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect); + + TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel); + + TimelineViewModel TimelineViewModel(ObservableCollection propertyGroupViewModels); + TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel); +} +public class LayerPropertyVmFactory : ILayerPropertyVmFactory +{ + private readonly IContainer _container; + + public LayerPropertyVmFactory(IContainer container) + { + _container = container; + } + + public PropertyViewModel PropertyViewModel(ILayerProperty layerProperty) + { + return _container.Resolve(new object[] { layerProperty }); + } + + public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup) + { + return _container.Resolve(new object[] { layerPropertyGroup }); + } + + public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush layerBrush) + { + return _container.Resolve(new object[] { layerPropertyGroup, layerBrush }); + } + + public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect) + { + return _container.Resolve(new object[] { layerPropertyGroup, layerEffect }); + } + + public TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel) + { + return _container.Resolve(new object[] { propertyGroupViewModel }); + } + + public TimelineViewModel TimelineViewModel(ObservableCollection propertyGroupViewModels) + { + return _container.Resolve(new object[] { propertyGroupViewModels }); + } + + public TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel) + { + return _container.Resolve(new object[] { propertyGroupViewModel }); + } +} + +public interface IDataBindingVmFactory : IVmFactory +{ + DataBindingViewModel DataBindingViewModel(); +} +public class DataBindingVmFactory : IDataBindingVmFactory +{ + private readonly IContainer _container; + + public DataBindingVmFactory(IContainer container) + { + _container = container; + } + + public DataBindingViewModel DataBindingViewModel() + { + return _container.Resolve(); + } +} + +public interface IPropertyVmFactory +{ + ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, PropertyViewModel propertyViewModel); + ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, PropertyViewModel propertyViewModel); +} + +public interface INodeVmFactory : IVmFactory +{ + NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript, bool isPreview); + NodePickerViewModel NodePickerViewModel(NodeScript nodeScript); + NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node); + CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to); + DragCableViewModel DragCableViewModel(PinViewModel pinViewModel); + InputPinViewModel InputPinViewModel(IPin inputPin, NodeScriptViewModel nodeScriptViewModel); + OutputPinViewModel OutputPinViewModel(IPin outputPin, NodeScriptViewModel nodeScriptViewModel); + InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel); + OutputPinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel); +} +public class NodeVmFactory : INodeVmFactory +{ + private readonly IContainer _container; + + public NodeVmFactory(IContainer container) + { + _container = container; + } + + public NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript, bool isPreview) + { + return _container.Resolve(new object[] { nodeScript, isPreview }); + } + + public NodePickerViewModel NodePickerViewModel(NodeScript nodeScript) + { + return _container.Resolve(new object[] { nodeScript }); + } + + public NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node) + { + return _container.Resolve(new object[] { nodeScriptViewModel, node }); + } + + public CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to) + { + return _container.Resolve(new object[] { nodeScriptViewModel, from, to }); + } + + public DragCableViewModel DragCableViewModel(PinViewModel pinViewModel) + { + return _container.Resolve(new object[] { pinViewModel }); + } + + public InputPinViewModel InputPinViewModel(IPin inputPin, NodeScriptViewModel nodeScriptViewModel) + { + return _container.Resolve(new object[] { inputPin, nodeScriptViewModel }); + } + + public OutputPinViewModel OutputPinViewModel(IPin outputPin, NodeScriptViewModel nodeScriptViewModel) + { + return _container.Resolve(new object[] { outputPin, nodeScriptViewModel }); + } + + public InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel) + { + return _container.Resolve(new object[] { inputPinCollection, nodeScriptViewModel }); + } + + public OutputPinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel) + { + return _container.Resolve(new object[] { outputPinCollection, nodeScriptViewModel }); + } +} + +public interface IConditionVmFactory : IVmFactory +{ + AlwaysOnConditionViewModel AlwaysOnConditionViewModel(AlwaysOnCondition alwaysOnCondition); + PlayOnceConditionViewModel PlayOnceConditionViewModel(PlayOnceCondition playOnceCondition); + StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition); + EventConditionViewModel EventConditionViewModel(EventCondition eventCondition); +} +public class ConditionVmFactory : IConditionVmFactory +{ + private readonly IContainer _container; + + public ConditionVmFactory(IContainer container) + { + _container = container; + } + + public AlwaysOnConditionViewModel AlwaysOnConditionViewModel(AlwaysOnCondition alwaysOnCondition) + { + return _container.Resolve(new object[] { alwaysOnCondition }); + } + + public PlayOnceConditionViewModel PlayOnceConditionViewModel(PlayOnceCondition playOnceCondition) + { + return _container.Resolve(new object[] { playOnceCondition }); + } + + public StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition) + { + return _container.Resolve(new object[] { staticCondition }); + } + + public EventConditionViewModel EventConditionViewModel(EventCondition eventCondition) + { + return _container.Resolve(new object[] { eventCondition }); + } +} + +public interface ILayerHintVmFactory : IVmFactory +{ + CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(Layer layer, CategoryAdaptionHint adaptionHint); + DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(Layer layer, DeviceAdaptionHint adaptionHint); + KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(Layer layer, KeyboardSectionAdaptionHint adaptionHint); +} +public class LayerHintVmFactory : ILayerHintVmFactory +{ + private readonly IContainer _container; + + public LayerHintVmFactory(IContainer container) + { + _container = container; + } + + public CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(Layer layer, CategoryAdaptionHint adaptionHint) + { + return _container.Resolve(new object[] { layer, adaptionHint }); + } + + public DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(Layer layer, DeviceAdaptionHint adaptionHint) + { + return _container.Resolve(new object[] { layer, adaptionHint }); + } + + public KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(Layer layer, KeyboardSectionAdaptionHint adaptionHint) + { + return _container.Resolve(new object[] { layer, adaptionHint }); + } +} + +public interface IScriptVmFactory : IVmFactory +{ + ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration); + ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration); +} +public class ScriptVmFactory : IScriptVmFactory +{ + private readonly IContainer _container; + + public ScriptVmFactory(IContainer container) + { + _container = container; + } + + public ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration) + { + return _container.Resolve(new object[] { scriptConfiguration }); + } + + public ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration) + { + return _container.Resolve(new object[] { profile, scriptConfiguration }); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/DryIoc/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs b/src/Artemis.UI/DryIoc/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs new file mode 100644 index 000000000..108caeb84 --- /dev/null +++ b/src/Artemis.UI/DryIoc/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs @@ -0,0 +1,47 @@ +using System; +using Artemis.Core; +using Artemis.UI.DryIoc.Factories; +using Artemis.UI.Exceptions; +using Artemis.UI.Screens.ProfileEditor.Properties; +using Artemis.UI.Screens.ProfileEditor.Properties.Timeline; +using Artemis.UI.Screens.ProfileEditor.Properties.Tree; +using DryIoc; + +namespace Artemis.UI.DryIoc.InstanceProviders; + +public class PropertyVmFactory : IPropertyVmFactory +{ + private readonly IContainer _container; + + public PropertyVmFactory(IContainer container) + { + _container = container; + } + + public ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, PropertyViewModel propertyViewModel) + { + // Find LayerProperty type + Type? layerPropertyType = layerProperty.GetType(); + while (layerPropertyType != null && (!layerPropertyType.IsGenericType || layerPropertyType.GetGenericTypeDefinition() != typeof(LayerProperty<>))) + layerPropertyType = layerPropertyType.BaseType; + if (layerPropertyType == null) + throw new ArtemisUIException("Could not find the LayerProperty type"); + + Type? genericType = typeof(TimelinePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments()); + return (ITimelinePropertyViewModel)_container.Resolve(genericType, new object[] { layerProperty, propertyViewModel }); + } + + public ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, PropertyViewModel propertyViewModel) + { + // Find LayerProperty type + Type? layerPropertyType = layerProperty.GetType(); + while (layerPropertyType != null && (!layerPropertyType.IsGenericType || layerPropertyType.GetGenericTypeDefinition() != typeof(LayerProperty<>))) + layerPropertyType = layerPropertyType.BaseType; + if (layerPropertyType == null) + throw new ArtemisUIException("Could not find the LayerProperty type"); + + Type? genericType = typeof(TreePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments()); + + return (ITreePropertyViewModel)_container.Resolve(genericType, new object[] { layerProperty, propertyViewModel }); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs b/src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs new file mode 100644 index 000000000..743035d3e --- /dev/null +++ b/src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs @@ -0,0 +1,14 @@ +using System.Reactive.Disposables; +using System.Threading; + +namespace Artemis.UI.Extensions; + +public static class CompositeDisposableExtensions +{ + public static CancellationToken AsCancellationToken(this CompositeDisposable disposable) + { + CancellationTokenSource tokenSource = new(); + Disposable.Create(tokenSource, s => s.Cancel()).DisposeWith(disposable); + return tokenSource.Token; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Extensions/HttpClientExtensions.cs b/src/Artemis.UI/Extensions/HttpClientExtensions.cs new file mode 100644 index 000000000..50af33443 --- /dev/null +++ b/src/Artemis.UI/Extensions/HttpClientExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.UI.Extensions +{ + public static class HttpClientProgressExtensions + { + public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress? progress, CancellationToken cancellationToken) + { + using HttpResponseMessage response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + response.EnsureSuccessStatusCode(); + + long? contentLength = response.Content.Headers.ContentLength; + await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken); + // no progress... no contentLength... very sad + if (progress is null || !contentLength.HasValue) + { + await download.CopyToAsync(destination, cancellationToken); + return; + } + + // Such progress and contentLength much reporting Wow! + Progress progressWrapper = new(totalBytes => progress.Report(GetProgressPercentage(totalBytes, contentLength.Value))); + await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken); + + float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f; + } + + static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress progress, CancellationToken cancellationToken) + { + if (bufferSize < 0) + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + if (source is null) + throw new ArgumentNullException(nameof(source)); + if (!source.CanRead) + throw new InvalidOperationException($"'{nameof(source)}' is not readable."); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + if (!destination.CanWrite) + throw new InvalidOperationException($"'{nameof(destination)}' is not writable."); + + byte[] buffer = new byte[bufferSize]; + long totalBytesRead = 0; + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + totalBytesRead += bytesRead; + progress?.Report(totalBytesRead); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs b/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs new file mode 100644 index 000000000..0d1fc507d --- /dev/null +++ b/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Threading; + +namespace Artemis.UI.Extensions; + +// Taken from System.IO.Compression with progress reporting slapped on top +public static class ZipArchiveExtensions +{ + /// + /// Extracts all the files in the zip archive to a directory on the file system. + /// + /// The zip archive to extract files from. + /// The path to the directory to place the extracted files in. You can specify either a relative or an absolute path. A relative path is interpreted as relative to the current working directory. + /// A boolean indicating whether to override existing files + /// The progress to report to. + /// A cancellation token + public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, IProgress progress, CancellationToken cancellationToken) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (destinationDirectoryName == null) + throw new ArgumentNullException(nameof(destinationDirectoryName)); + + for (int index = 0; index < source.Entries.Count; index++) + { + ZipArchiveEntry entry = source.Entries[index]; + entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles); + progress.Report((index + 1f) / source.Entries.Count * 100f); + cancellationToken.ThrowIfCancellationRequested(); + } + } + + private static void ExtractRelativeToDirectory(this ZipArchiveEntry source, string destinationDirectoryName, bool overwrite) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (destinationDirectoryName == null) + throw new ArgumentNullException(nameof(destinationDirectoryName)); + + // Note that this will give us a good DirectoryInfo even if destinationDirectoryName exists: + DirectoryInfo di = Directory.CreateDirectory(destinationDirectoryName); + string destinationDirectoryFullPath = di.FullName; + if (!destinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar)) + destinationDirectoryFullPath += Path.DirectorySeparatorChar; + + string fileDestinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, source.FullName)); + + if (!fileDestinationPath.StartsWith(destinationDirectoryFullPath, StringComparison)) + throw new IOException($"The file '{fileDestinationPath}' already exists."); + + if (Path.GetFileName(fileDestinationPath).Length == 0) + { + // If it is a directory: + + if (source.Length != 0) + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + + Directory.CreateDirectory(fileDestinationPath); + } + else + { + // If it is a file: + // Create containing directory: + Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); + source.ExtractToFile(fileDestinationPath, overwrite: overwrite); + } + } + private static StringComparison StringComparison => IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + private static bool IsCaseSensitive => !(OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS()); +} \ No newline at end of file diff --git a/src/Artemis.UI/Models/NodesClipboardModel.cs b/src/Artemis.UI/Models/NodesClipboardModel.cs index 842d5348d..42cd9832f 100644 --- a/src/Artemis.UI/Models/NodesClipboardModel.cs +++ b/src/Artemis.UI/Models/NodesClipboardModel.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using Artemis.Core; using Artemis.Storage.Entities.Profile.Nodes; -using FluentAvalonia.Core; namespace Artemis.UI.Models; diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs deleted file mode 100644 index 06d0b3dfa..000000000 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System.Collections.ObjectModel; -using System.Reactive; -using Artemis.Core; -using Artemis.Core.LayerBrushes; -using Artemis.Core.LayerEffects; -using Artemis.Core.ScriptingProviders; -using Artemis.UI.Screens.Device; -using Artemis.UI.Screens.Plugins; -using Artemis.UI.Screens.ProfileEditor; -using Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes; -using Artemis.UI.Screens.ProfileEditor.ProfileTree; -using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints; -using Artemis.UI.Screens.ProfileEditor.Properties; -using Artemis.UI.Screens.ProfileEditor.Properties.DataBinding; -using Artemis.UI.Screens.ProfileEditor.Properties.Timeline; -using Artemis.UI.Screens.ProfileEditor.Properties.Tree; -using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; -using Artemis.UI.Screens.Scripting; -using Artemis.UI.Screens.Settings; -using Artemis.UI.Screens.Sidebar; -using Artemis.UI.Screens.SurfaceEditor; -using Artemis.UI.Screens.VisualScripting; -using Artemis.UI.Screens.VisualScripting.Pins; -using ReactiveUI; - -namespace Artemis.UI.Ninject.Factories; - -public interface IVmFactory -{ -} - -public interface IDeviceVmFactory : IVmFactory -{ - DevicePropertiesViewModel DevicePropertiesViewModel(ArtemisDevice device); - DeviceSettingsViewModel DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel); - DeviceDetectInputViewModel DeviceDetectInputViewModel(ArtemisDevice device); - DevicePropertiesTabViewModel DevicePropertiesTabViewModel(ArtemisDevice device); - DeviceInfoTabViewModel DeviceInfoTabViewModel(ArtemisDevice device); - DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device, ObservableCollection selectedLeds); - InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection selectedLeds); -} - -public interface ISettingsVmFactory : IVmFactory -{ - PluginSettingsViewModel PluginSettingsViewModel(Plugin plugin); - PluginViewModel PluginViewModel(Plugin plugin, ReactiveCommand? reload); - PluginFeatureViewModel PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield); -} - -public interface ISidebarVmFactory : IVmFactory -{ - SidebarViewModel? SidebarViewModel(IScreen hostScreen); - SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); - SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); -} - -public interface ISurfaceVmFactory : IVmFactory -{ - SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel); - ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel); -} - -public interface IPrerequisitesVmFactory : IVmFactory -{ - PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall); -} - -public interface IProfileEditorVmFactory : IVmFactory -{ - ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen); - FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder); - LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer); - LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer); - LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer); -} - -public interface ILayerPropertyVmFactory : IVmFactory -{ - PropertyViewModel PropertyViewModel(ILayerProperty layerProperty); - PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); - PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush layerBrush); - PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect); - - TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel); - - TimelineViewModel TimelineViewModel(ObservableCollection propertyGroupViewModels); - TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel); -} - -public interface IDataBindingVmFactory : IVmFactory -{ - DataBindingViewModel DataBindingViewModel(); -} - -public interface IPropertyVmFactory -{ - ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, PropertyViewModel propertyViewModel); - ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, PropertyViewModel propertyViewModel); -} - -public interface INodeVmFactory : IVmFactory -{ - NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript, bool isPreview); - NodePickerViewModel NodePickerViewModel(NodeScript nodeScript); - NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node); - CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to); - DragCableViewModel DragCableViewModel(PinViewModel pinViewModel); - InputPinViewModel InputPinViewModel(IPin inputPin, NodeScriptViewModel nodeScriptViewModel); - OutputPinViewModel OutputPinViewModel(IPin outputPin, NodeScriptViewModel nodeScriptViewModel); - InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel); - OutputPinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel); -} - -public interface IConditionVmFactory : IVmFactory -{ - AlwaysOnConditionViewModel AlwaysOnConditionViewModel(AlwaysOnCondition alwaysOnCondition); - PlayOnceConditionViewModel PlayOnceConditionViewModel(PlayOnceCondition playOnceCondition); - StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition); - EventConditionViewModel EventConditionViewModel(EventCondition eventCondition); -} - -public interface ILayerHintVmFactory : IVmFactory -{ - CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(Layer layer, CategoryAdaptionHint adaptionHint); - DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(Layer layer, DeviceAdaptionHint adaptionHint); - KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(Layer layer, KeyboardSectionAdaptionHint adaptionHint); -} - -public interface IScriptVmFactory : IVmFactory -{ - ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration); - ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration); -} \ No newline at end of file diff --git a/src/Artemis.UI/Ninject/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs b/src/Artemis.UI/Ninject/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs deleted file mode 100644 index 9e0a40c4f..000000000 --- a/src/Artemis.UI/Ninject/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Reflection; -using Artemis.Core; -using Artemis.UI.Screens.ProfileEditor.Properties.Timeline; -using Artemis.UI.Screens.ProfileEditor.Properties.Tree; -using Ninject.Extensions.Factory; - -namespace Artemis.UI.Ninject.InstanceProviders; - -public class LayerPropertyViewModelInstanceProvider : StandardInstanceProvider -{ - protected override Type GetType(MethodInfo methodInfo, object[] arguments) - { - if (methodInfo.ReturnType != typeof(ITreePropertyViewModel) && methodInfo.ReturnType != typeof(ITimelinePropertyViewModel)) - return base.GetType(methodInfo, arguments); - - // Find LayerProperty type - Type? layerPropertyType = arguments[0].GetType(); - while (layerPropertyType != null && (!layerPropertyType.IsGenericType || layerPropertyType.GetGenericTypeDefinition() != typeof(LayerProperty<>))) - layerPropertyType = layerPropertyType.BaseType; - if (layerPropertyType == null) - return base.GetType(methodInfo, arguments); - - if (methodInfo.ReturnType == typeof(ITreePropertyViewModel)) - return typeof(TreePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments()); - if (methodInfo.ReturnType == typeof(ITimelinePropertyViewModel)) - return typeof(TimelinePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments()); - - return base.GetType(methodInfo, arguments); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Ninject/UiModule.cs b/src/Artemis.UI/Ninject/UiModule.cs deleted file mode 100644 index 41a355fbb..000000000 --- a/src/Artemis.UI/Ninject/UiModule.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using Artemis.UI.Ninject.Factories; -using Artemis.UI.Ninject.InstanceProviders; -using Artemis.UI.Screens; -using Artemis.UI.Screens.VisualScripting; -using Artemis.UI.Services.Interfaces; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Services.NodeEditor; -using Artemis.UI.Shared.Services.ProfileEditor; -using Avalonia.Platform; -using Avalonia.Shared.PlatformSupport; -using Ninject.Extensions.Conventions; -using Ninject.Extensions.Factory; -using Ninject.Modules; -using Ninject.Planning.Bindings.Resolvers; - -namespace Artemis.UI.Ninject; - -public class UIModule : NinjectModule -{ - public override void Load() - { - if (Kernel == null) - throw new ArgumentNullException("Kernel shouldn't be null here."); - - Kernel.Components.Add(); - Kernel.Bind().ToConstant(new AssetLoader()); - - Kernel.Bind(x => - { - x.FromThisAssembly() - .SelectAllClasses() - .InheritedFrom() - .BindToSelf(); - }); - - Kernel.Bind(x => - { - x.FromThisAssembly() - .SelectAllClasses() - .InheritedFrom() - .BindAllBaseClasses(); - }); - - Kernel.Bind(x => - { - x.FromThisAssembly() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces(); - }); - - // Bind UI factories - Kernel.Bind(x => - { - x.FromThisAssembly() - .SelectAllInterfaces() - .InheritedFrom() - .BindToFactory(); - }); - - Kernel.Bind().To(); - Kernel.Bind().ToFactory(() => new LayerPropertyViewModelInstanceProvider()); - - // Bind all UI services as singletons - Kernel.Bind(x => - { - x.FromThisAssembly() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.InSingletonScope()); - }); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Debugger/DebugView.axaml b/src/Artemis.UI/Screens/Debugger/DebugView.axaml index f98fdc13b..6387ce994 100644 --- a/src/Artemis.UI/Screens/Debugger/DebugView.axaml +++ b/src/Artemis.UI/Screens/Debugger/DebugView.axaml @@ -15,6 +15,7 @@ Height="800"> + diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml b/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml index 3a3207c8a..db82a857f 100644 --- a/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml +++ b/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml @@ -2,15 +2,25 @@ 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:aedit="https://github.com/avaloniaui/avaloniaedit" + xmlns:controls="clr-namespace:Artemis.UI.Controls" + xmlns:logs="clr-namespace:Artemis.UI.Screens.Debugger.Logs" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Debugger.Logs.LogsDebugView"> - - Logs - - On this page you can view Artemis's logs in real-time. Logging can come from Artemis itself, plugins and scripts. - + x:Class="Artemis.UI.Screens.Debugger.Logs.LogsDebugView" + x:DataType="logs:LogsDebugViewModel"> + - TODO as there's no FlowDocumentScrollViewer in Avalonia and I'm too lazy to come up with an alternative. - #feelsbadman - + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs b/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs index 16cf140d7..20bb00ff2 100644 --- a/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs +++ b/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs @@ -1,17 +1,68 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using Avalonia.Threading; +using AvaloniaEdit; namespace Artemis.UI.Screens.Debugger.Logs; public class LogsDebugView : ReactiveUserControl { + private int _lineCount; + private TextEditor _textEditor; + public LogsDebugView() { + _lineCount = 0; InitializeComponent(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); + _textEditor = this.FindControl("log"); + } + + protected override void OnInitialized() + { + base.OnInitialized(); + Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle); + } + + private void OnTextChanged(object? sender, EventArgs e) + { + if (_textEditor is null) + return; + if (_textEditor.ExtentHeight == 0) + return; + + int linesAdded = _textEditor.LineCount - _lineCount; + double lineHeight = _textEditor.ExtentHeight / _textEditor.LineCount; + double outOfScreenTextHeight = _textEditor.ExtentHeight - _textEditor.VerticalOffset - _textEditor.ViewportHeight; + double outOfScreenLines = outOfScreenTextHeight / lineHeight; + + //we need this help distance because of rounding. + //if we scroll slightly above the end, we still want it + //to scroll down to the new lines. + const double graceDistance = 1d; + + //if we were at the bottom of the log and + //if the last log event was 5 lines long + //we will be 5 lines out sync. + //if this is the case, scroll down. + + //if we are more than that out of sync, + //the user scrolled up and we should not + //mess with anything. + if (_lineCount == 0 || linesAdded + graceDistance > outOfScreenLines) + { + Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle); + _lineCount = _textEditor.LineCount; + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugViewModel.cs b/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugViewModel.cs index 93eaf4075..ca067a392 100644 --- a/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugViewModel.cs @@ -1,11 +1,74 @@ -using Artemis.UI.Shared; +using Artemis.Core; +using Artemis.UI.Shared; +using Avalonia.Threading; +using AvaloniaEdit.Document; +using ReactiveUI; +using Serilog.Events; +using Serilog.Formatting.Display; +using System.IO; +using System.Reactive.Disposables; namespace Artemis.UI.Screens.Debugger.Logs; public class LogsDebugViewModel : ActivatableViewModelBase { + private readonly MessageTemplateTextFormatter _formatter; + + public TextDocument Document { get; } + + private const int MAX_ENTRIES = 1000; + public LogsDebugViewModel() { DisplayName = "Logs"; + Document = new TextDocument(); + _formatter = new MessageTemplateTextFormatter( + "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" + ); + + foreach(LogEvent logEvent in LogStore.Events) + AddLogEvent(logEvent); + + this.WhenActivated(disp => + { + LogStore.EventAdded += OnLogEventAdded; + + Disposable.Create(() => + { + LogStore.EventAdded -= OnLogEventAdded; + }).DisposeWith(disp); + }); + } + + private void OnLogEventAdded(object? sender, LogEventEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + AddLogEvent(e.LogEvent); + }); + } + + private void AddLogEvent(LogEvent logEvent) + { + using StringWriter writer = new(); + _formatter.Format(logEvent, writer); + string line = writer.ToString(); + Document.Insert(Document.TextLength, '\n' + line.TrimEnd('\r', '\n')); + while (Document.LineCount > MAX_ENTRIES) + RemoveOldestLine(); + } + + private void RemoveOldestLine() + { + int firstNewLine = Document.Text.IndexOf('\n'); + if (firstNewLine == -1) + { + //this should never happen. + //just in case let's return + //instead of throwing + return; + } + + Document.Remove(0, firstNewLine + 1); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml b/src/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml index bb50ebe66..c5258f3a3 100644 --- a/src/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml +++ b/src/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml @@ -25,20 +25,13 @@ - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/DevicePropertiesViewModel.cs b/src/Artemis.UI/Screens/Device/DevicePropertiesViewModel.cs index d0200cb70..87ee3ae20 100644 --- a/src/Artemis.UI/Screens/Device/DevicePropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/Device/DevicePropertiesViewModel.cs @@ -4,7 +4,7 @@ using System.Reactive; using System.Reactive.Disposables; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Shared; using ReactiveUI; using RGB.NET.Core; diff --git a/src/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs b/src/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs index 01a293411..52faba0d3 100644 --- a/src/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.Settings; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; @@ -82,7 +82,7 @@ public class DeviceSettingsViewModel : ActivatableViewModelBase await _windowService.CreateContentDialog() .WithTitle($"{Device.RgbDevice.DeviceInfo.DeviceName} - Detect input") - .WithViewModel(out DeviceDetectInputViewModel? viewModel, ("device", Device)) + .WithViewModel(out DeviceDetectInputViewModel viewModel, Device) .WithCloseButtonText("Cancel") .ShowAsync(); diff --git a/src/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabLedViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabLedViewModel.cs index 05868d9d5..2b249fb4d 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabLedViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabLedViewModel.cs @@ -24,8 +24,7 @@ public class DeviceLedsTabLedViewModel : ViewModelBase get => _isSelected; set { - if (!RaiseAndSetIfChanged(ref _isSelected, value)) - return; + RaiseAndSetIfChanged(ref _isSelected, value); Apply(); } } diff --git a/src/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabView.axaml b/src/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabView.axaml index 10c134302..b298a7d73 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabView.axaml +++ b/src/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabView.axaml @@ -14,8 +14,14 @@ - - + + + + + + + + diff --git a/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogViewModel.cs index b3f25f1f4..47afd8fdf 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogViewModel.cs @@ -54,7 +54,7 @@ public class DeviceLogicalLayoutDialogViewModel : ContentDialogViewModelBase { await windowService.CreateContentDialog() .WithTitle("Select logical layout") - .WithViewModel(out DeviceLogicalLayoutDialogViewModel vm, ("device", device)) + .WithViewModel(out DeviceLogicalLayoutDialogViewModel vm, device) .WithCloseButtonText("Cancel") .WithDefaultButton(ContentDialogButton.Primary) .HavingPrimaryButton(b => b.WithText("Select").WithCommand(vm.ApplyLogicalLayout)) diff --git a/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogViewModel.cs index ca6361d81..52e13f6bc 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/DevicePhysicalLayoutDialogViewModel.cs @@ -25,7 +25,7 @@ public class DevicePhysicalLayoutDialogViewModel : ContentDialogViewModelBase { await windowService.CreateContentDialog() .WithTitle("Select physical layout") - .WithViewModel(out DevicePhysicalLayoutDialogViewModel vm, ("device", device)) + .WithViewModel(out DevicePhysicalLayoutDialogViewModel vm, device) .WithCloseButtonText("Cancel") .ShowAsync(); diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs index 4a483373a..b2a698040 100644 --- a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs @@ -7,7 +7,7 @@ using System.Reactive.Disposables; using System.Threading; using System.Threading.Tasks; using Artemis.Core; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Avalonia.Threading; @@ -89,7 +89,7 @@ public class PluginPrerequisitesInstallDialogViewModel : ContentDialogViewModelB { await windowService.CreateContentDialog() .WithTitle("Plugin prerequisites") - .WithViewModel(out PluginPrerequisitesInstallDialogViewModel vm, ("subjects", subjects)) + .WithViewModel(out PluginPrerequisitesInstallDialogViewModel vm, subjects) .WithCloseButtonText("Cancel") .HavingPrimaryButton(b => b.WithText("Install").WithCommand(vm.Install)) .WithDefaultButton(ContentDialogButton.Primary) diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs index d67ff075a..5bbd3e44d 100644 --- a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs @@ -8,7 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Avalonia.Threading; @@ -74,7 +74,7 @@ public class PluginPrerequisitesUninstallDialogViewModel : ContentDialogViewMode { await windowService.CreateContentDialog() .WithTitle("Plugin prerequisites") - .WithViewModel(out PluginPrerequisitesUninstallDialogViewModel vm, ("subjects", subjects)) + .WithViewModel(out PluginPrerequisitesUninstallDialogViewModel vm, subjects) .WithCloseButtonText(cancelLabel) .HavingPrimaryButton(b => b.WithText("Uninstall").WithCommand(vm.Uninstall)) .WithDefaultButton(ContentDialogButton.Primary) diff --git a/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs index 6f9d2322c..9d3e64329 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs @@ -4,7 +4,7 @@ using System.Reactive; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using ReactiveUI; diff --git a/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs index a9b6b037e..24102dc8c 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs @@ -13,8 +13,8 @@ using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Builders; using Avalonia.Controls; using Avalonia.Threading; +using DryIoc; using Material.Icons; -using Ninject; using ReactiveUI; namespace Artemis.UI.Screens.Plugins; @@ -209,8 +209,7 @@ public class PluginViewModel : ActivatableViewModelBase try { - PluginConfigurationViewModel? viewModel = Plugin.Kernel!.Get(Plugin.ConfigurationDialog.Type) as PluginConfigurationViewModel; - if (viewModel == null) + if (Plugin.Resolve(Plugin.ConfigurationDialog.Type) is not PluginConfigurationViewModel viewModel) throw new ArtemisUIException($"The type of a plugin configuration dialog must inherit {nameof(PluginConfigurationViewModel)}"); _window = _windowService.ShowWindow(new PluginSettingsWindowViewModel(viewModel)); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/EventConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/EventConditionViewModel.cs index 570c2391c..7333571c8 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/EventConditionViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/EventConditionViewModel.cs @@ -78,7 +78,7 @@ public class EventConditionViewModel : ActivatableViewModelBase private async Task ExecuteOpenEditor() { - await _windowService.ShowDialogAsync(("nodeScript", _eventCondition.Script)); + await _windowService.ShowDialogAsync(_eventCondition.Script); await _profileEditorService.SaveProfileAsync(); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/StaticConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/StaticConditionViewModel.cs index 470be0bc0..086c67091 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/StaticConditionViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/StaticConditionViewModel.cs @@ -51,7 +51,7 @@ public class StaticConditionViewModel : ActivatableViewModelBase private async Task ExecuteOpenEditor() { - await _windowService.ShowDialogAsync(("nodeScript", _staticCondition.Script)); + await _windowService.ShowDialogAsync(_staticCondition.Script); await _profileEditorService.SaveProfileAsync(); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/DisplayConditionScriptViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/DisplayConditionScriptViewModel.cs index 260270e6b..87b0a3104 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/DisplayConditionScriptViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/DisplayConditionScriptViewModel.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Reactive.Linq; using Artemis.Core; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index a722b3f3d..b7cf96087 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -137,10 +137,7 @@ public class MenuBarViewModel : ActivatableViewModelBase if (ProfileConfiguration == null) return; - await _windowService.ShowDialogAsync( - ("profileCategory", ProfileConfiguration.Category), - ("profileConfiguration", ProfileConfiguration) - ); + await _windowService.ShowDialogAsync(ProfileConfiguration.Category, ProfileConfiguration); } private async Task ExecuteViewScripts() @@ -148,7 +145,7 @@ public class MenuBarViewModel : ActivatableViewModelBase if (ProfileConfiguration?.Profile == null) return; - await _windowService.ShowDialogAsync(("profile", ProfileConfiguration.Profile)); + await _windowService.ShowDialogAsync(ProfileConfiguration.Profile); await _profileEditorService.SaveProfileAsync(); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs index baac1b81f..bb33b620b 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Reactive.Linq; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints; using Artemis.UI.Shared; using Avalonia.Controls.Mixins; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs index bb8e7492e..c8c54bdca 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs @@ -4,8 +4,8 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.Storage.Entities.Profile; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Extensions; -using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs index 526c5412d..f3c63fc4e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs @@ -4,8 +4,8 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.Storage.Entities.Profile; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Extensions; -using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs index 012ef9070..adbd6153d 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -7,7 +7,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs index 95e9b7f01..1ff9b311a 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs @@ -7,8 +7,8 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Extensions; -using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; @@ -274,7 +274,7 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase if (ProfileElement is not Layer layer) return; - await _windowService.ShowDialogAsync(("layer", layer)); + await _windowService.ShowDialogAsync(layer); await ProfileEditorService.SaveProfileAsync(); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingViewModel.cs index cb155b447..078452a81 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingViewModel.cs @@ -4,8 +4,8 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Extensions; -using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.VisualScripting; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; @@ -88,7 +88,7 @@ public class DataBindingViewModel : ActivatableViewModelBase try { _editorOpen = true; - await _windowService.ShowDialogAsync(("nodeScript", LayerProperty.BaseDataBinding.Script)); + await _windowService.ShowDialogAsync(LayerProperty.BaseDataBinding.Script); await _profileEditorService.SaveProfileAsync(); } finally diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs index 6471d49c8..c44a41f48 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs @@ -10,7 +10,7 @@ using Artemis.Core; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.ProfileEditor.Playback; using Artemis.UI.Screens.ProfileEditor.Properties.DataBinding; using Artemis.UI.Screens.ProfileEditor.Properties.Dialogs; @@ -124,7 +124,7 @@ public class PropertiesViewModel : ActivatableViewModelBase await _windowService.CreateContentDialog() .WithTitle("Add layer effect") - .WithViewModel(out AddEffectViewModel _, ("renderProfileElement", ProfileElement)) + .WithViewModel(out AddEffectViewModel _, ProfileElement) .WithCloseButtonText("Cancel") .ShowAsync(); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs index 0689a7ed2..bb0c78c20 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs @@ -6,7 +6,7 @@ using System.Reflection; using Artemis.Core; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes; using Artemis.UI.Screens.ProfileEditor.Properties.Tree; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs index bfb6b2353..2caf34931 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs @@ -1,7 +1,7 @@ using System; using System.Collections.ObjectModel; using Artemis.Core; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline; using Artemis.UI.Screens.ProfileEditor.Properties.Tree; using DynamicData; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs index cebf376f0..b2d7687f4 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs @@ -225,7 +225,7 @@ public abstract class TimelineSegmentViewModel : ActivatableViewModelBase { await _windowService.CreateContentDialog() .WithTitle("Edit segment length") - .WithViewModel(out TimelineSegmentEditViewModel vm, ("segmentLength", Length)) + .WithViewModel(out TimelineSegmentEditViewModel vm, Length) .HavingPrimaryButton(b => b.WithText("Save").WithAction(() => { if (_profileElement != null) diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreeGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreeGroupViewModel.cs index c870e291b..9a5409908 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreeGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreeGroupViewModel.cs @@ -1,6 +1,5 @@ using System; using System.Collections.ObjectModel; -using System.Linq; using System.Reactive; using System.Reactive.Disposables; using System.Reflection; @@ -18,8 +17,6 @@ using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; -using Ninject; -using Ninject.Parameters; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree; @@ -88,12 +85,7 @@ public class TreeGroupViewModel : ActivatableViewModelBase if (constructors.Length != 1) throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor"); - // Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure - ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType)); - ConstructorArgument argument = new(brushParameter.Name!, LayerBrush); - BrushConfigurationViewModel viewModel = - (BrushConfigurationViewModel) LayerBrush.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument); - + BrushConfigurationViewModel viewModel = (BrushConfigurationViewModel) LayerBrush.Descriptor.Provider.Plugin.Resolve(configurationViewModel.Type, LayerBrush); _brushConfigurationWindowViewModel = new BrushConfigurationWindowViewModel(viewModel, configurationViewModel); await _windowService.ShowDialogAsync(_brushConfigurationWindowViewModel); @@ -118,12 +110,7 @@ public class TreeGroupViewModel : ActivatableViewModelBase if (constructors.Length != 1) throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor"); - // Find the BaseLayerEffect parameter, it is required by the base constructor so its there for sure - ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType)); - ConstructorArgument argument = new(effectParameter.Name!, LayerEffect); - EffectConfigurationViewModel viewModel = - (EffectConfigurationViewModel) LayerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument); - + EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) LayerEffect.Descriptor.Provider.Plugin.Resolve(configurationViewModel.Type, LayerEffect); _effectConfigurationWindowViewModel = new EffectConfigurationWindowViewModel(viewModel, configurationViewModel); await _windowService.ShowDialogAsync(_effectConfigurationWindowViewModel); @@ -143,7 +130,7 @@ public class TreeGroupViewModel : ActivatableViewModelBase await _windowService.CreateContentDialog() .WithTitle("Rename layer effect") - .WithViewModel(out LayerEffectRenameViewModel vm, ("layerEffect", LayerEffect)) + .WithViewModel(out LayerEffectRenameViewModel vm, LayerEffect) .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm)) .WithCloseButtonText("Cancel") .WithDefaultButton(ContentDialogButton.Primary) diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs index 4e767743d..256595ce8 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs @@ -7,7 +7,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index b9175e9ed..fbd81e943 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -3,15 +3,15 @@ using System.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Models; -using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.Sidebar; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.MainWindow; using Avalonia; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; using Avalonia.Threading; diff --git a/src/Artemis.UI/Screens/Scripting/ScriptConfigurationViewModel.cs b/src/Artemis.UI/Screens/Scripting/ScriptConfigurationViewModel.cs index 02ea67370..6bba01f5d 100644 --- a/src/Artemis.UI/Screens/Scripting/ScriptConfigurationViewModel.cs +++ b/src/Artemis.UI/Screens/Scripting/ScriptConfigurationViewModel.cs @@ -44,7 +44,7 @@ public class ScriptConfigurationViewModel : ActivatableViewModelBase { ContentDialogResult contentDialogResult = await _windowService.CreateContentDialog() .WithTitle("Edit script") - .WithViewModel(out ScriptConfigurationEditViewModel vm, ("scriptConfiguration", scriptConfiguration)) + .WithViewModel(out ScriptConfigurationEditViewModel vm, scriptConfiguration) .WithCloseButtonText("Cancel") .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Submit)) .HavingSecondaryButton(b => b.WithText("Delete")) diff --git a/src/Artemis.UI/Screens/Scripting/ScriptsDialogViewModel.cs b/src/Artemis.UI/Screens/Scripting/ScriptsDialogViewModel.cs index 9f50cb387..dcd4b4a3f 100644 --- a/src/Artemis.UI/Screens/Scripting/ScriptsDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Scripting/ScriptsDialogViewModel.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.ScriptingProviders; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.Scripting.Dialogs; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; diff --git a/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml index 2b0c9aee5..3f3571c93 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml @@ -191,6 +191,7 @@ Avalonia + DryIoc FluentAvalonia EmbedIO Furl.Http @@ -198,7 +199,6 @@ LiteDB McMaster.NETCore.Plugins Newtonsoft.Json - Ninject RGB.NET Serilog SkiaSharp @@ -208,6 +208,9 @@ https://avaloniaui.net/ + + https://github.com/dadhi/DryIoc + https://github.com/amwx/FluentAvalonia @@ -229,9 +232,6 @@ https://www.newtonsoft.com/json - - http://www.ninject.org/ - https://github.com/DarthAffe/RGB.NET diff --git a/src/Artemis.UI/Screens/Settings/Tabs/DevicesTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/DevicesTabViewModel.cs index 2d10b9ba5..620da173e 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/DevicesTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/DevicesTabViewModel.cs @@ -6,7 +6,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.Device; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml index d8cd0ff65..953f0ac1c 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml @@ -137,7 +137,7 @@ - + Updating diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs index b0215e523..9d3aa0d1b 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs @@ -12,12 +12,13 @@ using Artemis.Core.Providers; using Artemis.Core.Services; using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; using Artemis.UI.Shared; using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Services; using Avalonia.Threading; +using DryIoc; using DynamicData; -using Ninject; using ReactiveUI; using Serilog.Events; @@ -30,25 +31,28 @@ public class GeneralTabViewModel : ActivatableViewModelBase private readonly PluginSetting _defaultLayerBrushDescriptor; private readonly ISettingsService _settingsService; private readonly IUpdateService _updateService; + private readonly INotificationService _notificationService; private readonly IWindowService _windowService; private bool _startupWizardOpen; - public GeneralTabViewModel(IKernel kernel, + public GeneralTabViewModel(IContainer container, ISettingsService settingsService, IPluginManagementService pluginManagementService, IDebugService debugService, IWindowService windowService, - IUpdateService updateService) + IUpdateService updateService, + INotificationService notificationService) { DisplayName = "General"; _settingsService = settingsService; _debugService = debugService; _windowService = windowService; _updateService = updateService; - _autoRunProvider = kernel.TryGet(); + _notificationService = notificationService; + _autoRunProvider = container.Resolve(IfUnresolved.ReturnDefault); List layerBrushProviders = pluginManagementService.GetFeaturesOfType(); - List graphicsContextProviders = kernel.Get>(); + List graphicsContextProviders = container.Resolve>(); LayerBrushDescriptors = new ObservableCollection(layerBrushProviders.SelectMany(l => l.LayerBrushDescriptors)); GraphicsContexts = new ObservableCollection {"Software"}; GraphicsContexts.AddRange(graphicsContextProviders.Select(p => p.GraphicsContextName)); @@ -88,7 +92,6 @@ public class GeneralTabViewModel : ActivatableViewModelBase public ReactiveCommand ShowDataFolder { get; } public bool IsAutoRunSupported => _autoRunProvider != null; - public bool IsUpdatingSupported => _updateService.UpdatingSupported; public ObservableCollection LayerBrushDescriptors { get; } public ObservableCollection GraphicsContexts { get; } @@ -142,8 +145,8 @@ public class GeneralTabViewModel : ActivatableViewModelBase public PluginSetting UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false); public PluginSetting UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15); public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); - public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.CheckForUpdates", true); - public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.AutoUpdate", false); + public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true); + public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", false); public PluginSetting ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); public PluginSetting CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information); public PluginSetting CorePreferredGraphicsContext => _settingsService.GetSetting("Core.PreferredGraphicsContext", "Software"); @@ -159,7 +162,14 @@ public class GeneralTabViewModel : ActivatableViewModelBase private async Task ExecuteCheckForUpdate(CancellationToken cancellationToken) { - await _updateService.ManualUpdate(); + // If an update was available a popup was shown, no need to continue + if (await _updateService.CheckForUpdate()) + return; + + _notificationService.CreateNotification() + .WithTitle("No update available") + .WithMessage("You are running the latest version in your current channel") + .Show(); } private async Task ExecuteShowSetupWizard() diff --git a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs index 388c7fbc4..b45ea27cb 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs @@ -7,7 +7,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.Plugins; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml new file mode 100644 index 000000000..acf70d5b4 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml @@ -0,0 +1,216 @@ + + + + + A new Artemis update is available! 🥳 + + + + Retrieving release... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs new file mode 100644 index 000000000..ed0373da4 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs @@ -0,0 +1,29 @@ +using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Settings.Updating; + +public partial class ReleaseAvailableView : ReactiveCoreWindow +{ + public ReleaseAvailableView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void Button_OnClick(object? sender, RoutedEventArgs e) + { + Close(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs new file mode 100644 index 000000000..118177c7f --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs @@ -0,0 +1,76 @@ +using System.Reactive; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Extensions; +using Artemis.UI.Services.Updating; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Updating; +using ReactiveUI; +using Serilog; +using StrawberryShake; + +namespace Artemis.UI.Screens.Settings.Updating; + +public class ReleaseAvailableViewModel : ActivatableViewModelBase +{ + private readonly string _nextReleaseId; + private readonly ILogger _logger; + private readonly IUpdateService _updateService; + private readonly IUpdatingClient _updatingClient; + private readonly INotificationService _notificationService; + private IGetReleaseById_Release? _release; + + public ReleaseAvailableViewModel(string nextReleaseId, ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, INotificationService notificationService) + { + _nextReleaseId = nextReleaseId; + _logger = logger; + _updateService = updateService; + _updatingClient = updatingClient; + _notificationService = notificationService; + + CurrentVersion = _updateService.CurrentVersion ?? "Development build"; + Install = ReactiveCommand.Create(ExecuteInstall, this.WhenAnyValue(vm => vm.Release).Select(r => r != null)); + + this.WhenActivated(async d => await RetrieveRelease(d.AsCancellationToken())); + } + + private void ExecuteInstall() + { + _updateService.InstallRelease(_nextReleaseId); + } + + private async Task RetrieveRelease(CancellationToken cancellationToken) + { + IOperationResult result = await _updatingClient.GetReleaseById.ExecuteAsync(_nextReleaseId, cancellationToken); + // Borrow GraphQLClientException for messaging, how lazy of me.. + if (result.Errors.Count > 0) + { + GraphQLClientException exception = new(result.Errors); + _logger.Error(exception, "Failed to retrieve release details"); + _notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage(exception.Message).Show(); + return; + } + + if (result.Data?.Release == null) + { + _notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage("Release not found").Show(); + return; + } + + Release = result.Data.Release; + } + + public string CurrentVersion { get; } + + public IGetReleaseById_Release? Release + { + get => _release; + set => RaiseAndSetIfChanged(ref _release, value); + } + + public ReactiveCommand Install { get; } + public ReactiveCommand AskLater { get; } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml new file mode 100644 index 000000000..46699f5ae --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml @@ -0,0 +1,40 @@ + + + + Downloading & installing update... + + + + This should not take long, when finished Artemis must restart. + + + + + + + + Done, click restart to apply the update 🫡 + + + Restart when finished + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs new file mode 100644 index 000000000..ec2a689e6 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs @@ -0,0 +1,28 @@ +using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.Settings.Updating; + +public partial class ReleaseInstallerView : ReactiveCoreWindow +{ + public ReleaseInstallerView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void Cancel_OnClick(object? sender, RoutedEventArgs e) + { + Close(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs new file mode 100644 index 000000000..79a80aa06 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs @@ -0,0 +1,81 @@ +using System; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Extensions; +using Artemis.UI.Services.Updating; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using ReactiveUI; + +namespace Artemis.UI.Screens.Settings.Updating; + +public class ReleaseInstallerViewModel : ActivatableViewModelBase +{ + private readonly ReleaseInstaller _releaseInstaller; + private readonly IWindowService _windowService; + private ObservableAsPropertyHelper? _overallProgress; + private ObservableAsPropertyHelper? _stepProgress; + private bool _ready; + private bool _restartWhenFinished; + + public ReleaseInstallerViewModel(ReleaseInstaller releaseInstaller, IWindowService windowService) + { + _releaseInstaller = releaseInstaller; + _windowService = windowService; + + Restart = ReactiveCommand.Create(() => Utilities.ApplyUpdate(false)); + this.WhenActivated(d => + { + _overallProgress = Observable.FromEventPattern(x => _releaseInstaller.OverallProgress.ProgressChanged += x, x => _releaseInstaller.OverallProgress.ProgressChanged -= x) + .Select(e => e.EventArgs) + .ToProperty(this, vm => vm.OverallProgress) + .DisposeWith(d); + _stepProgress = Observable.FromEventPattern(x => _releaseInstaller.StepProgress.ProgressChanged += x, x => _releaseInstaller.StepProgress.ProgressChanged -= x) + .Select(e => e.EventArgs) + .ToProperty(this, vm => vm.StepProgress) + .DisposeWith(d); + + Task.Run(() => InstallUpdate(d.AsCancellationToken())); + }); + } + + public ReactiveCommand Restart { get; } + + public float OverallProgress => _overallProgress?.Value ?? 0; + public float StepProgress => _stepProgress?.Value ?? 0; + + public bool Ready + { + get => _ready; + set => RaiseAndSetIfChanged(ref _ready, value); + } + + public bool RestartWhenFinished + { + get => _restartWhenFinished; + set => RaiseAndSetIfChanged(ref _restartWhenFinished, value); + } + + private async Task InstallUpdate(CancellationToken cancellationToken) + { + try + { + await _releaseInstaller.InstallAsync(cancellationToken); + Ready = true; + if (RestartWhenFinished) + Utilities.ApplyUpdate(false); + } + catch (TaskCanceledException) + { + // ignored + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Something went wrong while installing the update", e); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs index 66d186faf..5096cc2d3 100644 --- a/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs @@ -15,10 +15,10 @@ public class SidebarCategoryEditViewModel : ContentDialogViewModelBase private readonly IProfileService _profileService; private string? _categoryName; - public SidebarCategoryEditViewModel(IProfileService profileService, ProfileCategory? category) + public SidebarCategoryEditViewModel(IProfileService profileService, ProfileCategory category) { _profileService = profileService; - _category = category; + _category = category == ProfileCategory.Empty ? null : category; if (_category != null) _categoryName = _category.Name; diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs index eaaf00480..a84d10ef7 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Modules; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.VisualScripting; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; @@ -16,7 +16,6 @@ using Artemis.UI.Shared.Services.ProfileEditor; using Avalonia.Media.Imaging; using Avalonia.Threading; using Material.Icons; -using Newtonsoft.Json; using ReactiveUI; namespace Artemis.UI.Screens.Sidebar; @@ -43,7 +42,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase().First().ToString()); + + _profileConfiguration = profileConfiguration == ProfileConfiguration.Empty + ? profileService.CreateProfileConfiguration(profileCategory, "New profile", Enum.GetValues().First().ToString()) + : profileConfiguration; _profileName = _profileConfiguration.Name; _iconType = _profileConfiguration.Icon.IconType; _hotkeyMode = _profileConfiguration.HotkeyMode; @@ -64,11 +66,9 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase( - pluginManagementService.GetFeaturesOfType().Where(m => !m.IsAlwaysAvailable).Select(m => new ProfileModuleViewModel(m)) - ); + Modules = new ObservableCollection(pluginManagementService.GetFeaturesOfType().Where(m => !m.IsAlwaysAvailable).Select(m => new ProfileModuleViewModel(m))); Modules.Insert(0, null); _selectedModule = Modules.FirstOrDefault(m => m?.Module == _profileConfiguration.Module); @@ -258,7 +258,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase(("nodeScript", ProfileConfiguration.ActivationCondition)); + await _windowService.ShowDialogAsync(ProfileConfiguration.ActivationCondition); } #endregion diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs index 224bb4074..b19f2727c 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs @@ -9,7 +9,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Builders; @@ -146,7 +146,7 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase { await _windowService.CreateContentDialog() .WithTitle("Edit category") - .WithViewModel(out SidebarCategoryEditViewModel vm, ("category", ProfileCategory)) + .WithViewModel(out SidebarCategoryEditViewModel vm, ProfileCategory) .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm)) .WithCloseButtonText("Cancel") .WithDefaultButton(ContentDialogButton.Primary) @@ -165,10 +165,7 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase private async Task ExecuteAddProfile() { - ProfileConfiguration? result = await _windowService.ShowDialogAsync( - ("profileCategory", ProfileCategory), - ("profileConfiguration", null) - ); + ProfileConfiguration? result = await _windowService.ShowDialogAsync(ProfileCategory, ProfileConfiguration.Empty); if (result != null) { SidebarProfileConfigurationViewModel viewModel = _vmFactory.SidebarProfileConfigurationViewModel(result); diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs index 8f21f7eca..91824fde6 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs @@ -57,10 +57,7 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase private async Task ExecuteEditProfile() { - await _windowService.ShowDialogAsync( - ("profileCategory", ProfileConfiguration.Category), - ("profileConfiguration", ProfileConfiguration) - ); + await _windowService.ShowDialogAsync(ProfileConfiguration.Category, ProfileConfiguration); } private void ExecuteToggleSuspended() diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs index 95d1a643c..51986d0cb 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs @@ -1,8 +1,7 @@ using System; using Artemis.UI.Shared; +using DryIoc; using Material.Icons; -using Ninject; -using Ninject.Parameters; using ReactiveUI; namespace Artemis.UI.Screens.Sidebar; @@ -15,9 +14,9 @@ public class SidebarScreenViewModel : SidebarScreenViewModel where T : MainSc public override Type ScreenType => typeof(T); - public override MainScreenViewModel CreateInstance(IKernel kernel, IScreen screen) + public override MainScreenViewModel CreateInstance(IContainer container, IScreen screen) { - return kernel.Get(new ConstructorArgument("hostScreen", screen)); + return container.Resolve(new object[] { screen }); } } @@ -32,5 +31,5 @@ public abstract class SidebarScreenViewModel : ViewModelBase public MaterialIconKind Icon { get; } public abstract Type ScreenType { get; } - public abstract MainScreenViewModel CreateInstance(IKernel kernel, IScreen screen); + public abstract MainScreenViewModel CreateInstance(IContainer container, IScreen screen); } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs index 22ae73dec..4c3b971ad 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs @@ -7,7 +7,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.Home; using Artemis.UI.Screens.ProfileEditor; using Artemis.UI.Screens.Settings; @@ -18,10 +18,10 @@ using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.ProfileEditor; using Avalonia.Threading; +using DryIoc; using DynamicData; using DynamicData.Binding; using Material.Icons; -using Ninject; using ReactiveUI; namespace Artemis.UI.Screens.Sidebar; @@ -29,7 +29,7 @@ namespace Artemis.UI.Screens.Sidebar; public class SidebarViewModel : ActivatableViewModelBase { private readonly IScreen _hostScreen; - private readonly IKernel _kernel; + private readonly IContainer _container; private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorVmFactory _profileEditorVmFactory; private readonly IWindowService _windowService; @@ -37,7 +37,7 @@ public class SidebarViewModel : ActivatableViewModelBase private ReadOnlyObservableCollection _sidebarCategories = new(new ObservableCollection()); public SidebarViewModel(IScreen hostScreen, - IKernel kernel, + IContainer container, IProfileService profileService, IWindowService windowService, IProfileEditorService profileEditorService, @@ -45,7 +45,7 @@ public class SidebarViewModel : ActivatableViewModelBase IProfileEditorVmFactory profileEditorVmFactory) { _hostScreen = hostScreen; - _kernel = kernel; + _container = container; _windowService = windowService; _profileEditorService = profileEditorService; _profileEditorVmFactory = profileEditorVmFactory; @@ -120,7 +120,7 @@ public class SidebarViewModel : ActivatableViewModelBase { await _windowService.CreateContentDialog() .WithTitle("Add new category") - .WithViewModel(out SidebarCategoryEditViewModel vm, ("category", null)) + .WithViewModel(out SidebarCategoryEditViewModel vm, ProfileCategory.Empty) .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm)) .WithCloseButtonText("Cancel") .WithDefaultButton(ContentDialogButton.Primary) @@ -137,7 +137,7 @@ public class SidebarViewModel : ActivatableViewModelBase private void NavigateToScreen(SidebarScreenViewModel sidebarScreenViewModel) { - _hostScreen.Router.Navigate.Execute(sidebarScreenViewModel.CreateInstance(_kernel, _hostScreen)); + _hostScreen.Router.Navigate.Execute(sidebarScreenViewModel.CreateInstance(_container, _hostScreen)); _profileEditorService.ChangeCurrentProfileConfiguration(null); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs index 2c509c971..dd72303e1 100644 --- a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs @@ -8,13 +8,14 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.DeviceProviders; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.Plugins; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; using Artemis.UI.Shared; using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Services; -using Ninject; +using DryIoc; using ReactiveUI; namespace Artemis.UI.Screens.StartupWizard; @@ -31,14 +32,14 @@ public class StartupWizardViewModel : DialogViewModelBase private bool _showFinish; private bool _showGoBack; - public StartupWizardViewModel(IKernel kernel, ISettingsService settingsService, IRgbService rgbService, IPluginManagementService pluginManagementService, IWindowService windowService, + public StartupWizardViewModel(IContainer container, ISettingsService settingsService, IRgbService rgbService, IPluginManagementService pluginManagementService, IWindowService windowService, IUpdateService updateService, ISettingsVmFactory settingsVmFactory) { _settingsService = settingsService; _rgbService = rgbService; _windowService = windowService; _updateService = updateService; - _autoRunProvider = kernel.TryGet(); + _autoRunProvider = container.Resolve(IfUnresolved.ReturnDefault); Continue = ReactiveCommand.Create(ExecuteContinue); GoBack = ReactiveCommand.Create(ExecuteGoBack); @@ -81,13 +82,12 @@ public class StartupWizardViewModel : DialogViewModelBase public ObservableCollection DeviceProviders { get; } public bool IsAutoRunSupported => _autoRunProvider != null; - public bool IsUpdatingSupported => _updateService.UpdatingSupported; public PluginSetting UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false); public PluginSetting UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15); public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); - public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.CheckForUpdates", true); - public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.AutoUpdate", false); + public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true); + public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", false); public int CurrentStep { @@ -119,7 +119,7 @@ public class StartupWizardViewModel : DialogViewModelBase CurrentStep--; // Skip the settings step if none of it's contents are supported - if (CurrentStep == 4 && !IsAutoRunSupported && !IsUpdatingSupported) + if (CurrentStep == 4 && !IsAutoRunSupported) CurrentStep--; SetupButtons(); @@ -131,7 +131,7 @@ public class StartupWizardViewModel : DialogViewModelBase CurrentStep++; // Skip the settings step if none of it's contents are supported - if (CurrentStep == 4 && !IsAutoRunSupported && !IsUpdatingSupported) + if (CurrentStep == 4 && !IsAutoRunSupported) CurrentStep++; SetupButtons(); diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml index 294f59e91..bf68bf234 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml @@ -68,7 +68,7 @@ - + Updating diff --git a/src/Artemis.UI/Screens/SurfaceEditor/ListDeviceViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/ListDeviceViewModel.cs index 2c68379eb..94071022e 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/ListDeviceViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/ListDeviceViewModel.cs @@ -57,7 +57,7 @@ public class ListDeviceViewModel : ViewModelBase await _windowService.CreateContentDialog() .WithTitle($"{Device.RgbDevice.DeviceInfo.DeviceName} - Detect input") - .WithViewModel(out DeviceDetectInputViewModel? viewModel, ("device", Device)) + .WithViewModel(out DeviceDetectInputViewModel viewModel, Device) .WithCloseButtonText("Cancel") .ShowAsync(); diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs index 51991e2a8..7596ce067 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs @@ -115,7 +115,7 @@ public class SurfaceDeviceViewModel : ActivatableViewModelBase await _windowService.CreateContentDialog() .WithTitle($"{Device.RgbDevice.DeviceInfo.DeviceName} - Detect input") - .WithViewModel(out DeviceDetectInputViewModel? viewModel, ("device", Device)) + .WithViewModel(out DeviceDetectInputViewModel viewModel, Device) .WithCloseButtonText("Cancel") .ShowAsync(); diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs index dd0f738dd..0df1ca26c 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs @@ -7,8 +7,8 @@ using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Extensions; -using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared.Services; using Avalonia; using ReactiveUI; diff --git a/src/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs b/src/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs index 1cd822bf3..45e6b2042 100644 --- a/src/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs +++ b/src/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs @@ -8,7 +8,6 @@ using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.ReactiveUI; -using Avalonia.Rendering; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs index 92728f4d3..787f3fdca 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs @@ -14,7 +14,6 @@ using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.ReactiveUI; using Avalonia.Threading; -using Avalonia.VisualTree; using DynamicData.Binding; using ReactiveUI; diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs index 00ab99900..5628da274 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Linq; using System.Reactive; using System.Reactive.Linq; @@ -11,8 +10,8 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Events; using Artemis.Core.Services; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Models; -using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.NodeEditor; diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs index 0925e450c..7412f8d84 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs @@ -7,7 +7,7 @@ using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.NodeEditor.Commands; diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index 97afc468f..17b73778e 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -5,7 +5,7 @@ using System.Reactive; using System.Reactive.Linq; using Artemis.Core; using Artemis.Core.Events; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; diff --git a/src/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs index a889a8879..d5a7702eb 100644 --- a/src/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs @@ -1,5 +1,5 @@ using Artemis.Core; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Shared.Services.NodeEditor; namespace Artemis.UI.Screens.VisualScripting.Pins; diff --git a/src/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs index b842c9053..920f5f15c 100644 --- a/src/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs @@ -1,5 +1,5 @@ using Artemis.Core; -using Artemis.UI.Ninject.Factories; +using Artemis.UI.DryIoc.Factories; using Artemis.UI.Shared.Services.NodeEditor; namespace Artemis.UI.Screens.VisualScripting.Pins; diff --git a/src/Artemis.UI/Services/Interfaces/IUpdateService.cs b/src/Artemis.UI/Services/Interfaces/IUpdateService.cs deleted file mode 100644 index cc6236853..000000000 --- a/src/Artemis.UI/Services/Interfaces/IUpdateService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Threading.Tasks; - -namespace Artemis.UI.Services.Interfaces; - -public interface IUpdateService : IArtemisUIService -{ - /// - /// Gets a boolean indicating whether updating is supported. - /// - bool UpdatingSupported { get; } - - /// - /// Gets or sets a boolean indicating whether auto-updating is suspended. - /// - bool SuspendAutoUpdate { get; set; } - - /// - /// Manually checks for updates and offers to install it if found. - /// - /// Whether an update was found, regardless of whether the user chose to install it. - Task ManualUpdate(); -} \ No newline at end of file diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index 07e4660ca..e4d50ec9e 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -14,7 +14,7 @@ using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.PropertyInput; using Artemis.VisualScripting.Nodes.Mathematics; using Avalonia; -using Ninject; +using DryIoc; using SkiaSharp; namespace Artemis.UI.Services; @@ -23,13 +23,13 @@ public class RegistrationService : IRegistrationService { private readonly IDataModelUIService _dataModelUIService; private readonly IInputService _inputService; - private readonly IKernel _kernel; + private readonly IContainer _container; private readonly INodeService _nodeService; private readonly IPropertyInputService _propertyInputService; private readonly IWebServerService _webServerService; private bool _registeredBuiltInPropertyEditors; - public RegistrationService(IKernel kernel, + public RegistrationService(IContainer container, IInputService inputService, IPropertyInputService propertyInputService, IProfileEditorService profileEditorService, @@ -39,7 +39,7 @@ public class RegistrationService : IRegistrationService IDeviceLayoutService deviceLayoutService // here to make sure it is instantiated ) { - _kernel = kernel; + _container = container; _inputService = inputService; _propertyInputService = propertyInputService; _nodeService = nodeService; @@ -53,7 +53,7 @@ public class RegistrationService : IRegistrationService private void CreateCursorResources() { - ICursorProvider? cursorProvider = _kernel.TryGet(); + ICursorProvider? cursorProvider = _container.Resolve(IfUnresolved.ReturnDefault); if (cursorProvider == null) return; diff --git a/src/Artemis.UI/Services/UpdateService.cs b/src/Artemis.UI/Services/UpdateService.cs deleted file mode 100644 index f4cada5f7..000000000 --- a/src/Artemis.UI/Services/UpdateService.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Timers; -using Artemis.Core; -using Artemis.Core.Services; -using Artemis.UI.Services.Interfaces; -using Artemis.UI.Shared.Providers; -using Artemis.UI.Shared.Services.MainWindow; -using Avalonia.Threading; -using Ninject; -using Serilog; - -namespace Artemis.UI.Services; - -public class UpdateService : IUpdateService -{ - private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour - - private readonly PluginSetting _autoUpdate; - private readonly PluginSetting _checkForUpdates; - private readonly ILogger _logger; - private readonly IMainWindowService _mainWindowService; - private readonly IUpdateProvider? _updateProvider; - - public UpdateService(ILogger logger, IKernel kernel, ISettingsService settingsService, IMainWindowService mainWindowService) - { - _logger = logger; - _mainWindowService = mainWindowService; - - if (!Constants.BuildInfo.IsLocalBuild) - _updateProvider = kernel.TryGet(); - - _checkForUpdates = settingsService.GetSetting("UI.CheckForUpdates", true); - _autoUpdate = settingsService.GetSetting("UI.AutoUpdate", false); - _checkForUpdates.SettingChanged += CheckForUpdatesOnSettingChanged; - _mainWindowService.MainWindowOpened += WindowServiceOnMainWindowOpened; - - Timer timer = new(UPDATE_CHECK_INTERVAL); - timer.Elapsed += TimerOnElapsed; - timer.Start(); - } - - private async void TimerOnElapsed(object? sender, ElapsedEventArgs e) - { - await AutoUpdate(); - } - - private async void CheckForUpdatesOnSettingChanged(object? sender, EventArgs e) - { - // Run an auto-update as soon as the setting gets changed to enabled - if (_checkForUpdates.Value) - await AutoUpdate(); - } - - private async void WindowServiceOnMainWindowOpened(object? sender, EventArgs e) - { - await AutoUpdate(); - } - - private async Task AutoUpdate() - { - if (_updateProvider == null || !_checkForUpdates.Value || SuspendAutoUpdate) - return; - - try - { - bool updateAvailable = await _updateProvider.CheckForUpdate("master"); - if (!updateAvailable) - return; - - // Only offer it once per session - SuspendAutoUpdate = true; - - // If the window is open show the changelog, don't auto-update while the user is busy - if (_mainWindowService.IsMainWindowOpen) - { - await Dispatcher.UIThread.InvokeAsync(async () => - { - // Call OpenMainWindow anyway to focus the main window - _mainWindowService.OpenMainWindow(); - await _updateProvider.OfferUpdate("master", true); - }); - return; - } - - // If the window is closed but auto-update is enabled, update silently - if (_autoUpdate.Value) - await _updateProvider.ApplyUpdate("master", true); - // If auto-update is disabled the update provider can show a notification and handle the rest - else - await _updateProvider.OfferUpdate("master", false); - } - catch (Exception e) - { - _logger.Warning(e, "Auto update failed"); - } - } - - public bool SuspendAutoUpdate { get; set; } - public bool UpdatingSupported => _updateProvider != null; - - public async Task ManualUpdate() - { - if (_updateProvider == null || !_mainWindowService.IsMainWindowOpen) - return; - - bool updateAvailable = await _updateProvider.CheckForUpdate("master"); - if (!updateAvailable) - return; - - await _updateProvider.OfferUpdate("master", true); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs new file mode 100644 index 000000000..fcf306bb4 --- /dev/null +++ b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Artemis.UI.Services.Updating; + +public interface IUpdateNotificationProvider +{ + Task ShowNotification(string releaseId); +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/IUpdateService.cs b/src/Artemis.UI/Services/Updating/IUpdateService.cs new file mode 100644 index 000000000..5212e4eec --- /dev/null +++ b/src/Artemis.UI/Services/Updating/IUpdateService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Artemis.UI.Services.Interfaces; + +namespace Artemis.UI.Services.Updating; + +public interface IUpdateService : IArtemisUIService +{ + Task CheckForUpdate(); + Task InstallRelease(string releaseId); + string? CurrentVersion { get; } +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs new file mode 100644 index 000000000..55a3e6a26 --- /dev/null +++ b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs @@ -0,0 +1,151 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Extensions; +using Artemis.WebClient.Updating; +using Octodiff.Core; +using Octodiff.Diagnostics; +using Serilog; +using StrawberryShake; + +namespace Artemis.UI.Services.Updating; + +/// +/// Represents the installation process of a release +/// +public class ReleaseInstaller +{ + private readonly string _dataFolder; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly string _releaseId; + private readonly Platform _updatePlatform; + private readonly IUpdatingClient _updatingClient; + + public ReleaseInstaller(string releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient) + { + _releaseId = releaseId; + _logger = logger; + _updatingClient = updatingClient; + _httpClient = httpClient; + _dataFolder = Path.Combine(Constants.DataFolder, "updating"); + + if (OperatingSystem.IsWindows()) + _updatePlatform = Platform.Windows; + else if (OperatingSystem.IsLinux()) + _updatePlatform = Platform.Linux; + else if (OperatingSystem.IsMacOS()) + _updatePlatform = Platform.Osx; + else + throw new PlatformNotSupportedException("Cannot auto update on the current platform"); + + if (!Directory.Exists(_dataFolder)) + Directory.CreateDirectory(_dataFolder); + } + + + public Progress OverallProgress { get; } = new(); + public Progress StepProgress { get; } = new(); + + public async Task InstallAsync(CancellationToken cancellationToken) + { + ((IProgress) OverallProgress).Report(0); + + _logger.Information("Retrieving details for release {ReleaseId}", _releaseId); + IOperationResult result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken); + result.EnsureNoErrors(); + + IGetReleaseById_Release? release = result.Data?.Release; + if (release == null) + throw new Exception($"Could not find release with ID {_releaseId}"); + + IGetReleaseById_Release_Artifacts? artifact = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform); + if (artifact == null) + throw new Exception("Found the release but it has no artifact for the current platform"); + + ((IProgress) OverallProgress).Report(10); + + // Determine whether the last update matches our local version, then we can download the delta + if (release.PreviousRelease != null && File.Exists(Path.Combine(_dataFolder, $"{release.PreviousRelease}.zip")) && artifact.DeltaFileInfo.DownloadSize != 0) + await DownloadDelta(artifact, Path.Combine(_dataFolder, $"{release.PreviousRelease}.zip"), cancellationToken); + else + await Download(artifact, cancellationToken); + } + + private async Task DownloadDelta(IGetReleaseById_Release_Artifacts artifact, string previousRelease, CancellationToken cancellationToken) + { + await using MemoryStream stream = new(); + await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}/delta", stream, StepProgress, cancellationToken); + + ((IProgress) OverallProgress).Report(33); + + await PatchDelta(stream, previousRelease, cancellationToken); + } + + private async Task PatchDelta(Stream deltaStream, string previousRelease, CancellationToken cancellationToken) + { + await using FileStream baseStream = File.OpenRead(previousRelease); + await using FileStream newFileStream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + + deltaStream.Seek(0, SeekOrigin.Begin); + + await Task.Run(() => + { + DeltaApplier deltaApplier = new(); + deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new DeltaApplierProgressReporter(StepProgress)), newFileStream); + }); + cancellationToken.ThrowIfCancellationRequested(); + + ((IProgress) OverallProgress).Report(66); + await Extract(newFileStream, cancellationToken); + } + + private async Task Download(IGetReleaseById_Release_Artifacts artifact, CancellationToken cancellationToken) + { + await using MemoryStream stream = new(); + await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}", stream, StepProgress, cancellationToken); + + ((IProgress) OverallProgress).Report(50); + await Extract(stream, cancellationToken); + } + + private async Task Extract(Stream archiveStream, CancellationToken cancellationToken) + { + // Ensure the directory is empty + string extractDirectory = Path.Combine(_dataFolder, "pending"); + if (Directory.Exists(extractDirectory)) + Directory.Delete(extractDirectory, true); + Directory.CreateDirectory(extractDirectory); + + + + await Task.Run(() => + { + archiveStream.Seek(0, SeekOrigin.Begin); + using ZipArchive archive = new(archiveStream); + archive.ExtractToDirectory(extractDirectory, false, StepProgress, cancellationToken); + }); + + ((IProgress) OverallProgress).Report(100); + } +} + +internal class DeltaApplierProgressReporter : IProgressReporter +{ + private readonly IProgress _stepProgress; + + public DeltaApplierProgressReporter(IProgress stepProgress) + { + _stepProgress = stepProgress; + } + + public void ReportProgress(string operation, long currentPosition, long total) + { + _stepProgress.Report(currentPosition / total * 100); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs new file mode 100644 index 000000000..86f8f7a91 --- /dev/null +++ b/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Artemis.UI.Services.Updating; + +public class SimpleUpdateNotificationProvider : IUpdateNotificationProvider +{ + /// + public async Task ShowNotification(string releaseId) + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs new file mode 100644 index 000000000..83f65739e --- /dev/null +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -0,0 +1,155 @@ +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Screens.Settings.Updating; +using Artemis.UI.Services.Updating; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.MainWindow; +using Artemis.WebClient.Updating; +using Avalonia.Threading; +using Serilog; +using StrawberryShake; +using Timer = System.Timers.Timer; + +namespace Artemis.UI.Services; + +public class UpdateService : IUpdateService +{ + private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour + private readonly PluginSetting _autoCheck; + private readonly PluginSetting _autoInstall; + private readonly PluginSetting _channel; + private readonly Func _getReleaseInstaller; + + private readonly ILogger _logger; + private readonly IMainWindowService _mainWindowService; + private readonly Lazy _updateNotificationProvider; + private readonly Platform _updatePlatform; + private readonly IUpdatingClient _updatingClient; + private readonly IWindowService _windowService; + + private bool _suspendAutoCheck; + + public UpdateService(ILogger logger, + ISettingsService settingsService, + IMainWindowService mainWindowService, + IWindowService windowService, + IUpdatingClient updatingClient, + Lazy updateNotificationProvider, + Func getReleaseInstaller) + { + _logger = logger; + _mainWindowService = mainWindowService; + _windowService = windowService; + _updatingClient = updatingClient; + _updateNotificationProvider = updateNotificationProvider; + _getReleaseInstaller = getReleaseInstaller; + + if (OperatingSystem.IsWindows()) + _updatePlatform = Platform.Windows; + else if (OperatingSystem.IsLinux()) + _updatePlatform = Platform.Linux; + else if (OperatingSystem.IsMacOS()) + _updatePlatform = Platform.Osx; + else + throw new PlatformNotSupportedException("Cannot auto update on the current platform"); + + _channel = settingsService.GetSetting("UI.Updating.Channel", "master"); + _autoCheck = settingsService.GetSetting("UI.Updating.AutoCheck", true); + _autoInstall = settingsService.GetSetting("UI.Updating.AutoInstall", false); + _autoCheck.SettingChanged += HandleAutoUpdateEvent; + _mainWindowService.MainWindowOpened += HandleAutoUpdateEvent; + Timer timer = new(UPDATE_CHECK_INTERVAL); + timer.Elapsed += HandleAutoUpdateEvent; + timer.Start(); + } + + public string? CurrentVersion + { + get + { + object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); + return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion; + } + } + + private async Task ShowUpdateDialog(string nextReleaseId) + { + await Dispatcher.UIThread.InvokeAsync(async () => + { + // Main window is probably already open but this will bring it into focus + _mainWindowService.OpenMainWindow(); + await _windowService.ShowDialogAsync(nextReleaseId); + }); + } + + private async Task ShowUpdateNotification(string nextReleaseId) + { + await _updateNotificationProvider.Value.ShowNotification(nextReleaseId); + } + + private async Task AutoInstallUpdate(string nextReleaseId) + { + ReleaseInstaller installer = _getReleaseInstaller(nextReleaseId); + await installer.InstallAsync(CancellationToken.None); + Utilities.ApplyUpdate(true); + } + + private async void HandleAutoUpdateEvent(object? sender, EventArgs e) + { + if (!_autoCheck.Value || _suspendAutoCheck) + return; + + try + { + await CheckForUpdate(); + } + catch (Exception ex) + { + _logger.Warning(ex, "Auto update failed"); + } + } + + public async Task CheckForUpdate() + { + string? currentVersion = CurrentVersion; + if (currentVersion == null) + return false; + + // IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, _channel.Value, _updatePlatform); + IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, "feature/gh-actions", _updatePlatform); + result.EnsureNoErrors(); + + // No update was found + if (result.Data?.NextRelease == null) + return false; + + // Only offer it once per session + _suspendAutoCheck = true; + + // If the window is open show the changelog, don't auto-update while the user is busy + if (_mainWindowService.IsMainWindowOpen) + await ShowUpdateDialog(result.Data.NextRelease.Id); + else if (!_autoInstall.Value) + await ShowUpdateNotification(result.Data.NextRelease.Id); + else + await AutoInstallUpdate(result.Data.NextRelease.Id); + + return true; + } + + /// + public async Task InstallRelease(string releaseId) + { + ReleaseInstaller installer = _getReleaseInstaller(releaseId); + await Dispatcher.UIThread.InvokeAsync(() => + { + // Main window is probably already open but this will bring it into focus + _mainWindowService.OpenMainWindow(); + _windowService.ShowWindow(installer); + }); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Styles/Artemis.axaml b/src/Artemis.UI/Styles/Artemis.axaml index b10f8bdb6..d92851a90 100644 --- a/src/Artemis.UI/Styles/Artemis.axaml +++ b/src/Artemis.UI/Styles/Artemis.axaml @@ -6,4 +6,5 @@ + \ No newline at end of file diff --git a/src/Artemis.UI/Styles/Markdown.axaml b/src/Artemis.UI/Styles/Markdown.axaml new file mode 100644 index 000000000..34acbe75f --- /dev/null +++ b/src/Artemis.UI/Styles/Markdown.axaml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ## Core + * Cleaned up ProfileService render condition + * Core - Added fading in and out of profiles + * Core - Apply opacity layer only when fading + * Core - Fixed when condition stops being true mid-fade + * Core - Removed FadingStatus enum + + # General + - Meta - Fixed warnings + - Meta - Update RGB.NET + + # Plugins + - Plugins - Ignore version when loading shared assemblies + + # UI + - Sidebar - Improved category reordering code + + + + + + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj index ddfbc38c5..ac5bb81ea 100644 --- a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj +++ b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -11,7 +11,7 @@ - + @@ -23,4 +23,11 @@ + + + HotkeyPressNodeCustomView.axaml + Code + + + diff --git a/src/Artemis.VisualScripting/DryIoc/ContainerExtensions.cs b/src/Artemis.VisualScripting/DryIoc/ContainerExtensions.cs new file mode 100644 index 000000000..6e388f30d --- /dev/null +++ b/src/Artemis.VisualScripting/DryIoc/ContainerExtensions.cs @@ -0,0 +1,40 @@ +using DryIoc; +using Microsoft.Extensions.ObjectPool; +using NoStringEvaluating; +using NoStringEvaluating.Contract; +using NoStringEvaluating.Models.Values; +using NoStringEvaluating.Services.Cache; +using NoStringEvaluating.Services.Checking; +using NoStringEvaluating.Services.Parsing; +using NoStringEvaluating.Services.Parsing.NodeReaders; + +namespace Artemis.VisualScripting.DryIoc; + +/// +/// Provides an extension method to register services onto a DryIoc . +/// +public static class UIContainerExtensions +{ + /// + /// Registers NoStringEvaluating services into the container. + /// + /// The builder building the current container + public static void RegisterNoStringEvaluating(this IContainer container) + { + // Pooling + container.RegisterInstance(ObjectPool.Create>()); + container.RegisterInstance(ObjectPool.Create>()); + container.RegisterInstance(ObjectPool.Create()); + + // Parser + container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); + + // Checker + container.Register(Reuse.Singleton); + + // Evaluator + container.Register(Reuse.Singleton); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Ninject/NoStringNinjectModule.cs b/src/Artemis.VisualScripting/Ninject/NoStringNinjectModule.cs deleted file mode 100644 index 563b7f03d..000000000 --- a/src/Artemis.VisualScripting/Ninject/NoStringNinjectModule.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.Extensions.ObjectPool; -using Ninject.Modules; -using NoStringEvaluating; -using NoStringEvaluating.Contract; -using NoStringEvaluating.Models.Values; -using NoStringEvaluating.Services.Cache; -using NoStringEvaluating.Services.Checking; -using NoStringEvaluating.Services.Parsing; -using NoStringEvaluating.Services.Parsing.NodeReaders; - -namespace Artemis.VisualScripting.Ninject; - -public class NoStringNinjectModule : NinjectModule -{ - public override void Load() - { - // Pooling - Bind>>() - .ToConstant(ObjectPool.Create>()) - .InSingletonScope(); - - Bind>>() - .ToConstant(ObjectPool.Create>()) - .InSingletonScope(); - - Bind>() - .ToConstant(ObjectPool.Create()) - .InSingletonScope(); - - // Parser - Bind().To().InSingletonScope(); - Bind().To().InSingletonScope(); - Bind().To().InSingletonScope(); - - // Checker - Bind().To().InSingletonScope(); - - // Evaluator - Bind().To().InSingletonScope(); - } -} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Branching/EnumSwitchNode.cs b/src/Artemis.VisualScripting/Nodes/Branching/EnumSwitchNode.cs index e380c3446..5ca915053 100644 --- a/src/Artemis.VisualScripting/Nodes/Branching/EnumSwitchNode.cs +++ b/src/Artemis.VisualScripting/Nodes/Branching/EnumSwitchNode.cs @@ -66,6 +66,9 @@ public class EnumSwitchNode : Node return; Type enumType = SwitchValue.ConnectedTo[0].Type; + if (!enumType.IsEnum) + return; + foreach (Enum enumValue in Enum.GetValues(enumType).Cast()) { InputPin pin = CreateOrAddInputPin(typeof(object), enumValue.ToString().Humanize(LetterCasing.Sentence)); diff --git a/src/Artemis.VisualScripting/Nodes/Color/GradientBuilderNode.cs b/src/Artemis.VisualScripting/Nodes/Color/GradientBuilderNode.cs index e4d380196..26ff8067f 100644 --- a/src/Artemis.VisualScripting/Nodes/Color/GradientBuilderNode.cs +++ b/src/Artemis.VisualScripting/Nodes/Color/GradientBuilderNode.cs @@ -1,10 +1,5 @@ using Artemis.Core; using SkiaSharp; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Artemis.VisualScripting.Nodes.Color { diff --git a/src/Artemis.VisualScripting/Nodes/Color/SortedGradient.cs b/src/Artemis.VisualScripting/Nodes/Color/SortedGradient.cs index eea7329fe..00cdd61d0 100644 --- a/src/Artemis.VisualScripting/Nodes/Color/SortedGradient.cs +++ b/src/Artemis.VisualScripting/Nodes/Color/SortedGradient.cs @@ -1,17 +1,13 @@ using Artemis.Core; using Artemis.Core.ColorScience; using SkiaSharp; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Artemis.VisualScripting.Nodes.Color { [Node("Sorted Gradient", "Generates a sorted gradient from the given colors", "Color", InputType = typeof(SKColor), OutputType = typeof(ColorGradient))] public class SortedGradientNode : Node { + private int lastComputedColorGroup; public InputPinCollection Inputs { get; } public OutputPin Output { get; } @@ -19,10 +15,15 @@ namespace Artemis.VisualScripting.Nodes.Color { Inputs = CreateInputPinCollection(); Output = CreateOutputPin(); + lastComputedColorGroup = 0; } public override void Evaluate() { + int newHash = GetInputColorHash(); + if (newHash == lastComputedColorGroup) + return; + SKColor[] colors = Inputs.Values.ToArray(); if (colors.Length == 0) @@ -40,6 +41,17 @@ namespace Artemis.VisualScripting.Nodes.Color } Output.Value = gradient; + lastComputedColorGroup = newHash; + } + + private int GetInputColorHash() + { + int hash = 0; + + foreach (SKColor color in Inputs.Values) + hash = HashCode.Combine(hash, color.GetHashCode()); + + return hash; } } } diff --git a/src/Artemis.VisualScripting/Nodes/Easing/EasingTypeNode.cs b/src/Artemis.VisualScripting/Nodes/Easing/EasingTypeNode.cs deleted file mode 100644 index 8b120cd8f..000000000 --- a/src/Artemis.VisualScripting/Nodes/Easing/EasingTypeNode.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Artemis.Core; -using Artemis.VisualScripting.Nodes.Easing.Screens; - -namespace Artemis.VisualScripting.Nodes.Easing; - -[Node("Easing Type", "Outputs a selectable easing type.", "Easing", OutputType = typeof(Easings.Functions))] -public class EasingTypeNode : Node -{ - public EasingTypeNode() - { - Output = CreateOutputPin(); - } - - public OutputPin Output { get; } - - public override void Evaluate() - { - Output.Value = Storage; - } -} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeCustomView.axaml.cs deleted file mode 100644 index 5bf5aaa52..000000000 --- a/src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeCustomView.axaml.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Avalonia.Markup.Xaml; -using Avalonia.ReactiveUI; - -namespace Artemis.VisualScripting.Nodes.Easing.Screens; - -public class EasingTypeNodeCustomView : ReactiveUserControl -{ - public EasingTypeNodeCustomView() - { - InitializeComponent(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } -} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/HotkeyEnableDisableNode.cs b/src/Artemis.VisualScripting/Nodes/Input/HotkeyEnableDisableNode.cs new file mode 100644 index 000000000..12946deb8 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/HotkeyEnableDisableNode.cs @@ -0,0 +1,74 @@ +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.VisualScripting.Nodes.Input.Screens; + +namespace Artemis.VisualScripting.Nodes.Input; + +[Node("Hotkey enable/disable", "Outputs a boolean value enabled and disabled by a set of hotkeys", "Input", OutputType = typeof(bool))] +public class HotkeyEnableDisableNode : Node, IDisposable +{ + private readonly IInputService _inputService; + private Hotkey? _disableHotkey; + private Hotkey? _enableHotkey; + private bool _value; + private bool _retrievedInitialValue; + + public HotkeyEnableDisableNode(IInputService inputService) + { + _inputService = inputService; + _inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp; + + InitialValue = CreateInputPin(); + Output = CreateOutputPin(); + StorageModified += OnStorageModified; + } + + public InputPin InitialValue { get; } + public OutputPin Output { get; } + + public override void Initialize(INodeScript script) + { + LoadHotkeys(); + } + + public override void Evaluate() + { + if (!_retrievedInitialValue) + { + _value = InitialValue.Value; + _retrievedInitialValue = true; + } + + Output.Value = _value; + } + + private void OnStorageModified(object? sender, EventArgs e) + { + LoadHotkeys(); + } + + private void LoadHotkeys() + { + if (Storage == null) + return; + + _enableHotkey = Storage.EnableHotkey != null ? new Hotkey(Storage.EnableHotkey) : null; + _disableHotkey = Storage.DisableHotkey != null ? new Hotkey(Storage.DisableHotkey) : null; + } + + private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e) + { + if (Storage == null) + return; + + if (_disableHotkey != null && _disableHotkey.MatchesEventArgs(e)) + _value = false; + else if (_enableHotkey != null && _enableHotkey.MatchesEventArgs(e)) + _value = true; + } + + public void Dispose() + { + _inputService.KeyboardKeyUp -= InputServiceOnKeyboardKeyUp; + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/HotkeyEnableDisableNodeEntity.cs b/src/Artemis.VisualScripting/Nodes/Input/HotkeyEnableDisableNodeEntity.cs new file mode 100644 index 000000000..98f84b2b0 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/HotkeyEnableDisableNodeEntity.cs @@ -0,0 +1,18 @@ +using Artemis.Core; +using Artemis.Storage.Entities.Profile; + +namespace Artemis.VisualScripting.Nodes.Input; + +public class HotkeyEnableDisableNodeEntity +{ + public HotkeyEnableDisableNodeEntity(Hotkey? enableHotkey, Hotkey? disableHotkey) + { + enableHotkey?.Save(); + EnableHotkey = enableHotkey?.Entity; + disableHotkey?.Save(); + DisableHotkey = disableHotkey?.Entity; + } + + public ProfileConfigurationHotkeyEntity? EnableHotkey { get; set; } + public ProfileConfigurationHotkeyEntity? DisableHotkey { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/HotkeyPressNode.cs b/src/Artemis.VisualScripting/Nodes/Input/HotkeyPressNode.cs new file mode 100644 index 000000000..593eaf8bf --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/HotkeyPressNode.cs @@ -0,0 +1,69 @@ +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.VisualScripting.Nodes.Input.Screens; + +namespace Artemis.VisualScripting.Nodes.Input; + +[Node("Hotkey press", "Outputs a boolean value for as long as a hotkey is pressed", "Input", OutputType = typeof(bool))] +public class HotkeyPressNode : Node, IDisposable +{ + private readonly IInputService _inputService; + private Hotkey? _toggleHotkey; + private bool _value; + + public HotkeyPressNode(IInputService inputService) + { + _inputService = inputService; + _inputService.KeyboardKeyDown += InputServiceOnKeyboardKeyDown; + _inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp; + + Output = CreateOutputPin(); + StorageModified += OnStorageModified; + } + + public OutputPin Output { get; } + + public override void Initialize(INodeScript script) + { + LoadHotkeys(); + } + + public override void Evaluate() + { + Output.Value = _value; + } + + private void OnStorageModified(object? sender, EventArgs e) + { + LoadHotkeys(); + } + + private void LoadHotkeys() + { + if (Storage == null) + return; + + _toggleHotkey = Storage.EnableHotkey != null ? new Hotkey(Storage.EnableHotkey) : null; + } + + private void InputServiceOnKeyboardKeyDown(object? sender, ArtemisKeyboardKeyEventArgs e) + { + if (Storage == null) + return; + if (_toggleHotkey != null && _toggleHotkey.MatchesEventArgs(e)) + _value = true; + } + + private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e) + { + if (Storage == null) + return; + if (_toggleHotkey != null && _toggleHotkey.MatchesEventArgs(e)) + _value = false; + } + + public void Dispose() + { + _inputService.KeyboardKeyUp -= InputServiceOnKeyboardKeyUp; + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/HotkeyToggleNode.cs b/src/Artemis.VisualScripting/Nodes/Input/HotkeyToggleNode.cs new file mode 100644 index 000000000..a31d5d253 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/HotkeyToggleNode.cs @@ -0,0 +1,69 @@ +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.VisualScripting.Nodes.Input.Screens; + +namespace Artemis.VisualScripting.Nodes.Input; + +[Node("Hotkey toggle", "Outputs a boolean value toggled by a hotkey", "Input", OutputType = typeof(bool))] +public class HotkeyToggleNode : Node, IDisposable +{ + private readonly IInputService _inputService; + private Hotkey? _toggleHotkey; + private bool _value; + private bool _retrievedInitialValue; + + public HotkeyToggleNode(IInputService inputService) + { + _inputService = inputService; + _inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp; + + InitialValue = CreateInputPin(); + Output = CreateOutputPin(); + StorageModified += OnStorageModified; + } + + public InputPin InitialValue { get; } + public OutputPin Output { get; } + + public override void Initialize(INodeScript script) + { + LoadHotkeys(); + } + + public override void Evaluate() + { + if (!_retrievedInitialValue) + { + _value = InitialValue.Value; + _retrievedInitialValue = true; + } + + Output.Value = _value; + } + + private void OnStorageModified(object? sender, EventArgs e) + { + LoadHotkeys(); + } + + private void LoadHotkeys() + { + if (Storage == null) + return; + + _toggleHotkey = Storage.EnableHotkey != null ? new Hotkey(Storage.EnableHotkey) : null; + } + + private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e) + { + if (Storage == null) + return; + if (_toggleHotkey != null && _toggleHotkey.MatchesEventArgs(e)) + _value = !_value; + } + + public void Dispose() + { + _inputService.KeyboardKeyUp -= InputServiceOnKeyboardKeyUp; + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyEnableDisableNodeCustomView.axaml b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyEnableDisableNodeCustomView.axaml new file mode 100644 index 000000000..7285adced --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyEnableDisableNodeCustomView.axaml @@ -0,0 +1,16 @@ + + + Enable + + Disable + + + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyEnableDisableNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyEnableDisableNodeCustomView.axaml.cs new file mode 100644 index 000000000..90bf774e0 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyEnableDisableNodeCustomView.axaml.cs @@ -0,0 +1,25 @@ +using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.Input.Screens; + +public partial class HotkeyEnableDisableNodeCustomView : ReactiveUserControl +{ + public HotkeyEnableDisableNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void HotkeyBox_OnHotkeyChanged(HotkeyBox sender, EventArgs args) + { + ViewModel?.Save(); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyEnableDisableNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyEnableDisableNodeCustomViewModel.cs new file mode 100644 index 000000000..ccb1d7342 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyEnableDisableNodeCustomViewModel.cs @@ -0,0 +1,65 @@ +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; +using Artemis.UI.Shared.VisualScripting; +using Avalonia.Controls.Mixins; +using ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.Input.Screens; + +public class HotkeyEnableDisableNodeCustomViewModel : CustomNodeViewModel +{ + private readonly HotkeyEnableDisableNode _enableDisableNode; + private readonly INodeEditorService _nodeEditorService; + private Hotkey? _enableHotkey; + private Hotkey? _disableHotkey; + private bool _updating; + + /// + public HotkeyEnableDisableNodeCustomViewModel(HotkeyEnableDisableNode enableDisableNode, INodeScript script, INodeEditorService nodeEditorService) : base(enableDisableNode, script) + { + _enableDisableNode = enableDisableNode; + _nodeEditorService = nodeEditorService; + + this.WhenActivated(d => + { + Observable.FromEventPattern(x => _enableDisableNode.StorageModified += x, x => _enableDisableNode.StorageModified -= x).Subscribe(_ => Update()).DisposeWith(d); + Update(); + }); + + } + + private void Update() + { + _updating = true; + + EnableHotkey = _enableDisableNode.Storage?.EnableHotkey != null ? new Hotkey(_enableDisableNode.Storage.EnableHotkey) : null; + DisableHotkey = _enableDisableNode.Storage?.DisableHotkey != null ? new Hotkey(_enableDisableNode.Storage.DisableHotkey) : null; + + _updating = false; + } + + public Hotkey? EnableHotkey + { + get => _enableHotkey; + set => this.RaiseAndSetIfChanged(ref _enableHotkey, value); + } + + public Hotkey? DisableHotkey + { + get => _disableHotkey; + set => this.RaiseAndSetIfChanged(ref _disableHotkey, value); + } + + public void Save() + { + if (_updating) + return; + + _nodeEditorService.ExecuteCommand( + Script, + new UpdateStorage(_enableDisableNode, new HotkeyEnableDisableNodeEntity(EnableHotkey, DisableHotkey), "hotkey") + ); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyPressNodeCustomView.axaml b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyPressNodeCustomView.axaml new file mode 100644 index 000000000..19097f664 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyPressNodeCustomView.axaml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyPressNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyPressNodeCustomView.axaml.cs new file mode 100644 index 000000000..fb8f76f7d --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyPressNodeCustomView.axaml.cs @@ -0,0 +1,25 @@ +using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.Input.Screens; + +public partial class HotkeyPressNodeCustomView : ReactiveUserControl +{ + public HotkeyPressNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void HotkeyBox_OnHotkeyChanged(HotkeyBox sender, EventArgs args) + { + ViewModel?.Save(); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyPressNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyPressNodeCustomViewModel.cs new file mode 100644 index 000000000..631a4644a --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyPressNodeCustomViewModel.cs @@ -0,0 +1,55 @@ +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; +using Artemis.UI.Shared.VisualScripting; +using Avalonia.Controls.Mixins; +using ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.Input.Screens; + +public class HotkeyPressNodeCustomViewModel : CustomNodeViewModel +{ + private readonly HotkeyPressNode _pressNode; + private readonly INodeEditorService _nodeEditorService; + private Hotkey? _toggleHotkey; + private bool _updating; + + /// + public HotkeyPressNodeCustomViewModel(HotkeyPressNode pressNode, INodeScript script, INodeEditorService nodeEditorService) : base(pressNode, script) + { + _pressNode = pressNode; + _nodeEditorService = nodeEditorService; + + this.WhenActivated(d => + { + Observable.FromEventPattern(x => _pressNode.StorageModified += x, x => _pressNode.StorageModified -= x).Subscribe(_ => Update()).DisposeWith(d); + Update(); + }); + + } + + private void Update() + { + _updating = true; + ToggleHotkey = _pressNode.Storage?.EnableHotkey != null ? new Hotkey(_pressNode.Storage.EnableHotkey) : null; + _updating = false; + } + + public Hotkey? ToggleHotkey + { + get => _toggleHotkey; + set => this.RaiseAndSetIfChanged(ref _toggleHotkey, value); + } + + public void Save() + { + if (_updating) + return; + + _nodeEditorService.ExecuteCommand( + Script, + new UpdateStorage(_pressNode, new HotkeyEnableDisableNodeEntity(ToggleHotkey, null), "hotkey") + ); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyToggleNodeCustomView.axaml b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyToggleNodeCustomView.axaml new file mode 100644 index 000000000..b147737f3 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyToggleNodeCustomView.axaml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyToggleNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyToggleNodeCustomView.axaml.cs new file mode 100644 index 000000000..46aed80ce --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyToggleNodeCustomView.axaml.cs @@ -0,0 +1,25 @@ +using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.Input.Screens; + +public partial class HotkeyToggleNodeCustomView : ReactiveUserControl +{ + public HotkeyToggleNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void HotkeyBox_OnHotkeyChanged(HotkeyBox sender, EventArgs args) + { + ViewModel?.Save(); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyToggleNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyToggleNodeCustomViewModel.cs new file mode 100644 index 000000000..57ea098d6 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/HotkeyToggleNodeCustomViewModel.cs @@ -0,0 +1,55 @@ +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; +using Artemis.UI.Shared.VisualScripting; +using Avalonia.Controls.Mixins; +using ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.Input.Screens; + +public class HotkeyToggleNodeCustomViewModel : CustomNodeViewModel +{ + private readonly HotkeyToggleNode _toggleNode; + private readonly INodeEditorService _nodeEditorService; + private Hotkey? _toggleHotkey; + private bool _updating; + + /// + public HotkeyToggleNodeCustomViewModel(HotkeyToggleNode toggleNode, INodeScript script, INodeEditorService nodeEditorService) : base(toggleNode, script) + { + _toggleNode = toggleNode; + _nodeEditorService = nodeEditorService; + + this.WhenActivated(d => + { + Observable.FromEventPattern(x => _toggleNode.StorageModified += x, x => _toggleNode.StorageModified -= x).Subscribe(_ => Update()).DisposeWith(d); + Update(); + }); + + } + + private void Update() + { + _updating = true; + ToggleHotkey = _toggleNode.Storage?.EnableHotkey != null ? new Hotkey(_toggleNode.Storage.EnableHotkey) : null; + _updating = false; + } + + public Hotkey? ToggleHotkey + { + get => _toggleHotkey; + set => this.RaiseAndSetIfChanged(ref _toggleHotkey, value); + } + + public void Save() + { + if (_updating) + return; + + _nodeEditorService.ExecuteCommand( + Script, + new UpdateStorage(_toggleNode, new HotkeyEnableDisableNodeEntity(ToggleHotkey, null), "hotkey") + ); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml.cs index 0896825e0..767c68b4b 100644 --- a/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml.cs +++ b/src/Artemis.VisualScripting/Nodes/Input/Screens/PressedKeyPositionNodeCustomView.axaml.cs @@ -1,5 +1,3 @@ -using Avalonia; -using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; diff --git a/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorNodeCustomView.axaml.cs index 000d729e8..7ebdf311b 100644 --- a/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorNodeCustomView.axaml.cs +++ b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorNodeCustomView.axaml.cs @@ -1,5 +1,3 @@ -using Avalonia; -using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; diff --git a/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml.cs index ab950da39..c8a8b3d19 100644 --- a/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml.cs +++ b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml.cs @@ -1,5 +1,3 @@ -using Avalonia; -using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; diff --git a/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomViewModel.cs index b840e0e8f..b1a922752 100644 --- a/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomViewModel.cs @@ -56,7 +56,7 @@ public class ListOperatorPredicateNodeCustomViewModel : CustomNodeViewModel if (_node.Script == null) return; - await _windowService.ShowDialogAsync(("nodeScript", _node.Script)); + await _windowService.ShowDialogAsync(_node.Script); _node.Script.Save(); _node.Storage ??= new ListOperatorEntity(); diff --git a/src/Artemis.VisualScripting/Nodes/Mathematics/NormalizeNode.cs b/src/Artemis.VisualScripting/Nodes/Mathematics/NormalizeNode.cs new file mode 100644 index 000000000..59a6f1f31 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Mathematics/NormalizeNode.cs @@ -0,0 +1,30 @@ +using Artemis.Core; + +namespace Artemis.VisualScripting.Nodes.Mathematics; + +[Node("Normalize", "Normalizes the number into range between 0-1", "Mathematics", InputType = typeof(Numeric), OutputType = typeof(Numeric))] +public class NormalizeNode : Node +{ + public InputPin Input { get; } + public InputPin Start { get; } + public InputPin End { get; } + + public OutputPin Result { get; } + + public NormalizeNode() + { + Input = CreateInputPin("Input"); + Start = CreateInputPin("Start"); + End = CreateInputPin("End"); + + Result = CreateOutputPin(); + } + + public override void Evaluate() + { + double inputValue = Input.Value; + double startValue = Start.Value; + double endValue = End.Value; + Result.Value = (Math.Clamp(inputValue, startValue, endValue) - startValue) / (endValue - startValue); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Mathematics/NumericEasingNode.cs b/src/Artemis.VisualScripting/Nodes/Mathematics/NumericEasingNode.cs new file mode 100644 index 000000000..c2305adaa --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Mathematics/NumericEasingNode.cs @@ -0,0 +1,29 @@ +using Artemis.Core; + +namespace Artemis.VisualScripting.Nodes.Mathematics; + +[Node("Numeric Easing", "Interpolates a value from 0-1 to 0-1 with the given function", "Mathematics", InputType = typeof(Numeric), OutputType = typeof(Numeric))] +public class NumericEasingNode : Node +{ + public InputPin Input { get; } + + public InputPin EasingFunction { get; } + + public OutputPin Result { get; } + + public NumericEasingNode() + { + Input = CreateInputPin("Input"); + EasingFunction = CreateInputPin("Function"); + + Result = CreateOutputPin(); + } + + public override void Evaluate() + { + double inputValue = Input.Value; + double progress = Math.Clamp(inputValue, 0, 1); + + Result.Value = Easings.Interpolate(progress, EasingFunction.Value); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Static/Screens/StaticBooleanValueNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/Static/Screens/StaticBooleanValueNodeCustomView.axaml.cs index a60961c46..6d619c059 100644 --- a/src/Artemis.VisualScripting/Nodes/Static/Screens/StaticBooleanValueNodeCustomView.axaml.cs +++ b/src/Artemis.VisualScripting/Nodes/Static/Screens/StaticBooleanValueNodeCustomView.axaml.cs @@ -1,5 +1,3 @@ -using Avalonia; -using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; diff --git a/src/Artemis.VisualScripting/Nodes/Easing/ColorGradientEasingNode.cs b/src/Artemis.VisualScripting/Nodes/Transition/ColorGradientTransitionNode.cs similarity index 80% rename from src/Artemis.VisualScripting/Nodes/Easing/ColorGradientEasingNode.cs rename to src/Artemis.VisualScripting/Nodes/Transition/ColorGradientTransitionNode.cs index 913497a10..69430d4ea 100644 --- a/src/Artemis.VisualScripting/Nodes/Easing/ColorGradientEasingNode.cs +++ b/src/Artemis.VisualScripting/Nodes/Transition/ColorGradientTransitionNode.cs @@ -1,9 +1,9 @@ using Artemis.Core; -namespace Artemis.VisualScripting.Nodes.Easing; +namespace Artemis.VisualScripting.Nodes.Transition; -[Node("Color Gradient Easing", "Outputs an eased color gradient value", "Easing", InputType = typeof(ColorGradient), OutputType = typeof(ColorGradient))] -public class ColorGradientEasingNode : Node +[Node("Color Gradient Transition", "Outputs smoothly transitioned changes to the input color gradient", "Transition", InputType = typeof(ColorGradient), OutputType = typeof(ColorGradient))] +public class ColorGradientTransitionNode : Node { private DateTime _lastEvaluate = DateTime.MinValue; private float _progress; @@ -11,11 +11,11 @@ public class ColorGradientEasingNode : Node private ColorGradient? _sourceValue; private ColorGradient? _targetValue; - public ColorGradientEasingNode() + public ColorGradientTransitionNode() { Input = CreateInputPin(); - EasingTime = CreateInputPin("delay"); - EasingFunction = CreateInputPin("function"); + EasingTime = CreateInputPin("Delay"); + EasingFunction = CreateInputPin("Function"); Output = CreateOutputPin(); } diff --git a/src/Artemis.VisualScripting/Nodes/Transition/EasingFunctionNode.cs b/src/Artemis.VisualScripting/Nodes/Transition/EasingFunctionNode.cs new file mode 100644 index 000000000..a0086bf27 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Transition/EasingFunctionNode.cs @@ -0,0 +1,20 @@ +using Artemis.Core; +using Artemis.VisualScripting.Nodes.Transition.Screens; + +namespace Artemis.VisualScripting.Nodes.Transition; + +[Node("Easing Function", "Outputs a selectable easing function", "Transition", OutputType = typeof(Easings.Functions))] +public class EasingFunctionNode : Node +{ + public EasingFunctionNode() + { + Output = CreateOutputPin(); + } + + public OutputPin Output { get; } + + public override void Evaluate() + { + Output.Value = Storage; + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Easing/NumericEasingNode.cs b/src/Artemis.VisualScripting/Nodes/Transition/NumericTransitionNode.cs similarity index 80% rename from src/Artemis.VisualScripting/Nodes/Easing/NumericEasingNode.cs rename to src/Artemis.VisualScripting/Nodes/Transition/NumericTransitionNode.cs index 426398869..658ceedf3 100644 --- a/src/Artemis.VisualScripting/Nodes/Easing/NumericEasingNode.cs +++ b/src/Artemis.VisualScripting/Nodes/Transition/NumericTransitionNode.cs @@ -1,9 +1,9 @@ using Artemis.Core; -namespace Artemis.VisualScripting.Nodes.Easing; +namespace Artemis.VisualScripting.Nodes.Transition; -[Node("Numeric Easing", "Outputs an eased numeric value", "Easing", InputType = typeof(Numeric), OutputType = typeof(Numeric))] -public class NumericEasingNode : Node +[Node("Numeric Transition", "Outputs smoothly transitioned changes to the input numeric value", "Transition", InputType = typeof(Numeric), OutputType = typeof(Numeric))] +public class NumericTransitionNode : Node { private float _currentValue; private DateTime _lastEvaluate = DateTime.MinValue; @@ -11,11 +11,11 @@ public class NumericEasingNode : Node private float _sourceValue; private float _targetValue; - public NumericEasingNode() + public NumericTransitionNode() { Input = CreateInputPin(); - EasingTime = CreateInputPin("delay"); - EasingFunction = CreateInputPin("function"); + EasingTime = CreateInputPin("Delay"); + EasingFunction = CreateInputPin("Function"); Output = CreateOutputPin(); } diff --git a/src/Artemis.VisualScripting/Nodes/Easing/SKColorEasingNode.cs b/src/Artemis.VisualScripting/Nodes/Transition/SKColorTransitionNode.cs similarity index 80% rename from src/Artemis.VisualScripting/Nodes/Easing/SKColorEasingNode.cs rename to src/Artemis.VisualScripting/Nodes/Transition/SKColorTransitionNode.cs index c3e41db37..7ffd037b8 100644 --- a/src/Artemis.VisualScripting/Nodes/Easing/SKColorEasingNode.cs +++ b/src/Artemis.VisualScripting/Nodes/Transition/SKColorTransitionNode.cs @@ -1,10 +1,10 @@ using Artemis.Core; using SkiaSharp; -namespace Artemis.VisualScripting.Nodes.Easing; +namespace Artemis.VisualScripting.Nodes.Transition; -[Node("Color Easing", "Outputs an eased color value", "Easing", InputType = typeof(SKColor), OutputType = typeof(SKColor))] -public class SKColorEasingNode : Node +[Node("Color Transition", "Outputs smoothly transitioned changes to the input color", "Transition", InputType = typeof(SKColor), OutputType = typeof(SKColor))] +public class SKColorTransitionNode : Node { private SKColor _currentValue; private DateTime _lastEvaluate = DateTime.MinValue; @@ -12,11 +12,11 @@ public class SKColorEasingNode : Node private SKColor _sourceValue; private SKColor _targetValue; - public SKColorEasingNode() + public SKColorTransitionNode() { Input = CreateInputPin(); - EasingTime = CreateInputPin("delay"); - EasingFunction = CreateInputPin("function"); + EasingTime = CreateInputPin("Delay"); + EasingFunction = CreateInputPin("Function"); Output = CreateOutputPin(); } diff --git a/src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeCustomView.axaml b/src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionNodeCustomView.axaml similarity index 74% rename from src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeCustomView.axaml rename to src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionNodeCustomView.axaml index c4418d2fc..0170bd620 100644 --- a/src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeCustomView.axaml +++ b/src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionNodeCustomView.axaml @@ -2,9 +2,9 @@ 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:screens="clr-namespace:Artemis.VisualScripting.Nodes.Easing.Screens" + xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.Transition.Screens" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.VisualScripting.Nodes.Easing.Screens.EasingTypeNodeCustomView" - x:DataType="screens:EasingTypeNodeCustomViewModel"> + x:Class="Artemis.VisualScripting.Nodes.Transition.Screens.EasingFunctionNodeCustomView" + x:DataType="screens:EasingFunctionNodeCustomViewModel"> \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionNodeCustomView.axaml.cs new file mode 100644 index 000000000..9af92d995 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionNodeCustomView.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.Transition.Screens; + +public class EasingFunctionNodeCustomView : ReactiveUserControl +{ + public EasingFunctionNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionNodeCustomViewModel.cs similarity index 55% rename from src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeCustomViewModel.cs rename to src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionNodeCustomViewModel.cs index 70af9b5a8..01540d7e6 100644 --- a/src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionNodeCustomViewModel.cs @@ -5,26 +5,26 @@ using Artemis.UI.Shared.Services.NodeEditor.Commands; using Artemis.UI.Shared.VisualScripting; using ReactiveUI; -namespace Artemis.VisualScripting.Nodes.Easing.Screens; +namespace Artemis.VisualScripting.Nodes.Transition.Screens; -public class EasingTypeNodeCustomViewModel : CustomNodeViewModel +public class EasingFunctionNodeCustomViewModel : CustomNodeViewModel { - private readonly EasingTypeNode _node; + private readonly EasingFunctionNode _node; private readonly INodeEditorService _nodeEditorService; - public EasingTypeNodeCustomViewModel(EasingTypeNode node, INodeScript script, INodeEditorService nodeEditorService) : base(node, script) + public EasingFunctionNodeCustomViewModel(EasingFunctionNode node, INodeScript script, INodeEditorService nodeEditorService) : base(node, script) { _node = node; _nodeEditorService = nodeEditorService; NodeModified += (_, _) => this.RaisePropertyChanged(nameof(SelectedEasingViewModel)); EasingViewModels = - new ObservableCollection(Enum.GetValues(typeof(Easings.Functions)).Cast().Select(e => new EasingTypeNodeEasingViewModel(e))); + new ObservableCollection(Enum.GetValues(typeof(Easings.Functions)).Cast().Select(e => new EasingFunctionViewModel(e))); } - public ObservableCollection EasingViewModels { get; } + public ObservableCollection EasingViewModels { get; } - public EasingTypeNodeEasingViewModel? SelectedEasingViewModel + public EasingFunctionViewModel? SelectedEasingViewModel { get => EasingViewModels.FirstOrDefault(e => e.EasingFunction == _node.Storage); set diff --git a/src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeEasingView.axaml b/src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionView.axaml similarity index 82% rename from src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeEasingView.axaml rename to src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionView.axaml index 52f2e7de3..93f9b1de0 100644 --- a/src/Artemis.VisualScripting/Nodes/Easing/Screens/EasingTypeNodeEasingView.axaml +++ b/src/Artemis.VisualScripting/Nodes/Transition/Screens/EasingFunctionView.axaml @@ -2,10 +2,10 @@ 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:screens="clr-namespace:Artemis.VisualScripting.Nodes.Easing.Screens" + xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.Transition.Screens" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.VisualScripting.Nodes.Easing.Screens.EasingTypeNodeEasingView" - x:DataType="screens:EasingTypeNodeEasingViewModel"> + x:Class="Artemis.VisualScripting.Nodes.Transition.Screens.EasingFunctionView" + x:DataType="screens:EasingFunctionViewModel"> + + + net6.0 + enable + enable + + + + + + + + + + diff --git a/src/Artemis.WebClient.Updating/DryIoc/ContainerExtensions.cs b/src/Artemis.WebClient.Updating/DryIoc/ContainerExtensions.cs new file mode 100644 index 000000000..e52e454d3 --- /dev/null +++ b/src/Artemis.WebClient.Updating/DryIoc/ContainerExtensions.cs @@ -0,0 +1,26 @@ +using DryIoc; +using DryIoc.Microsoft.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Artemis.WebClient.Updating.DryIoc; + +/// +/// Provides an extension method to register services onto a DryIoc . +/// +public static class ContainerExtensions +{ + /// + /// Registers the updating client into the container. + /// + /// The builder building the current container + public static void RegisterUpdatingClient(this IContainer container) + { + ServiceCollection serviceCollection = new(); + serviceCollection + .AddHttpClient() + .AddUpdatingClient() + .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://updating.artemis-rgb.com/graphql")); + + container.WithDependencyInjectionAdapter(serviceCollection); + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql new file mode 100644 index 000000000..56af7ecb5 --- /dev/null +++ b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql @@ -0,0 +1,31 @@ +query GetReleaseById($id: String!) { + release(id: $id) { + branch + commit + version + previousRelease + changelog + artifacts { + platform + artifactId + fileInfo { + ...fileInfo + } + deltaFileInfo { + ...fileInfo + } + } + } +} + +fragment fileInfo on ArtifactFileInfo { + md5Hash + downloadSize +} + +query GetNextRelease($currentVersion: String!, $branch: String!, $platform: Platform!) { + nextRelease(version: $currentVersion, branch: $branch, platform: $platform) { + id + version + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/schema.extensions.graphql b/src/Artemis.WebClient.Updating/schema.extensions.graphql new file mode 100644 index 000000000..0b5fbd98b --- /dev/null +++ b/src/Artemis.WebClient.Updating/schema.extensions.graphql @@ -0,0 +1,13 @@ +scalar _KeyFieldSet + +directive @key(fields: _KeyFieldSet!) on SCHEMA | OBJECT + +directive @serializationType(name: String!) on SCALAR + +directive @runtimeType(name: String!) on SCALAR + +directive @enumValue(value: String!) on ENUM_VALUE + +directive @rename(name: String!) on INPUT_FIELD_DEFINITION | INPUT_OBJECT | ENUM | ENUM_VALUE + +extend schema @key(fields: "id") \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/schema.graphql b/src/Artemis.WebClient.Updating/schema.graphql new file mode 100644 index 000000000..8a4840e8e --- /dev/null +++ b/src/Artemis.WebClient.Updating/schema.graphql @@ -0,0 +1,262 @@ +# This file was generated based on ".graphqlconfig". Do not edit manually. + +schema { + query: Query + mutation: Mutation +} + +"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." +directive @defer( + "Deferred when true." + if: Boolean, + "If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." + label: String +) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." +directive @stream( + "Streamed when true." + if: Boolean, + "The initial elements that shall be send down to the consumer." + initialCount: Int! = 0, + "If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." + label: String +) on FIELD + +directive @authorize( + "Defines when when the resolver shall be executed.By default the resolver is executed after the policy has determined that the current user is allowed to access the field." + apply: ApplyPolicy! = BEFORE_RESOLVER, + "The name of the authorization policy that determines access to the annotated resource." + policy: String, + "Roles that are allowed to access the annotated resource." + roles: [String!] +) on SCHEMA | OBJECT | FIELD_DEFINITION + +type ArtemisChannel { + branch: String! + releases: Int! +} + +type Artifact { + artifactId: Long! + deltaFileInfo: ArtifactFileInfo! + fileInfo: ArtifactFileInfo! + fileName(deltaFile: Boolean!): String! + platform: Platform! +} + +type ArtifactFileInfo { + downloadSize: Long! + downloads: Long! + md5Hash: String +} + +type Mutation { + updateReleaseChangelog(input: UpdateReleaseChangelogInput!): UpdateReleaseChangelogPayload! +} + +type Query { + channelByBranch(branch: String!): ArtemisChannel + channels: [ArtemisChannel!]! + nextRelease(branch: String!, platform: Platform!, version: String!): Release + release(id: String!): Release + releaseStatistics(order: [ReleaseStatisticSortInput!], where: ReleaseStatisticFilterInput): [ReleaseStatistic!]! + releases(order: [ReleaseSortInput!], where: ReleaseFilterInput): [Release!]! +} + +type Release { + artifacts: [Artifact!]! + branch: String! + changelog: String! + commit: String! + createdAt: DateTime! + id: String! + isDraft: Boolean! + previousRelease: String + version: String! + workflowRunId: Long! +} + +type ReleaseStatistic { + count: Int! + lastReportedUsage: DateTime! + linuxCount: Int! + oSXCount: Int! + releaseId: String! + windowsCount: Int! +} + +type UpdateReleaseChangelogPayload { + release: Release +} + +enum ApplyPolicy { + AFTER_RESOLVER + BEFORE_RESOLVER +} + +enum Platform { + LINUX + OSX + WINDOWS +} + +enum SortEnumType { + ASC + DESC +} + +"The `DateTime` scalar represents an ISO-8601 compliant date time type." +scalar DateTime + +"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." +scalar Long + +input ArtifactFileInfoFilterInput { + and: [ArtifactFileInfoFilterInput!] + downloadSize: ComparableInt64OperationFilterInput + downloads: ComparableInt64OperationFilterInput + md5Hash: StringOperationFilterInput + or: [ArtifactFileInfoFilterInput!] +} + +input ArtifactFilterInput { + and: [ArtifactFilterInput!] + artifactId: ComparableInt64OperationFilterInput + deltaFileInfo: ArtifactFileInfoFilterInput + fileInfo: ArtifactFileInfoFilterInput + or: [ArtifactFilterInput!] + platform: PlatformOperationFilterInput +} + +input BooleanOperationFilterInput { + eq: Boolean + neq: Boolean +} + +input ComparableDateTimeOffsetOperationFilterInput { + eq: DateTime + gt: DateTime + gte: DateTime + in: [DateTime!] + lt: DateTime + lte: DateTime + neq: DateTime + ngt: DateTime + ngte: DateTime + nin: [DateTime!] + nlt: DateTime + nlte: DateTime +} + +input ComparableInt32OperationFilterInput { + eq: Int + gt: Int + gte: Int + in: [Int!] + lt: Int + lte: Int + neq: Int + ngt: Int + ngte: Int + nin: [Int!] + nlt: Int + nlte: Int +} + +input ComparableInt64OperationFilterInput { + eq: Long + gt: Long + gte: Long + in: [Long!] + lt: Long + lte: Long + neq: Long + ngt: Long + ngte: Long + nin: [Long!] + nlt: Long + nlte: Long +} + +input ListFilterInputTypeOfArtifactFilterInput { + all: ArtifactFilterInput + any: Boolean + none: ArtifactFilterInput + some: ArtifactFilterInput +} + +input PlatformOperationFilterInput { + eq: Platform + in: [Platform!] + neq: Platform + nin: [Platform!] +} + +input ReleaseFilterInput { + and: [ReleaseFilterInput!] + artifacts: ListFilterInputTypeOfArtifactFilterInput + branch: StringOperationFilterInput + changelog: StringOperationFilterInput + commit: StringOperationFilterInput + createdAt: ComparableDateTimeOffsetOperationFilterInput + id: StringOperationFilterInput + isDraft: BooleanOperationFilterInput + or: [ReleaseFilterInput!] + previousRelease: StringOperationFilterInput + version: StringOperationFilterInput + workflowRunId: ComparableInt64OperationFilterInput +} + +input ReleaseSortInput { + branch: SortEnumType + changelog: SortEnumType + commit: SortEnumType + createdAt: SortEnumType + id: SortEnumType + isDraft: SortEnumType + previousRelease: SortEnumType + version: SortEnumType + workflowRunId: SortEnumType +} + +input ReleaseStatisticFilterInput { + and: [ReleaseStatisticFilterInput!] + count: ComparableInt32OperationFilterInput + lastReportedUsage: ComparableDateTimeOffsetOperationFilterInput + linuxCount: ComparableInt32OperationFilterInput + oSXCount: ComparableInt32OperationFilterInput + or: [ReleaseStatisticFilterInput!] + releaseId: StringOperationFilterInput + windowsCount: ComparableInt32OperationFilterInput +} + +input ReleaseStatisticSortInput { + count: SortEnumType + lastReportedUsage: SortEnumType + linuxCount: SortEnumType + oSXCount: SortEnumType + releaseId: SortEnumType + windowsCount: SortEnumType +} + +input StringOperationFilterInput { + and: [StringOperationFilterInput!] + contains: String + endsWith: String + eq: String + in: [String] + ncontains: String + nendsWith: String + neq: String + nin: [String] + nstartsWith: String + or: [StringOperationFilterInput!] + startsWith: String +} + +input UpdateReleaseChangelogInput { + changelog: String! + id: String! + isDraft: Boolean! +} diff --git a/src/Artemis.sln b/src/Artemis.sln index dec26a200..2e53b5d9a 100644 --- a/src/Artemis.sln +++ b/src/Artemis.sln @@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.MacOS", "Artemis EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.VisualScripting", "Artemis.VisualScripting\Artemis.VisualScripting.csproj", "{412B921A-26F5-4AE6-8B32-0C19BE54F421}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.WebClient.Updating", "Artemis.WebClient.Updating\Artemis.WebClient.Updating.csproj", "{7C8C6F50-0CC8-45B3-B608-A7218C005E4B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -57,6 +59,10 @@ Global {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Debug|x64.Build.0 = Debug|x64 {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|x64.ActiveCfg = Release|x64 {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|x64.Build.0 = Release|x64 + {7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Debug|x64.Build.0 = Debug|Any CPU + {7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Release|x64.ActiveCfg = Release|Any CPU + {7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE