Storage - Replace LiteDB with SQLite + EF Core (#843)

Storage - Added LiteDB to SQLite migration
UI - Try to die a bit more gracefully
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:
RobertBeekman authored and GitHub committed 2024-03-13 20:19:21 +01:00
1 parent 9132301dbf
commit 551921db9f
130 files changed
+2965 -1135

No files matched your search

+1 -1
View File
@@ -29,7 +29,6 @@ internal class CoreService : ICoreService
// ReSharper disable UnusedParameter.Local
public CoreService(IContainer container,
ILogger logger,
StorageMigrationService _1, // injected to ensure migration runs early
ISettingsService settingsService,
IPluginManagementService pluginManagementService,
IProfileService profileService,
@@ -72,6 +71,7 @@ internal class CoreService : ICoreService
// Initialize the services
_pluginManagementService.CopyBuiltInPlugins();
_pluginManagementService.LoadPlugins(IsElevated);
_pluginManagementService.StartHotReload();
_renderService.Initialize();
OnInitialized();
+2 -1
View File
@@ -236,7 +236,7 @@ internal class DeviceService : IDeviceService
{
foreach (ArtemisDevice artemisDevice in _devices)
artemisDevice.Save();
_deviceRepository.Save(_devices.Select(d => d.DeviceEntity));
_deviceRepository.SaveRange(_devices.Select(d => d.DeviceEntity));
UpdateLeds();
}
@@ -254,6 +254,7 @@ internal class DeviceService : IDeviceService
{
_logger.Information("No device config found for {DeviceInfo}, device hash: {DeviceHashCode}. Adding a new entry", rgbDevice.DeviceInfo, deviceIdentifier);
device = new ArtemisDevice(rgbDevice, deviceProvider);
_deviceRepository.Add(device.DeviceEntity);
}
LoadDeviceLayout(device);
@@ -12,7 +12,7 @@ public class InputProviderIdentifierEventArgs : EventArgs
/// </summary>
/// <param name="identifier">A value that can be used to identify this device</param>
/// <param name="deviceType">The type of device this identifier belongs to</param>
public InputProviderIdentifierEventArgs(object identifier, InputDeviceType deviceType)
public InputProviderIdentifierEventArgs(string identifier, InputDeviceType deviceType)
{
Identifier = identifier;
DeviceType = deviceType;
@@ -21,7 +21,7 @@ public class InputProviderIdentifierEventArgs : EventArgs
/// <summary>
/// Gets a value that can be used to identify this device
/// </summary>
public object Identifier { get; }
public string Identifier { get; }
/// <summary>
/// Gets the type of device this identifier belongs to
@@ -113,7 +113,7 @@ public abstract class InputProvider : IDisposable
/// </summary>
/// <param name="identifier">A value that can be used to identify this device</param>
/// <param name="deviceType">The type of device this identifier belongs to</param>
protected virtual void OnIdentifierReceived(object identifier, InputDeviceType deviceType)
protected virtual void OnIdentifierReceived(string identifier, InputDeviceType deviceType)
{
IdentifierReceived?.Invoke(this, new InputProviderIdentifierEventArgs(identifier, deviceType));
}
@@ -16,7 +16,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// Gets a list containing additional directories in which plugins are located, used while loading plugins.
/// </summary>
List<DirectoryInfo> AdditionalPluginDirectories { get; }
/// <summary>
/// Indicates whether or not plugins are currently being loaded
/// </summary>
@@ -33,6 +33,11 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// </summary>
void LoadPlugins(bool isElevated);
/// <summary>
/// Starts monitoring plugin directories for changes and reloads plugins when changes are detected
/// </summary>
void StartHotReload();
/// <summary>
/// Unloads all installed plugins.
/// </summary>
@@ -146,18 +151,6 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// <returns></returns>
DeviceProvider GetDeviceProviderByDevice(IRGBDevice device);
/// <summary>
/// Queues the provided plugin to be deleted the next time Artemis starts, before plugins are loaded
/// </summary>
/// <param name="plugin">The plugin to delete</param>
void QueuePluginDeletion(Plugin plugin);
/// <summary>
/// Removes the provided plugin for the deletion queue it was added to via <see cref="QueuePluginDeletion" />
/// </summary>
/// <param name="plugin">The plugin to dequeue</param>
void DequeuePluginDeletion(Plugin plugin);
/// <summary>
/// Occurs when built-in plugins are being loaded
/// </summary>
@@ -30,52 +30,17 @@ internal class PluginManagementService : IPluginManagementService
private readonly ILogger _logger;
private readonly IPluginRepository _pluginRepository;
private readonly List<Plugin> _plugins;
private readonly IQueuedActionRepository _queuedActionRepository;
private FileSystemWatcher? _hotReloadWatcher;
private bool _disposed;
private bool _isElevated;
public PluginManagementService(IContainer container, ILogger logger, IPluginRepository pluginRepository, IDeviceRepository deviceRepository, IQueuedActionRepository queuedActionRepository)
public PluginManagementService(IContainer container, ILogger logger, IPluginRepository pluginRepository, IDeviceRepository deviceRepository)
{
_container = container;
_logger = logger;
_pluginRepository = pluginRepository;
_deviceRepository = deviceRepository;
_queuedActionRepository = queuedActionRepository;
_plugins = new List<Plugin>();
ProcessPluginDeletionQueue();
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();
@@ -159,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<Plugin> GetAllPlugins()
@@ -376,7 +370,15 @@ internal class PluginManagementService : IPluginManagementService
}
// Load the entity and fall back on creating a new one
Plugin plugin = new(pluginInfo, directory, _pluginRepository.GetPluginByGuid(pluginInfo.Guid));
PluginEntity? entity = _pluginRepository.GetPluginByPluginGuid(pluginInfo.Guid);
bool loadedFromStorage = entity != null;
if (entity == null)
{
entity = new PluginEntity {PluginGuid = pluginInfo.Guid};
_pluginRepository.SavePlugin(entity);
}
Plugin plugin = new(pluginInfo, directory, entity, loadedFromStorage);
OnPluginLoading(new PluginEventArgs(plugin));
// Locate the main assembly entry
@@ -440,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<Type> bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(PluginBootstrapper).IsAssignableFrom(t)).ToList();
if (bootstrappers.Count > 1)
@@ -801,58 +805,6 @@ internal class PluginManagementService : IPluginManagementService
#endregion
#region Queued actions
public void QueuePluginDeletion(Plugin plugin)
{
_queuedActionRepository.Add(new QueuedActionEntity
{
Type = "DeletePlugin",
CreatedAt = DateTimeOffset.Now,
Parameters = new Dictionary<string, object>
{
{"pluginGuid", plugin.Guid.ToString()},
{"plugin", plugin.ToString()},
{"directory", plugin.Directory.FullName}
}
});
}
public void DequeuePluginDeletion(Plugin plugin)
{
QueuedActionEntity? queuedActionEntity = _queuedActionRepository.GetByType("DeletePlugin").FirstOrDefault(q => q.Parameters["pluginGuid"].Equals(plugin.Guid.ToString()));
if (queuedActionEntity != null)
_queuedActionRepository.Remove(queuedActionEntity);
}
private void ProcessPluginDeletionQueue()
{
foreach (QueuedActionEntity queuedActionEntity in _queuedActionRepository.GetByType("DeletePlugin"))
{
string? directory = queuedActionEntity.Parameters["directory"].ToString();
try
{
if (Directory.Exists(directory))
{
_logger.Information("Queued plugin deletion - deleting folder - {plugin}", queuedActionEntity.Parameters["plugin"]);
Directory.Delete(directory!, true);
}
else
{
_logger.Information("Queued plugin deletion - folder already deleted - {plugin}", queuedActionEntity.Parameters["plugin"]);
}
_queuedActionRepository.Remove(queuedActionEntity);
}
catch (Exception e)
{
_logger.Warning(e, "Queued plugin deletion failed - {plugin}", queuedActionEntity.Parameters["plugin"]);
}
}
}
#endregion
#region Storage
private void SavePlugin(Plugin plugin)
@@ -942,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.
+1 -1
View File
@@ -40,7 +40,7 @@ internal class RenderService : IRenderService, IRenderer, IDisposable
_graphicsContextProviders = graphicsContextProviders;
_targetFrameRateSetting = settingsService.GetSetting("Core.TargetFrameRate", 30);
_renderScaleSetting = settingsService.GetSetting("Core.RenderScale", 0.25);
_renderScaleSetting = settingsService.GetSetting("Core.RenderScale", 0.5);
_preferredGraphicsContext = settingsService.GetSetting("Core.PreferredGraphicsContext", "Software");
_targetFrameRateSetting.SettingChanged += OnRenderSettingsChanged;
_renderScaleSetting.SettingChanged += RenderScaleSettingOnSettingChanged;
+14 -8
View File
@@ -12,7 +12,7 @@ internal class ScriptingService : IScriptingService
private readonly IPluginManagementService _pluginManagementService;
private readonly IProfileService _profileService;
private readonly List<ScriptingProvider> _scriptingProviders;
public ScriptingService(IPluginManagementService pluginManagementService, IProfileService profileService)
{
_pluginManagementService = pluginManagementService;
@@ -29,10 +29,13 @@ internal class ScriptingService : IScriptingService
// No need to sub to Deactivated, scripts will deactivate themselves
profileService.ProfileActivated += ProfileServiceOnProfileActivated;
foreach (ProfileConfiguration profileConfiguration in _profileService.ProfileConfigurations)
foreach (ProfileCategory profileCategory in _profileService.ProfileCategories)
{
if (profileConfiguration.Profile != null)
InitializeProfileScripts(profileConfiguration.Profile);
foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations)
{
if (profileConfiguration.Profile != null)
InitializeProfileScripts(profileConfiguration.Profile);
}
}
}
@@ -112,11 +115,14 @@ internal class ScriptingService : IScriptingService
{
_scriptingProviders.Clear();
_scriptingProviders.AddRange(_pluginManagementService.GetFeaturesOfType<ScriptingProvider>());
foreach (ProfileConfiguration profileConfiguration in _profileService.ProfileConfigurations)
foreach (ProfileCategory profileCategory in _profileService.ProfileCategories)
{
if (profileConfiguration.Profile != null)
InitializeProfileScripts(profileConfiguration.Profile);
foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations)
{
if (profileConfiguration.Profile != null)
InitializeProfileScripts(profileConfiguration.Profile);
}
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ public interface ISettingsService : IProtectedArtemisService
/// Gets the setting with the provided name. If the setting does not exist yet, it is created.
/// </summary>
/// <typeparam name="T">The type of the setting, can be any serializable type</typeparam>
/// <param name="name">The name of the setting</param>
/// <param name="name">The name of the setting, may not be longer than 128 characters</param>
/// <param name="defaultValue">The default value to use if the setting does not exist yet</param>
/// <returns></returns>
PluginSetting<T> GetSetting<T>(string name, T? defaultValue = default);
@@ -16,11 +16,6 @@ public interface IProfileService : IArtemisService
/// </summary>
ReadOnlyCollection<ProfileCategory> ProfileCategories { get; }
/// <summary>
/// Gets a read only collection containing all the profile configurations.
/// </summary>
ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations { get; }
/// <summary>
/// Gets or sets the focused profile configuration which is rendered exclusively.
/// </summary>
@@ -59,13 +54,7 @@ public interface IProfileService : IArtemisService
/// </summary>
/// <param name="profileConfiguration">The profile configuration of the profile to activate.</param>
void DeactivateProfile(ProfileConfiguration profileConfiguration);
/// <summary>
/// Permanently deletes the profile of the given <see cref="ProfileConfiguration" />.
/// </summary>
/// <param name="profileConfiguration">The profile configuration of the profile to delete.</param>
void DeleteProfile(ProfileConfiguration profileConfiguration);
/// <summary>
/// Saves the provided <see cref="ProfileCategory" /> and it's <see cref="ProfileConfiguration" />s but not the
/// <see cref="Profile" />s themselves.
@@ -101,16 +90,6 @@ public interface IProfileService : IArtemisService
/// <param name="profileConfiguration"></param>
void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration);
/// <summary>
/// Loads the icon of this profile configuration if needed and puts it into <c>ProfileConfiguration.Icon.FileIcon</c>.
/// </summary>
void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration);
/// <summary>
/// Saves the current icon of this profile.
/// </summary>
void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration);
/// <summary>
/// Writes the profile to persistent storage.
/// </summary>
@@ -136,9 +115,9 @@ public interface IProfileService : IArtemisService
/// any changes are made to it.
/// </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>
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>
/// Imports the provided ZIP archive stream into the provided profile configuration
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
@@ -21,11 +22,10 @@ internal class ProfileService : IProfileService
{
private readonly ILogger _logger;
private readonly IProfileCategoryRepository _profileCategoryRepository;
private readonly IProfileRepository _profileRepository;
private readonly IPluginManagementService _pluginManagementService;
private readonly IDeviceService _deviceService;
private readonly List<ArtemisKeyboardKeyEventArgs> _pendingKeyboardEvents = new();
private readonly List<ProfileCategory> _profileCategories;
private readonly IProfileRepository _profileRepository;
private readonly List<IProfileMigration> _profileMigrators;
private readonly List<Exception> _renderExceptions = new();
private readonly List<Exception> _updateExceptions = new();
@@ -35,19 +35,20 @@ internal class ProfileService : IProfileService
public ProfileService(ILogger logger,
IProfileCategoryRepository profileCategoryRepository,
IProfileRepository profileRepository,
IPluginManagementService pluginManagementService,
IInputService inputService,
IDeviceService deviceService,
IProfileRepository profileRepository,
List<IProfileMigration> profileMigrators)
{
_logger = logger;
_profileCategoryRepository = profileCategoryRepository;
_profileRepository = profileRepository;
_pluginManagementService = pluginManagementService;
_deviceService = deviceService;
_profileRepository = profileRepository;
_profileMigrators = profileMigrators;
_profileCategories = new List<ProfileCategory>(_profileCategoryRepository.GetAll().Select(c => new ProfileCategory(c)).OrderBy(c => c.Order));
ProfileCategories = new ReadOnlyCollection<ProfileCategory>(_profileCategoryRepository.GetAll().Select(c => new ProfileCategory(c)).OrderBy(c => c.Order).ToList());
_deviceService.LedsChanged += DeviceServiceOnLedsChanged;
_pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled;
@@ -55,7 +56,7 @@ internal class ProfileService : IProfileService
inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp;
if (!_profileCategories.Any())
if (!ProfileCategories.Any())
CreateDefaultProfileCategories();
UpdateModules();
}
@@ -77,55 +78,52 @@ internal class ProfileService : IProfileService
return;
}
lock (_profileCategories)
// Iterate the children in reverse because the first category must be rendered last to end up on top
for (int i = ProfileCategories.Count - 1; i > -1; i--)
{
// Iterate the children in reverse because the first category must be rendered last to end up on top
for (int i = _profileCategories.Count - 1; i > -1; i--)
ProfileCategory profileCategory = ProfileCategories[i];
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
{
ProfileCategory profileCategory = _profileCategories[i];
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
// Process hotkeys that where pressed since this profile last updated
ProcessPendingKeyEvents(profileConfiguration);
bool shouldBeActive = profileConfiguration.ShouldBeActive(false);
if (shouldBeActive)
{
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
profileConfiguration.Update();
shouldBeActive = profileConfiguration.ActivationConditionMet;
}
// Process hotkeys that where pressed since this profile last updated
ProcessPendingKeyEvents(profileConfiguration);
bool shouldBeActive = profileConfiguration.ShouldBeActive(false);
if (shouldBeActive)
try
{
// Make sure the profile is active or inactive according to the parameters above
if (shouldBeActive && profileConfiguration.Profile == null && profileConfiguration.BrokenState != "Failed to activate profile")
profileConfiguration.TryOrBreak(() => ActivateProfile(profileConfiguration), "Failed to activate profile");
if (shouldBeActive && profileConfiguration.Profile != null && !profileConfiguration.Profile.ShouldDisplay)
profileConfiguration.Profile.ShouldDisplay = true;
else if (!shouldBeActive && profileConfiguration.Profile != null)
{
profileConfiguration.Update();
shouldBeActive = profileConfiguration.ActivationConditionMet;
if (!profileConfiguration.FadeInAndOut)
DeactivateProfile(profileConfiguration);
else if (!profileConfiguration.Profile.ShouldDisplay && profileConfiguration.Profile.Opacity <= 0)
DeactivateProfile(profileConfiguration);
else if (profileConfiguration.Profile.Opacity > 0)
RequestDeactivation(profileConfiguration);
}
try
{
// Make sure the profile is active or inactive according to the parameters above
if (shouldBeActive && profileConfiguration.Profile == null && profileConfiguration.BrokenState != "Failed to activate profile")
profileConfiguration.TryOrBreak(() => ActivateProfile(profileConfiguration), "Failed to activate profile");
if (shouldBeActive && profileConfiguration.Profile != null && !profileConfiguration.Profile.ShouldDisplay)
profileConfiguration.Profile.ShouldDisplay = true;
else if (!shouldBeActive && profileConfiguration.Profile != null)
{
if (!profileConfiguration.FadeInAndOut)
DeactivateProfile(profileConfiguration);
else if (!profileConfiguration.Profile.ShouldDisplay && profileConfiguration.Profile.Opacity <= 0)
DeactivateProfile(profileConfiguration);
else if (profileConfiguration.Profile.Opacity > 0)
RequestDeactivation(profileConfiguration);
}
profileConfiguration.Profile?.Update(deltaTime);
}
catch (Exception e)
{
_updateExceptions.Add(e);
}
profileConfiguration.Profile?.Update(deltaTime);
}
catch (Exception e)
{
_updateExceptions.Add(e);
}
}
LogProfileUpdateExceptions();
_pendingKeyboardEvents.Clear();
}
LogProfileUpdateExceptions();
_pendingKeyboardEvents.Clear();
}
/// <inheritdoc />
@@ -138,78 +136,32 @@ internal class ProfileService : IProfileService
return;
}
lock (_profileCategories)
// Iterate the children in reverse because the first category must be rendered last to end up on top
for (int i = ProfileCategories.Count - 1; i > -1; i--)
{
// Iterate the children in reverse because the first category must be rendered last to end up on top
for (int i = _profileCategories.Count - 1; i > -1; i--)
ProfileCategory profileCategory = ProfileCategories[i];
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
{
ProfileCategory profileCategory = _profileCategories[i];
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
try
{
try
{
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
// Ensure all criteria are met before rendering
bool fadingOut = profileConfiguration.Profile?.ShouldDisplay == false && profileConfiguration.Profile?.Opacity > 0;
if (!profileConfiguration.IsSuspended && !profileConfiguration.IsMissingModule && (profileConfiguration.ActivationConditionMet || fadingOut))
profileConfiguration.Profile?.Render(canvas, SKPointI.Empty, null);
}
catch (Exception e)
{
_renderExceptions.Add(e);
}
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
// Ensure all criteria are met before rendering
bool fadingOut = profileConfiguration.Profile?.ShouldDisplay == false && profileConfiguration.Profile?.Opacity > 0;
if (!profileConfiguration.IsSuspended && !profileConfiguration.IsMissingModule && (profileConfiguration.ActivationConditionMet || fadingOut))
profileConfiguration.Profile?.Render(canvas, SKPointI.Empty, null);
}
catch (Exception e)
{
_renderExceptions.Add(e);
}
}
LogProfileRenderExceptions();
}
LogProfileRenderExceptions();
}
/// <inheritdoc />
public ReadOnlyCollection<ProfileCategory> ProfileCategories
{
get
{
lock (_profileRepository)
{
return _profileCategories.AsReadOnly();
}
}
}
/// <inheritdoc />
public ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations
{
get
{
lock (_profileRepository)
{
return _profileCategories.SelectMany(c => c.ProfileConfigurations).ToList().AsReadOnly();
}
}
}
/// <inheritdoc />
public void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
{
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
return;
using Stream? stream = _profileCategoryRepository.GetProfileIconStream(profileConfiguration.Entity.FileIconId);
if (stream != null)
profileConfiguration.Icon.SetIconByStream(stream);
}
/// <inheritdoc />
public void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
{
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
return;
using Stream? stream = profileConfiguration.Icon.GetIconStream();
if (stream != null)
_profileCategoryRepository.SaveProfileIconStream(profileConfiguration.Entity, stream);
}
public ReadOnlyCollection<ProfileCategory> ProfileCategories { get; private set; }
/// <inheritdoc />
public ProfileConfiguration CloneProfileConfiguration(ProfileConfiguration profileConfiguration)
@@ -226,21 +178,7 @@ internal class ProfileService : IProfileService
return profileConfiguration.Profile;
}
ProfileEntity? profileEntity;
try
{
profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
}
catch (Exception e)
{
profileConfiguration.SetBrokenState("Failed to activate profile", e);
throw;
}
if (profileEntity == null)
throw new ArtemisCoreException($"Cannot find profile named: {profileConfiguration.Name} ID: {profileConfiguration.Entity.ProfileId}");
Profile profile = new(profileConfiguration, profileEntity);
Profile profile = new(profileConfiguration, profileConfiguration.Entity.Profile);
profile.PopulateLeds(_deviceService.EnabledDevices);
if (profile.IsFreshImport)
@@ -279,45 +217,29 @@ internal class ProfileService : IProfileService
profileConfiguration.Profile.ShouldDisplay = false;
}
/// <inheritdoc />
public void DeleteProfile(ProfileConfiguration profileConfiguration)
{
DeactivateProfile(profileConfiguration);
ProfileEntity? profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
if (profileEntity == null)
return;
profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration);
_profileRepository.Remove(profileEntity);
SaveProfileCategory(profileConfiguration.Category);
}
/// <inheritdoc />
public ProfileCategory CreateProfileCategory(string name, bool addToTop = false)
{
ProfileCategory profileCategory;
lock (_profileRepository)
if (addToTop)
{
if (addToTop)
profileCategory = new ProfileCategory(name, 1);
foreach (ProfileCategory category in ProfileCategories)
{
profileCategory = new ProfileCategory(name, 1);
foreach (ProfileCategory category in _profileCategories)
{
category.Order++;
category.Save();
_profileCategoryRepository.Save(category.Entity);
}
}
else
{
profileCategory = new ProfileCategory(name, _profileCategories.Count + 1);
category.Order++;
category.Save();
}
_profileCategories.Add(profileCategory);
SaveProfileCategory(profileCategory);
_profileCategoryRepository.SaveRange(ProfileCategories.Select(c => c.Entity).ToList());
}
else
{
profileCategory = new ProfileCategory(name, ProfileCategories.Count + 1);
}
_profileCategoryRepository.Add(profileCategory.Entity);
ProfileCategories = new ReadOnlyCollection<ProfileCategory>([..ProfileCategories, profileCategory]);
OnProfileCategoryAdded(new ProfileCategoryEventArgs(profileCategory));
return profileCategory;
@@ -326,15 +248,11 @@ internal class ProfileService : IProfileService
/// <inheritdoc />
public void DeleteProfileCategory(ProfileCategory profileCategory)
{
List<ProfileConfiguration> profileConfigurations = profileCategory.ProfileConfigurations.ToList();
foreach (ProfileConfiguration profileConfiguration in profileConfigurations)
foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations.ToList())
RemoveProfileConfiguration(profileConfiguration);
lock (_profileRepository)
{
_profileCategories.Remove(profileCategory);
_profileCategoryRepository.Remove(profileCategory.Entity);
}
ProfileCategories = new ReadOnlyCollection<ProfileCategory>(ProfileCategories.Where(c => c != profileCategory).ToList());
_profileCategoryRepository.Remove(profileCategory.Entity);
OnProfileCategoryRemoved(new ProfileCategoryEventArgs(profileCategory));
}
@@ -343,26 +261,24 @@ internal class ProfileService : IProfileService
public ProfileConfiguration CreateProfileConfiguration(ProfileCategory category, string name, string icon)
{
ProfileConfiguration configuration = new(category, name, icon);
ProfileEntity entity = new();
_profileRepository.Add(entity);
configuration.Entity.ProfileId = entity.Id;
category.AddProfileConfiguration(configuration, 0);
category.AddProfileConfiguration(configuration, category.ProfileConfigurations.FirstOrDefault());
SaveProfileCategory(category);
return configuration;
}
/// <inheritdoc />
public void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration)
{
profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration);
DeactivateProfile(profileConfiguration);
SaveProfileCategory(profileConfiguration.Category);
ProfileEntity? profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
if (profileEntity != null)
_profileRepository.Remove(profileEntity);
profileConfiguration.Dispose();
ProfileCategory category = profileConfiguration.Category;
category.RemoveProfileConfiguration(profileConfiguration);
category.Save();
_profileRepository.Remove(profileConfiguration.Entity);
_profileCategoryRepository.Save(category.Entity);
}
/// <inheritdoc />
@@ -370,11 +286,7 @@ internal class ProfileService : IProfileService
{
profileCategory.Save();
_profileCategoryRepository.Save(profileCategory.Entity);
lock (_profileCategories)
{
_profileCategories.Sort((a, b) => a.Order - b.Order);
}
ProfileCategories = new ReadOnlyCollection<ProfileCategory>(ProfileCategories.OrderBy(c => c.Order).ToList());
}
/// <inheritdoc />
@@ -392,13 +304,16 @@ internal class ProfileService : IProfileService
profile.IsFreshImport = false;
profile.ProfileEntity.IsFreshImport = false;
_profileRepository.Save(profile.ProfileEntity);
_profileRepository.Save(profile.Configuration.Entity);
// If the provided profile is external (cloned or from the workshop?) but it is loaded locally too, reload the local instance
// A bit dodge but it ensures local instances always represent the latest stored version
ProfileConfiguration? localInstance = ProfileConfigurations.FirstOrDefault(p => p.Profile != null && p.Profile != profile && p.ProfileId == profile.ProfileEntity.Id);
ProfileConfiguration? localInstance = ProfileCategories
.SelectMany(c => c.ProfileConfigurations)
.FirstOrDefault(p => p.Profile != null && p.Profile != profile && p.ProfileId == profile.ProfileEntity.Id);
if (localInstance == null)
return;
DeactivateProfile(localInstance);
ActivateProfile(localInstance);
}
@@ -406,12 +321,8 @@ internal class ProfileService : IProfileService
/// <inheritdoc />
public async Task<Stream> ExportProfile(ProfileConfiguration profileConfiguration)
{
ProfileEntity? profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
if (profileEntity == null)
throw new ArtemisCoreException("Could not locate profile entity");
string configurationJson = CoreJson.Serialize(profileConfiguration.Entity);
string profileJson = CoreJson.Serialize(profileEntity);
string configurationJson = CoreJson.Serialize(profileConfiguration.Entity.ProfileConfiguration);
string profileJson = CoreJson.Serialize(profileConfiguration.Entity.Profile);
MemoryStream archiveStream = new();
@@ -430,12 +341,11 @@ internal class ProfileService : IProfileService
await entryStream.WriteAsync(Encoding.Default.GetBytes(profileJson));
}
await using Stream? iconStream = profileConfiguration.Icon.GetIconStream();
if (iconStream != null)
if (profileConfiguration.Icon.IconBytes != null)
{
ZipArchiveEntry iconEntry = archive.CreateEntry("icon.png");
await using Stream entryStream = iconEntry.Open();
await iconStream.CopyToAsync(entryStream);
await entryStream.WriteAsync(profileConfiguration.Icon.IconBytes);
}
}
@@ -444,7 +354,7 @@ internal class ProfileService : IProfileService
}
/// <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);
@@ -468,7 +378,7 @@ internal class ProfileService : IProfileService
JsonObject? profileJson = CoreJson.Deserialize<JsonObject>(await profileReader.ReadToEndAsync());
// Before deserializing, apply any pending migrations
MigrateProfile(configurationJson, profileJson);
_profileRepository.MigrateProfile(configurationJson, profileJson);
// Deserialize profile configuration to ProfileConfigurationEntity
ProfileConfigurationEntity? configurationEntity = configurationJson?.Deserialize<ProfileConfigurationEntity>(Constants.JsonConvertSettings);
@@ -495,27 +405,26 @@ internal class ProfileService : IProfileService
if (markAsFreshImport)
profileEntity.IsFreshImport = true;
if (_profileRepository.Get(profileEntity.Id) == null)
_profileRepository.Add(profileEntity);
else
if (makeUnique && ProfileCategories.SelectMany(c => c.ProfileConfigurations).Any(c => c.ProfileId == profileEntity.Id))
throw new ArtemisCoreException($"Cannot import this profile without {nameof(makeUnique)} being true");
// A new GUID will be given on save
configurationEntity.FileIconId = Guid.Empty;
ProfileConfiguration profileConfiguration = new(category, configurationEntity);
if (nameAffix != null)
profileConfiguration.Name = $"{profileConfiguration.Name} - {nameAffix}";
ProfileContainerEntity containerEntity = new() {ProfileConfiguration = configurationEntity, Profile = profileEntity};
// If an icon was provided, import that as well
if (iconEntry != null)
{
await using Stream iconStream = iconEntry.Open();
profileConfiguration.Icon.SetIconByStream(iconStream);
SaveProfileConfigurationIcon(profileConfiguration);
using MemoryStream ms = new();
await iconStream.CopyToAsync(ms);
containerEntity.Icon = ms.ToArray();
}
profileConfiguration.Entity.ProfileId = profileEntity.Id;
category.AddProfileConfiguration(profileConfiguration, targetIndex);
// A new GUID will be given on save
ProfileConfiguration profileConfiguration = new(category, containerEntity);
if (nameAffix != null)
profileConfiguration.Name = $"{profileConfiguration.Name} - {nameAffix}";
profileConfiguration.Entity.ProfileConfiguration.ProfileId = profileEntity.Id;
category.AddProfileConfiguration(profileConfiguration, target);
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>();
profileConfiguration.LoadModules(modules);
@@ -527,9 +436,9 @@ internal class ProfileService : IProfileService
/// <inheritdoc />
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);
DeleteProfile(profileConfiguration);
RemoveProfileConfiguration(profileConfiguration);
SaveProfileCategory(imported.Category);
return imported;
@@ -548,60 +457,40 @@ internal class ProfileService : IProfileService
renderProfileElement.Save();
_logger.Debug("Adapt profile - Saving " + profile);
_profileRepository.Save(profile.ProfileEntity);
SaveProfileCategory(profile.Configuration.Category);
}
private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e)
{
lock (_profileCategories)
{
_pendingKeyboardEvents.Add(e);
}
_pendingKeyboardEvents.Add(e);
}
private void MigrateProfile(JsonObject? configurationJson, JsonObject? profileJson)
{
if (configurationJson == null || profileJson == null)
return;
configurationJson["Version"] ??= 0;
foreach (IProfileMigration profileMigrator in _profileMigrators.OrderBy(m => m.Version))
{
if (profileMigrator.Version <= configurationJson["Version"]!.GetValue<int>())
continue;
profileMigrator.Migrate(configurationJson, profileJson);
configurationJson["Version"] = profileMigrator.Version;
}
}
/// <summary>
/// Populates all missing LEDs on all currently active profiles
/// </summary>
private void ActiveProfilesPopulateLeds()
{
foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations)
foreach (ProfileCategory profileCategory in ProfileCategories)
{
if (profileConfiguration.Profile == null) continue;
profileConfiguration.Profile.PopulateLeds(_deviceService.EnabledDevices);
foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations)
{
if (profileConfiguration.Profile == null) continue;
profileConfiguration.Profile.PopulateLeds(_deviceService.EnabledDevices);
if (!profileConfiguration.Profile.IsFreshImport) continue;
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileConfiguration.Profile);
AdaptProfile(profileConfiguration.Profile);
if (!profileConfiguration.Profile.IsFreshImport) continue;
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileConfiguration.Profile);
AdaptProfile(profileConfiguration.Profile);
}
}
}
private void UpdateModules()
{
lock (_profileRepository)
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>();
foreach (ProfileCategory profileCategory in ProfileCategories)
{
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>();
foreach (ProfileCategory profileCategory in _profileCategories)
{
foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations)
profileConfiguration.LoadModules(modules);
}
foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations)
profileConfiguration.LoadModules(modules);
}
}