diff --git a/.github/workflows/docfx.yml b/.github/workflows/docfx.yml index 519777e52..f4fffc5fe 100644 --- a/.github/workflows/docfx.yml +++ b/.github/workflows/docfx.yml @@ -12,9 +12,9 @@ jobs: runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v4 with: dotnet-version: "8.0.x" - name: Setup DocFX @@ -26,7 +26,7 @@ jobs: - name: Build DocFX run: docfx docfx/docfx_project/docfx.json - name: Upload to FTP - uses: SamKirkland/FTP-Deploy-Action@4.3.2 + uses: maverage/FTP-Deploy-Action@4.3.5 with: server: www360.your-server.de protocol: ftps diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 5633f29e6..59204a4bf 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -13,7 +13,7 @@ jobs: version-number: ${{ steps.get-version.outputs.version-number }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get Version String @@ -49,16 +49,16 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout Artemis - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: Artemis - name: Checkout Plugins - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: Artemis-RGB/Artemis.Plugins path: Artemis.Plugins - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' - name: Publish Artemis @@ -73,7 +73,7 @@ jobs: } shell: pwsh - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: artemis-${{ matrix.rid }}-${{ needs.version.outputs.version-number }} path: build/${{ matrix.rid }} diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index a297799f0..364f05704 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -13,7 +13,7 @@ jobs: version-number: ${{ steps.get-version.outputs.version-number }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get Version String @@ -35,11 +35,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup .NET - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Pack Artemis.Core run: dotnet pack -c Release -p:Version=${{ needs.version.outputs.version-number }} -p:BuildingNuget=True src/Artemis.Core/Artemis.Core.csproj - name: Pack Artemis.UI.Shared diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index 6c084ff51..cda82170d 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -9,13 +9,8 @@ namespace Artemis.Core; /// /// Represents basic info about a plugin feature and contains a reference to the instance of said feature /// -public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject +public class PluginFeatureInfo : IPrerequisitesSubject { - private string? _description; - private PluginFeature? _instance; - private Exception? _loadException; - private string _name = null!; - internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureEntity pluginFeatureEntity, PluginFeatureAttribute? attribute) { Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); @@ -53,29 +48,17 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject /// /// Gets the exception thrown while loading /// - public Exception? LoadException - { - get => _loadException; - internal set => SetAndNotify(ref _loadException, value); - } + public Exception? LoadException { get; internal set; } /// /// The name of the feature /// - public string Name - { - get => _name; - internal set => SetAndNotify(ref _name, value); - } + public string Name { get; } /// /// A short description of the feature /// - public string? Description - { - get => _description; - set => SetAndNotify(ref _description, value); - } + public string? Description { get; } /// /// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled. @@ -92,19 +75,7 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject /// Gets the feature this info is associated with /// Note: if the associated is disabled /// - public PluginFeature? Instance - { - get => _instance; - internal set => SetAndNotify(ref _instance, value); - } - - internal PluginFeatureEntity Entity { get; } - - /// - public override string ToString() - { - return Instance?.Id ?? "Uninitialized feature"; - } + public PluginFeature? Instance { get; internal set; } /// public List Prerequisites { get; } = new(); @@ -117,4 +88,12 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject { return PlatformPrerequisites.All(p => p.IsMet()); } + + /// + public override string ToString() + { + return Instance?.Id ?? "Uninitialized feature"; + } + + internal PluginFeatureEntity Entity { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index d957469e0..5563a67fd 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -6,237 +6,134 @@ using System.Text.Json.Serialization; namespace Artemis.Core; /// -/// Represents basic info about a plugin and contains a reference to the instance of said plugin +/// Represents basic info about a plugin and contains a reference to the instance of said plugin /// -public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject +public class PluginInfo : IPrerequisitesSubject { - private Version? _api = new(1, 0, 0); - private string? _author; - private bool _autoEnableFeatures = true; - private string? _description; - private Guid _guid; - private string? _icon; - private string _main = null!; - private string _name = null!; - private PluginPlatform? _platforms; - private Plugin _plugin = null!; - private Uri? _repository; - private bool _requiresAdmin; - private string _version = null!; - private Uri? _website; - private Uri? _helpPage; - private bool _hotReloadSupported = true; - private Uri? _license; - private string? _licenseName; - [JsonConstructor] internal PluginInfo() { } /// - /// The plugins GUID + /// The plugins GUID /// [JsonRequired] [JsonInclude] - public Guid Guid - { - get => _guid; - internal set => SetAndNotify(ref _guid, value); - } + public Guid Guid { get; internal init; } /// - /// The name of the plugin + /// The name of the plugin /// [JsonRequired] [JsonInclude] - public string Name - { - get => _name; - internal set => SetAndNotify(ref _name, value); - } + public string Name { get; internal init; } = null!; /// - /// A short description of the plugin - /// - public string? Description - { - get => _description; - set => SetAndNotify(ref _description, value); - } - - /// - /// Gets or sets the author of this plugin - /// - public string? Author - { - get => _author; - set => SetAndNotify(ref _author, value); - } - - /// - /// Gets or sets the website of this plugin or its author - /// - public Uri? Website - { - get => _website; - set => SetAndNotify(ref _website, value); - } - - /// - /// Gets or sets the repository of this plugin - /// - public Uri? Repository - { - get => _repository; - set => SetAndNotify(ref _repository, value); - } - - /// - /// Gets or sets the help page of this plugin - /// - public Uri? HelpPage - { - get => _helpPage; - set => SetAndNotify(ref _helpPage, value); - } - - /// - /// Gets or sets the help page of this plugin - /// - public Uri? License - { - get => _license; - set => SetAndNotify(ref _license, value); - } - - /// - /// Gets or sets the author of this plugin - /// - public string? LicenseName - { - get => _licenseName; - set => SetAndNotify(ref _licenseName, value); - } - - /// - /// The plugins display icon that's shown in the settings see for - /// available icons - /// - public string? Icon - { - get => _icon; - set => SetAndNotify(ref _icon, value); - } - - /// - /// The version of the plugin + /// The version of the plugin /// [JsonRequired] [JsonInclude] - public string Version - { - get => _version; - internal set => SetAndNotify(ref _version, value); - } + public string Version { get; internal init; } = null!; /// - /// The main entry DLL, should contain a class implementing Plugin + /// The main entry DLL, should contain a class implementing Plugin /// [JsonRequired] [JsonInclude] - public string Main - { - get => _main; - internal set => SetAndNotify(ref _main, value); - } + public string Main { get; internal init; } = null!; /// - /// Gets or sets a boolean indicating whether this plugin should automatically enable all its features when it is first - /// loaded - /// - public bool AutoEnableFeatures - { - get => _autoEnableFeatures; - set => SetAndNotify(ref _autoEnableFeatures, value); - } - - /// - /// Gets a boolean indicating whether this plugin requires elevated admin privileges + /// A short description of the plugin /// [JsonInclude] - public bool RequiresAdmin - { - get => _requiresAdmin; - internal set => SetAndNotify(ref _requiresAdmin, value); - } + public string? Description { get; internal init; } + + /// + /// Gets or sets the author of this plugin + /// + [JsonInclude] + public string? Author { get; internal init; } + + /// + /// Gets or sets the website of this plugin or its author + /// + [JsonInclude] + public Uri? Website { get; internal init; } + + /// + /// Gets or sets the repository of this plugin + /// + [JsonInclude] + public Uri? Repository { get; internal init; } + + /// + /// Gets or sets the help page of this plugin + /// + [JsonInclude] + public Uri? HelpPage { get; internal init; } + + /// + /// Gets or sets the help page of this plugin + /// + [JsonInclude] + public Uri? License { get; internal init; } + + /// + /// Gets or sets the author of this plugin + /// + [JsonInclude] + public string? LicenseName { get; internal init; } + + /// + /// The plugins display icon that's shown in the settings see for + /// available icons + /// + [JsonInclude] + public string? Icon { get; internal init; } + + /// + /// Gets a boolean indicating whether this plugin requires elevated admin privileges + /// + [JsonInclude] + public bool RequiresAdmin { get; internal init; } /// /// Gets or sets a boolean indicating whether hot reloading this plugin is supported /// - public bool HotReloadSupported - { - get => _hotReloadSupported; - set => SetAndNotify(ref _hotReloadSupported, value); - } + [JsonInclude] + public bool HotReloadSupported { get; internal init; } = true; /// - /// Gets + /// Gets /// [JsonInclude] - public PluginPlatform? Platforms - { - get => _platforms; - internal set => SetAndNotify(ref _platforms, value); - } + public PluginPlatform? Platforms { get; internal init; } /// - /// Gets the API version the plugin was built for + /// Gets the API version the plugin was built for /// [JsonInclude] - public Version? Api - { - get => _api; - internal set => SetAndNotify(ref _api, value); - } + public Version? Api { get; internal init; } = new(1, 0, 0); /// - /// Gets the plugin this info is associated with + /// Gets the plugin this info is associated with /// [JsonIgnore] - public Plugin Plugin - { - get => _plugin; - internal set => SetAndNotify(ref _plugin, value); - } + public Plugin Plugin { get; internal set; } = null!; /// - /// Gets a string representing either a full path pointing to an svg or the markdown icon + /// Gets a string representing either a full path pointing to an svg or the markdown icon /// [JsonIgnore] - public string? ResolvedIcon - { - get - { - if (Icon == null) - return null; - return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon; - } - } + public string? ResolvedIcon => Icon == null ? null : Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon; /// - /// Gets a boolean indicating whether this plugin is compatible with the current operating system and API version + /// Gets a boolean indicating whether this plugin is compatible with the current operating system and API version /// + [JsonIgnore] public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem() && Api != null && Api.Major >= Constants.PluginApiVersion; - internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}"; - - /// - public override string ToString() - { - return $"{Name} v{Version} - {Guid}"; - } - /// [JsonIgnore] public List Prerequisites { get; } = new(); @@ -245,9 +142,18 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject [JsonIgnore] public IEnumerable PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform()); + [JsonIgnore] + internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}"; + /// public bool ArePrerequisitesMet() { return PlatformPrerequisites.All(p => p.IsMet()); } + + /// + public override string ToString() + { + return $"{Name} v{Version} - {Guid}"; + } } \ No newline at end of file diff --git a/src/Artemis.Storage/Migrations/Profile/M0003SystemTextJson.cs b/src/Artemis.Storage/Migrations/Profile/M0003SystemTextJson.cs index b48cf3b1e..47771f2cb 100644 --- a/src/Artemis.Storage/Migrations/Profile/M0003SystemTextJson.cs +++ b/src/Artemis.Storage/Migrations/Profile/M0003SystemTextJson.cs @@ -18,7 +18,7 @@ internal class M0003SystemTextJson : IProfileMigration ConvertToSystemTextJson(profileJson); } - private void ConvertToSystemTextJson(JsonObject jsonObject) + internal static void ConvertToSystemTextJson(JsonObject jsonObject) { FilterType(jsonObject); @@ -52,7 +52,7 @@ internal class M0003SystemTextJson : IProfileMigration } } - private void FilterType(JsonObject jsonObject) + internal 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. diff --git a/src/Artemis.Storage/Migrations/Profile/M0004NodeStorage.cs b/src/Artemis.Storage/Migrations/Profile/M0004NodeStorage.cs new file mode 100644 index 000000000..f82ad9b98 --- /dev/null +++ b/src/Artemis.Storage/Migrations/Profile/M0004NodeStorage.cs @@ -0,0 +1,147 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Serilog; + +namespace Artemis.Storage.Migrations.Profile +{ + /// + /// Migrates nodes to be provider-based. + /// This requires giving them a ProviderId and updating the their namespaces to match the namespace of the new plugin. + /// + internal class M0004NodeStorage : IProfileMigration + { + private readonly ILogger _logger; + + public M0004NodeStorage(ILogger logger) + { + _logger = logger; + } + + /// + public int Version => 4; + + /// + public void Migrate(JsonObject configurationJson, JsonObject profileJson) + { + MigrateNodeScript(configurationJson["ActivationCondition"]); + + JsonArray? folders = profileJson["Folders"]?.AsArray(); + JsonArray? layers = profileJson["Layers"]?.AsArray(); + + if (folders != null) + { + foreach (JsonNode? folder in folders) + MigrateProfileElement(folder); + } + + if (layers != null) + { + foreach (JsonNode? layer in layers) + { + MigrateProfileElement(layer); + MigratePropertyGroup(layer?["GeneralPropertyGroup"]); + MigratePropertyGroup(layer?["TransformPropertyGroup"]); + MigratePropertyGroup(layer?["LayerBrush"]?["PropertyGroup"]); + } + } + } + + private void MigrateProfileElement(JsonNode? profileElement) + { + if (profileElement == null) + return; + + JsonArray? layerEffects = profileElement["LayerEffects"]?.AsArray(); + if (layerEffects != null) + { + foreach (JsonNode? layerEffect in layerEffects) + MigratePropertyGroup(layerEffect?["PropertyGroup"]); + } + + JsonNode? displayCondition = profileElement["DisplayCondition"]; + if (displayCondition != null) + MigrateNodeScript(displayCondition["Script"]); + } + + private void MigratePropertyGroup(JsonNode? propertyGroup) + { + if (propertyGroup == null) + return; + + JsonArray? properties = propertyGroup["Properties"]?.AsArray(); + JsonArray? propertyGroups = propertyGroup["PropertyGroups"]?.AsArray(); + if (properties != null) + { + foreach (JsonNode? property in properties) + MigrateNodeScript(property?["DataBinding"]?["NodeScript"]); + } + + if (propertyGroups != null) + { + foreach (JsonNode? childPropertyGroup in propertyGroups) + MigratePropertyGroup(childPropertyGroup); + } + } + + private void MigrateNodeScript(JsonNode? nodeScript) + { + JsonArray? nodes = nodeScript?["Nodes"]?.AsArray(); + if (nodes == null) + return; + + foreach (JsonNode? jsonNode in nodes) + { + if (jsonNode == null) + continue; + + JsonObject nodeObject = jsonNode.AsObject(); + nodeObject["Storage"] = MigrateNodeStorageJson(nodeObject["Storage"]?.GetValue(), _logger); + } + } + + internal 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) + M0003SystemTextJson.ConvertToSystemTextJson(childObject); + } + + return values.ToJsonString(); + } + } + else + { + M0003SystemTextJson.ConvertToSystemTextJson(jsonObject); + } + + return jsonObject.ToJsonString(); + } + catch (Exception e) + { + logger.Error(e, "Failed to migrate node storage JSON"); + return json; + } + } + } +} diff --git a/src/Artemis.Storage/Migrations/Storage/M0026NodeStorage.cs b/src/Artemis.Storage/Migrations/Storage/M0026NodeStorage.cs new file mode 100644 index 000000000..f181f9698 --- /dev/null +++ b/src/Artemis.Storage/Migrations/Storage/M0026NodeStorage.cs @@ -0,0 +1,119 @@ +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/StorageMigrationService.cs b/src/Artemis.Storage/StorageMigrationService.cs index 1d726ac8f..eca2a7dde 100644 --- a/src/Artemis.Storage/StorageMigrationService.cs +++ b/src/Artemis.Storage/StorageMigrationService.cs @@ -9,7 +9,7 @@ namespace Artemis.Storage; public class StorageMigrationService { - public const int PROFILE_VERSION = 3; + public const int PROFILE_VERSION = 4; private readonly ILogger _logger; private readonly IList _migrations;