diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index 75fa9a3a1..42daefc59 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -40,7 +40,7 @@ - + diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index c61aa021c..dfa7791c5 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Text.Json; using Artemis.Core.JsonConverters; +using Artemis.Storage.Entities.Plugins; namespace Artemis.Core; @@ -90,7 +91,7 @@ public static class Constants /// /// The plugin used by core components of Artemis /// - public static readonly Plugin CorePlugin = new(CorePluginInfo, new DirectoryInfo(ApplicationFolder), null); + public static readonly Plugin CorePlugin = new(CorePluginInfo, new DirectoryInfo(ApplicationFolder), new PluginEntity(){PluginGuid = CorePluginInfo.Guid}, false); /// /// A read-only collection containing all primitive numeric types diff --git a/src/Artemis.Core/DryIoc/ContainerExtensions.cs b/src/Artemis.Core/DryIoc/ContainerExtensions.cs index 8aa35b65f..eaa2282a8 100644 --- a/src/Artemis.Core/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.Core/DryIoc/ContainerExtensions.cs @@ -22,20 +22,18 @@ public static class ContainerExtensions /// The builder building the current container public static void RegisterCore(this IContainer container) { - Assembly[] coreAssembly = {typeof(IArtemisService).Assembly}; - Assembly[] storageAssembly = {typeof(IRepository).Assembly}; + 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.RegisterDelegate(() => StorageManager.CreateDbContext(Constants.DataFolder), Reuse.Transient); container.RegisterMany(storageAssembly, type => type.IsAssignableTo(), Reuse.Singleton); - + // Bind migrations - container.RegisterMany(storageAssembly, type => type.IsAssignableTo(), Reuse.Singleton, nonPublicServiceTypes: true); container.RegisterMany(storageAssembly, type => type.IsAssignableTo(), Reuse.Singleton, nonPublicServiceTypes: true); container.RegisterMany(coreAssembly, type => type.IsAssignableTo(), Reuse.Singleton); diff --git a/src/Artemis.Core/Models/Profile/ProfileCategory.cs b/src/Artemis.Core/Models/Profile/ProfileCategory.cs index 4dc3f73e6..1a5c6f269 100644 --- a/src/Artemis.Core/Models/Profile/ProfileCategory.cs +++ b/src/Artemis.Core/Models/Profile/ProfileCategory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using Artemis.Storage.Entities.Profile; namespace Artemis.Core; @@ -15,7 +16,6 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel /// public static readonly ProfileCategory Empty = new("Empty", -1); - private readonly List _profileConfigurations = new(); private bool _isCollapsed; private bool _isSuspended; private string _name; @@ -31,14 +31,16 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel _name = name; _order = order; Entity = new ProfileCategoryEntity(); - ProfileConfigurations = new ReadOnlyCollection(_profileConfigurations); + ProfileConfigurations = new ReadOnlyCollection([]); + + Save(); } internal ProfileCategory(ProfileCategoryEntity entity) { _name = null!; Entity = entity; - ProfileConfigurations = new ReadOnlyCollection(_profileConfigurations); + ProfileConfigurations = new ReadOnlyCollection([]); Load(); } @@ -83,7 +85,7 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel /// /// Gets a read only collection of the profiles inside this category /// - public ReadOnlyCollection ProfileConfigurations { get; } + public ReadOnlyCollection ProfileConfigurations { get; private set; } /// /// Gets the unique ID of this category @@ -96,29 +98,21 @@ 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) { - // 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 > _profileConfigurations.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, _profileConfigurations.Count); - _profileConfigurations.Insert(targetIndex.Value, configuration); - } + + if (target != null) + targetList.Insert(targetList.IndexOf(target), configuration); else - { - _profileConfigurations.Add(configuration); - } + targetList.Add(configuration); configuration.Category = this; + ProfileConfigurations = new ReadOnlyCollection(targetList); - for (int index = 0; index < _profileConfigurations.Count; index++) - _profileConfigurations[index].Order = index; + for (int index = 0; index < ProfileConfigurations.Count; index++) + ProfileConfigurations[index].Order = index; OnProfileConfigurationAdded(new ProfileConfigurationEventArgs(configuration)); } @@ -156,11 +150,10 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel internal void RemoveProfileConfiguration(ProfileConfiguration configuration) { - if (!_profileConfigurations.Remove(configuration)) - return; - - for (int index = 0; index < _profileConfigurations.Count; index++) - _profileConfigurations[index].Order = index; + ProfileConfigurations = new ReadOnlyCollection(ProfileConfigurations.Where(pc => pc != configuration).ToList()); + for (int index = 0; index < ProfileConfigurations.Count; index++) + ProfileConfigurations[index].Order = index; + OnProfileConfigurationRemoved(new ProfileConfigurationEventArgs(configuration)); } @@ -174,9 +167,7 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel IsSuspended = Entity.IsSuspended; Order = Entity.Order; - _profileConfigurations.Clear(); - foreach (ProfileConfigurationEntity entityProfileConfiguration in Entity.ProfileConfigurations) - _profileConfigurations.Add(new ProfileConfiguration(this, entityProfileConfiguration)); + ProfileConfigurations = new ReadOnlyCollection(Entity.ProfileConfigurations.Select(pc => new ProfileConfiguration(this, pc)).ToList()); } /// @@ -186,13 +177,10 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel Entity.IsCollapsed = IsCollapsed; Entity.IsSuspended = IsSuspended; Entity.Order = Order; - + Entity.ProfileConfigurations.Clear(); foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations) - { - profileConfiguration.Save(); Entity.ProfileConfigurations.Add(profileConfiguration.Entity); - } } #endregion diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index a4c1e89f4..521561610 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -37,13 +37,13 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable, _name = name; _category = category; - Entity = new ProfileConfigurationEntity(); + Entity = new ProfileContainerEntity(); Icon = new ProfileConfigurationIcon(Entity); Icon.SetIconByName(icon); ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active", this); } - internal ProfileConfiguration(ProfileCategory category, ProfileConfigurationEntity entity) + internal ProfileConfiguration(ProfileCategory category, ProfileContainerEntity entity) { // Will be loaded from the entity _name = null!; @@ -192,12 +192,12 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable, /// /// Gets the entity used by this profile config /// - public ProfileConfigurationEntity Entity { get; } + public ProfileContainerEntity Entity { get; } /// /// Gets the ID of the profile of this profile configuration /// - public Guid ProfileId => Entity.ProfileId; + public Guid ProfileId => Entity.Profile.Id; #region Overrides of BreakableModel @@ -265,8 +265,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable, if (_disposed) throw new ObjectDisposedException("ProfileConfiguration"); - Module = enabledModules.FirstOrDefault(m => m.Id == Entity.ModuleId); - IsMissingModule = Module == null && Entity.ModuleId != null; + Module = enabledModules.FirstOrDefault(m => m.Id == Entity.ProfileConfiguration.ModuleId); + IsMissingModule = Module == null && Entity.ProfileConfiguration.ModuleId != null; } /// @@ -284,20 +284,20 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable, if (_disposed) throw new ObjectDisposedException("ProfileConfiguration"); - Name = Entity.Name; - IsSuspended = Entity.IsSuspended; - ActivationBehaviour = (ActivationBehaviour) Entity.ActivationBehaviour; - HotkeyMode = (ProfileConfigurationHotkeyMode) Entity.HotkeyMode; - FadeInAndOut = Entity.FadeInAndOut; - Order = Entity.Order; + Name = Entity.ProfileConfiguration.Name; + IsSuspended = Entity.ProfileConfiguration.IsSuspended; + ActivationBehaviour = (ActivationBehaviour) Entity.ProfileConfiguration.ActivationBehaviour; + HotkeyMode = (ProfileConfigurationHotkeyMode) Entity.ProfileConfiguration.HotkeyMode; + FadeInAndOut = Entity.ProfileConfiguration.FadeInAndOut; + Order = Entity.ProfileConfiguration.Order; Icon.Load(); - if (Entity.ActivationCondition != null) - ActivationCondition.LoadFromEntity(Entity.ActivationCondition); + if (Entity.ProfileConfiguration.ActivationCondition != null) + ActivationCondition.LoadFromEntity(Entity.ProfileConfiguration.ActivationCondition); - EnableHotkey = Entity.EnableHotkey != null ? new Hotkey(Entity.EnableHotkey) : null; - DisableHotkey = Entity.DisableHotkey != null ? new Hotkey(Entity.DisableHotkey) : null; + EnableHotkey = Entity.ProfileConfiguration.EnableHotkey != null ? new Hotkey(Entity.ProfileConfiguration.EnableHotkey) : null; + DisableHotkey = Entity.ProfileConfiguration.DisableHotkey != null ? new Hotkey(Entity.ProfileConfiguration.DisableHotkey) : null; } /// @@ -306,26 +306,26 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable, if (_disposed) throw new ObjectDisposedException("ProfileConfiguration"); - Entity.Name = Name; - Entity.IsSuspended = IsSuspended; - Entity.ActivationBehaviour = (int) ActivationBehaviour; - Entity.HotkeyMode = (int) HotkeyMode; - Entity.ProfileCategoryId = Category.Entity.Id; - Entity.FadeInAndOut = FadeInAndOut; - Entity.Order = Order; + Entity.ProfileConfiguration.Name = Name; + Entity.ProfileConfiguration.IsSuspended = IsSuspended; + Entity.ProfileConfiguration.ActivationBehaviour = (int) ActivationBehaviour; + Entity.ProfileConfiguration.HotkeyMode = (int) HotkeyMode; + Entity.ProfileConfiguration.ProfileCategoryId = Category.Entity.Id; + Entity.ProfileConfiguration.FadeInAndOut = FadeInAndOut; + Entity.ProfileConfiguration.Order = Order; Icon.Save(); ActivationCondition.Save(); - Entity.ActivationCondition = ActivationCondition.Entity; + Entity.ProfileConfiguration.ActivationCondition = ActivationCondition.Entity; EnableHotkey?.Save(); - Entity.EnableHotkey = EnableHotkey?.Entity; + Entity.ProfileConfiguration.EnableHotkey = EnableHotkey?.Entity; DisableHotkey?.Save(); - Entity.DisableHotkey = DisableHotkey?.Entity; + Entity.ProfileConfiguration.DisableHotkey = DisableHotkey?.Entity; if (!IsMissingModule) - Entity.ModuleId = Module?.Id; + Entity.ProfileConfiguration.ModuleId = Module?.Id; } #endregion diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs index 5a477b5fd..c935836db 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs @@ -10,13 +10,13 @@ namespace Artemis.Core; /// public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel { - private readonly ProfileConfigurationEntity _entity; + private readonly ProfileContainerEntity _entity; private bool _fill; private string? _iconName; - private Stream? _iconStream; + private byte[]? _iconBytes; private ProfileConfigurationIconType _iconType; - internal ProfileConfigurationIcon(ProfileConfigurationEntity entity) + internal ProfileConfigurationIcon(ProfileContainerEntity entity) { _entity = entity; } @@ -48,6 +48,15 @@ public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel set => SetAndNotify(ref _fill, value); } + /// + /// Gets or sets the icon bytes if is + /// + public byte[]? IconBytes + { + get => _iconBytes; + private set => SetAndNotify(ref _iconBytes, value); + } + /// /// Updates the to the provided value and changes the is /// @@ -55,9 +64,9 @@ public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel /// The name of the icon public void SetIconByName(string iconName) { - if (iconName == null) throw new ArgumentNullException(nameof(iconName)); + ArgumentNullException.ThrowIfNull(iconName); - _iconStream?.Dispose(); + IconBytes = null; IconName = iconName; IconType = ProfileConfigurationIconType.MaterialIcon; @@ -65,42 +74,27 @@ public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel } /// - /// Updates the stream returned by to the provided stream + /// Updates the to the provided value and changes the is /// /// The stream to copy public void SetIconByStream(Stream stream) { - if (stream == null) throw new ArgumentNullException(nameof(stream)); + ArgumentNullException.ThrowIfNull(stream); - _iconStream?.Dispose(); - _iconStream = new MemoryStream(); if (stream.CanSeek) stream.Seek(0, SeekOrigin.Begin); - stream.CopyTo(_iconStream); - _iconStream.Seek(0, SeekOrigin.Begin); + + using (MemoryStream ms = new()) + { + stream.CopyTo(ms); + IconBytes = ms.ToArray(); + } IconName = null; IconType = ProfileConfigurationIconType.BitmapImage; OnIconUpdated(); } - /// - /// Creates a copy of the stream containing the icon - /// - /// A stream containing the icon - public Stream? GetIconStream() - { - if (_iconStream == null) - return null; - - MemoryStream stream = new(); - _iconStream.CopyTo(stream); - - stream.Seek(0, SeekOrigin.Begin); - _iconStream.Seek(0, SeekOrigin.Begin); - return stream; - } - /// /// Occurs when the icon was updated /// @@ -119,21 +113,24 @@ public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel /// public void Load() { - IconType = (ProfileConfigurationIconType) _entity.IconType; - Fill = _entity.IconFill; - if (IconType != ProfileConfigurationIconType.MaterialIcon) - return; + IconType = (ProfileConfigurationIconType) _entity.ProfileConfiguration.IconType; + Fill = _entity.ProfileConfiguration.IconFill; - IconName = _entity.MaterialIcon; + if (IconType == ProfileConfigurationIconType.MaterialIcon) + IconName = _entity.ProfileConfiguration.MaterialIcon; + else + IconBytes = _entity.Icon; + OnIconUpdated(); } /// public void Save() { - _entity.IconType = (int) IconType; - _entity.MaterialIcon = IconType == ProfileConfigurationIconType.MaterialIcon ? IconName : null; - _entity.IconFill = Fill; + _entity.ProfileConfiguration.IconType = (int) IconType; + _entity.ProfileConfiguration.MaterialIcon = IconType == ProfileConfigurationIconType.MaterialIcon ? IconName : null; + _entity.ProfileConfiguration.IconFill = Fill; + _entity.Icon = IconBytes ?? Array.Empty(); } #endregion diff --git a/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs b/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs index 6d101cdf7..410134624 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs @@ -14,7 +14,7 @@ public class ArtemisDeviceInputIdentifier /// used by /// /// A value used to identify the device - internal ArtemisDeviceInputIdentifier(string inputProvider, object identifier) + internal ArtemisDeviceInputIdentifier(string inputProvider, string identifier) { InputProvider = inputProvider; Identifier = identifier; @@ -28,5 +28,5 @@ public class ArtemisDeviceInputIdentifier /// /// Gets or sets a value used to identify the device /// - public object Identifier { get; set; } + public string Identifier { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index ff90181bc..6f2d2b4d4 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -23,14 +23,14 @@ public class Plugin : CorePropertyChanged, IDisposable private bool _isEnabled; - internal Plugin(PluginInfo info, DirectoryInfo directory, PluginEntity? pluginEntity) + internal Plugin(PluginInfo info, DirectoryInfo directory, PluginEntity pluginEntity, bool loadedFromStorage) { Info = info; Directory = directory; - Entity = pluginEntity ?? new PluginEntity {Id = Guid}; + Entity = pluginEntity; Info.Plugin = this; - _loadedFromStorage = pluginEntity != null; + _loadedFromStorage = loadedFromStorage; _features = new List(); _profilers = new List(); @@ -309,13 +309,7 @@ public class Plugin : CorePropertyChanged, IDisposable { FeatureRemoved?.Invoke(this, e); } - - internal void ApplyToEntity() - { - Entity.Id = Guid; - Entity.IsEnabled = IsEnabled; - } - + internal void AddFeature(PluginFeatureInfo featureInfo) { if (featureInfo.Plugin != this) @@ -363,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 @@ -377,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/Plugins/Settings/IPluginSetting.cs b/src/Artemis.Core/Plugins/Settings/IPluginSetting.cs index 57a454420..e27282063 100644 --- a/src/Artemis.Core/Plugins/Settings/IPluginSetting.cs +++ b/src/Artemis.Core/Plugins/Settings/IPluginSetting.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json; namespace Artemis.Core; @@ -7,19 +8,18 @@ namespace Artemis.Core; /// public interface IPluginSetting { + /// + /// The JSON serializer options used when serializing settings + /// + protected static readonly JsonSerializerOptions SerializerOptions = CoreJson.GetJsonSerializerOptions(); + /// /// The name of the setting, unique to this plugin /// string Name { get; } /// - /// Determines whether the setting has been changed - /// - bool HasChanged { get; } - - /// - /// Gets or sets whether changes must automatically be saved - /// Note: When set to true is always false + /// Gets or sets whether changes must automatically be saved /// bool AutoSave { get; set; } diff --git a/src/Artemis.Core/Plugins/Settings/PluginSettings.cs b/src/Artemis.Core/Plugins/Settings/PluginSettings.cs index 89c1a10ea..811dcf817 100644 --- a/src/Artemis.Core/Plugins/Settings/PluginSettings.cs +++ b/src/Artemis.Core/Plugins/Settings/PluginSettings.cs @@ -31,10 +31,13 @@ public class PluginSettings /// Gets the setting with the provided name. If the setting does not exist yet, it is created. /// /// The type of the setting, can be any serializable type - /// The name of the setting + /// The name of the setting, may not be longer than 128 characters /// The default value to use if the setting does not exist yet public PluginSetting GetSetting(string name, T? defaultValue = default) { + if (name.Length > 128) + throw new ArtemisCoreException("Setting name cannot be longer than 128 characters"); + lock (_settingEntities) { // Return cached value if available @@ -51,7 +54,7 @@ public class PluginSettings PluginGuid = Plugin.Guid, Value = CoreJson.Serialize(defaultValue) }; - _pluginRepository.AddSetting(settingEntity); + _pluginRepository.SaveSetting(settingEntity); } PluginSetting pluginSetting = new(_pluginRepository, settingEntity); diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 89a951149..da0f62fd7 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -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(); diff --git a/src/Artemis.Core/Services/DeviceService.cs b/src/Artemis.Core/Services/DeviceService.cs index afc949bba..da81a82a6 100644 --- a/src/Artemis.Core/Services/DeviceService.cs +++ b/src/Artemis.Core/Services/DeviceService.cs @@ -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); diff --git a/src/Artemis.Core/Services/Input/Events/InputProviderIdentifierEventArgs.cs b/src/Artemis.Core/Services/Input/Events/InputProviderIdentifierEventArgs.cs index cbc41d950..a6660b1a3 100644 --- a/src/Artemis.Core/Services/Input/Events/InputProviderIdentifierEventArgs.cs +++ b/src/Artemis.Core/Services/Input/Events/InputProviderIdentifierEventArgs.cs @@ -12,7 +12,7 @@ public class InputProviderIdentifierEventArgs : EventArgs /// /// A value that can be used to identify this device /// The type of device this identifier belongs to - public InputProviderIdentifierEventArgs(object identifier, InputDeviceType deviceType) + public InputProviderIdentifierEventArgs(string identifier, InputDeviceType deviceType) { Identifier = identifier; DeviceType = deviceType; @@ -21,7 +21,7 @@ public class InputProviderIdentifierEventArgs : EventArgs /// /// Gets a value that can be used to identify this device /// - public object Identifier { get; } + public string Identifier { get; } /// /// Gets the type of device this identifier belongs to diff --git a/src/Artemis.Core/Services/Input/InputProvider.cs b/src/Artemis.Core/Services/Input/InputProvider.cs index 8db4dec76..e5af94e48 100644 --- a/src/Artemis.Core/Services/Input/InputProvider.cs +++ b/src/Artemis.Core/Services/Input/InputProvider.cs @@ -113,7 +113,7 @@ public abstract class InputProvider : IDisposable /// /// A value that can be used to identify this device /// The type of device this identifier belongs to - protected virtual void OnIdentifierReceived(object identifier, InputDeviceType deviceType) + protected virtual void OnIdentifierReceived(string identifier, InputDeviceType deviceType) { IdentifierReceived?.Invoke(this, new InputProviderIdentifierEventArgs(identifier, deviceType)); } diff --git a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs index bbc4afe7a..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. /// @@ -146,18 +151,6 @@ public interface IPluginManagementService : IArtemisService, IDisposable /// DeviceProvider GetDeviceProviderByDevice(IRGBDevice device); - /// - /// Queues the provided plugin to be deleted the next time Artemis starts, before plugins are loaded - /// - /// The plugin to delete - void QueuePluginDeletion(Plugin plugin); - - /// - /// Removes the provided plugin for the deletion queue it was added to via - /// - /// The plugin to dequeue - void DequeuePluginDeletion(Plugin plugin); - /// /// 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 67e3083e4..0334417f6 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -30,52 +30,17 @@ internal class PluginManagementService : IPluginManagementService private readonly ILogger _logger; private readonly IPluginRepository _pluginRepository; private readonly List _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(); - - 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 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 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 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 - { - {"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. diff --git a/src/Artemis.Core/Services/RenderService.cs b/src/Artemis.Core/Services/RenderService.cs index bd73a0f0d..25c6e6185 100644 --- a/src/Artemis.Core/Services/RenderService.cs +++ b/src/Artemis.Core/Services/RenderService.cs @@ -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; diff --git a/src/Artemis.Core/Services/ScriptingService.cs b/src/Artemis.Core/Services/ScriptingService.cs index f768c2028..19011eefa 100644 --- a/src/Artemis.Core/Services/ScriptingService.cs +++ b/src/Artemis.Core/Services/ScriptingService.cs @@ -12,7 +12,7 @@ internal class ScriptingService : IScriptingService private readonly IPluginManagementService _pluginManagementService; private readonly IProfileService _profileService; private readonly List _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()); - - 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); + } } } diff --git a/src/Artemis.Core/Services/SettingsService.cs b/src/Artemis.Core/Services/SettingsService.cs index 96719eae0..a3cf4c030 100644 --- a/src/Artemis.Core/Services/SettingsService.cs +++ b/src/Artemis.Core/Services/SettingsService.cs @@ -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. /// /// The type of the setting, can be any serializable type - /// The name of the setting + /// The name of the setting, may not be longer than 128 characters /// The default value to use if the setting does not exist yet /// PluginSetting GetSetting(string name, T? defaultValue = default); diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index 2d2dfae7c..2a55664a9 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -16,11 +16,6 @@ public interface IProfileService : IArtemisService /// ReadOnlyCollection ProfileCategories { get; } - /// - /// Gets a read only collection containing all the profile configurations. - /// - ReadOnlyCollection ProfileConfigurations { get; } - /// /// Gets or sets the focused profile configuration which is rendered exclusively. /// @@ -59,13 +54,7 @@ public interface IProfileService : IArtemisService /// /// The profile configuration of the profile to activate. void DeactivateProfile(ProfileConfiguration profileConfiguration); - - /// - /// Permanently deletes the profile of the given . - /// - /// The profile configuration of the profile to delete. - void DeleteProfile(ProfileConfiguration profileConfiguration); - + /// /// Saves the provided and it's s but not the /// s themselves. @@ -101,16 +90,6 @@ public interface IProfileService : IArtemisService /// void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration); - /// - /// Loads the icon of this profile configuration if needed and puts it into ProfileConfiguration.Icon.FileIcon. - /// - void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration); - - /// - /// Saves the current icon of this profile. - /// - void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration); - /// /// Writes the profile to persistent storage. /// @@ -136,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 5cafbd7db..913c38e0e 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -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 _pendingKeyboardEvents = new(); - private readonly List _profileCategories; - private readonly IProfileRepository _profileRepository; private readonly List _profileMigrators; private readonly List _renderExceptions = new(); private readonly List _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 profileMigrators) { _logger = logger; _profileCategoryRepository = profileCategoryRepository; + _profileRepository = profileRepository; _pluginManagementService = pluginManagementService; _deviceService = deviceService; - _profileRepository = profileRepository; _profileMigrators = profileMigrators; - _profileCategories = new List(_profileCategoryRepository.GetAll().Select(c => new ProfileCategory(c)).OrderBy(c => c.Order)); + + ProfileCategories = new ReadOnlyCollection(_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(); } /// @@ -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(); } /// - public ReadOnlyCollection ProfileCategories - { - get - { - lock (_profileRepository) - { - return _profileCategories.AsReadOnly(); - } - } - } - - /// - public ReadOnlyCollection ProfileConfigurations - { - get - { - lock (_profileRepository) - { - return _profileCategories.SelectMany(c => c.ProfileConfigurations).ToList().AsReadOnly(); - } - } - } - - /// - 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); - } - - /// - 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 ProfileCategories { get; private set; } /// 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; } - - /// - 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); - } - + /// 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([..ProfileCategories, profileCategory]); OnProfileCategoryAdded(new ProfileCategoryEventArgs(profileCategory)); return profileCategory; @@ -326,15 +248,11 @@ internal class ProfileService : IProfileService /// public void DeleteProfileCategory(ProfileCategory profileCategory) { - List 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(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; } - + /// 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); } /// @@ -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(ProfileCategories.OrderBy(c => c.Order).ToList()); } /// @@ -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 /// public async Task 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 } /// - 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); @@ -468,7 +378,7 @@ internal class ProfileService : IProfileService JsonObject? profileJson = CoreJson.Deserialize(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(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 modules = _pluginManagementService.GetFeaturesOfType(); profileConfiguration.LoadModules(modules); @@ -527,9 +436,9 @@ 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); - 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()) - continue; - - profileMigrator.Migrate(configurationJson, profileJson); - configurationJson["Version"] = profileMigrator.Version; - } - } - + /// /// Populates all missing LEDs on all currently active profiles /// 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 modules = _pluginManagementService.GetFeaturesOfType(); + foreach (ProfileCategory profileCategory in ProfileCategories) { - List modules = _pluginManagementService.GetFeaturesOfType(); - foreach (ProfileCategory profileCategory in _profileCategories) - { - foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations) - profileConfiguration.LoadModules(modules); - } + foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations) + profileConfiguration.LoadModules(modules); } } diff --git a/src/Artemis.Storage.Legacy/Artemis.Storage.Legacy.csproj b/src/Artemis.Storage.Legacy/Artemis.Storage.Legacy.csproj new file mode 100644 index 000000000..2da8bd8c3 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Artemis.Storage.Legacy.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/Artemis.Storage/Entities/General/QueuedActionEntity.cs b/src/Artemis.Storage.Legacy/Entities/General/QueuedActionEntity.cs similarity index 70% rename from src/Artemis.Storage/Entities/General/QueuedActionEntity.cs rename to src/Artemis.Storage.Legacy/Entities/General/QueuedActionEntity.cs index 942a6501d..90770d565 100644 --- a/src/Artemis.Storage/Entities/General/QueuedActionEntity.cs +++ b/src/Artemis.Storage.Legacy/Entities/General/QueuedActionEntity.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; +namespace Artemis.Storage.Legacy.Entities.General; -namespace Artemis.Storage.Entities.General; - -public class QueuedActionEntity +internal class QueuedActionEntity { public QueuedActionEntity() { diff --git a/src/Artemis.Storage.Legacy/Entities/General/ReleaseEntity.cs b/src/Artemis.Storage.Legacy/Entities/General/ReleaseEntity.cs new file mode 100644 index 000000000..a8aea26a8 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/General/ReleaseEntity.cs @@ -0,0 +1,19 @@ +namespace Artemis.Storage.Legacy.Entities.General; + +internal class ReleaseEntity +{ + public Guid Id { get; set; } + + public string Version { get; set; } = string.Empty; + public DateTimeOffset? InstalledAt { get; set; } + + public Storage.Entities.General.ReleaseEntity Migrate() + { + return new Storage.Entities.General.ReleaseEntity() + { + Id = Id, + Version = Version, + InstalledAt = InstalledAt ?? DateTimeOffset.Now + }; + } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/General/ScriptConfigurationEntity.cs b/src/Artemis.Storage.Legacy/Entities/General/ScriptConfigurationEntity.cs new file mode 100644 index 000000000..94fce07e4 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/General/ScriptConfigurationEntity.cs @@ -0,0 +1,10 @@ +namespace Artemis.Storage.Legacy.Entities.General; + +internal class ScriptConfigurationEntity +{ + public Guid Id { get; set; } + + public string Name { get; set; } = string.Empty; + public string ScriptingProviderId { get; set; } = string.Empty; + public string? ScriptContent { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Plugins/PluginEntity.cs b/src/Artemis.Storage.Legacy/Entities/Plugins/PluginEntity.cs new file mode 100644 index 000000000..47e2ba74b --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Plugins/PluginEntity.cs @@ -0,0 +1,45 @@ +namespace Artemis.Storage.Legacy.Entities.Plugins; + +/// +/// Represents the configuration of a plugin, each plugin has one configuration +/// +internal class PluginEntity +{ + public PluginEntity() + { + Features = new List(); + } + + public Guid Id { get; set; } + public bool IsEnabled { get; set; } + + public List Features { get; set; } + + public Artemis.Storage.Entities.Plugins.PluginEntity Migrate() + { + return new Artemis.Storage.Entities.Plugins.PluginEntity() + { + PluginGuid = Id, + IsEnabled = IsEnabled, + Features = Features.Select(f => f.Migrate()).ToList() + }; + } +} + +/// +/// Represents the configuration of a plugin feature, each feature has one configuration +/// +internal class PluginFeatureEntity +{ + public string Type { get; set; } = string.Empty; + public bool IsEnabled { get; set; } + + public Artemis.Storage.Entities.Plugins.PluginFeatureEntity Migrate() + { + return new Artemis.Storage.Entities.Plugins.PluginFeatureEntity() + { + Type = Type, + IsEnabled = IsEnabled + }; + } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Plugins/PluginSettingEntity.cs b/src/Artemis.Storage.Legacy/Entities/Plugins/PluginSettingEntity.cs new file mode 100644 index 000000000..587e7d1c8 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Plugins/PluginSettingEntity.cs @@ -0,0 +1,24 @@ +namespace Artemis.Storage.Legacy.Entities.Plugins; + +/// +/// Represents the setting of a plugin, a plugin can have multiple settings +/// +internal class PluginSettingEntity +{ + public Guid Id { get; set; } + public Guid PluginGuid { get; set; } + + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + + public Artemis.Storage.Entities.Plugins.PluginSettingEntity Migrate() + { + return new Storage.Entities.Plugins.PluginSettingEntity + { + Id = Id, + PluginGuid = PluginGuid, + Name = Name, + Value = Value + }; + } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/Abstract/RenderElementEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/Abstract/RenderElementEntity.cs new file mode 100644 index 000000000..56b4e97fd --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/Abstract/RenderElementEntity.cs @@ -0,0 +1,14 @@ +using Artemis.Storage.Legacy.Entities.Profile.Conditions; + +namespace Artemis.Storage.Legacy.Entities.Profile.Abstract; + +internal abstract class RenderElementEntity +{ + public Guid Id { get; set; } + public Guid ParentId { get; set; } + + public List LayerEffects { get; set; } = new(); + + public IConditionEntity? DisplayCondition { get; set; } + public TimelineEntity? Timeline { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs new file mode 100644 index 000000000..16ceb4d79 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs @@ -0,0 +1,10 @@ +namespace Artemis.Storage.Legacy.Entities.Profile.AdaptionHints; + +internal class CategoryAdaptionHintEntity : IAdaptionHintEntity +{ + public int Category { get; set; } + + public bool LimitAmount { get; set; } + public int Skip { get; set; } + public int Amount { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/DeviceAdaptionHintEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/DeviceAdaptionHintEntity.cs new file mode 100644 index 000000000..dc4005707 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/DeviceAdaptionHintEntity.cs @@ -0,0 +1,10 @@ +namespace Artemis.Storage.Legacy.Entities.Profile.AdaptionHints; + +internal class DeviceAdaptionHintEntity : IAdaptionHintEntity +{ + public int DeviceType { get; set; } + + public bool LimitAmount { get; set; } + public int Skip { get; set; } + public int Amount { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs new file mode 100644 index 000000000..a18f3b6ac --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Artemis.Storage.Legacy.Entities.Profile.AdaptionHints; + +[JsonDerivedType(typeof(CategoryAdaptionHintEntity), "Category")] +[JsonDerivedType(typeof(DeviceAdaptionHintEntity), "Device")] +[JsonDerivedType(typeof(KeyboardSectionAdaptionHintEntity), "KeyboardSection")] +[JsonDerivedType(typeof(SingleLedAdaptionHintEntity), "SingleLed")] +public interface IAdaptionHintEntity; \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/KeyboardSectionAdaptionHintEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/KeyboardSectionAdaptionHintEntity.cs new file mode 100644 index 000000000..4d67359c6 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/KeyboardSectionAdaptionHintEntity.cs @@ -0,0 +1,6 @@ +namespace Artemis.Storage.Legacy.Entities.Profile.AdaptionHints; + +internal class KeyboardSectionAdaptionHintEntity : IAdaptionHintEntity +{ + public int Section { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/SingleLedAdaptionHintEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/SingleLedAdaptionHintEntity.cs new file mode 100644 index 000000000..9d03535d3 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/AdaptionHints/SingleLedAdaptionHintEntity.cs @@ -0,0 +1,10 @@ +namespace Artemis.Storage.Legacy.Entities.Profile.AdaptionHints; + +internal class SingleLedAdaptionHintEntity : IAdaptionHintEntity +{ + public int LedId { get; set; } + + public bool LimitAmount { get; set; } + public int Skip { get; set; } + public int Amount { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/AlwaysOnConditionEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/AlwaysOnConditionEntity.cs new file mode 100644 index 000000000..498d97885 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/AlwaysOnConditionEntity.cs @@ -0,0 +1,3 @@ +namespace Artemis.Storage.Legacy.Entities.Profile.Conditions; + +internal class AlwaysOnConditionEntity : IConditionEntity; \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/EventConditionEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/EventConditionEntity.cs new file mode 100644 index 000000000..574d5d42e --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/EventConditionEntity.cs @@ -0,0 +1,12 @@ +using Artemis.Storage.Legacy.Entities.Profile.Nodes; + +namespace Artemis.Storage.Legacy.Entities.Profile.Conditions; + +internal class EventConditionEntity : IConditionEntity +{ + public int TriggerMode { get; set; } + public int OverlapMode { get; set; } + public int ToggleOffMode { get; set; } + public DataModelPathEntity? EventPath { get; set; } + public NodeScriptEntity? Script { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/IConditionEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/IConditionEntity.cs new file mode 100644 index 000000000..ab5086aa9 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/IConditionEntity.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Artemis.Storage.Legacy.Entities.Profile.Conditions; + +[JsonDerivedType(typeof(AlwaysOnConditionEntity), "AlwaysOn")] +[JsonDerivedType(typeof(EventConditionEntity), "Event")] +[JsonDerivedType(typeof(PlayOnceConditionEntity), "PlayOnce")] +[JsonDerivedType(typeof(StaticConditionEntity), "Static")] +public interface IConditionEntity; \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/PlayOnceConditionEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/PlayOnceConditionEntity.cs new file mode 100644 index 000000000..798ef08c2 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/PlayOnceConditionEntity.cs @@ -0,0 +1,3 @@ +namespace Artemis.Storage.Legacy.Entities.Profile.Conditions; + +internal class PlayOnceConditionEntity : IConditionEntity; \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/StaticConditionEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/StaticConditionEntity.cs new file mode 100644 index 000000000..a4bfcc06b --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/Conditions/StaticConditionEntity.cs @@ -0,0 +1,10 @@ +using Artemis.Storage.Legacy.Entities.Profile.Nodes; + +namespace Artemis.Storage.Legacy.Entities.Profile.Conditions; + +internal class StaticConditionEntity : IConditionEntity +{ + public int PlayMode { get; set; } + public int StopMode { get; set; } + public NodeScriptEntity? Script { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/DataBindings/DataBindingEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/DataBindings/DataBindingEntity.cs new file mode 100644 index 000000000..39be84d72 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/DataBindings/DataBindingEntity.cs @@ -0,0 +1,9 @@ +using Artemis.Storage.Legacy.Entities.Profile.Nodes; + +namespace Artemis.Storage.Legacy.Entities.Profile.DataBindings; + +internal class DataBindingEntity +{ + public bool IsEnabled { get; set; } + public NodeScriptEntity? NodeScript { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/DataModelPathEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/DataModelPathEntity.cs new file mode 100644 index 000000000..4ebef8bc4 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/DataModelPathEntity.cs @@ -0,0 +1,8 @@ +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class DataModelPathEntity +{ + public string Path { get; set; } = string.Empty; + public string? DataModelId { get; set; } + public string? Type { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/FolderEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/FolderEntity.cs new file mode 100644 index 000000000..ae467caa7 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/FolderEntity.cs @@ -0,0 +1,17 @@ +using Artemis.Storage.Legacy.Entities.Profile.Abstract; +using LiteDB; + +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class FolderEntity : RenderElementEntity +{ + public int Order { get; set; } + public string? Name { get; set; } + public bool IsExpanded { get; set; } + public bool Suspended { get; set; } + + [BsonRef("ProfileEntity")] + public ProfileEntity Profile { get; set; } = null!; + + public Guid ProfileId { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/KeyframeEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/KeyframeEntity.cs new file mode 100644 index 000000000..6f75fe047 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/KeyframeEntity.cs @@ -0,0 +1,9 @@ +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class KeyframeEntity +{ + public TimeSpan Position { get; set; } + public int Timeline { get; set; } + public string Value { get; set; } = string.Empty; + public int EasingFunction { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/LayerBrushEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/LayerBrushEntity.cs new file mode 100644 index 000000000..254c19e71 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/LayerBrushEntity.cs @@ -0,0 +1,9 @@ +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class LayerBrushEntity +{ + public string ProviderId { get; set; } = string.Empty; + public string BrushType { get; set; } = string.Empty; + + public PropertyGroupEntity? PropertyGroup { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/LayerEffectEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/LayerEffectEntity.cs new file mode 100644 index 000000000..7f4c7e696 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/LayerEffectEntity.cs @@ -0,0 +1,12 @@ +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class LayerEffectEntity +{ + public string ProviderId { get; set; } = string.Empty; + public string EffectType { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public bool HasBeenRenamed { get; set; } + public int Order { get; set; } + + public PropertyGroupEntity? PropertyGroup { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/LayerEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/LayerEntity.cs new file mode 100644 index 000000000..d00a62c20 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/LayerEntity.cs @@ -0,0 +1,30 @@ +using Artemis.Storage.Legacy.Entities.Profile.Abstract; +using Artemis.Storage.Legacy.Entities.Profile.AdaptionHints; +using LiteDB; + +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class LayerEntity : RenderElementEntity +{ + public LayerEntity() + { + Leds = new List(); + AdaptionHints = new List(); + } + + public int Order { get; set; } + public string? Name { get; set; } + public bool Suspended { get; set; } + + public List Leds { get; set; } + public List AdaptionHints { get; set; } + + public PropertyGroupEntity? GeneralPropertyGroup { get; set; } + public PropertyGroupEntity? TransformPropertyGroup { get; set; } + public LayerBrushEntity? LayerBrush { get; set; } + + [BsonRef("ProfileEntity")] + public ProfileEntity Profile { get; set; } = null!; + + public Guid ProfileId { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/LedEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/LedEntity.cs new file mode 100644 index 000000000..5f7328bd6 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/LedEntity.cs @@ -0,0 +1,36 @@ +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class LedEntity +{ + public string LedName { get; set; } = string.Empty; + public string DeviceIdentifier { get; set; } = string.Empty; + + public int? PhysicalLayout { get; set; } + + #region LedEntityEqualityComparer + + private sealed class LedEntityEqualityComparer : IEqualityComparer + { + public bool Equals(LedEntity? x, LedEntity? y) + { + if (ReferenceEquals(x, y)) + return true; + if (ReferenceEquals(x, null)) + return false; + if (ReferenceEquals(y, null)) + return false; + if (x.GetType() != y.GetType()) + return false; + return x.LedName == y.LedName && x.DeviceIdentifier == y.DeviceIdentifier && x.PhysicalLayout == y.PhysicalLayout; + } + + public int GetHashCode(LedEntity obj) + { + return HashCode.Combine(obj.LedName, obj.DeviceIdentifier, obj.PhysicalLayout); + } + } + + public static IEqualityComparer LedEntityComparer { get; } = new LedEntityEqualityComparer(); + + #endregion +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodeConnectionEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodeConnectionEntity.cs new file mode 100644 index 000000000..c186db470 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodeConnectionEntity.cs @@ -0,0 +1,29 @@ +namespace Artemis.Storage.Legacy.Entities.Profile.Nodes; + +internal class NodeConnectionEntity +{ + public NodeConnectionEntity() + { + } + + public NodeConnectionEntity(NodeConnectionEntity nodeConnectionEntity) + { + SourceType = nodeConnectionEntity.SourceType; + SourceNode = nodeConnectionEntity.SourceNode; + TargetNode = nodeConnectionEntity.TargetNode; + SourcePinCollectionId = nodeConnectionEntity.SourcePinCollectionId; + SourcePinId = nodeConnectionEntity.SourcePinId; + TargetType = nodeConnectionEntity.TargetType; + TargetPinCollectionId = nodeConnectionEntity.TargetPinCollectionId; + TargetPinId = nodeConnectionEntity.TargetPinId; + } + + public string SourceType { get; set; } = string.Empty; + public Guid SourceNode { get; set; } + public Guid TargetNode { get; set; } + public int SourcePinCollectionId { get; set; } + public int SourcePinId { get; set; } + public string TargetType { get; set; } = string.Empty; + public int TargetPinCollectionId { get; set; } + public int TargetPinId { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodeEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodeEntity.cs new file mode 100644 index 000000000..314964430 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodeEntity.cs @@ -0,0 +1,38 @@ +namespace Artemis.Storage.Legacy.Entities.Profile.Nodes; + +internal class NodeEntity +{ + public NodeEntity() + { + PinCollections = new List(); + } + + public NodeEntity(NodeEntity nodeEntity) + { + Id = nodeEntity.Id; + Type = nodeEntity.Type; + ProviderId = nodeEntity.ProviderId; + + Name = nodeEntity.Name; + Description = nodeEntity.Description; + IsExitNode = nodeEntity.IsExitNode; + X = nodeEntity.X; + Y = nodeEntity.Y; + Storage = nodeEntity.Storage; + + PinCollections = nodeEntity.PinCollections.Select(p => new NodePinCollectionEntity(p)).ToList(); + } + + public Guid Id { get; set; } + public string Type { get; set; } = string.Empty; + public string ProviderId { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public bool IsExitNode { get; set; } + public double X { get; set; } + public double Y { get; set; } + public string Storage { get; set; } = string.Empty; + + public List PinCollections { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodePinCollectionEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodePinCollectionEntity.cs new file mode 100644 index 000000000..b10944b48 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodePinCollectionEntity.cs @@ -0,0 +1,19 @@ +namespace Artemis.Storage.Legacy.Entities.Profile.Nodes; + +internal class NodePinCollectionEntity +{ + public NodePinCollectionEntity() + { + } + + public NodePinCollectionEntity(NodePinCollectionEntity nodePinCollectionEntity) + { + Id = nodePinCollectionEntity.Id; + Direction = nodePinCollectionEntity.Direction; + Amount = nodePinCollectionEntity.Amount; + } + + public int Id { get; set; } + public int Direction { set; get; } + public int Amount { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodeScriptEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodeScriptEntity.cs new file mode 100644 index 000000000..8da434571 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/Nodes/NodeScriptEntity.cs @@ -0,0 +1,16 @@ +namespace Artemis.Storage.Legacy.Entities.Profile.Nodes; + +internal class NodeScriptEntity +{ + public NodeScriptEntity() + { + Nodes = new List(); + Connections = new List(); + } + + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + + public List Nodes { get; set; } + public List Connections { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/ProfileCategoryEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/ProfileCategoryEntity.cs new file mode 100644 index 000000000..c5b61012b --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/ProfileCategoryEntity.cs @@ -0,0 +1,78 @@ +using Artemis.Core; +using Artemis.Storage.Entities.Profile; +using LiteDB; +using Serilog; + +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class ProfileCategoryEntity +{ + public Guid Id { get; set; } + + public string Name { get; set; } = string.Empty; + public bool IsCollapsed { get; set; } + public bool IsSuspended { get; set; } + public int Order { get; set; } + + public List ProfileConfigurations { get; set; } = new(); + + public Storage.Entities.Profile.ProfileCategoryEntity Migrate(ILogger logger, List legacyProfiles, ILiteStorage profileIcons) + { + Storage.Entities.Profile.ProfileCategoryEntity category = new() + { + Id = Id, + Name = Name, + IsCollapsed = IsCollapsed, + IsSuspended = IsSuspended, + Order = Order + }; + + foreach (ProfileConfigurationEntity legacyProfileConfiguration in ProfileConfigurations) + { + // Find the profile + ProfileEntity? legacyProfile = legacyProfiles.FirstOrDefault(p => p.Id == legacyProfileConfiguration.ProfileId); + if (legacyProfile == null) + { + logger.Information("Profile not found for profile configuration {ProfileId}", legacyProfileConfiguration.ProfileId); + continue; + } + + // Clone to the new format via JSON, as both are serializable + string profileJson = CoreJson.Serialize(legacyProfile); + string configJson = CoreJson.Serialize(legacyProfileConfiguration); + Storage.Entities.Profile.ProfileEntity? profile = CoreJson.Deserialize(profileJson); + Storage.Entities.Profile.ProfileConfigurationEntity? config = CoreJson.Deserialize(configJson); + + if (profile == null) + { + logger.Information("Failed to deserialize profile JSON for profile configuration {ProfileId}", legacyProfileConfiguration.ProfileId); + continue; + } + + if (config == null) + { + logger.Information("Failed to deserialize profile configuration JSON for profile configuration {ProfileId}", legacyProfileConfiguration.ProfileId); + continue; + } + + // Add a container + ProfileContainerEntity container = new() + { + Profile = profile, + ProfileConfiguration = config, + }; + + // If available, download the profile icon + if (profileIcons.Exists(legacyProfileConfiguration.FileIconId)) + { + using MemoryStream memoryStream = new(); + profileIcons.Download(legacyProfileConfiguration.FileIconId, memoryStream); + container.Icon = memoryStream.ToArray(); + } + + category.ProfileConfigurations.Add(container); + } + + return category; + } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/ProfileConfigurationEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/ProfileConfigurationEntity.cs new file mode 100644 index 000000000..482672a60 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/ProfileConfigurationEntity.cs @@ -0,0 +1,29 @@ +using Artemis.Storage.Legacy.Entities.Profile.Nodes; + +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class ProfileConfigurationEntity +{ + public string Name { get; set; } = string.Empty; + public string? MaterialIcon { get; set; } + public Guid FileIconId { get; set; } + public int IconType { get; set; } + public bool IconFill { get; set; } + public int Order { get; set; } + + public bool IsSuspended { get; set; } + public int ActivationBehaviour { get; set; } + public NodeScriptEntity? ActivationCondition { get; set; } + + public int HotkeyMode { get; set; } + public ProfileConfigurationHotkeyEntity? EnableHotkey { get; set; } + public ProfileConfigurationHotkeyEntity? DisableHotkey { get; set; } + + public string? ModuleId { get; set; } + + public Guid ProfileCategoryId { get; set; } + public Guid ProfileId { get; set; } + + public bool FadeInAndOut { get; set; } + public int Version { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/ProfileConfigurationHotkeyEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/ProfileConfigurationHotkeyEntity.cs new file mode 100644 index 000000000..30cfb4a06 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/ProfileConfigurationHotkeyEntity.cs @@ -0,0 +1,7 @@ +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class ProfileConfigurationHotkeyEntity +{ + public int? Key { get; set; } + public int? Modifiers { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/ProfileEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/ProfileEntity.cs new file mode 100644 index 000000000..dbbbfee9d --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/ProfileEntity.cs @@ -0,0 +1,32 @@ +using Artemis.Storage.Legacy.Entities.General; + +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class ProfileEntity +{ + public ProfileEntity() + { + Folders = new List(); + Layers = new List(); + ScriptConfigurations = new List(); + } + + public Guid Id { get; set; } + + public string Name { get; set; } = string.Empty; + public bool IsFreshImport { get; set; } + + public List Folders { get; set; } + public List Layers { get; set; } + public List ScriptConfigurations { get; set; } + + public void UpdateGuid(Guid guid) + { + Guid oldGuid = Id; + Id = guid; + + FolderEntity? rootFolder = Folders.FirstOrDefault(f => f.ParentId == oldGuid); + if (rootFolder != null) + rootFolder.ParentId = Id; + } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/PropertyEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/PropertyEntity.cs new file mode 100644 index 000000000..44a4406ab --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/PropertyEntity.cs @@ -0,0 +1,13 @@ +using Artemis.Storage.Legacy.Entities.Profile.DataBindings; + +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class PropertyEntity +{ + public string Identifier { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public bool KeyframesEnabled { get; set; } + + public DataBindingEntity? DataBinding { get; set; } + public List KeyframeEntities { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/PropertyGroupEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/PropertyGroupEntity.cs new file mode 100644 index 000000000..2c7f316bb --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/PropertyGroupEntity.cs @@ -0,0 +1,8 @@ +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class PropertyGroupEntity +{ + public string Identifier { get; set; } = string.Empty; + public List Properties { get; set; } = new(); + public List PropertyGroups { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Profile/TimelineEntity.cs b/src/Artemis.Storage.Legacy/Entities/Profile/TimelineEntity.cs new file mode 100644 index 000000000..9a1f400e4 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Profile/TimelineEntity.cs @@ -0,0 +1,8 @@ +namespace Artemis.Storage.Legacy.Entities.Profile; + +internal class TimelineEntity +{ + public TimeSpan StartSegmentLength { get; set; } + public TimeSpan MainSegmentLength { get; set; } + public TimeSpan EndSegmentLength { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Surface/DeviceEntity.cs b/src/Artemis.Storage.Legacy/Entities/Surface/DeviceEntity.cs new file mode 100644 index 000000000..fc3988dba --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Surface/DeviceEntity.cs @@ -0,0 +1,78 @@ +namespace Artemis.Storage.Legacy.Entities.Surface; + +internal class DeviceEntity +{ + public DeviceEntity() + { + InputIdentifiers = new List(); + InputMappings = new List(); + Categories = new List(); + } + + public string Id { get; set; } = string.Empty; + public string DeviceProvider { get; set; } = string.Empty; + public float X { get; set; } + public float Y { get; set; } + public float Rotation { get; set; } + public float Scale { get; set; } + public int ZIndex { get; set; } + public float RedScale { get; set; } + public float GreenScale { get; set; } + public float BlueScale { get; set; } + public bool IsEnabled { get; set; } + + public int PhysicalLayout { get; set; } + public string? LogicalLayout { get; set; } + public string? LayoutType { get; set; } + public string? LayoutParameter { get; set; } + + public List InputIdentifiers { get; set; } + public List InputMappings { get; set; } + public List Categories { get; set; } + + public Storage.Entities.Surface.DeviceEntity Migrate() + { + // All properties match, return a copy + return new Storage.Entities.Surface.DeviceEntity() + { + Id = Id, + DeviceProvider = DeviceProvider, + X = X, + Y = Y, + Rotation = Rotation, + Scale = Scale, + ZIndex = ZIndex, + RedScale = RedScale, + GreenScale = GreenScale, + BlueScale = BlueScale, + IsEnabled = IsEnabled, + PhysicalLayout = PhysicalLayout, + LogicalLayout = LogicalLayout, + LayoutType = LayoutType, + LayoutParameter = LayoutParameter, + InputIdentifiers = InputIdentifiers.Select(i => new Storage.Entities.Surface.DeviceInputIdentifierEntity + { + InputProvider = i.InputProvider, + Identifier = i.Identifier.ToString() ?? string.Empty + }).ToList(), + InputMappings = InputMappings.Select(i => new Storage.Entities.Surface.InputMappingEntity + { + OriginalLedId = i.OriginalLedId, + MappedLedId = i.MappedLedId + }).ToList(), + Categories = Categories + }; + } +} + +internal class InputMappingEntity +{ + public int OriginalLedId { get; set; } + public int MappedLedId { get; set; } +} + +internal class DeviceInputIdentifierEntity +{ + public string InputProvider { get; set; } = string.Empty; + public object Identifier { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Entities/Workshop/EntryEntity.cs b/src/Artemis.Storage.Legacy/Entities/Workshop/EntryEntity.cs new file mode 100644 index 000000000..24c7a2efb --- /dev/null +++ b/src/Artemis.Storage.Legacy/Entities/Workshop/EntryEntity.cs @@ -0,0 +1,38 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Artemis.Storage.Legacy.Entities.Workshop; + +internal class EntryEntity +{ + public Guid Id { get; set; } + + public long EntryId { get; set; } + public int EntryType { get; set; } + + public string Author { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + + public long ReleaseId { get; set; } + public string ReleaseVersion { get; set; } = string.Empty; + public DateTimeOffset InstalledAt { get; set; } + + public Dictionary? Metadata { get; set; } + + public Storage.Entities.Workshop.EntryEntity Migrate() + { + // Create a copy + return new Storage.Entities.Workshop.EntryEntity() + { + Id = Id, + EntryId = EntryId, + EntryType = EntryType, + Author = Author, + Name = Name, + ReleaseId = ReleaseId, + ReleaseVersion = ReleaseVersion, + InstalledAt = InstalledAt, + Metadata = Metadata?.ToDictionary(kvp => kvp.Key, kvp => JsonSerializer.SerializeToNode(kvp.Value) ?? new JsonObject()) + }; + } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/LegacyMigrationService.cs b/src/Artemis.Storage.Legacy/LegacyMigrationService.cs new file mode 100644 index 000000000..1c3b35d4f --- /dev/null +++ b/src/Artemis.Storage.Legacy/LegacyMigrationService.cs @@ -0,0 +1,163 @@ +using Artemis.Core; +using Artemis.Storage.Legacy.Entities.General; +using Artemis.Storage.Legacy.Entities.Plugins; +using Artemis.Storage.Legacy.Entities.Profile; +using Artemis.Storage.Legacy.Entities.Surface; +using Artemis.Storage.Legacy.Entities.Workshop; +using Artemis.Storage.Legacy.Migrations; +using Artemis.Storage.Legacy.Migrations.Storage; +using DryIoc; +using LiteDB; +using Serilog; + +namespace Artemis.Storage.Legacy; + +public static class LegacyMigrationService +{ + public static void MigrateToSqlite(IContainer container) + { + ILogger logger = container.Resolve(); + + // Before creating a DB context which is kinda expensive, check if there's anything to migrate + if (!File.Exists(Path.Combine(Constants.DataFolder, "database.db"))) + { + logger.Information("No legacy database found, nothing to migrate"); + return; + } + + using ArtemisDbContext dbContext = container.Resolve(); + MigrateToSqlite(logger, dbContext); + } + + public static void MigrateToSqlite(ILogger logger, ArtemisDbContext dbContext) + { + if (!File.Exists(Path.Combine(Constants.DataFolder, "database.db"))) + { + logger.Information("No legacy database found, nothing to migrate"); + return; + } + + logger.Information("Migrating legacy database..."); + + try + { + // Copy the database before using it, we're going to make some modifications to it and we don't want to mess up the original + string databasePath = Path.Combine(Constants.DataFolder, "database.db"); + string tempPath = Path.Combine(Constants.DataFolder, "temp.db"); + File.Copy(databasePath, tempPath, true); + + using LiteRepository repository = new($"FileName={tempPath}"); + + // Apply pending LiteDB migrations, this includes a migration that transforms namespaces to Artemis.Storage.Legacy + ApplyPendingMigrations(logger, repository); + + // Devices + if (!dbContext.Devices.Any()) + { + logger.Information("Migrating devices"); + List legacyDevices = repository.Query().Include(s => s.InputIdentifiers).ToList(); + dbContext.Devices.AddRange(legacyDevices.Select(l => l.Migrate())); + dbContext.SaveChanges(); + } + + // Entries + if (!dbContext.Entries.Any()) + { + logger.Information("Migrating entries"); + List legacyEntries = repository.Query().ToList(); + dbContext.Entries.AddRange(legacyEntries.Select(l => l.Migrate())); + dbContext.SaveChanges(); + } + + // Plugins + if (!dbContext.Plugins.Any()) + { + logger.Information("Migrating plugins"); + List legacyPlugins = repository.Query().ToList(); + dbContext.Plugins.AddRange(legacyPlugins.Select(l => l.Migrate())); + dbContext.SaveChanges(); + } + + // PluginSettings + if (!dbContext.PluginSettings.Any()) + { + logger.Information("Migrating plugin settings"); + List legacyPluginSettings = repository.Query().ToList(); + dbContext.PluginSettings.AddRange(legacyPluginSettings.Select(l => l.Migrate())); + dbContext.SaveChanges(); + } + + // ProfileCategories + if (!dbContext.ProfileCategories.Any()) + { + logger.Information("Migrating profile categories"); + List legacyProfileCategories = repository.Query().ToList(); + ILiteStorage profileIcons = repository.Database.GetStorage("profileIcons"); + List legacyProfiles = repository.Query().ToList(); + dbContext.ProfileCategories.AddRange(legacyProfileCategories.Select(l => l.Migrate(logger, legacyProfiles, profileIcons))); + dbContext.SaveChanges(); + } + + // Releases + if (!dbContext.Releases.Any()) + { + logger.Information("Migrating releases"); + List legacyReleases = repository.Query().ToList(); + dbContext.Releases.AddRange(legacyReleases.Select(l => l.Migrate())); + dbContext.SaveChanges(); + } + + // After a successful migration, keep the legacy database around for a while + File.Move(Path.Combine(Constants.DataFolder, "database.db"), Path.Combine(Constants.DataFolder, "legacy.db")); + + logger.Information("Legacy database migrated"); + } + catch (Exception e) + { + logger.Error(e, "Failed to migrate legacy database"); + throw; + } + finally + { + File.Delete(Path.Combine(Constants.DataFolder, "temp.db")); + } + } + + private static void ApplyPendingMigrations(ILogger logger, LiteRepository repository) + { + List migrations = + [ + new M0020AvaloniaReset(), + new M0021GradientNodes(), + new M0022TransitionNodes(), + new M0023LayoutProviders(), + new M0024NodeProviders(), + new M0025NodeProvidersProfileConfig(), + new M0026NodeStorage(logger), + new M0027Namespace() + ]; + + foreach (IStorageMigration storageMigration in migrations.OrderBy(m => m.UserVersion)) + { + if (repository.Database.UserVersion >= storageMigration.UserVersion) + continue; + + logger.Information("Applying storage migration {storageMigration} to update DB from v{oldVersion} to v{newVersion}", + storageMigration.GetType().Name, repository.Database.UserVersion, storageMigration.UserVersion); + + repository.Database.BeginTrans(); + try + { + storageMigration.Apply(repository); + } + catch (Exception) + { + repository.Database.Rollback(); + throw; + } + + repository.Database.Commit(); + repository.Database.UserVersion = storageMigration.UserVersion; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Migrations/IProfileMigration.cs b/src/Artemis.Storage.Legacy/Migrations/IProfileMigration.cs new file mode 100644 index 000000000..53d1d1eda --- /dev/null +++ b/src/Artemis.Storage.Legacy/Migrations/IProfileMigration.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Nodes; + +namespace Artemis.Storage.Legacy.Migrations; + +public interface IProfileMigration +{ + int Version { get; } + void Migrate(JsonObject configurationJson, JsonObject profileJson); +} \ No newline at end of file diff --git a/src/Artemis.Storage/Migrations/IStorageMigration.cs b/src/Artemis.Storage.Legacy/Migrations/IStorageMigration.cs similarity index 74% rename from src/Artemis.Storage/Migrations/IStorageMigration.cs rename to src/Artemis.Storage.Legacy/Migrations/IStorageMigration.cs index 4382a837d..18ba559ca 100644 --- a/src/Artemis.Storage/Migrations/IStorageMigration.cs +++ b/src/Artemis.Storage.Legacy/Migrations/IStorageMigration.cs @@ -1,6 +1,6 @@ using LiteDB; -namespace Artemis.Storage.Migrations; +namespace Artemis.Storage.Legacy.Migrations; public interface IStorageMigration { diff --git a/src/Artemis.Storage/Migrations/Storage/M0020AvaloniaReset.cs b/src/Artemis.Storage.Legacy/Migrations/Storage/M0020AvaloniaReset.cs similarity index 67% rename from src/Artemis.Storage/Migrations/Storage/M0020AvaloniaReset.cs rename to src/Artemis.Storage.Legacy/Migrations/Storage/M0020AvaloniaReset.cs index 293fc1ff7..685585ffb 100644 --- a/src/Artemis.Storage/Migrations/Storage/M0020AvaloniaReset.cs +++ b/src/Artemis.Storage.Legacy/Migrations/Storage/M0020AvaloniaReset.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; -using System.Linq; -using LiteDB; +using LiteDB; -namespace Artemis.Storage.Migrations.Storage; +namespace Artemis.Storage.Legacy.Migrations.Storage; -public class M0020AvaloniaReset : IStorageMigration +internal class M0020AvaloniaReset : IStorageMigration { public int UserVersion => 20; diff --git a/src/Artemis.Storage/Migrations/Storage/M0021GradientNodes.cs b/src/Artemis.Storage.Legacy/Migrations/Storage/M0021GradientNodes.cs similarity index 91% rename from src/Artemis.Storage/Migrations/Storage/M0021GradientNodes.cs rename to src/Artemis.Storage.Legacy/Migrations/Storage/M0021GradientNodes.cs index 2bb90a45c..02998e98c 100644 --- a/src/Artemis.Storage/Migrations/Storage/M0021GradientNodes.cs +++ b/src/Artemis.Storage.Legacy/Migrations/Storage/M0021GradientNodes.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Storage.Entities.Profile; -using Artemis.Storage.Entities.Profile.Nodes; +using Artemis.Storage.Legacy.Entities.Profile; +using Artemis.Storage.Legacy.Entities.Profile.Nodes; using LiteDB; -namespace Artemis.Storage.Migrations.Storage; +namespace Artemis.Storage.Legacy.Migrations.Storage; -public class M0021GradientNodes : IStorageMigration +internal class M0021GradientNodes : IStorageMigration { private void MigrateDataBinding(PropertyEntity property) { @@ -62,7 +59,7 @@ public class M0021GradientNodes : IStorageMigration { if (propertyGroup == null) return; - + foreach (PropertyGroupEntity propertyGroupPropertyGroup in propertyGroup.PropertyGroups) MigrateDataBinding(propertyGroupPropertyGroup); diff --git a/src/Artemis.Storage/Migrations/Storage/M0022TransitionNodes.cs b/src/Artemis.Storage.Legacy/Migrations/Storage/M0022TransitionNodes.cs similarity index 90% rename from src/Artemis.Storage/Migrations/Storage/M0022TransitionNodes.cs rename to src/Artemis.Storage.Legacy/Migrations/Storage/M0022TransitionNodes.cs index 791b95ea9..369ab130c 100644 --- a/src/Artemis.Storage/Migrations/Storage/M0022TransitionNodes.cs +++ b/src/Artemis.Storage.Legacy/Migrations/Storage/M0022TransitionNodes.cs @@ -1,13 +1,11 @@ -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.Legacy.Entities.Profile; +using Artemis.Storage.Legacy.Entities.Profile.Conditions; +using Artemis.Storage.Legacy.Entities.Profile.Nodes; using LiteDB; -namespace Artemis.Storage.Migrations.Storage; +namespace Artemis.Storage.Legacy.Migrations.Storage; -public class M0022TransitionNodes : IStorageMigration +internal class M0022TransitionNodes : IStorageMigration { private void MigrateNodeScript(NodeScriptEntity? nodeScript) { diff --git a/src/Artemis.Storage/Migrations/Storage/M0023LayoutProviders.cs b/src/Artemis.Storage.Legacy/Migrations/Storage/M0023LayoutProviders.cs similarity index 86% rename from src/Artemis.Storage/Migrations/Storage/M0023LayoutProviders.cs rename to src/Artemis.Storage.Legacy/Migrations/Storage/M0023LayoutProviders.cs index 5296d5842..1a8d7500b 100644 --- a/src/Artemis.Storage/Migrations/Storage/M0023LayoutProviders.cs +++ b/src/Artemis.Storage.Legacy/Migrations/Storage/M0023LayoutProviders.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; -using LiteDB; +using LiteDB; -namespace Artemis.Storage.Migrations.Storage; +namespace Artemis.Storage.Legacy.Migrations.Storage; -public class M0023LayoutProviders : IStorageMigration +internal class M0023LayoutProviders : IStorageMigration { public int UserVersion => 23; @@ -20,9 +19,13 @@ public class M0023LayoutProviders : IStorageMigration bsonDocument.Add("LayoutParameter", new BsonValue(customLayoutPath.AsString)); } else if (bsonDocument.TryGetValue("DisableDefaultLayout", out BsonValue disableDefaultLayout) && disableDefaultLayout.AsBoolean) + { bsonDocument.Add("LayoutType", new BsonValue("None")); + } else + { bsonDocument.Add("LayoutType", new BsonValue("Default")); + } bsonDocument.Remove("CustomLayoutPath"); bsonDocument.Remove("DisableDefaultLayout"); diff --git a/src/Artemis.Storage/Migrations/Storage/M0024NodeProviders.cs b/src/Artemis.Storage.Legacy/Migrations/Storage/M0024NodeProviders.cs similarity index 91% rename from src/Artemis.Storage/Migrations/Storage/M0024NodeProviders.cs rename to src/Artemis.Storage.Legacy/Migrations/Storage/M0024NodeProviders.cs index c09c0f1fc..4617ae6d1 100644 --- a/src/Artemis.Storage/Migrations/Storage/M0024NodeProviders.cs +++ b/src/Artemis.Storage.Legacy/Migrations/Storage/M0024NodeProviders.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; -using Artemis.Storage.Entities.Profile; -using LiteDB; +using LiteDB; -namespace Artemis.Storage.Migrations.Storage; +namespace Artemis.Storage.Legacy.Migrations.Storage; -public class M0024NodeProviders : IStorageMigration +internal class M0024NodeProviders : IStorageMigration { public int UserVersion => 24; @@ -22,6 +20,7 @@ public class M0024NodeProviders : IStorageMigration categoriesToUpdate.Add(profileCategoryBson); } } + categoryCollection.Update(categoriesToUpdate); ILiteCollection collection = repository.Database.GetCollection("ProfileEntity"); @@ -30,15 +29,12 @@ public class M0024NodeProviders : IStorageMigration { BsonArray? folders = profileBson["Folders"]?.AsArray; BsonArray? layers = profileBson["Layers"]?.AsArray; - + if (folders != null) - { foreach (BsonValue folder in folders) MigrateProfileElement(folder.AsDocument); - } - + if (layers != null) - { foreach (BsonValue layer in layers) { MigrateProfileElement(layer.AsDocument); @@ -46,8 +42,7 @@ public class M0024NodeProviders : IStorageMigration MigratePropertyGroup(layer.AsDocument["TransformPropertyGroup"].AsDocument); MigratePropertyGroup(layer.AsDocument["LayerBrush"]?["PropertyGroup"].AsDocument); } - } - + profilesToUpdate.Add(profileBson); } @@ -58,10 +53,8 @@ public class M0024NodeProviders : IStorageMigration { BsonArray? layerEffects = profileElement["LayerEffects"]?.AsArray; if (layerEffects != null) - { foreach (BsonValue layerEffect in layerEffects) MigratePropertyGroup(layerEffect.AsDocument["PropertyGroup"].AsDocument); - } BsonValue? displayCondition = profileElement["DisplayCondition"]; if (displayCondition != null) @@ -77,16 +70,12 @@ public class M0024NodeProviders : IStorageMigration BsonArray? propertyGroups = propertyGroup["PropertyGroups"]?.AsArray; if (properties != null) - { foreach (BsonValue property in properties) MigrateNodeScript(property.AsDocument["DataBinding"]?["NodeScript"]?.AsDocument); - } if (propertyGroups != null) - { foreach (BsonValue childPropertyGroup in propertyGroups) MigratePropertyGroup(childPropertyGroup.AsDocument); - } } private void MigrateNodeScript(BsonDocument? nodeScript) diff --git a/src/Artemis.Storage/Migrations/Storage/M0025NodeProvidersProfileConfig.cs b/src/Artemis.Storage.Legacy/Migrations/Storage/M0025NodeProvidersProfileConfig.cs similarity index 89% rename from src/Artemis.Storage/Migrations/Storage/M0025NodeProvidersProfileConfig.cs rename to src/Artemis.Storage.Legacy/Migrations/Storage/M0025NodeProvidersProfileConfig.cs index 46d8c3fe7..bff6ff6ff 100644 --- a/src/Artemis.Storage/Migrations/Storage/M0025NodeProvidersProfileConfig.cs +++ b/src/Artemis.Storage.Legacy/Migrations/Storage/M0025NodeProvidersProfileConfig.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; -using LiteDB; +using LiteDB; -namespace Artemis.Storage.Migrations.Storage; +namespace Artemis.Storage.Legacy.Migrations.Storage; -public class M0025NodeProvidersProfileConfig : IStorageMigration +internal class M0025NodeProvidersProfileConfig : IStorageMigration { public int UserVersion => 25; @@ -21,13 +20,14 @@ public class M0025NodeProvidersProfileConfig : IStorageMigration profile["Version"] = 2; MigrateNodeScript(profile["ActivationCondition"]?.AsDocument); } + toUpdate.Add(profileCategoryBson); } } - + categoryCollection.Update(toUpdate); } - + private void MigrateNodeScript(BsonDocument? nodeScript) { if (nodeScript == null || nodeScript.Keys.Count == 0) diff --git a/src/Artemis.Storage.Legacy/Migrations/Storage/M0026NodeStorage.cs b/src/Artemis.Storage.Legacy/Migrations/Storage/M0026NodeStorage.cs new file mode 100644 index 000000000..cbea0db8d --- /dev/null +++ b/src/Artemis.Storage.Legacy/Migrations/Storage/M0026NodeStorage.cs @@ -0,0 +1,215 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using LiteDB; +using Serilog; + +namespace Artemis.Storage.Legacy.Migrations.Storage; + +internal class M0026NodeStorage : IStorageMigration +{ + private readonly ILogger _logger; + + public M0026NodeStorage(ILogger logger) + { + _logger = logger; + } + + public int UserVersion => 26; + + public void Apply(LiteRepository repository) + { + ILiteCollection categoryCollection = repository.Database.GetCollection("ProfileCategoryEntity"); + List toUpdate = new(); + foreach (BsonDocument profileCategoryBson in categoryCollection.FindAll()) + { + BsonArray? profiles = profileCategoryBson["ProfileConfigurations"]?.AsArray; + if (profiles != null) + { + foreach (BsonValue profile in profiles) + { + profile["Version"] = 4; + MigrateNodeScript(profile["ActivationCondition"]?.AsDocument); + } + + toUpdate.Add(profileCategoryBson); + } + } + + categoryCollection.Update(toUpdate); + + ILiteCollection collection = repository.Database.GetCollection("ProfileEntity"); + List profilesToUpdate = new(); + foreach (BsonDocument profileBson in collection.FindAll()) + { + BsonArray? folders = profileBson["Folders"]?.AsArray; + BsonArray? layers = profileBson["Layers"]?.AsArray; + + if (folders != null) + foreach (BsonValue folder in folders) + MigrateProfileElement(folder.AsDocument); + + if (layers != null) + foreach (BsonValue layer in layers) + { + MigrateProfileElement(layer.AsDocument); + MigratePropertyGroup(layer.AsDocument["GeneralPropertyGroup"].AsDocument); + MigratePropertyGroup(layer.AsDocument["TransformPropertyGroup"].AsDocument); + MigratePropertyGroup(layer.AsDocument["LayerBrush"]?["PropertyGroup"].AsDocument); + } + + profilesToUpdate.Add(profileBson); + } + + collection.Update(profilesToUpdate); + } + + private void MigrateProfileElement(BsonDocument profileElement) + { + BsonArray? layerEffects = profileElement["LayerEffects"]?.AsArray; + if (layerEffects != null) + foreach (BsonValue layerEffect in layerEffects) + MigratePropertyGroup(layerEffect.AsDocument["PropertyGroup"].AsDocument); + + BsonValue? displayCondition = profileElement["DisplayCondition"]; + if (displayCondition != null) + MigrateNodeScript(displayCondition.AsDocument["Script"].AsDocument); + } + + private void MigratePropertyGroup(BsonDocument? propertyGroup) + { + if (propertyGroup == null || propertyGroup.Keys.Count == 0) + return; + + BsonArray? properties = propertyGroup["Properties"]?.AsArray; + BsonArray? propertyGroups = propertyGroup["PropertyGroups"]?.AsArray; + + if (properties != null) + foreach (BsonValue property in properties) + MigrateNodeScript(property.AsDocument["DataBinding"]?["NodeScript"]?.AsDocument); + + if (propertyGroups != null) + foreach (BsonValue childPropertyGroup in propertyGroups) + MigratePropertyGroup(childPropertyGroup.AsDocument); + } + + private void MigrateNodeScript(BsonDocument? nodeScript) + { + if (nodeScript == null || nodeScript.Keys.Count == 0) + return; + + BsonArray? nodes = nodeScript["Nodes"]?.AsArray; + if (nodes == null) + return; + + foreach (BsonValue node in nodes) + // Migrate the storage of the node + node["Storage"] = MigrateNodeStorageJson(node.AsDocument["Storage"]?.AsString, _logger); + } + + private static string? MigrateNodeStorageJson(string? json, ILogger logger) + { + if (string.IsNullOrEmpty(json)) + return json; + + try + { + JsonDocument jsonDocument = JsonDocument.Parse(json); + if (jsonDocument.RootElement.ValueKind != JsonValueKind.Object) + return json; + + JsonObject? jsonObject = JsonObject.Create(jsonDocument.RootElement); + if (jsonObject == null) + return json; + + if (jsonObject["$type"] != null && jsonObject["$values"] != null) + { + JsonArray? values = jsonObject["$values"]?.AsArray(); + if (values != null) + { + foreach (JsonNode? jsonNode in values.ToList()) + { + if (jsonNode is JsonObject childObject) + ConvertToSystemTextJson(childObject); + } + + return values.ToJsonString(); + } + } + else + { + ConvertToSystemTextJson(jsonObject); + } + + return jsonObject.ToJsonString(); + } + catch (Exception e) + { + logger.Error(e, "Failed to migrate node storage JSON"); + return json; + } + } + + private static void ConvertToSystemTextJson(JsonObject jsonObject) + { + FilterType(jsonObject); + + // Recursively convert all JSON arrays from {$type: "xyz", $values: []} to [] + foreach ((string? key, JsonNode? value) in jsonObject.ToDictionary()) + { + if (value is not JsonObject obj) + continue; + + // if there is a $type and a $values, replace the entire node with $values regardless of the value of $type + if (obj["$type"] != null && obj["$values"] != null) + { + JsonArray? values = obj["$values"]?.AsArray(); + if (values != null) + { + obj.Remove("$values"); + jsonObject[key] = values; + foreach (JsonNode? jsonNode in values.ToList()) + { + if (jsonNode is JsonObject childObject) + ConvertToSystemTextJson(childObject); + } + } + + obj.Remove("$type"); + } + else + { + ConvertToSystemTextJson(obj); + } + } + } + + private static void FilterType(JsonObject jsonObject) + { + // Replace or remove $type depending on whether there's a matching JsonDerivedType + // This could be done with reflection but that would mean this migration automatically gains new behaviour over time. + JsonNode? type = jsonObject["$type"]; + if (type != null) + { + // Adaption Hints + if (type.GetValue() == "Artemis.Storage.Entities.Profile.AdaptionHints.CategoryAdaptionHintEntity, Artemis.Storage") + jsonObject["$type"] = "Category"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.AdaptionHints.DeviceAdaptionHintEntity, Artemis.Storage") + jsonObject["$type"] = "Device"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.AdaptionHints.KeyboardSectionAdaptionHintEntity, Artemis.Storage") + jsonObject["$type"] = "KeyboardSection"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.AdaptionHints.SingleLedAdaptionHintEntity, Artemis.Storage") + jsonObject["$type"] = "SingleLed"; + // Conditions + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.Conditions.AlwaysOnConditionEntity, Artemis.Storage") + jsonObject["$type"] = "AlwaysOn"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.Conditions.EventConditionEntity, Artemis.Storage") + jsonObject["$type"] = "Event"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.Conditions.PlayOnceConditionEntity, Artemis.Storage") + jsonObject["$type"] = "PlayOnce"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.Conditions.StaticConditionEntity, Artemis.Storage") + jsonObject["$type"] = "Static"; + else + jsonObject.Remove("$type"); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Migrations/Storage/M0027Namespace.cs b/src/Artemis.Storage.Legacy/Migrations/Storage/M0027Namespace.cs new file mode 100644 index 000000000..b0bee99a1 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Migrations/Storage/M0027Namespace.cs @@ -0,0 +1,41 @@ +using LiteDB; + +namespace Artemis.Storage.Legacy.Migrations.Storage; + +internal class M0027Namespace : IStorageMigration +{ + public int UserVersion => 27; + + public void Apply(LiteRepository repository) + { + ILiteCollection collection = repository.Database.GetCollection("ProfileEntity"); + List profilesToUpdate = new(); + + foreach (BsonDocument profileBson in collection.FindAll()) + { + MigrateDocument(profileBson); + profilesToUpdate.Add(profileBson); + } + + collection.Update(profilesToUpdate); + } + + private void MigrateDocument(BsonDocument document) + { + foreach ((string? key, BsonValue? value) in document) + { + if (key == "_type") + document[key] = document[key].AsString + .Replace("Artemis.Storage.Entities", "Artemis.Storage.Legacy.Entities") + .Replace(", Artemis.Storage", ", Artemis.Storage.Legacy"); + else if (value.IsDocument) + MigrateDocument(value.AsDocument); + else if (value.IsArray) + foreach (BsonValue bsonValue in value.AsArray) + { + if (bsonValue.IsDocument) + MigrateDocument(bsonValue.AsDocument); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage.Legacy/Program.cs b/src/Artemis.Storage.Legacy/Program.cs new file mode 100644 index 000000000..c225e03f5 --- /dev/null +++ b/src/Artemis.Storage.Legacy/Program.cs @@ -0,0 +1,22 @@ +// using Artemis.Core.DryIoc; +// using Artemis.Storage; +// using Artemis.Storage.Legacy; +// using DryIoc; +// using Microsoft.EntityFrameworkCore; +// using Serilog; +// +// using Container container = new(rules => rules +// .WithMicrosoftDependencyInjectionRules() +// .WithConcreteTypeDynamicRegistrations() +// .WithoutThrowOnRegisteringDisposableTransient()); +// +// container.RegisterCore(); +// +// ILogger logger = container.Resolve(); +// ArtemisDbContext dbContext = container.Resolve(); +// +// logger.Information("Applying pending migrations..."); +// dbContext.Database.Migrate(); +// logger.Information("Pending migrations applied"); +// +// MigrationService.MigrateToSqlite(logger, dbContext); \ No newline at end of file diff --git a/src/Artemis.Storage/Artemis.Storage.csproj b/src/Artemis.Storage/Artemis.Storage.csproj index b01c7ef62..fa7b8fe61 100644 --- a/src/Artemis.Storage/Artemis.Storage.csproj +++ b/src/Artemis.Storage/Artemis.Storage.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Artemis.Storage/ArtemisDbContext.cs b/src/Artemis.Storage/ArtemisDbContext.cs new file mode 100644 index 000000000..32acba6a6 --- /dev/null +++ b/src/Artemis.Storage/ArtemisDbContext.cs @@ -0,0 +1,59 @@ +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; +using Artemis.Storage.Entities.Surface; +using Artemis.Storage.Entities.Workshop; +using Microsoft.EntityFrameworkCore; + +namespace Artemis.Storage; + +public class ArtemisDbContext : DbContext +{ + public DbSet Devices => Set(); + public DbSet Entries => Set(); + public DbSet Plugins => Set(); + public DbSet PluginFeatures => Set(); + public DbSet PluginSettings => Set(); + public DbSet ProfileCategories => Set(); + public DbSet ProfileContainers => Set(); + public DbSet Releases => Set(); + + public string DataFolder { get; set; } = string.Empty; + public JsonSerializerOptions JsonSerializerOptions { get; set; } = JsonSerializerOptions.Default; + + /// + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlite($"Data Source={Path.Combine(DataFolder, "artemis.db")}"); + } + + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .OwnsOne(d => d.InputIdentifiers, builder => builder.ToJson()) + .OwnsOne(d => d.InputMappings, builder => builder.ToJson()); + + modelBuilder.Entity() + .Property(e => e.Metadata) + .HasConversion( + v => JsonSerializer.Serialize(v, JsonSerializerOptions), + v => JsonSerializer.Deserialize>(v, JsonSerializerOptions) ?? new Dictionary()); + + modelBuilder.Entity() + .Property(e => e.ProfileConfiguration) + .HasConversion( + v => JsonSerializer.Serialize(v, JsonSerializerOptions), + v => JsonSerializer.Deserialize(v, JsonSerializerOptions) ?? new ProfileConfigurationEntity()); + + modelBuilder.Entity() + .Property(e => e.Profile) + .HasConversion( + v => JsonSerializer.Serialize(v, JsonSerializerOptions), + v => JsonSerializer.Deserialize(v, JsonSerializerOptions) ?? new ProfileEntity()); + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/General/ReleaseEntity.cs b/src/Artemis.Storage/Entities/General/ReleaseEntity.cs index f83efb6f1..47619b3c7 100644 --- a/src/Artemis.Storage/Entities/General/ReleaseEntity.cs +++ b/src/Artemis.Storage/Entities/General/ReleaseEntity.cs @@ -1,11 +1,17 @@ using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; namespace Artemis.Storage.Entities.General; +[Index(nameof(Version), IsUnique = true)] +[Index(nameof(InstalledAt))] public class ReleaseEntity { public Guid Id { get; set; } + [MaxLength(64)] public string Version { get; set; } = string.Empty; + public DateTimeOffset? InstalledAt { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Plugins/PluginEntity.cs b/src/Artemis.Storage/Entities/Plugins/PluginEntity.cs index 611200781..faf43f951 100644 --- a/src/Artemis.Storage/Entities/Plugins/PluginEntity.cs +++ b/src/Artemis.Storage/Entities/Plugins/PluginEntity.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.EntityFrameworkCore; namespace Artemis.Storage.Entities.Plugins; /// /// Represents the configuration of a plugin, each plugin has one configuration /// +[Index(nameof(PluginGuid), IsUnique = true)] public class PluginEntity { public PluginEntity() @@ -14,6 +17,7 @@ public class PluginEntity } public Guid Id { get; set; } + public Guid PluginGuid { get; set; } public bool IsEnabled { get; set; } public List Features { get; set; } @@ -24,6 +28,8 @@ public class PluginEntity /// public class PluginFeatureEntity { + public Guid Id { get; set; } + public string Type { get; set; } = string.Empty; public bool IsEnabled { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs b/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs index 1d604c28a..b386b34d6 100644 --- a/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs +++ b/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs @@ -1,15 +1,20 @@ using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; namespace Artemis.Storage.Entities.Plugins; /// /// Represents the setting of a plugin, a plugin can have multiple settings /// +[Index(nameof(Name), nameof(PluginGuid), IsUnique = true)] +[Index(nameof(PluginGuid))] public class PluginSettingEntity { public Guid Id { get; set; } public Guid PluginGuid { get; set; } + [MaxLength(128)] public string Name { get; set; } = string.Empty; public string Value { get; set; } = string.Empty; } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/FolderEntity.cs b/src/Artemis.Storage/Entities/Profile/FolderEntity.cs index f47aa6bb8..f761dd98b 100644 --- a/src/Artemis.Storage/Entities/Profile/FolderEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/FolderEntity.cs @@ -1,6 +1,5 @@ using System; using Artemis.Storage.Entities.Profile.Abstract; -using LiteDB; namespace Artemis.Storage.Entities.Profile; @@ -11,8 +10,5 @@ public class FolderEntity : RenderElementEntity public bool IsExpanded { get; set; } public bool Suspended { get; set; } - [BsonRef("ProfileEntity")] - public ProfileEntity Profile { get; set; } = null!; - public Guid ProfileId { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs index 521550b34..d7e9dfe09 100644 --- a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.AdaptionHints; -using LiteDB; namespace Artemis.Storage.Entities.Profile; @@ -25,8 +24,5 @@ public class LayerEntity : RenderElementEntity public PropertyGroupEntity? TransformPropertyGroup { get; set; } public LayerBrushEntity? LayerBrush { get; set; } - [BsonRef("ProfileEntity")] - public ProfileEntity Profile { get; set; } = null!; - public Guid ProfileId { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/ProfileCategoryEntity.cs b/src/Artemis.Storage/Entities/Profile/ProfileCategoryEntity.cs index 126845b38..b7a3c15f4 100644 --- a/src/Artemis.Storage/Entities/Profile/ProfileCategoryEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/ProfileCategoryEntity.cs @@ -1,16 +1,20 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; namespace Artemis.Storage.Entities.Profile; +[Index(nameof(Name), IsUnique = true)] public class ProfileCategoryEntity { public Guid Id { get; set; } + [MaxLength(64)] public string Name { get; set; } = string.Empty; public bool IsCollapsed { get; set; } public bool IsSuspended { get; set; } public int Order { get; set; } - public List ProfileConfigurations { get; set; } = new(); + public List ProfileConfigurations { get; set; } = new(); } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs b/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs index aa20adf7f..fb58e8d8a 100644 --- a/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs @@ -1,6 +1,5 @@ using System; using Artemis.Storage.Entities.Profile.Nodes; -using Serilog.Core; namespace Artemis.Storage.Entities.Profile; @@ -8,7 +7,6 @@ public class ProfileConfigurationEntity { public string Name { get; set; } = string.Empty; public string? MaterialIcon { get; set; } - public Guid FileIconId { get; set; } public int IconType { get; set; } public bool IconFill { get; set; } public int Order { get; set; } @@ -27,5 +25,5 @@ public class ProfileConfigurationEntity public Guid ProfileId { get; set; } public bool FadeInAndOut { get; set; } - public int Version { get; set; } = StorageMigrationService.PROFILE_VERSION; + public int Version { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/ProfileContainerEntity.cs b/src/Artemis.Storage/Entities/Profile/ProfileContainerEntity.cs new file mode 100644 index 000000000..b6360ab76 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/ProfileContainerEntity.cs @@ -0,0 +1,14 @@ +using System; + +namespace Artemis.Storage.Entities.Profile; + +public class ProfileContainerEntity +{ + public Guid Id { get; set; } + public byte[] Icon { get; set; } = Array.Empty(); + + public ProfileCategoryEntity ProfileCategory { get; set; } = null!; + + public ProfileConfigurationEntity ProfileConfiguration { get; set; } = new(); + public ProfileEntity Profile { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/RawProfileContainer.cs b/src/Artemis.Storage/Entities/RawProfileContainer.cs new file mode 100644 index 000000000..4638f3e29 --- /dev/null +++ b/src/Artemis.Storage/Entities/RawProfileContainer.cs @@ -0,0 +1,10 @@ +using System; + +namespace Artemis.Storage.Entities; + +internal class RawProfileContainer +{ + public Guid Id { get; set; } + public string ProfileConfiguration { get; set; } + public string Profile { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs index 5075131c2..6500b9b1d 100644 --- a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs +++ b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace Artemis.Storage.Entities.Surface; @@ -11,8 +12,12 @@ public class DeviceEntity Categories = new List(); } + [MaxLength(512)] public string Id { get; set; } = string.Empty; + + [MaxLength(512)] public string DeviceProvider { get; set; } = string.Empty; + public float X { get; set; } public float Y { get; set; } public float Rotation { get; set; } @@ -22,10 +27,16 @@ public class DeviceEntity public float GreenScale { get; set; } public float BlueScale { get; set; } public bool IsEnabled { get; set; } - + public int PhysicalLayout { get; set; } + + [MaxLength(32)] public string? LogicalLayout { get; set; } + + [MaxLength(64)] public string? LayoutType { get; set; } + + [MaxLength(512)] public string? LayoutParameter { get; set; } public List InputIdentifiers { get; set; } @@ -42,5 +53,5 @@ public class InputMappingEntity public class DeviceInputIdentifierEntity { public string InputProvider { get; set; } = string.Empty; - public object Identifier { get; set; } = string.Empty; + public string Identifier { get; set; } = string.Empty; } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs b/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs index 4be84d9e8..ad11d6188 100644 --- a/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs +++ b/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs @@ -1,21 +1,24 @@ using System; using System.Collections.Generic; +using System.Text.Json.Nodes; +using Microsoft.EntityFrameworkCore; namespace Artemis.Storage.Entities.Workshop; +[Index(nameof(EntryId), IsUnique = true)] public class EntryEntity { public Guid Id { get; set; } - + public long EntryId { get; set; } public int EntryType { get; set; } - - public string Author { get; set; } = string.Empty; + + public string Author { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public long ReleaseId { get; set; } 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/Exceptions/ArtemisStorageException.cs b/src/Artemis.Storage/Exceptions/ArtemisStorageException.cs new file mode 100644 index 000000000..85e08a3df --- /dev/null +++ b/src/Artemis.Storage/Exceptions/ArtemisStorageException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Artemis.Storage.Exceptions; + +public class ArtemisStorageException : Exception +{ + public ArtemisStorageException(string message) : base(message) + { + } + + public ArtemisStorageException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Migrations/20240310201706_Initial.Designer.cs b/src/Artemis.Storage/Migrations/20240310201706_Initial.Designer.cs new file mode 100644 index 000000000..bb62ee588 --- /dev/null +++ b/src/Artemis.Storage/Migrations/20240310201706_Initial.Designer.cs @@ -0,0 +1,352 @@ +// +using System; +using Artemis.Storage; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Artemis.Storage.Migrations +{ + [DbContext(typeof(ArtemisDbContext))] + [Migration("20240310201706_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.2"); + + modelBuilder.Entity("Artemis.Storage.Entities.General.ReleaseEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("InstalledAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstalledAt"); + + b.HasIndex("Version") + .IsUnique(); + + b.ToTable("Releases"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("PluginGuid") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PluginGuid") + .IsUnique(); + + b.ToTable("Plugins"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("PluginEntityId") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PluginEntityId"); + + b.ToTable("PluginFeatures"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginSettingEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("PluginGuid") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PluginGuid"); + + b.HasIndex("Name", "PluginGuid") + .IsUnique(); + + b.ToTable("PluginSettings"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsCollapsed") + .HasColumnType("INTEGER"); + + b.Property("IsSuspended") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProfileCategories"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileContainerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("Profile") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProfileCategoryId") + .HasColumnType("TEXT"); + + b.Property("ProfileConfiguration") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileCategoryId"); + + b.ToTable("ProfileContainers"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b => + { + b.Property("Id") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("BlueScale") + .HasColumnType("REAL"); + + b.Property("Categories") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceProvider") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("GreenScale") + .HasColumnType("REAL"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LayoutParameter") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("LayoutType") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LogicalLayout") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("PhysicalLayout") + .HasColumnType("INTEGER"); + + b.Property("RedScale") + .HasColumnType("REAL"); + + b.Property("Rotation") + .HasColumnType("REAL"); + + b.Property("Scale") + .HasColumnType("REAL"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("ZIndex") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Workshop.EntryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EntryId") + .HasColumnType("INTEGER"); + + b.Property("EntryType") + .HasColumnType("INTEGER"); + + b.Property("InstalledAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReleaseId") + .HasColumnType("INTEGER"); + + b.Property("ReleaseVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b => + { + b.HasOne("Artemis.Storage.Entities.Plugins.PluginEntity", null) + .WithMany("Features") + .HasForeignKey("PluginEntityId"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileContainerEntity", b => + { + b.HasOne("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", "ProfileCategory") + .WithMany("ProfileConfigurations") + .HasForeignKey("ProfileCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProfileCategory"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b => + { + b.OwnsOne("System.Collections.Generic.List", "InputIdentifiers", b1 => + { + b1.Property("DeviceEntityId") + .HasColumnType("TEXT"); + + b1.Property("Capacity") + .HasColumnType("INTEGER"); + + b1.HasKey("DeviceEntityId"); + + b1.ToTable("Devices"); + + b1.ToJson("InputIdentifiers"); + + b1.WithOwner() + .HasForeignKey("DeviceEntityId"); + }); + + b.OwnsOne("System.Collections.Generic.List", "InputMappings", b1 => + { + b1.Property("DeviceEntityId") + .HasColumnType("TEXT"); + + b1.Property("Capacity") + .HasColumnType("INTEGER"); + + b1.HasKey("DeviceEntityId"); + + b1.ToTable("Devices"); + + b1.ToJson("InputMappings"); + + b1.WithOwner() + .HasForeignKey("DeviceEntityId"); + }); + + b.Navigation("InputIdentifiers") + .IsRequired(); + + b.Navigation("InputMappings") + .IsRequired(); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginEntity", b => + { + b.Navigation("Features"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", b => + { + b.Navigation("ProfileConfigurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Artemis.Storage/Migrations/20240310201706_Initial.cs b/src/Artemis.Storage/Migrations/20240310201706_Initial.cs new file mode 100644 index 000000000..286f29922 --- /dev/null +++ b/src/Artemis.Storage/Migrations/20240310201706_Initial.cs @@ -0,0 +1,235 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Artemis.Storage.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Devices", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 512, nullable: false), + DeviceProvider = table.Column(type: "TEXT", maxLength: 512, nullable: false), + X = table.Column(type: "REAL", nullable: false), + Y = table.Column(type: "REAL", nullable: false), + Rotation = table.Column(type: "REAL", nullable: false), + Scale = table.Column(type: "REAL", nullable: false), + ZIndex = table.Column(type: "INTEGER", nullable: false), + RedScale = table.Column(type: "REAL", nullable: false), + GreenScale = table.Column(type: "REAL", nullable: false), + BlueScale = table.Column(type: "REAL", nullable: false), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + PhysicalLayout = table.Column(type: "INTEGER", nullable: false), + LogicalLayout = table.Column(type: "TEXT", maxLength: 32, nullable: true), + LayoutType = table.Column(type: "TEXT", maxLength: 64, nullable: true), + LayoutParameter = table.Column(type: "TEXT", maxLength: 512, nullable: true), + Categories = table.Column(type: "TEXT", nullable: false), + InputIdentifiers = table.Column(type: "TEXT", nullable: false), + InputMappings = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Devices", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Entries", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + EntryId = table.Column(type: "INTEGER", nullable: false), + EntryType = table.Column(type: "INTEGER", nullable: false), + Author = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + ReleaseId = table.Column(type: "INTEGER", nullable: false), + ReleaseVersion = table.Column(type: "TEXT", nullable: false), + InstalledAt = table.Column(type: "TEXT", nullable: false), + Metadata = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Entries", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Plugins", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + PluginGuid = table.Column(type: "TEXT", nullable: false), + IsEnabled = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Plugins", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PluginSettings", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + PluginGuid = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Value = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PluginSettings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ProfileCategories", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 64, nullable: false), + IsCollapsed = table.Column(type: "INTEGER", nullable: false), + IsSuspended = table.Column(type: "INTEGER", nullable: false), + Order = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProfileCategories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Releases", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Version = table.Column(type: "TEXT", maxLength: 64, nullable: false), + InstalledAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Releases", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PluginFeatures", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Type = table.Column(type: "TEXT", nullable: false), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + PluginEntityId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PluginFeatures", x => x.Id); + table.ForeignKey( + name: "FK_PluginFeatures_Plugins_PluginEntityId", + column: x => x.PluginEntityId, + principalTable: "Plugins", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ProfileContainers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Icon = table.Column(type: "BLOB", nullable: false), + ProfileCategoryId = table.Column(type: "TEXT", nullable: false), + ProfileConfiguration = table.Column(type: "TEXT", nullable: false), + Profile = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProfileContainers", x => x.Id); + table.ForeignKey( + name: "FK_ProfileContainers_ProfileCategories_ProfileCategoryId", + column: x => x.ProfileCategoryId, + principalTable: "ProfileCategories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Entries_EntryId", + table: "Entries", + column: "EntryId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PluginFeatures_PluginEntityId", + table: "PluginFeatures", + column: "PluginEntityId"); + + migrationBuilder.CreateIndex( + name: "IX_Plugins_PluginGuid", + table: "Plugins", + column: "PluginGuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PluginSettings_Name_PluginGuid", + table: "PluginSettings", + columns: new[] { "Name", "PluginGuid" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PluginSettings_PluginGuid", + table: "PluginSettings", + column: "PluginGuid"); + + migrationBuilder.CreateIndex( + name: "IX_ProfileCategories_Name", + table: "ProfileCategories", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ProfileContainers_ProfileCategoryId", + table: "ProfileContainers", + column: "ProfileCategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Releases_InstalledAt", + table: "Releases", + column: "InstalledAt"); + + migrationBuilder.CreateIndex( + name: "IX_Releases_Version", + table: "Releases", + column: "Version", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Devices"); + + migrationBuilder.DropTable( + name: "Entries"); + + migrationBuilder.DropTable( + name: "PluginFeatures"); + + migrationBuilder.DropTable( + name: "PluginSettings"); + + migrationBuilder.DropTable( + name: "ProfileContainers"); + + migrationBuilder.DropTable( + name: "Releases"); + + migrationBuilder.DropTable( + name: "Plugins"); + + migrationBuilder.DropTable( + name: "ProfileCategories"); + } + } +} diff --git a/src/Artemis.Storage/Migrations/ArtemisDbContextModelSnapshot.cs b/src/Artemis.Storage/Migrations/ArtemisDbContextModelSnapshot.cs new file mode 100644 index 000000000..270d88797 --- /dev/null +++ b/src/Artemis.Storage/Migrations/ArtemisDbContextModelSnapshot.cs @@ -0,0 +1,349 @@ +// +using System; +using Artemis.Storage; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Artemis.Storage.Migrations +{ + [DbContext(typeof(ArtemisDbContext))] + partial class ArtemisDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.2"); + + modelBuilder.Entity("Artemis.Storage.Entities.General.ReleaseEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("InstalledAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstalledAt"); + + b.HasIndex("Version") + .IsUnique(); + + b.ToTable("Releases"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("PluginGuid") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PluginGuid") + .IsUnique(); + + b.ToTable("Plugins"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("PluginEntityId") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PluginEntityId"); + + b.ToTable("PluginFeatures"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginSettingEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("PluginGuid") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PluginGuid"); + + b.HasIndex("Name", "PluginGuid") + .IsUnique(); + + b.ToTable("PluginSettings"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsCollapsed") + .HasColumnType("INTEGER"); + + b.Property("IsSuspended") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProfileCategories"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileContainerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("Profile") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProfileCategoryId") + .HasColumnType("TEXT"); + + b.Property("ProfileConfiguration") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileCategoryId"); + + b.ToTable("ProfileContainers"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b => + { + b.Property("Id") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("BlueScale") + .HasColumnType("REAL"); + + b.Property("Categories") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceProvider") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("GreenScale") + .HasColumnType("REAL"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LayoutParameter") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("LayoutType") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LogicalLayout") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("PhysicalLayout") + .HasColumnType("INTEGER"); + + b.Property("RedScale") + .HasColumnType("REAL"); + + b.Property("Rotation") + .HasColumnType("REAL"); + + b.Property("Scale") + .HasColumnType("REAL"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("ZIndex") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Workshop.EntryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EntryId") + .HasColumnType("INTEGER"); + + b.Property("EntryType") + .HasColumnType("INTEGER"); + + b.Property("InstalledAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReleaseId") + .HasColumnType("INTEGER"); + + b.Property("ReleaseVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginFeatureEntity", b => + { + b.HasOne("Artemis.Storage.Entities.Plugins.PluginEntity", null) + .WithMany("Features") + .HasForeignKey("PluginEntityId"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileContainerEntity", b => + { + b.HasOne("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", "ProfileCategory") + .WithMany("ProfileConfigurations") + .HasForeignKey("ProfileCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProfileCategory"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Surface.DeviceEntity", b => + { + b.OwnsOne("System.Collections.Generic.List", "InputIdentifiers", b1 => + { + b1.Property("DeviceEntityId") + .HasColumnType("TEXT"); + + b1.Property("Capacity") + .HasColumnType("INTEGER"); + + b1.HasKey("DeviceEntityId"); + + b1.ToTable("Devices"); + + b1.ToJson("InputIdentifiers"); + + b1.WithOwner() + .HasForeignKey("DeviceEntityId"); + }); + + b.OwnsOne("System.Collections.Generic.List", "InputMappings", b1 => + { + b1.Property("DeviceEntityId") + .HasColumnType("TEXT"); + + b1.Property("Capacity") + .HasColumnType("INTEGER"); + + b1.HasKey("DeviceEntityId"); + + b1.ToTable("Devices"); + + b1.ToJson("InputMappings"); + + b1.WithOwner() + .HasForeignKey("DeviceEntityId"); + }); + + b.Navigation("InputIdentifiers") + .IsRequired(); + + b.Navigation("InputMappings") + .IsRequired(); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Plugins.PluginEntity", b => + { + b.Navigation("Features"); + }); + + modelBuilder.Entity("Artemis.Storage.Entities.Profile.ProfileCategoryEntity", b => + { + b.Navigation("ProfileConfigurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Artemis.Storage/Migrations/Storage/M0026NodeStorage.cs b/src/Artemis.Storage/Migrations/Storage/M0026NodeStorage.cs deleted file mode 100644 index f181f9698..000000000 --- a/src/Artemis.Storage/Migrations/Storage/M0026NodeStorage.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Collections.Generic; -using Artemis.Storage.Migrations.Profile; -using LiteDB; -using Serilog; - -namespace Artemis.Storage.Migrations.Storage; - -public class M0026NodeStorage : IStorageMigration -{ - private readonly ILogger _logger; - - public M0026NodeStorage(ILogger logger) - { - _logger = logger; - } - public int UserVersion => 26; - - public void Apply(LiteRepository repository) - { - ILiteCollection categoryCollection = repository.Database.GetCollection("ProfileCategoryEntity"); - List toUpdate = new(); - foreach (BsonDocument profileCategoryBson in categoryCollection.FindAll()) - { - BsonArray? profiles = profileCategoryBson["ProfileConfigurations"]?.AsArray; - if (profiles != null) - { - foreach (BsonValue profile in profiles) - { - profile["Version"] = 4; - MigrateNodeScript(profile["ActivationCondition"]?.AsDocument); - } - - toUpdate.Add(profileCategoryBson); - } - } - - categoryCollection.Update(toUpdate); - - ILiteCollection collection = repository.Database.GetCollection("ProfileEntity"); - List profilesToUpdate = new(); - foreach (BsonDocument profileBson in collection.FindAll()) - { - BsonArray? folders = profileBson["Folders"]?.AsArray; - BsonArray? layers = profileBson["Layers"]?.AsArray; - - if (folders != null) - { - foreach (BsonValue folder in folders) - MigrateProfileElement(folder.AsDocument); - } - - if (layers != null) - { - foreach (BsonValue layer in layers) - { - MigrateProfileElement(layer.AsDocument); - MigratePropertyGroup(layer.AsDocument["GeneralPropertyGroup"].AsDocument); - MigratePropertyGroup(layer.AsDocument["TransformPropertyGroup"].AsDocument); - MigratePropertyGroup(layer.AsDocument["LayerBrush"]?["PropertyGroup"].AsDocument); - } - } - - profilesToUpdate.Add(profileBson); - } - - collection.Update(profilesToUpdate); - } - - private void MigrateProfileElement(BsonDocument profileElement) - { - BsonArray? layerEffects = profileElement["LayerEffects"]?.AsArray; - if (layerEffects != null) - { - foreach (BsonValue layerEffect in layerEffects) - MigratePropertyGroup(layerEffect.AsDocument["PropertyGroup"].AsDocument); - } - - BsonValue? displayCondition = profileElement["DisplayCondition"]; - if (displayCondition != null) - MigrateNodeScript(displayCondition.AsDocument["Script"].AsDocument); - } - - private void MigratePropertyGroup(BsonDocument? propertyGroup) - { - if (propertyGroup == null || propertyGroup.Keys.Count == 0) - return; - - BsonArray? properties = propertyGroup["Properties"]?.AsArray; - BsonArray? propertyGroups = propertyGroup["PropertyGroups"]?.AsArray; - - if (properties != null) - { - foreach (BsonValue property in properties) - MigrateNodeScript(property.AsDocument["DataBinding"]?["NodeScript"]?.AsDocument); - } - - if (propertyGroups != null) - { - foreach (BsonValue childPropertyGroup in propertyGroups) - MigratePropertyGroup(childPropertyGroup.AsDocument); - } - } - - private void MigrateNodeScript(BsonDocument? nodeScript) - { - if (nodeScript == null || nodeScript.Keys.Count == 0) - return; - - BsonArray? nodes = nodeScript["Nodes"]?.AsArray; - if (nodes == null) - return; - - foreach (BsonValue node in nodes) - { - // Migrate the storage of the node - node["Storage"] = M0004NodeStorage.MigrateNodeStorageJson(node.AsDocument["Storage"]?.AsString, _logger); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/DeviceRepository.cs b/src/Artemis.Storage/Repositories/DeviceRepository.cs index 47a947333..9e7353a3c 100644 --- a/src/Artemis.Storage/Repositories/DeviceRepository.cs +++ b/src/Artemis.Storage/Repositories/DeviceRepository.cs @@ -1,47 +1,50 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using Artemis.Storage.Entities.Surface; using Artemis.Storage.Repositories.Interfaces; -using LiteDB; namespace Artemis.Storage.Repositories; -internal class DeviceRepository : IDeviceRepository +internal class DeviceRepository(Func getContext) : IDeviceRepository { - private readonly LiteRepository _repository; - - public DeviceRepository(LiteRepository repository) - { - _repository = repository; - _repository.Database.GetCollection().EnsureIndex(s => s.Id); - } - public void Add(DeviceEntity deviceEntity) { - _repository.Insert(deviceEntity); + using ArtemisDbContext dbContext = getContext(); + dbContext.Devices.Add(deviceEntity); + dbContext.SaveChanges(); } public void Remove(DeviceEntity deviceEntity) { - _repository.Delete(deviceEntity.Id); + using ArtemisDbContext dbContext = getContext(); + dbContext.Devices.Remove(deviceEntity); + dbContext.SaveChanges(); } public DeviceEntity? Get(string id) { - return _repository.FirstOrDefault(s => s.Id == id); + using ArtemisDbContext dbContext = getContext(); + return dbContext.Devices.FirstOrDefault(d => d.Id == id); } public List GetAll() { - return _repository.Query().Include(s => s.InputIdentifiers).ToList(); + using ArtemisDbContext dbContext = getContext(); + return dbContext.Devices.ToList(); } - + public void Save(DeviceEntity deviceEntity) { - _repository.Upsert(deviceEntity); + using ArtemisDbContext dbContext = getContext(); + dbContext.Update(deviceEntity); + dbContext.SaveChanges(); } - - public void Save(IEnumerable deviceEntities) + + public void SaveRange(IEnumerable deviceEntities) { - _repository.Upsert(deviceEntities); + using ArtemisDbContext dbContext = getContext(); + dbContext.UpdateRange(deviceEntities); + dbContext.SaveChanges(); } } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/EntryRepository.cs b/src/Artemis.Storage/Repositories/EntryRepository.cs index 81775f967..43a155e66 100644 --- a/src/Artemis.Storage/Repositories/EntryRepository.cs +++ b/src/Artemis.Storage/Repositories/EntryRepository.cs @@ -1,54 +1,49 @@ using System; using System.Collections.Generic; +using System.Linq; using Artemis.Storage.Entities.Workshop; using Artemis.Storage.Repositories.Interfaces; -using LiteDB; namespace Artemis.Storage.Repositories; -internal class EntryRepository : IEntryRepository +internal class EntryRepository(Func getContext) : IEntryRepository { - private readonly LiteRepository _repository; - - public EntryRepository(LiteRepository repository) - { - _repository = repository; - _repository.Database.GetCollection().EnsureIndex(s => s.Id); - _repository.Database.GetCollection().EnsureIndex(s => s.EntryId); - } - public void Add(EntryEntity entryEntity) { - _repository.Insert(entryEntity); + using ArtemisDbContext dbContext = getContext(); + dbContext.Entries.Add(entryEntity); + dbContext.SaveChanges(); } public void Remove(EntryEntity entryEntity) { - _repository.Delete(entryEntity.Id); + using ArtemisDbContext dbContext = getContext(); + dbContext.Entries.Remove(entryEntity); + dbContext.SaveChanges(); } public EntryEntity? Get(Guid id) { - return _repository.FirstOrDefault(s => s.Id == id); + using ArtemisDbContext dbContext = getContext(); + return dbContext.Entries.FirstOrDefault(s => s.Id == id); } public EntryEntity? GetByEntryId(long entryId) { - return _repository.FirstOrDefault(s => s.EntryId == entryId); + using ArtemisDbContext dbContext = getContext(); + return dbContext.Entries.FirstOrDefault(s => s.EntryId == entryId); } public List GetAll() { - return _repository.Query().ToList(); + using ArtemisDbContext dbContext = getContext(); + return dbContext.Entries.ToList(); } - + public void Save(EntryEntity entryEntity) { - _repository.Upsert(entryEntity); - } - - public void Save(IEnumerable entryEntities) - { - _repository.Upsert(entryEntities); + using ArtemisDbContext dbContext = getContext(); + dbContext.Update(entryEntity); + dbContext.SaveChanges(); } } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IDeviceRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IDeviceRepository.cs index 50fd1df86..52e5eec15 100644 --- a/src/Artemis.Storage/Repositories/Interfaces/IDeviceRepository.cs +++ b/src/Artemis.Storage/Repositories/Interfaces/IDeviceRepository.cs @@ -10,5 +10,5 @@ public interface IDeviceRepository : IRepository DeviceEntity? Get(string id); List GetAll(); void Save(DeviceEntity deviceEntity); - void Save(IEnumerable deviceEntities); + void SaveRange(IEnumerable deviceEntities); } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IEntryRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IEntryRepository.cs index 59d610f95..586542692 100644 --- a/src/Artemis.Storage/Repositories/Interfaces/IEntryRepository.cs +++ b/src/Artemis.Storage/Repositories/Interfaces/IEntryRepository.cs @@ -12,5 +12,4 @@ public interface IEntryRepository : IRepository EntryEntity? GetByEntryId(long entryId); List GetAll(); void Save(EntryEntity entryEntity); - void Save(IEnumerable entryEntities); } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs index c180ee33f..cb25caf5d 100644 --- a/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs +++ b/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs @@ -5,13 +5,9 @@ namespace Artemis.Storage.Repositories.Interfaces; public interface IPluginRepository : IRepository { - void AddPlugin(PluginEntity pluginEntity); - PluginEntity? GetPluginByGuid(Guid pluginGuid); - void SavePlugin(PluginEntity pluginEntity); - - void AddSetting(PluginSettingEntity pluginSettingEntity); - PluginSettingEntity? GetSettingByGuid(Guid pluginGuid); - PluginSettingEntity? GetSettingByNameAndGuid(string name, Guid pluginGuid); + PluginEntity? GetPluginByPluginGuid(Guid pluginGuid); void SaveSetting(PluginSettingEntity pluginSettingEntity); + void SavePlugin(PluginEntity pluginEntity); + PluginSettingEntity? GetSettingByNameAndGuid(string name, Guid pluginGuid); void RemoveSettings(Guid pluginGuid); } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IProfileCategoryRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IProfileCategoryRepository.cs index 878040e2c..96ca0d7d2 100644 --- a/src/Artemis.Storage/Repositories/Interfaces/IProfileCategoryRepository.cs +++ b/src/Artemis.Storage/Repositories/Interfaces/IProfileCategoryRepository.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using Artemis.Storage.Entities.Profile; namespace Artemis.Storage.Repositories.Interfaces; @@ -11,8 +10,7 @@ public interface IProfileCategoryRepository : IRepository void Remove(ProfileCategoryEntity profileCategoryEntity); List GetAll(); ProfileCategoryEntity? Get(Guid id); - Stream? GetProfileIconStream(Guid id); - void SaveProfileIconStream(ProfileConfigurationEntity profileConfigurationEntity, Stream stream); - ProfileCategoryEntity IsUnique(string name, Guid? id); + bool IsUnique(string name, Guid? id); void Save(ProfileCategoryEntity profileCategoryEntity); + void SaveRange(List profileCategoryEntities); } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IProfileRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IProfileRepository.cs index 8430b82c4..bc26090a5 100644 --- a/src/Artemis.Storage/Repositories/Interfaces/IProfileRepository.cs +++ b/src/Artemis.Storage/Repositories/Interfaces/IProfileRepository.cs @@ -1,14 +1,15 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Text.Json.Nodes; using Artemis.Storage.Entities.Profile; namespace Artemis.Storage.Repositories.Interfaces; public interface IProfileRepository : IRepository { - void Add(ProfileEntity profileEntity); - void Remove(ProfileEntity profileEntity); - List GetAll(); - ProfileEntity? Get(Guid id); - void Save(ProfileEntity profileEntity); + void Add(ProfileContainerEntity profileContainerEntity); + void Remove(ProfileContainerEntity profileContainerEntity); + void Save(ProfileContainerEntity profileContainerEntity); + void SaveRange(List profileContainerEntities); + void MigrateProfiles(); + void MigrateProfile(JsonObject? configurationJson, JsonObject? profileJson); } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs deleted file mode 100644 index cb5852eaa..000000000 --- a/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Artemis.Storage.Entities.General; - -namespace Artemis.Storage.Repositories.Interfaces; - -public interface IQueuedActionRepository : IRepository -{ - void Add(QueuedActionEntity queuedActionEntity); - void Remove(QueuedActionEntity queuedActionEntity); - List GetAll(); - List GetByType(string type); - bool IsTypeQueued(string type); - void ClearByType(string type); -} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IReleaseRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IReleaseRepository.cs new file mode 100644 index 000000000..9404bf8dd --- /dev/null +++ b/src/Artemis.Storage/Repositories/Interfaces/IReleaseRepository.cs @@ -0,0 +1,9 @@ +using Artemis.Storage.Entities.General; + +namespace Artemis.Storage.Repositories.Interfaces; + +public interface IReleaseRepository : IRepository +{ + bool SaveVersionInstallDate(string version); + ReleaseEntity? GetPreviousInstalledVersion(); +} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/PluginRepository.cs b/src/Artemis.Storage/Repositories/PluginRepository.cs index 02a8985c0..abb335893 100644 --- a/src/Artemis.Storage/Repositories/PluginRepository.cs +++ b/src/Artemis.Storage/Repositories/PluginRepository.cs @@ -1,59 +1,44 @@ using System; +using System.Linq; using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Repositories.Interfaces; -using LiteDB; +using Microsoft.EntityFrameworkCore; namespace Artemis.Storage.Repositories; -internal class PluginRepository : IPluginRepository +internal class PluginRepository(Func getContext) : IPluginRepository { - private readonly LiteRepository _repository; - - public PluginRepository(LiteRepository repository) + public PluginEntity? GetPluginByPluginGuid(Guid pluginGuid) { - _repository = repository; - - _repository.Database.GetCollection().EnsureIndex(s => new {s.Name, s.PluginGuid}, true); - } - - public void AddPlugin(PluginEntity pluginEntity) - { - _repository.Insert(pluginEntity); - } - - public PluginEntity? GetPluginByGuid(Guid pluginGuid) - { - return _repository.FirstOrDefault(p => p.Id == pluginGuid); - } - - public void SavePlugin(PluginEntity pluginEntity) - { - _repository.Upsert(pluginEntity); - } - - public void AddSetting(PluginSettingEntity pluginSettingEntity) - { - _repository.Insert(pluginSettingEntity); - } - - public PluginSettingEntity? GetSettingByGuid(Guid pluginGuid) - { - return _repository.FirstOrDefault(p => p.PluginGuid == pluginGuid); + using ArtemisDbContext dbContext = getContext(); + return dbContext.Plugins.Include(p => p.Features).FirstOrDefault(p => p.PluginGuid == pluginGuid); } public PluginSettingEntity? GetSettingByNameAndGuid(string name, Guid pluginGuid) { - return _repository.FirstOrDefault(p => p.Name == name && p.PluginGuid == pluginGuid); + using ArtemisDbContext dbContext = getContext(); + return dbContext.PluginSettings.FirstOrDefault(p => p.Name == name && p.PluginGuid == pluginGuid); } - public void SaveSetting(PluginSettingEntity pluginSettingEntity) - { - _repository.Upsert(pluginSettingEntity); - } - - /// public void RemoveSettings(Guid pluginGuid) { - _repository.DeleteMany(s => s.PluginGuid == pluginGuid); + using ArtemisDbContext dbContext = getContext(); + dbContext.PluginSettings.RemoveRange(dbContext.PluginSettings.Where(s => s.PluginGuid == pluginGuid)); + dbContext.SaveChanges(); } + + public void SaveSetting(PluginSettingEntity pluginSettingEntity) + { + using ArtemisDbContext dbContext = getContext(); + dbContext.PluginSettings.Update(pluginSettingEntity); + dbContext.SaveChanges(); + } + + public void SavePlugin(PluginEntity pluginEntity) + { + using ArtemisDbContext dbContext = getContext(); + dbContext.Update(pluginEntity); + dbContext.SaveChanges(); + } + } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs b/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs index 8036b9d7a..5ad5ae4d7 100644 --- a/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs +++ b/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs @@ -1,93 +1,75 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Linq; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Repositories.Interfaces; -using LiteDB; +using Microsoft.EntityFrameworkCore; namespace Artemis.Storage.Repositories; -internal class ProfileCategoryRepository : IProfileCategoryRepository +internal class ProfileCategoryRepository(Func getContext, IProfileRepository profileRepository) : IProfileCategoryRepository { - private readonly ILiteStorage _profileIcons; - private readonly LiteRepository _repository; - - public ProfileCategoryRepository(LiteRepository repository) - { - _repository = repository; - _repository.Database.GetCollection().EnsureIndex(s => s.Name, true); - _profileIcons = _repository.Database.GetStorage("profileIcons"); - } + private bool _migratedProfiles; public void Add(ProfileCategoryEntity profileCategoryEntity) { - _repository.Insert(profileCategoryEntity); + using ArtemisDbContext dbContext = getContext(); + dbContext.ProfileCategories.Add(profileCategoryEntity); + dbContext.SaveChanges(); } public void Remove(ProfileCategoryEntity profileCategoryEntity) { - _repository.Delete(profileCategoryEntity.Id); + using ArtemisDbContext dbContext = getContext(); + dbContext.ProfileCategories.Remove(profileCategoryEntity); + dbContext.SaveChanges(); } public List GetAll() { - List categories = _repository.Query().ToList(); - - // Update all profile versions to the current version, profile migrations don't apply to LiteDB so anything loadable is assumed to be up to date - foreach (ProfileCategoryEntity profileCategoryEntity in categories) - UpdateProfileVersions(profileCategoryEntity); + if (!_migratedProfiles) + { + profileRepository.MigrateProfiles(); + _migratedProfiles = true; + } - return categories; + using ArtemisDbContext dbContext = getContext(); + return dbContext.ProfileCategories.Include(c => c.ProfileConfigurations).ToList(); } public ProfileCategoryEntity? Get(Guid id) { - ProfileCategoryEntity? result = _repository.FirstOrDefault(p => p.Id == id); - if (result == null) - return null; + if (!_migratedProfiles) + { + profileRepository.MigrateProfiles(); + _migratedProfiles = true; + } - // Update all profile versions to the current version, profile migrations don't apply to LiteDB so anything loadable is assumed to be up to date - UpdateProfileVersions(result); - return result; - } - - public ProfileCategoryEntity IsUnique(string name, Guid? id) - { - name = name.Trim(); - if (id == null) - return _repository.FirstOrDefault(p => p.Name == name); - return _repository.FirstOrDefault(p => p.Name == name && p.Id != id.Value); + using ArtemisDbContext dbContext = getContext(); + return dbContext.ProfileCategories.Include(c => c.ProfileConfigurations).FirstOrDefault(c => c.Id == id); } public void Save(ProfileCategoryEntity profileCategoryEntity) { - _repository.Upsert(profileCategoryEntity); + using ArtemisDbContext dbContext = getContext(); + dbContext.Update(profileCategoryEntity); + dbContext.SaveChanges(); } - public Stream? GetProfileIconStream(Guid id) + public void SaveRange(List profileCategoryEntities) { - if (!_profileIcons.Exists(id)) - return null; - - MemoryStream stream = new(); - _profileIcons.Download(id, stream); - return stream; + using ArtemisDbContext dbContext = getContext(); + dbContext.UpdateRange(profileCategoryEntities); + dbContext.SaveChanges(); } - public void SaveProfileIconStream(ProfileConfigurationEntity profileConfigurationEntity, Stream stream) + public bool IsUnique(string name, Guid? id) { - if (profileConfigurationEntity.FileIconId == Guid.Empty) - profileConfigurationEntity.FileIconId = Guid.NewGuid(); + using ArtemisDbContext dbContext = getContext(); - if (stream == null && _profileIcons.Exists(profileConfigurationEntity.FileIconId)) - _profileIcons.Delete(profileConfigurationEntity.FileIconId); - - _profileIcons.Upload(profileConfigurationEntity.FileIconId, profileConfigurationEntity.FileIconId + ".png", stream); - } - - private static void UpdateProfileVersions(ProfileCategoryEntity profileCategoryEntity) - { - foreach (ProfileConfigurationEntity profileConfigurationEntity in profileCategoryEntity.ProfileConfigurations) - profileConfigurationEntity.Version = StorageMigrationService.PROFILE_VERSION; + name = name.Trim(); + return id == null + ? dbContext.ProfileCategories.Any(p => p.Name == name) + : dbContext.ProfileCategories.Any(p => p.Name == name && p.Id != id.Value); } } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ProfileRepository.cs b/src/Artemis.Storage/Repositories/ProfileRepository.cs index 738099a32..84e72cb2b 100644 --- a/src/Artemis.Storage/Repositories/ProfileRepository.cs +++ b/src/Artemis.Storage/Repositories/ProfileRepository.cs @@ -1,43 +1,104 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; +using Artemis.Storage.Entities; using Artemis.Storage.Entities.Profile; +using Artemis.Storage.Exceptions; +using Artemis.Storage.Migrations; using Artemis.Storage.Repositories.Interfaces; -using LiteDB; +using Microsoft.EntityFrameworkCore; +using Serilog; namespace Artemis.Storage.Repositories; -internal class ProfileRepository : IProfileRepository +public class ProfileRepository(ILogger logger, Func getContext, List profileMigrators) : IProfileRepository { - private readonly LiteRepository _repository; - - public ProfileRepository(LiteRepository repository) + public void Add(ProfileContainerEntity profileContainerEntity) { - _repository = repository; - _repository.Database.GetCollection().EnsureIndex(s => s.Name); + using ArtemisDbContext dbContext = getContext(); + dbContext.ProfileContainers.Add(profileContainerEntity); + dbContext.SaveChanges(); } - public void Add(ProfileEntity profileEntity) + public void Remove(ProfileContainerEntity profileContainerEntity) { - _repository.Insert(profileEntity); + using ArtemisDbContext dbContext = getContext(); + dbContext.ProfileContainers.Remove(profileContainerEntity); + dbContext.SaveChanges(); } - public void Remove(ProfileEntity profileEntity) + public void Save(ProfileContainerEntity profileContainerEntity) { - _repository.Delete(profileEntity.Id); + using ArtemisDbContext dbContext = getContext(); + dbContext.Update(profileContainerEntity); + dbContext.SaveChanges(); } - public List GetAll() + public void SaveRange(List profileContainerEntities) { - return _repository.Query().ToList(); + using ArtemisDbContext dbContext = getContext(); + dbContext.UpdateRange(profileContainerEntities); + dbContext.SaveChanges(); } - public ProfileEntity? Get(Guid id) + public void MigrateProfiles() { - return _repository.FirstOrDefault(p => p.Id == id); + using ArtemisDbContext dbContext = getContext(); + int max = profileMigrators.Max(m => m.Version); + + // Query the ProfileContainerEntity table directly, grabbing the ID, profile, and configuration + List containers = dbContext.Database + .SqlQueryRaw("SELECT Id, Profile, ProfileConfiguration FROM ProfileContainers WHERE json_extract(ProfileConfiguration, '$.Version') < {0}", max) + .ToList(); + + foreach (RawProfileContainer rawProfileContainer in containers) + { + try + { + JsonObject? profileConfiguration = JsonNode.Parse(rawProfileContainer.ProfileConfiguration)?.AsObject(); + JsonObject? profile = JsonNode.Parse(rawProfileContainer.Profile)?.AsObject(); + + if (profileConfiguration == null || profile == null) + { + logger.Error("Failed to parse profile or profile configuration of profile container {Id}", rawProfileContainer.Id); + continue; + } + + MigrateProfile(profileConfiguration, profile); + rawProfileContainer.Profile = profile.ToString(); + rawProfileContainer.ProfileConfiguration = profileConfiguration.ToString(); + + // Write the updated containers back to the database + dbContext.Database.ExecuteSqlRaw( + "UPDATE ProfileContainers SET Profile = {0}, ProfileConfiguration = {1} WHERE Id = {2}", + rawProfileContainer.Profile, + rawProfileContainer.ProfileConfiguration, + rawProfileContainer.Id); + } + catch (Exception e) + { + logger.Error(e, "Failed to migrate profile container {Id}", rawProfileContainer.Id); + } + } } - public void Save(ProfileEntity profileEntity) + public void MigrateProfile(JsonObject? configurationJson, JsonObject? profileJson) { - _repository.Upsert(profileEntity); + if (configurationJson == null || profileJson == null) + return; + + configurationJson["Version"] ??= 0; + + foreach (IProfileMigration profileMigrator in profileMigrators.OrderBy(m => m.Version)) + { + if (profileMigrator.Version <= configurationJson["Version"]!.GetValue()) + continue; + + logger.Information("Migrating profile '{Name}' from version {OldVersion} to {NewVersion}", configurationJson["Name"], configurationJson["Version"], profileMigrator.Version); + + profileMigrator.Migrate(configurationJson, profileJson); + configurationJson["Version"] = profileMigrator.Version; + } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/QueuedActionRepository.cs b/src/Artemis.Storage/Repositories/QueuedActionRepository.cs deleted file mode 100644 index cf2bc862d..000000000 --- a/src/Artemis.Storage/Repositories/QueuedActionRepository.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using Artemis.Storage.Entities.General; -using Artemis.Storage.Repositories.Interfaces; -using LiteDB; - -namespace Artemis.Storage.Repositories; - -public class QueuedActionRepository : IQueuedActionRepository -{ - private readonly LiteRepository _repository; - - public QueuedActionRepository(LiteRepository repository) - { - _repository = repository; - _repository.Database.GetCollection().EnsureIndex(s => s.Type); - } - - #region Implementation of IQueuedActionRepository - - /// - public void Add(QueuedActionEntity queuedActionEntity) - { - _repository.Insert(queuedActionEntity); - } - - /// - public void Remove(QueuedActionEntity queuedActionEntity) - { - _repository.Delete(queuedActionEntity.Id); - } - - /// - public List GetAll() - { - return _repository.Query().ToList(); - } - - /// - public List GetByType(string type) - { - return _repository.Query().Where(q => q.Type == type).ToList(); - } - - /// - public bool IsTypeQueued(string type) - { - return _repository.Query().Where(q => q.Type == type).Count() > 0; - } - - /// - public void ClearByType(string type) - { - _repository.DeleteMany(q => q.Type == type); - } - - #endregion -} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ReleaseRepository.cs b/src/Artemis.Storage/Repositories/ReleaseRepository.cs index c5c18ae27..7dff21928 100644 --- a/src/Artemis.Storage/Repositories/ReleaseRepository.cs +++ b/src/Artemis.Storage/Repositories/ReleaseRepository.cs @@ -1,38 +1,28 @@ using System; +using System.Linq; using Artemis.Storage.Entities.General; using Artemis.Storage.Repositories.Interfaces; -using LiteDB; namespace Artemis.Storage.Repositories; -public class ReleaseRepository : IReleaseRepository +public class ReleaseRepository(Func getContext) : IReleaseRepository { - private readonly LiteRepository _repository; - - public ReleaseRepository(LiteRepository repository) - { - _repository = repository; - _repository.Database.GetCollection().EnsureIndex(s => s.Version, true); - } - public bool SaveVersionInstallDate(string version) { - ReleaseEntity release = _repository.Query().Where(r => r.Version == version).FirstOrDefault(); + using ArtemisDbContext dbContext = getContext(); + + ReleaseEntity? release = dbContext.Releases.FirstOrDefault(r => r.Version == version); if (release != null) return false; - _repository.Insert(new ReleaseEntity {Version = version, InstalledAt = DateTimeOffset.UtcNow}); + dbContext.Releases.Add(new ReleaseEntity {Version = version, InstalledAt = DateTimeOffset.UtcNow}); + dbContext.SaveChanges(); return true; } - public ReleaseEntity GetPreviousInstalledVersion() + public ReleaseEntity? GetPreviousInstalledVersion() { - return _repository.Query().OrderByDescending(r => r.InstalledAt).Skip(1).FirstOrDefault(); + using ArtemisDbContext dbContext = getContext(); + return dbContext.Releases.AsEnumerable().OrderByDescending(r => r.InstalledAt).Skip(1).FirstOrDefault(); } -} - -public interface IReleaseRepository : IRepository -{ - bool SaveVersionInstallDate(string version); - ReleaseEntity GetPreviousInstalledVersion(); } \ No newline at end of file diff --git a/src/Artemis.Storage/StorageManager.cs b/src/Artemis.Storage/StorageManager.cs index 78fcc3643..c87a86be1 100644 --- a/src/Artemis.Storage/StorageManager.cs +++ b/src/Artemis.Storage/StorageManager.cs @@ -1,12 +1,13 @@ using System; using System.IO; using System.Linq; -using LiteDB; +using Microsoft.EntityFrameworkCore; namespace Artemis.Storage; public static class StorageManager { + private static bool _ranMigrations; private static bool _inUse; /// @@ -19,7 +20,7 @@ public static class StorageManager if (_inUse) throw new Exception("Storage is already in use, can't backup now."); - string database = Path.Combine(dataFolder, "database.db"); + string database = Path.Combine(dataFolder, "artemis.db"); if (!File.Exists(database)) return; @@ -36,27 +37,21 @@ public static class StorageManager oldest.Delete(); } - File.Copy(database, Path.Combine(backupFolder, $"database-{DateTime.Now:yyyy-dd-M--HH-mm-ss}.db")); + File.Copy(database, Path.Combine(backupFolder, $"artemis-{DateTime.Now:yyyy-dd-M--HH-mm-ss}.db")); } - - /// - /// Creates the LiteRepository that will be managed by dependency injection - /// - /// The Artemis data folder - public static LiteRepository CreateRepository(string dataFolder) + + public static ArtemisDbContext CreateDbContext(string dataFolder) { - if (_inUse) - throw new Exception("Storage is already in use, use dependency injection to get the repository."); + _inUse = true; - try - { - _inUse = true; - return new LiteRepository($"FileName={Path.Combine(dataFolder, "database.db")}"); - } - catch (LiteException e) - { - // I don't like this way of error reporting, now I need to use reflection if I want a meaningful error message - throw new Exception($"LiteDB threw error code {e.ErrorCode}. See inner exception for more details", e); - } + ArtemisDbContext dbContext = new() {DataFolder = dataFolder}; + if (_ranMigrations) + return dbContext; + + dbContext.Database.Migrate(); + dbContext.Database.ExecuteSqlRaw("PRAGMA optimize"); + _ranMigrations = true; + + return dbContext; } } \ No newline at end of file diff --git a/src/Artemis.Storage/StorageMigrationService.cs b/src/Artemis.Storage/StorageMigrationService.cs deleted file mode 100644 index eca2a7dde..000000000 --- a/src/Artemis.Storage/StorageMigrationService.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Storage.Migrations; -using LiteDB; -using Serilog; - -namespace Artemis.Storage; - -public class StorageMigrationService -{ - public const int PROFILE_VERSION = 4; - - private readonly ILogger _logger; - private readonly IList _migrations; - private readonly LiteRepository _repository; - - public StorageMigrationService(ILogger logger, LiteRepository repository, IList migrations) - { - _logger = logger; - _repository = repository; - _migrations = migrations; - - ApplyPendingMigrations(); - } - - public void ApplyPendingMigrations() - { - foreach (IStorageMigration storageMigration in _migrations.OrderBy(m => m.UserVersion)) - { - if (_repository.Database.UserVersion >= storageMigration.UserVersion) - continue; - - _logger.Information("Applying storage migration {storageMigration} to update DB from v{oldVersion} to v{newVersion}", - storageMigration.GetType().Name, _repository.Database.UserVersion, storageMigration.UserVersion); - - _repository.Database.BeginTrans(); - try - { - storageMigration.Apply(_repository); - } - catch (Exception) - { - _repository.Database.Rollback(); - throw; - } - - _repository.Database.Commit(); - - _repository.Database.UserVersion = storageMigration.UserVersion; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Linux/App.axaml.cs b/src/Artemis.UI.Linux/App.axaml.cs index 88f0f8179..dbba1fb2f 100644 --- a/src/Artemis.UI.Linux/App.axaml.cs +++ b/src/Artemis.UI.Linux/App.axaml.cs @@ -1,5 +1,6 @@ using System; using Artemis.Core.Services; +using Artemis.Storage.Legacy; using Artemis.UI.Linux.DryIoc; using Artemis.UI.Linux.Providers.Input; using Avalonia; @@ -20,7 +21,10 @@ public class App : Application public override void Initialize() { _container = ArtemisBootstrapper.Bootstrap(this, c => c.RegisterProviders()); + Program.CreateLogger(_container); + LegacyMigrationService.MigrateToSqlite(_container); + RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); } diff --git a/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj b/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj index c41b62a40..ed8f08fda 100644 --- a/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj +++ b/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Artemis.UI.MacOS/App.axaml.cs b/src/Artemis.UI.MacOS/App.axaml.cs index 0d81b318d..800bb523f 100644 --- a/src/Artemis.UI.MacOS/App.axaml.cs +++ b/src/Artemis.UI.MacOS/App.axaml.cs @@ -1,3 +1,4 @@ +using Artemis.Storage.Legacy; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; @@ -14,7 +15,10 @@ public class App : Application public override void Initialize() { _container = ArtemisBootstrapper.Bootstrap(this); + Program.CreateLogger(_container); + LegacyMigrationService.MigrateToSqlite(_container); + RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); } diff --git a/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj b/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj index d762afde1..f322b73d7 100644 --- a/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj +++ b/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj @@ -16,6 +16,7 @@ + \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj index bf6734a2a..42faecb23 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs index dd652f0d8..6b7fd2a02 100644 --- a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs +++ b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs @@ -46,17 +46,10 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable ? new MaterialIcon {Kind = parsedIcon!} : new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; } + else if (ConfigurationIcon.IconBytes != null) + Dispatcher.UIThread.Post(LoadFromBitmap, DispatcherPriority.ApplicationIdle); else - { - Dispatcher.UIThread.Post(() => - { - Stream? stream = ConfigurationIcon?.GetIconStream(); - if (stream == null || ConfigurationIcon == null) - Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; - else - LoadFromBitmap(ConfigurationIcon, stream); - }, DispatcherPriority.ApplicationIdle); - } + Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; } catch (Exception) { @@ -64,22 +57,32 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable } } - private void LoadFromBitmap(Core.ProfileConfigurationIcon configurationIcon, Stream stream) + private void LoadFromBitmap() { - _stream = stream; - if (!configurationIcon.Fill) + try { - Content = new Image {Source = new Bitmap(stream)}; - return; - } + if (ConfigurationIcon?.IconBytes == null) + return; - Content = new Border + _stream = new MemoryStream(ConfigurationIcon.IconBytes); + if (!ConfigurationIcon.Fill) + { + Content = new Image {Source = new Bitmap(_stream)}; + return; + } + + Content = new Border + { + Background = TextElement.GetForeground(this), + VerticalAlignment = VerticalAlignment.Stretch, + HorizontalAlignment = HorizontalAlignment.Stretch, + OpacityMask = new ImageBrush(new Bitmap(_stream)) + }; + } + catch (Exception) { - Background = TextElement.GetForeground(this), - VerticalAlignment = VerticalAlignment.Stretch, - HorizontalAlignment = HorizontalAlignment.Stretch, - OpacityMask = new ImageBrush(new Bitmap(stream)) - }; + Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; + } } private void OnDetachedFromLogicalTree(object? sender, LogicalTreeAttachmentEventArgs e) diff --git a/src/Artemis.UI.Windows/App.axaml.cs b/src/Artemis.UI.Windows/App.axaml.cs index 41ed8e923..66d5aac1b 100644 --- a/src/Artemis.UI.Windows/App.axaml.cs +++ b/src/Artemis.UI.Windows/App.axaml.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Threading; using Artemis.Core; using Artemis.Core.Services; +using Artemis.Storage.Legacy; using Artemis.UI.Windows.DryIoc; using Artemis.UI.Windows.Providers.Input; using Avalonia; @@ -34,7 +35,10 @@ public class App : Application } _container = ArtemisBootstrapper.Bootstrap(this, c => c.RegisterProviders()); + Program.CreateLogger(_container); + LegacyMigrationService.MigrateToSqlite(_container); + RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); } diff --git a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj index 52a121156..f74f2bd4b 100644 --- a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index 1b6af5381..eca2ee350 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -186,7 +186,7 @@ public partial class ProfileEditorViewModel : RoutableScreen public override async Task OnNavigating(ProfileEditorViewModelParameters parameters, NavigationArguments args, CancellationToken cancellationToken) { - ProfileConfiguration? profileConfiguration = _profileService.ProfileConfigurations.FirstOrDefault(c => c.ProfileId == parameters.ProfileId); + ProfileConfiguration? profileConfiguration = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == parameters.ProfileId); // If the profile doesn't exist, cancel navigation if (profileConfiguration == null) diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index 6b01838a9..f005ce788 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -19,6 +19,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; using ReactiveUI; +using Serilog; namespace Artemis.UI.Screens.Root; @@ -28,13 +29,15 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv private readonly IDebugService _debugService; private readonly DefaultTitleBarViewModel _defaultTitleBarViewModel; private readonly IClassicDesktopStyleApplicationLifetime _lifeTime; + private readonly ILogger _logger; private readonly IRouter _router; private readonly ISettingsService _settingsService; private readonly IUpdateService _updateService; private readonly IWindowService _windowService; private readonly ObservableAsPropertyHelper _titleBarViewModel; - public RootViewModel(IRouter router, + public RootViewModel(ILogger logger, + IRouter router, ICoreService coreService, ISettingsService settingsService, IRegistrationService registrationService, @@ -50,6 +53,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv WindowSizeSetting = settingsService.GetSetting("WindowSize"); SidebarViewModel = sidebarViewModel; + _logger = logger; _router = router; _coreService = coreService; _settingsService = settingsService; @@ -81,19 +85,27 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv Task.Run(() => { - // Before doing heavy lifting, initialize the update service which may prompt a restart - // Only initialize with an update check if we're not going to show the UI - if (_updateService.Initialize(!ShouldShowUI())) - return; + try + { + // Before doing heavy lifting, initialize the update service which may prompt a restart + // Only initialize with an update check if we're not going to show the UI + if (_updateService.Initialize(!ShouldShowUI())) + return; - // Workshop service goes first so it has a chance to clean up old workshop entries and introduce new ones - workshopService.Initialize(); - // Core is initialized now that everything is ready to go - coreService.Initialize(); + // Workshop service goes first so it has a chance to clean up old workshop entries and introduce new ones + workshopService.Initialize(); + // Core is initialized now that everything is ready to go + coreService.Initialize(); - registrationService.RegisterBuiltInDataModelDisplays(); - registrationService.RegisterBuiltInDataModelInputs(); - registrationService.RegisterBuiltInPropertyEditors(); + registrationService.RegisterBuiltInDataModelDisplays(); + registrationService.RegisterBuiltInDataModelInputs(); + registrationService.RegisterBuiltInPropertyEditors(); + } + catch (Exception e) + { + _logger.Fatal(e, "Error during initialization"); + _windowService.ShowExceptionDialog("Fatal error occured during initialization", e); + } }); } diff --git a/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml index e29210098..70b2bc390 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml @@ -174,16 +174,15 @@ Avalonia DryIoc + Entity Framework Core FluentAvalonia EmbedIO Humanizer - LiteDB McMaster.NETCore.Plugins - Newtonsoft.Json RGB.NET Serilog SkiaSharp - Unclassified.NetRevisionTask + SQLite @@ -192,6 +191,9 @@ https://github.com/dadhi/DryIoc + + https://learn.microsoft.com/en-us/ef/core/ + https://github.com/amwx/FluentAvalonia @@ -201,15 +203,9 @@ https://github.com/Humanizr/Humanizer - - https://www.litedb.org/ - https://github.com/natemcmaster/DotNetCorePlugins - - https://www.newtonsoft.com/json - https://github.com/DarthAffe/RGB.NET @@ -218,9 +214,9 @@ https://github.com/mono/SkiaSharp - - - https://unclassified.software/en/apps/netrevisiontask + + + https://www.sqlite.org/ diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs index 6e3ee0d0b..87bb9c949 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs @@ -162,7 +162,7 @@ public class GeneralTabViewModel : RoutableScreen 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"); - public PluginSetting CoreRenderScale => _settingsService.GetSetting("Core.RenderScale", 0.25); + public PluginSetting CoreRenderScale => _settingsService.GetSetting("Core.RenderScale", 0.5); public PluginSetting CoreTargetFrameRate => _settingsService.GetSetting("Core.TargetFrameRate", 30); public PluginSetting WebServerEnabled => _settingsService.GetSetting("WebServer.Enabled", true); public PluginSetting WebServerPort => _settingsService.GetSetting("WebServer.Port", 9696); 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/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs index 1f1c9c500..afa278f98 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs @@ -122,8 +122,8 @@ public partial class ProfileConfigurationEditViewModel : DialogViewModelBase _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.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs index bfc179095..99f9330d7 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs @@ -40,15 +40,6 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase .Select(p => p == null) .ToProperty(this, vm => vm.IsDisabled) .DisposeWith(d)); - - try - { - _profileService.LoadProfileConfigurationIcon(ProfileConfiguration); - } - catch (Exception) - { - // ignored, too bad but don't crash over corrupt icons - } } public ProfileConfiguration ProfileConfiguration { get; } diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs index 47bff74f0..be21aeb89 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs @@ -97,7 +97,7 @@ public partial class SurfaceDeviceViewModel : ActivatableViewModelBase if (x < 0 || y < 0) return false; - double maxTextureSize = 4096 / _settingsService.GetSetting("Core.RenderScale", 0.25).Value; + double maxTextureSize = 4096 / _settingsService.GetSetting("Core.RenderScale", 0.5).Value; if (x + Device.Rectangle.Width > maxTextureSize || y + Device.Rectangle.Height > maxTextureSize) return false; diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs index 932313e96..248ca19de 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs @@ -87,7 +87,7 @@ public partial class SurfaceEditorViewModel : RoutableScreen, IMainScreenViewMod public ReactiveCommand SendToBack { get; } public ReactiveCommand SendBackward { get; } - public double MaxTextureSize => 4096 / _settingsService.GetSetting("Core.RenderScale", 0.25).Value; + public double MaxTextureSize => 4096 / _settingsService.GetSetting("Core.RenderScale", 0.5).Value; public void UpdateSelection(List devices, bool expand, bool invert) { diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs index 4a5b9cae9..5afebcf4b 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -27,10 +28,7 @@ public partial class ProfileSelectionStepViewModel : SubmissionViewModel _profileService = profileService; // Use copies of the profiles, the originals are used by the core and could be disposed - Profiles = new ObservableCollection(_profileService.ProfileConfigurations.Select(_profileService.CloneProfileConfiguration)); - foreach (ProfileConfiguration profileConfiguration in Profiles) - _profileService.LoadProfileConfigurationIcon(profileConfiguration); - + Profiles = new ObservableCollection(_profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).Select(_profileService.CloneProfileConfiguration)); ProfilePreview = profilePreviewViewModel; GoBack = ReactiveCommand.Create(() => State.ChangeScreen()); @@ -70,7 +68,7 @@ public partial class ProfileSelectionStepViewModel : SubmissionViewModel State.EntrySource = new ProfileEntrySource(SelectedProfile, SelectedProfile.GetFeatureDependencies().Distinct().ToList()); State.Name = SelectedProfile.Name; - State.Icon = SelectedProfile.Icon.GetIconStream(); + State.Icon = SelectedProfile.Icon.IconBytes != null ? new MemoryStream(SelectedProfile.Icon.IconBytes) : null; // Render the material icon of the profile if (State.Icon == null && SelectedProfile.Icon.IconName != null) diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index deb6484ea..a6259db37 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.Storage.Repositories; +using Artemis.Storage.Repositories.Interfaces; using Artemis.UI.Exceptions; using Artemis.UI.Shared.Services.MainWindow; using Artemis.WebClient.Updating; diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs index 218665d05..1219f84d6 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs @@ -74,9 +74,9 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler try { // Find the profile if still there - ProfileConfiguration? profile = _profileService.ProfileConfigurations.FirstOrDefault(c => c.ProfileId == profileId); + ProfileConfiguration? profile = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == profileId); if (profile != null) - _profileService.DeleteProfile(profile); + _profileService.RemoveProfileConfiguration(profile); // Remove the release _workshopService.RemoveInstalledEntry(installedEntry); 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(); } /// diff --git a/src/Artemis.WebClient.Workshop/Repositories/AuthenticationRepository.cs b/src/Artemis.WebClient.Workshop/Repositories/AuthenticationRepository.cs index 9a3021c1f..8f96a314c 100644 --- a/src/Artemis.WebClient.Workshop/Repositories/AuthenticationRepository.cs +++ b/src/Artemis.WebClient.Workshop/Repositories/AuthenticationRepository.cs @@ -1,31 +1,30 @@ -using Artemis.WebClient.Workshop.Entities; -using LiteDB; +using Artemis.Core; +using Artemis.Core.Services; namespace Artemis.WebClient.Workshop.Repositories; internal class AuthenticationRepository : IAuthenticationRepository { - private readonly LiteRepository _repository; + private readonly PluginSetting _refreshToken; - public AuthenticationRepository(LiteRepository repository) + public AuthenticationRepository(ISettingsService settingsService) { - _repository = repository; - _repository.Database.GetCollection().EnsureIndex(s => s.RefreshToken); + // Of course anyone can grab these indirectly, but that goes for whatever we do. + // ISettingsService is a protected service so we at least don't make it very straightforward. + _refreshToken = settingsService.GetSetting("Workshop.RefreshToken"); } /// public void SetRefreshToken(string? refreshToken) { - _repository.Database.GetCollection().DeleteAll(); - - if (refreshToken != null) - _repository.Insert(new RefreshTokenEntity {RefreshToken = refreshToken}); + _refreshToken.Value = refreshToken; + _refreshToken.Save(); } /// public string? GetRefreshToken() { - return _repository.Query().FirstOrDefault()?.RefreshToken; + return _refreshToken.Value; } } diff --git a/src/Artemis.sln b/src/Artemis.sln index e7643caba..7f7a038f0 100644 --- a/src/Artemis.sln +++ b/src/Artemis.sln @@ -29,6 +29,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.Storage.Legacy", "Artemis.Storage.Legacy\Artemis.Storage.Legacy.csproj", "{D7B0966D-774A-40E4-9455-00C1261ACB6A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -71,6 +73,10 @@ Global {2B982C2E-3CBC-4DAB-9167-CCCA4C78E92B}.Debug|x64.Build.0 = Debug|x64 {2B982C2E-3CBC-4DAB-9167-CCCA4C78E92B}.Release|x64.ActiveCfg = Release|x64 {2B982C2E-3CBC-4DAB-9167-CCCA4C78E92B}.Release|x64.Build.0 = Release|x64 + {D7B0966D-774A-40E4-9455-00C1261ACB6A}.Debug|x64.ActiveCfg = Debug|Any CPU + {D7B0966D-774A-40E4-9455-00C1261ACB6A}.Debug|x64.Build.0 = Debug|Any CPU + {D7B0966D-774A-40E4-9455-00C1261ACB6A}.Release|x64.ActiveCfg = Release|Any CPU + {D7B0966D-774A-40E4-9455-00C1261ACB6A}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 214bbcd9c..f997dd00e 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -29,6 +29,9 @@ + + +