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

Core - Delay start watching plugins for hot reload after initializing

UI - Simplify category management logic
UI - Avoid crash during profile icon load
Storage - Fix entry metadata retrieval
This commit is contained in:
Robert 2024-03-12 21:26:47 +01:00
parent f66df43cc2
commit d7a0c2ac4a
15 changed files with 91 additions and 82 deletions

View File

@ -98,26 +98,15 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel
/// <summary> /// <summary>
/// Adds a profile configuration to this category /// Adds a profile configuration to this category
/// </summary> /// </summary>
public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex) public void AddProfileConfiguration(ProfileConfiguration configuration, ProfileConfiguration? target)
{ {
List<ProfileConfiguration> targetList = ProfileConfigurations.ToList(); List<ProfileConfiguration> targetList = ProfileConfigurations.Where(c => c!= configuration).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;
configuration.Category.RemoveProfileConfiguration(configuration); configuration.Category.RemoveProfileConfiguration(configuration);
if (targetIndex != null) if (target != null)
{ targetList.Insert(targetList.IndexOf(target), configuration);
targetIndex = Math.Clamp(targetIndex.Value, 0, targetList.Count);
targetList.Insert(targetIndex.Value, configuration);
}
else else
{
targetList.Add(configuration); targetList.Add(configuration);
}
configuration.Category = this; configuration.Category = this;
ProfileConfigurations = new ReadOnlyCollection<ProfileConfiguration>(targetList); ProfileConfigurations = new ReadOnlyCollection<ProfileConfiguration>(targetList);

View File

@ -357,10 +357,10 @@ public class Plugin : CorePropertyChanged, IDisposable
return Entity.Features.Any(f => f.IsEnabled) || Features.Any(f => f.AlwaysEnabled); return Entity.Features.Any(f => f.IsEnabled) || Features.Any(f => f.AlwaysEnabled);
} }
internal void AutoEnableIfNew() internal bool AutoEnableIfNew()
{ {
if (_loadedFromStorage) if (_loadedFromStorage)
return; return false;
// Enabled is preset to true if the plugin meets the following criteria // Enabled is preset to true if the plugin meets the following criteria
// - Requires no admin rights // - Requires no admin rights
@ -371,11 +371,13 @@ public class Plugin : CorePropertyChanged, IDisposable
Info.ArePrerequisitesMet(); Info.ArePrerequisitesMet();
if (!Entity.IsEnabled) if (!Entity.IsEnabled)
return; return false;
// Also auto-enable any non-device provider feature // Also auto-enable any non-device provider feature
foreach (PluginFeatureInfo pluginFeatureInfo in Features) foreach (PluginFeatureInfo pluginFeatureInfo in Features)
pluginFeatureInfo.Entity.IsEnabled = !pluginFeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider)); pluginFeatureInfo.Entity.IsEnabled = !pluginFeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider));
return true;
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -71,6 +71,7 @@ internal class CoreService : ICoreService
// Initialize the services // Initialize the services
_pluginManagementService.CopyBuiltInPlugins(); _pluginManagementService.CopyBuiltInPlugins();
_pluginManagementService.LoadPlugins(IsElevated); _pluginManagementService.LoadPlugins(IsElevated);
_pluginManagementService.StartHotReload();
_renderService.Initialize(); _renderService.Initialize();
OnInitialized(); OnInitialized();

View File

@ -16,7 +16,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// Gets a list containing additional directories in which plugins are located, used while loading plugins. /// Gets a list containing additional directories in which plugins are located, used while loading plugins.
/// </summary> /// </summary>
List<DirectoryInfo> AdditionalPluginDirectories { get; } List<DirectoryInfo> AdditionalPluginDirectories { get; }
/// <summary> /// <summary>
/// Indicates whether or not plugins are currently being loaded /// Indicates whether or not plugins are currently being loaded
/// </summary> /// </summary>
@ -33,6 +33,11 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// </summary> /// </summary>
void LoadPlugins(bool isElevated); void LoadPlugins(bool isElevated);
/// <summary>
/// Starts monitoring plugin directories for changes and reloads plugins when changes are detected
/// </summary>
void StartHotReload();
/// <summary> /// <summary>
/// Unloads all installed plugins. /// Unloads all installed plugins.
/// </summary> /// </summary>
@ -145,7 +150,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// <param name="device"></param> /// <param name="device"></param>
/// <returns></returns> /// <returns></returns>
DeviceProvider GetDeviceProviderByDevice(IRGBDevice device); DeviceProvider GetDeviceProviderByDevice(IRGBDevice device);
/// <summary> /// <summary>
/// Occurs when built-in plugins are being loaded /// Occurs when built-in plugins are being loaded
/// </summary> /// </summary>

View File

@ -41,37 +41,6 @@ internal class PluginManagementService : IPluginManagementService
_pluginRepository = pluginRepository; _pluginRepository = pluginRepository;
_deviceRepository = deviceRepository; _deviceRepository = deviceRepository;
_plugins = new List<Plugin>(); _plugins = new List<Plugin>();
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<DirectoryInfo> AdditionalPluginDirectories { get; } = new(); public List<DirectoryInfo> 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 #endregion
public List<Plugin> GetAllPlugins() public List<Plugin> GetAllPlugins()
@ -444,7 +442,9 @@ internal class PluginManagementService : IPluginManagementService
_logger.Warning("Plugin {plugin} contains no features", plugin); _logger.Warning("Plugin {plugin} contains no features", plugin);
// It is appropriate to call this now that we have the features of this 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<Type> bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(PluginBootstrapper).IsAssignableFrom(t)).ToList(); List<Type> bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(PluginBootstrapper).IsAssignableFrom(t)).ToList();
if (bootstrappers.Count > 1) if (bootstrappers.Count > 1)
@ -894,7 +894,7 @@ internal class PluginManagementService : IPluginManagementService
#region Hot Reload #region Hot Reload
private void StartHotReload() public void StartHotReload()
{ {
// Watch for changes in the plugin directory, "plugin.json". // Watch for changes in the plugin directory, "plugin.json".
// If this file is changed, reload the plugin. // If this file is changed, reload the plugin.

View File

@ -115,9 +115,9 @@ public interface IProfileService : IArtemisService
/// any changes are made to it. /// any changes are made to it.
/// </param> /// </param>
/// <param name="nameAffix">Text to add after the name of the profile (separated by a dash).</param> /// <param name="nameAffix">Text to add after the name of the profile (separated by a dash).</param>
/// <param name="targetIndex">The index at which to import the profile into the category.</param> /// <param name="target">The profile before which to import the profile into the category.</param>
/// <returns>The resulting profile configuration.</returns> /// <returns>The resulting profile configuration.</returns>
Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported", int targetIndex = 0); Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported", ProfileConfiguration? target = null);
/// <summary> /// <summary>
/// Imports the provided ZIP archive stream into the provided profile configuration /// Imports the provided ZIP archive stream into the provided profile configuration

View File

@ -262,7 +262,7 @@ internal class ProfileService : IProfileService
{ {
ProfileConfiguration configuration = new(category, name, icon); ProfileConfiguration configuration = new(category, name, icon);
category.AddProfileConfiguration(configuration, 0); category.AddProfileConfiguration(configuration, category.ProfileConfigurations.FirstOrDefault());
SaveProfileCategory(category); SaveProfileCategory(category);
return configuration; return configuration;
} }
@ -354,7 +354,7 @@ internal class ProfileService : IProfileService
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix, int targetIndex = 0) public async Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix, ProfileConfiguration? target)
{ {
using ZipArchive archive = new(archiveStream, ZipArchiveMode.Read, true); using ZipArchive archive = new(archiveStream, ZipArchiveMode.Read, true);
@ -424,7 +424,7 @@ internal class ProfileService : IProfileService
profileConfiguration.Name = $"{profileConfiguration.Name} - {nameAffix}"; profileConfiguration.Name = $"{profileConfiguration.Name} - {nameAffix}";
profileConfiguration.Entity.ProfileConfiguration.ProfileId = profileEntity.Id; profileConfiguration.Entity.ProfileConfiguration.ProfileId = profileEntity.Id;
category.AddProfileConfiguration(profileConfiguration, targetIndex); category.AddProfileConfiguration(profileConfiguration, target);
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>(); List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>();
profileConfiguration.LoadModules(modules); profileConfiguration.LoadModules(modules);
@ -436,7 +436,7 @@ internal class ProfileService : IProfileService
/// <inheritdoc /> /// <inheritdoc />
public async Task<ProfileConfiguration> OverwriteProfile(MemoryStream archiveStream, ProfileConfiguration profileConfiguration) public async Task<ProfileConfiguration> 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); RemoveProfileConfiguration(profileConfiguration);
SaveProfileCategory(imported.Category); SaveProfileCategory(imported.Category);

View File

@ -1,4 +1,6 @@
namespace Artemis.Storage.Legacy.Entities.Workshop; using System.Text.Json.Nodes;
namespace Artemis.Storage.Legacy.Entities.Workshop;
internal class EntryEntity internal class EntryEntity
{ {
@ -14,7 +16,7 @@ internal class EntryEntity
public string ReleaseVersion { get; set; } = string.Empty; public string ReleaseVersion { get; set; } = string.Empty;
public DateTimeOffset InstalledAt { get; set; } public DateTimeOffset InstalledAt { get; set; }
public Dictionary<string, object>? Metadata { get; set; } public Dictionary<string, JsonNode>? Metadata { get; set; }
public Storage.Entities.Workshop.EntryEntity Migrate() public Storage.Entities.Workshop.EntryEntity Migrate()
{ {
@ -29,7 +31,7 @@ internal class EntryEntity
ReleaseId = ReleaseId, ReleaseId = ReleaseId,
ReleaseVersion = ReleaseVersion, ReleaseVersion = ReleaseVersion,
InstalledAt = InstalledAt, InstalledAt = InstalledAt,
Metadata = Metadata ?? new Dictionary<string, object>() Metadata = Metadata ?? new Dictionary<string, JsonNode>()
}; };
} }
} }

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using Artemis.Storage.Entities.General; using Artemis.Storage.Entities.General;
using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Plugins;
using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile;
@ -41,7 +42,7 @@ public class ArtemisDbContext : DbContext
.Property(e => e.Metadata) .Property(e => e.Metadata)
.HasConversion( .HasConversion(
v => JsonSerializer.Serialize(v, JsonSerializerOptions), v => JsonSerializer.Serialize(v, JsonSerializerOptions),
v => JsonSerializer.Deserialize<Dictionary<string, object>>(v, JsonSerializerOptions) ?? new Dictionary<string, object>()); v => JsonSerializer.Deserialize<Dictionary<string, JsonNode>>(v, JsonSerializerOptions) ?? new Dictionary<string, JsonNode>());
modelBuilder.Entity<ProfileContainerEntity>() modelBuilder.Entity<ProfileContainerEntity>()
.Property(e => e.ProfileConfiguration) .Property(e => e.ProfileConfiguration)

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Nodes;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Artemis.Storage.Entities.Workshop; namespace Artemis.Storage.Entities.Workshop;
@ -19,5 +20,5 @@ public class EntryEntity
public string ReleaseVersion { get; set; } = string.Empty; public string ReleaseVersion { get; set; } = string.Empty;
public DateTimeOffset InstalledAt { get; set; } public DateTimeOffset InstalledAt { get; set; }
public Dictionary<string, object>? Metadata { get; set; } public Dictionary<string, JsonNode>? Metadata { get; set; }
} }

View File

@ -8,10 +8,11 @@ using Artemis.Storage.Exceptions;
using Artemis.Storage.Migrations; using Artemis.Storage.Migrations;
using Artemis.Storage.Repositories.Interfaces; using Artemis.Storage.Repositories.Interfaces;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Serilog;
namespace Artemis.Storage.Repositories; namespace Artemis.Storage.Repositories;
public class ProfileRepository(Func<ArtemisDbContext> getContext, List<IProfileMigration> profileMigrators) : IProfileRepository public class ProfileRepository(ILogger logger, Func<ArtemisDbContext> getContext, List<IProfileMigration> profileMigrators) : IProfileRepository
{ {
public void Add(ProfileContainerEntity profileContainerEntity) public void Add(ProfileContainerEntity profileContainerEntity)
{ {
@ -83,6 +84,8 @@ public class ProfileRepository(Func<ArtemisDbContext> getContext, List<IProfileM
{ {
if (profileMigrator.Version <= configurationJson["Version"]!.GetValue<int>()) if (profileMigrator.Version <= configurationJson["Version"]!.GetValue<int>())
continue; continue;
logger.Information("Migrating profile from version {OldVersion} to {NewVersion}", configurationJson["Version"], profileMigrator.Version);
profileMigrator.Migrate(configurationJson, profileJson); profileMigrator.Migrate(configurationJson, profileJson);
configurationJson["Version"] = profileMigrator.Version; configurationJson["Version"] = profileMigrator.Version;

View File

@ -47,7 +47,7 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
: new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; : new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
} }
else if (ConfigurationIcon.IconBytes != null) else if (ConfigurationIcon.IconBytes != null)
Dispatcher.UIThread.Post(() => LoadFromBitmap(ConfigurationIcon, new MemoryStream(ConfigurationIcon.IconBytes)), DispatcherPriority.ApplicationIdle); Dispatcher.UIThread.Post(LoadFromBitmap, DispatcherPriority.ApplicationIdle);
else else
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; 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 try
{ {
_stream = stream; if (ConfigurationIcon?.IconBytes == null)
if (!configurationIcon.Fill) return;
_stream = new MemoryStream(ConfigurationIcon.IconBytes);
if (!ConfigurationIcon.Fill)
{ {
Content = new Image {Source = new Bitmap(stream)}; Content = new Image {Source = new Bitmap(_stream)};
return; return;
} }
@ -73,7 +76,7 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable
Background = TextElement.GetForeground(this), Background = TextElement.GetForeground(this),
VerticalAlignment = VerticalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch,
OpacityMask = new ImageBrush(new Bitmap(stream)) OpacityMask = new ImageBrush(new Bitmap(_stream))
}; };
} }
catch (Exception) catch (Exception)

View File

@ -68,8 +68,8 @@ public class SidebarCategoryViewDropHandler : DropHandlerBase
{ {
int index = vm.ProfileConfigurations.IndexOf(targetItem); int index = vm.ProfileConfigurations.IndexOf(targetItem);
if (!before) if (!before)
index++; targetItem = index < vm.ProfileConfigurations.Count - 1 ? vm.ProfileConfigurations[index + 1] : null;
vm.AddProfileConfiguration(sourceItem.ProfileConfiguration, index); vm.AddProfileConfiguration(sourceItem.ProfileConfiguration, targetItem?.ProfileConfiguration);
} }
else else
{ {

View File

@ -103,10 +103,10 @@ public partial class SidebarCategoryViewModel : ActivatableViewModelBase
public bool IsCollapsed => _isCollapsed?.Value ?? false; public bool IsCollapsed => _isCollapsed?.Value ?? false;
public bool IsSuspended => _isSuspended?.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 oldCategory = profileConfiguration.Category;
ProfileCategory.AddProfileConfiguration(profileConfiguration, index); ProfileCategory.AddProfileConfiguration(profileConfiguration, target);
_profileService.SaveProfileCategory(ProfileCategory); _profileService.SaveProfileCategory(ProfileCategory);
// If the profile moved to a new category, also save the old category // If the profile moved to a new category, also save the old category

View File

@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using Artemis.Core; using Artemis.Core;
using Artemis.Storage.Entities.Workshop; using Artemis.Storage.Entities.Workshop;
@ -6,7 +8,7 @@ namespace Artemis.WebClient.Workshop.Models;
public class InstalledEntry public class InstalledEntry
{ {
private Dictionary<string, object> _metadata = new(); private Dictionary<string, JsonNode> _metadata = new();
internal InstalledEntry(EntryEntity entity) internal InstalledEntry(EntryEntity entity)
{ {
@ -52,7 +54,7 @@ public class InstalledEntry
ReleaseVersion = Entity.ReleaseVersion; ReleaseVersion = Entity.ReleaseVersion;
InstalledAt = Entity.InstalledAt; InstalledAt = Entity.InstalledAt;
_metadata = Entity.Metadata != null ? new Dictionary<string, object>(Entity.Metadata) : new Dictionary<string, object>(); _metadata = Entity.Metadata != null ? new Dictionary<string, JsonNode>(Entity.Metadata) : new Dictionary<string, JsonNode>();
} }
internal void Save() internal void Save()
@ -67,7 +69,7 @@ public class InstalledEntry
Entity.ReleaseVersion = ReleaseVersion; Entity.ReleaseVersion = ReleaseVersion;
Entity.InstalledAt = InstalledAt; Entity.InstalledAt = InstalledAt;
Entity.Metadata = new Dictionary<string, object>(_metadata); Entity.Metadata = new Dictionary<string, JsonNode>(_metadata);
} }
/// <summary> /// <summary>
@ -80,14 +82,14 @@ public class InstalledEntry
/// <returns><see langword="true"/> if the metadata contains an element with the specified key; otherwise, <see langword="false"/>.</returns> /// <returns><see langword="true"/> if the metadata contains an element with the specified key; otherwise, <see langword="false"/>.</returns>
public bool TryGetMetadata<T>(string key, [NotNullWhen(true)] out T? value) public bool TryGetMetadata<T>(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; value = default;
return false; return false;
} }
value = result; value = element.GetValue<T>();
return true; return value != null;
} }
/// <summary> /// <summary>
@ -97,7 +99,7 @@ public class InstalledEntry
/// <param name="value">The value to set.</param> /// <param name="value">The value to set.</param>
public void SetMetadata(string key, object value) public void SetMetadata(string key, object value)
{ {
_metadata[key] = value; _metadata[key] = JsonSerializer.SerializeToNode(value) ?? throw new InvalidOperationException();
} }
/// <summary> /// <summary>