diff --git a/src/Artemis.Core/Models/Profile/ProfileCategory.cs b/src/Artemis.Core/Models/Profile/ProfileCategory.cs
index cdab02e8a..1a5c6f269 100644
--- a/src/Artemis.Core/Models/Profile/ProfileCategory.cs
+++ b/src/Artemis.Core/Models/Profile/ProfileCategory.cs
@@ -98,26 +98,15 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel
///
/// Adds a profile configuration to this category
///
- public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex)
+ public void AddProfileConfiguration(ProfileConfiguration configuration, ProfileConfiguration? target)
{
- List targetList = ProfileConfigurations.ToList();
-
- // TODO: Look into this, it doesn't seem to make sense
- // Removing the original will shift every item in the list forwards, keep that in mind with the target index
- if (configuration.Category == this && targetIndex != null && targetIndex.Value > targetList.IndexOf(configuration))
- targetIndex -= 1;
-
+ List targetList = ProfileConfigurations.Where(c => c!= configuration).ToList();
configuration.Category.RemoveProfileConfiguration(configuration);
- if (targetIndex != null)
- {
- targetIndex = Math.Clamp(targetIndex.Value, 0, targetList.Count);
- targetList.Insert(targetIndex.Value, configuration);
- }
+ if (target != null)
+ targetList.Insert(targetList.IndexOf(target), configuration);
else
- {
targetList.Add(configuration);
- }
configuration.Category = this;
ProfileConfigurations = new ReadOnlyCollection(targetList);
diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs
index 5eacf18f0..6f2d2b4d4 100644
--- a/src/Artemis.Core/Plugins/Plugin.cs
+++ b/src/Artemis.Core/Plugins/Plugin.cs
@@ -357,10 +357,10 @@ public class Plugin : CorePropertyChanged, IDisposable
return Entity.Features.Any(f => f.IsEnabled) || Features.Any(f => f.AlwaysEnabled);
}
- internal void AutoEnableIfNew()
+ internal bool AutoEnableIfNew()
{
if (_loadedFromStorage)
- return;
+ return false;
// Enabled is preset to true if the plugin meets the following criteria
// - Requires no admin rights
@@ -371,11 +371,13 @@ public class Plugin : CorePropertyChanged, IDisposable
Info.ArePrerequisitesMet();
if (!Entity.IsEnabled)
- return;
+ return false;
// Also auto-enable any non-device provider feature
foreach (PluginFeatureInfo pluginFeatureInfo in Features)
pluginFeatureInfo.Entity.IsEnabled = !pluginFeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider));
+
+ return true;
}
///
diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs
index b27a8601b..da0f62fd7 100644
--- a/src/Artemis.Core/Services/CoreService.cs
+++ b/src/Artemis.Core/Services/CoreService.cs
@@ -71,6 +71,7 @@ internal class CoreService : ICoreService
// Initialize the services
_pluginManagementService.CopyBuiltInPlugins();
_pluginManagementService.LoadPlugins(IsElevated);
+ _pluginManagementService.StartHotReload();
_renderService.Initialize();
OnInitialized();
diff --git a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs
index 932e3f507..fbd97344d 100644
--- a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs
+++ b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs
@@ -16,7 +16,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// Gets a list containing additional directories in which plugins are located, used while loading plugins.
///
List AdditionalPluginDirectories { get; }
-
+
///
/// Indicates whether or not plugins are currently being loaded
///
@@ -33,6 +33,11 @@ public interface IPluginManagementService : IArtemisService, IDisposable
///
void LoadPlugins(bool isElevated);
+ ///
+ /// Starts monitoring plugin directories for changes and reloads plugins when changes are detected
+ ///
+ void StartHotReload();
+
///
/// Unloads all installed plugins.
///
@@ -145,7 +150,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable
///
///
DeviceProvider GetDeviceProviderByDevice(IRGBDevice device);
-
+
///
/// Occurs when built-in plugins are being loaded
///
diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs
index 62bc658ac..0334417f6 100644
--- a/src/Artemis.Core/Services/PluginManagementService.cs
+++ b/src/Artemis.Core/Services/PluginManagementService.cs
@@ -41,37 +41,6 @@ internal class PluginManagementService : IPluginManagementService
_pluginRepository = pluginRepository;
_deviceRepository = deviceRepository;
_plugins = new List();
-
- StartHotReload();
- }
-
- private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory)
- {
- ZipArchiveEntry metaDataFileEntry = zipArchive.Entries.First(e => e.Name == "plugin.json");
- DirectoryInfo pluginDirectory = new(Path.Combine(Constants.PluginsFolder, targetDirectory));
- bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock"));
-
- // Remove the old directory if it exists
- if (Directory.Exists(pluginDirectory.FullName))
- pluginDirectory.Delete(true);
-
- // Extract everything in the same archive directory to the unique plugin directory
- Utilities.CreateAccessibleDirectory(pluginDirectory.FullName);
- string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, "");
- foreach (ZipArchiveEntry zipArchiveEntry in zipArchive.Entries)
- {
- if (zipArchiveEntry.FullName.StartsWith(metaDataDirectory) && !zipArchiveEntry.FullName.EndsWith("/"))
- {
- string target = Path.Combine(pluginDirectory.FullName, zipArchiveEntry.FullName.Remove(0, metaDataDirectory.Length));
- // Create folders
- Utilities.CreateAccessibleDirectory(Path.GetDirectoryName(target)!);
- // Extract files
- zipArchiveEntry.ExtractToFile(target);
- }
- }
-
- if (createLockFile)
- File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close();
}
public List AdditionalPluginDirectories { get; } = new();
@@ -155,6 +124,35 @@ internal class PluginManagementService : IPluginManagementService
}
}
+ private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory)
+ {
+ ZipArchiveEntry metaDataFileEntry = zipArchive.Entries.First(e => e.Name == "plugin.json");
+ DirectoryInfo pluginDirectory = new(Path.Combine(Constants.PluginsFolder, targetDirectory));
+ bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock"));
+
+ // Remove the old directory if it exists
+ if (Directory.Exists(pluginDirectory.FullName))
+ pluginDirectory.Delete(true);
+
+ // Extract everything in the same archive directory to the unique plugin directory
+ Utilities.CreateAccessibleDirectory(pluginDirectory.FullName);
+ string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, "");
+ foreach (ZipArchiveEntry zipArchiveEntry in zipArchive.Entries)
+ {
+ if (zipArchiveEntry.FullName.StartsWith(metaDataDirectory) && !zipArchiveEntry.FullName.EndsWith("/"))
+ {
+ string target = Path.Combine(pluginDirectory.FullName, zipArchiveEntry.FullName.Remove(0, metaDataDirectory.Length));
+ // Create folders
+ Utilities.CreateAccessibleDirectory(Path.GetDirectoryName(target)!);
+ // Extract files
+ zipArchiveEntry.ExtractToFile(target);
+ }
+ }
+
+ if (createLockFile)
+ File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close();
+ }
+
#endregion
public List GetAllPlugins()
@@ -444,7 +442,9 @@ internal class PluginManagementService : IPluginManagementService
_logger.Warning("Plugin {plugin} contains no features", plugin);
// It is appropriate to call this now that we have the features of this plugin
- plugin.AutoEnableIfNew();
+ bool autoEnabled = plugin.AutoEnableIfNew();
+ if (autoEnabled)
+ _pluginRepository.SavePlugin(entity);
List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(PluginBootstrapper).IsAssignableFrom(t)).ToList();
if (bootstrappers.Count > 1)
@@ -894,7 +894,7 @@ internal class PluginManagementService : IPluginManagementService
#region Hot Reload
- private void StartHotReload()
+ public void StartHotReload()
{
// Watch for changes in the plugin directory, "plugin.json".
// If this file is changed, reload the plugin.
diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs
index c8a5f8435..2a55664a9 100644
--- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs
+++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs
@@ -115,9 +115,9 @@ public interface IProfileService : IArtemisService
/// any changes are made to it.
///
/// Text to add after the name of the profile (separated by a dash).
- /// The index at which to import the profile into the category.
+ /// The profile before which to import the profile into the category.
/// The resulting profile configuration.
- Task ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported", int targetIndex = 0);
+ Task ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported", ProfileConfiguration? target = null);
///
/// Imports the provided ZIP archive stream into the provided profile configuration
diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs
index 6b9889e19..913c38e0e 100644
--- a/src/Artemis.Core/Services/Storage/ProfileService.cs
+++ b/src/Artemis.Core/Services/Storage/ProfileService.cs
@@ -262,7 +262,7 @@ internal class ProfileService : IProfileService
{
ProfileConfiguration configuration = new(category, name, icon);
- category.AddProfileConfiguration(configuration, 0);
+ category.AddProfileConfiguration(configuration, category.ProfileConfigurations.FirstOrDefault());
SaveProfileCategory(category);
return configuration;
}
@@ -354,7 +354,7 @@ internal class ProfileService : IProfileService
}
///
- public async Task ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix, int targetIndex = 0)
+ public async Task ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix, ProfileConfiguration? target)
{
using ZipArchive archive = new(archiveStream, ZipArchiveMode.Read, true);
@@ -424,7 +424,7 @@ internal class ProfileService : IProfileService
profileConfiguration.Name = $"{profileConfiguration.Name} - {nameAffix}";
profileConfiguration.Entity.ProfileConfiguration.ProfileId = profileEntity.Id;
- category.AddProfileConfiguration(profileConfiguration, targetIndex);
+ category.AddProfileConfiguration(profileConfiguration, target);
List modules = _pluginManagementService.GetFeaturesOfType();
profileConfiguration.LoadModules(modules);
@@ -436,7 +436,7 @@ internal class ProfileService : IProfileService
///
public async Task OverwriteProfile(MemoryStream archiveStream, ProfileConfiguration profileConfiguration)
{
- ProfileConfiguration imported = await ImportProfile(archiveStream, profileConfiguration.Category, true, true, null, profileConfiguration.Order + 1);
+ ProfileConfiguration imported = await ImportProfile(archiveStream, profileConfiguration.Category, true, true, null, profileConfiguration);
RemoveProfileConfiguration(profileConfiguration);
SaveProfileCategory(imported.Category);
diff --git a/src/Artemis.Storage.Legacy/Entities/Workshop/EntryEntity.cs b/src/Artemis.Storage.Legacy/Entities/Workshop/EntryEntity.cs
index ec0d6c091..aaeecd3bd 100644
--- a/src/Artemis.Storage.Legacy/Entities/Workshop/EntryEntity.cs
+++ b/src/Artemis.Storage.Legacy/Entities/Workshop/EntryEntity.cs
@@ -1,4 +1,6 @@
-namespace Artemis.Storage.Legacy.Entities.Workshop;
+using System.Text.Json.Nodes;
+
+namespace Artemis.Storage.Legacy.Entities.Workshop;
internal class EntryEntity
{
@@ -14,7 +16,7 @@ internal class EntryEntity
public string ReleaseVersion { get; set; } = string.Empty;
public DateTimeOffset InstalledAt { get; set; }
- public Dictionary? Metadata { get; set; }
+ public Dictionary? Metadata { get; set; }
public Storage.Entities.Workshop.EntryEntity Migrate()
{
@@ -29,7 +31,7 @@ internal class EntryEntity
ReleaseId = ReleaseId,
ReleaseVersion = ReleaseVersion,
InstalledAt = InstalledAt,
- Metadata = Metadata ?? new Dictionary()
+ Metadata = Metadata ?? new Dictionary()
};
}
}
\ No newline at end of file
diff --git a/src/Artemis.Storage/ArtemisDbContext.cs b/src/Artemis.Storage/ArtemisDbContext.cs
index 2a77604fa..32acba6a6 100644
--- a/src/Artemis.Storage/ArtemisDbContext.cs
+++ b/src/Artemis.Storage/ArtemisDbContext.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
+using System.Text.Json.Nodes;
using Artemis.Storage.Entities.General;
using Artemis.Storage.Entities.Plugins;
using Artemis.Storage.Entities.Profile;
@@ -41,7 +42,7 @@ public class ArtemisDbContext : DbContext
.Property(e => e.Metadata)
.HasConversion(
v => JsonSerializer.Serialize(v, JsonSerializerOptions),
- v => JsonSerializer.Deserialize>(v, JsonSerializerOptions) ?? new Dictionary());
+ v => JsonSerializer.Deserialize>(v, JsonSerializerOptions) ?? new Dictionary());
modelBuilder.Entity()
.Property(e => e.ProfileConfiguration)
diff --git a/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs b/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs
index 37cdd4cfa..ad11d6188 100644
--- a/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs
+++ b/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Text.Json.Nodes;
using Microsoft.EntityFrameworkCore;
namespace Artemis.Storage.Entities.Workshop;
@@ -19,5 +20,5 @@ public class EntryEntity
public string ReleaseVersion { get; set; } = string.Empty;
public DateTimeOffset InstalledAt { get; set; }
- public Dictionary? Metadata { get; set; }
+ public Dictionary? Metadata { get; set; }
}
\ No newline at end of file
diff --git a/src/Artemis.Storage/Repositories/ProfileRepository.cs b/src/Artemis.Storage/Repositories/ProfileRepository.cs
index 78c9ca6a0..65e7dab78 100644
--- a/src/Artemis.Storage/Repositories/ProfileRepository.cs
+++ b/src/Artemis.Storage/Repositories/ProfileRepository.cs
@@ -8,10 +8,11 @@ using Artemis.Storage.Exceptions;
using Artemis.Storage.Migrations;
using Artemis.Storage.Repositories.Interfaces;
using Microsoft.EntityFrameworkCore;
+using Serilog;
namespace Artemis.Storage.Repositories;
-public class ProfileRepository(Func getContext, List profileMigrators) : IProfileRepository
+public class ProfileRepository(ILogger logger, Func getContext, List profileMigrators) : IProfileRepository
{
public void Add(ProfileContainerEntity profileContainerEntity)
{
@@ -83,6 +84,8 @@ public class ProfileRepository(Func getContext, List())
continue;
+
+ logger.Information("Migrating profile from version {OldVersion} to {NewVersion}", configurationJson["Version"], profileMigrator.Version);
profileMigrator.Migrate(configurationJson, profileJson);
configurationJson["Version"] = profileMigrator.Version;
diff --git a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs
index 2ee9934d6..6b7fd2a02 100644
--- a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs
+++ b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs
@@ -47,7 +47,7 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
: new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
}
else if (ConfigurationIcon.IconBytes != null)
- Dispatcher.UIThread.Post(() => LoadFromBitmap(ConfigurationIcon, new MemoryStream(ConfigurationIcon.IconBytes)), DispatcherPriority.ApplicationIdle);
+ Dispatcher.UIThread.Post(LoadFromBitmap, DispatcherPriority.ApplicationIdle);
else
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
}
@@ -57,14 +57,17 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
}
}
- private void LoadFromBitmap(Core.ProfileConfigurationIcon configurationIcon, Stream stream)
+ private void LoadFromBitmap()
{
try
{
- _stream = stream;
- if (!configurationIcon.Fill)
+ if (ConfigurationIcon?.IconBytes == null)
+ return;
+
+ _stream = new MemoryStream(ConfigurationIcon.IconBytes);
+ if (!ConfigurationIcon.Fill)
{
- Content = new Image {Source = new Bitmap(stream)};
+ Content = new Image {Source = new Bitmap(_stream)};
return;
}
@@ -73,7 +76,7 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
Background = TextElement.GetForeground(this),
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch,
- OpacityMask = new ImageBrush(new Bitmap(stream))
+ OpacityMask = new ImageBrush(new Bitmap(_stream))
};
}
catch (Exception)
diff --git a/src/Artemis.UI/Screens/Sidebar/Behaviors/ProfileConfigurationDropHandler.cs b/src/Artemis.UI/Screens/Sidebar/Behaviors/ProfileConfigurationDropHandler.cs
index 8e4d4d108..b07a43196 100644
--- a/src/Artemis.UI/Screens/Sidebar/Behaviors/ProfileConfigurationDropHandler.cs
+++ b/src/Artemis.UI/Screens/Sidebar/Behaviors/ProfileConfigurationDropHandler.cs
@@ -68,8 +68,8 @@ public class SidebarCategoryViewDropHandler : DropHandlerBase
{
int index = vm.ProfileConfigurations.IndexOf(targetItem);
if (!before)
- index++;
- vm.AddProfileConfiguration(sourceItem.ProfileConfiguration, index);
+ targetItem = index < vm.ProfileConfigurations.Count - 1 ? vm.ProfileConfigurations[index + 1] : null;
+ vm.AddProfileConfiguration(sourceItem.ProfileConfiguration, targetItem?.ProfileConfiguration);
}
else
{
diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs
index 3892fee70..fe6cf2d9e 100644
--- a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs
+++ b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs
@@ -103,10 +103,10 @@ public partial class SidebarCategoryViewModel : ActivatableViewModelBase
public bool IsCollapsed => _isCollapsed?.Value ?? false;
public bool IsSuspended => _isSuspended?.Value ?? false;
- public void AddProfileConfiguration(ProfileConfiguration profileConfiguration, int? index)
+ public void AddProfileConfiguration(ProfileConfiguration profileConfiguration, ProfileConfiguration? target)
{
ProfileCategory oldCategory = profileConfiguration.Category;
- ProfileCategory.AddProfileConfiguration(profileConfiguration, index);
+ ProfileCategory.AddProfileConfiguration(profileConfiguration, target);
_profileService.SaveProfileCategory(ProfileCategory);
// If the profile moved to a new category, also save the old category
diff --git a/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs b/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs
index 90aa4108e..ce90444b0 100644
--- a/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs
+++ b/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs
@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Nodes;
using Artemis.Core;
using Artemis.Storage.Entities.Workshop;
@@ -6,7 +8,7 @@ namespace Artemis.WebClient.Workshop.Models;
public class InstalledEntry
{
- private Dictionary _metadata = new();
+ private Dictionary _metadata = new();
internal InstalledEntry(EntryEntity entity)
{
@@ -52,7 +54,7 @@ public class InstalledEntry
ReleaseVersion = Entity.ReleaseVersion;
InstalledAt = Entity.InstalledAt;
- _metadata = Entity.Metadata != null ? new Dictionary(Entity.Metadata) : new Dictionary();
+ _metadata = Entity.Metadata != null ? new Dictionary(Entity.Metadata) : new Dictionary();
}
internal void Save()
@@ -67,7 +69,7 @@ public class InstalledEntry
Entity.ReleaseVersion = ReleaseVersion;
Entity.InstalledAt = InstalledAt;
- Entity.Metadata = new Dictionary(_metadata);
+ Entity.Metadata = new Dictionary(_metadata);
}
///
@@ -80,14 +82,14 @@ public class InstalledEntry
/// if the metadata contains an element with the specified key; otherwise, .
public bool TryGetMetadata(string key, [NotNullWhen(true)] out T? value)
{
- if (!_metadata.TryGetValue(key, out object? objectValue) || objectValue is not T result)
+ if (!_metadata.TryGetValue(key, out JsonNode? element))
{
value = default;
return false;
}
- value = result;
- return true;
+ value = element.GetValue();
+ return value != null;
}
///
@@ -97,7 +99,7 @@ public class InstalledEntry
/// The value to set.
public void SetMetadata(string key, object value)
{
- _metadata[key] = value;
+ _metadata[key] = JsonSerializer.SerializeToNode(value) ?? throw new InvalidOperationException();
}
///