diff --git a/.github/workflows/docfx.yml b/.github/workflows/docfx.yml index af0986a83..0f3e998b3 100644 --- a/.github/workflows/docfx.yml +++ b/.github/workflows/docfx.yml @@ -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.4 + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 with: server: www360.your-server.de protocol: ftps diff --git a/src/Artemis.Core/Models/BreakableModel.cs b/src/Artemis.Core/Models/BreakableModel.cs index f0338cd0f..9f2c60ed9 100644 --- a/src/Artemis.Core/Models/BreakableModel.cs +++ b/src/Artemis.Core/Models/BreakableModel.cs @@ -61,6 +61,9 @@ public abstract class BreakableModel : CorePropertyChanged, IBreakableModel /// public void SetBrokenState(string state, Exception? exception = null) { + if (state == BrokenState && BrokenStateException?.StackTrace == exception?.StackTrace) + return; + BrokenState = state ?? throw new ArgumentNullException(nameof(state)); BrokenStateException = exception; OnBrokenStateChanged(); diff --git a/src/Artemis.Core/Models/IPluginFeatureDependent.cs b/src/Artemis.Core/Models/IPluginFeatureDependent.cs new file mode 100644 index 000000000..a1531bfc5 --- /dev/null +++ b/src/Artemis.Core/Models/IPluginFeatureDependent.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Artemis.Core; + +/// +/// Represents a class that depends on plugin features +/// +public interface IPluginFeatureDependent +{ + /// + /// Gets the plugin features this class depends on, may contain the same plugin feature twice if depending on it in multiple ways. + /// + /// A of this class depends on. + public IEnumerable GetFeatureDependencies(); +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/AlwaysOnCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/AlwaysOnCondition.cs index eeeece9c5..b42c1e1fb 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/AlwaysOnCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/AlwaysOnCondition.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; @@ -82,4 +83,14 @@ public class AlwaysOnCondition : ICondition } #endregion + + #region Implementation of IPluginFeatureDependent + + /// + public IEnumerable GetFeatureDependencies() + { + return []; + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs index f198b5397..826634d67 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs @@ -325,4 +325,14 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition } #endregion + + #region Implementation of IPluginFeatureDependent + + /// + public IEnumerable GetFeatureDependencies() + { + return Script.GetFeatureDependencies().Concat(EventPath?.GetFeatureDependencies() ?? []); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs b/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs index ef3b17c13..b1d3fa9f4 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs @@ -6,7 +6,7 @@ namespace Artemis.Core; /// /// Represents a condition applied to a /// -public interface ICondition : IDisposable, IStorageModel +public interface ICondition : IDisposable, IStorageModel, IPluginFeatureDependent { /// /// Gets the entity used to store this condition diff --git a/src/Artemis.Core/Models/Profile/Conditions/PlayOnceCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/PlayOnceCondition.cs index 40178ad6e..4e3ff0ff1 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/PlayOnceCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/PlayOnceCondition.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; @@ -82,4 +83,14 @@ public class PlayOnceCondition : ICondition } #endregion + + #region Implementation of IPluginFeatureDependent + + /// + public IEnumerable GetFeatureDependencies() + { + return []; + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs index 5ab81e22c..fddc62306 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; @@ -159,6 +160,16 @@ public class StaticCondition : CorePropertyChanged, INodeScriptCondition } #endregion + + #region Implementation of IPluginFeatureDependent + + /// + public IEnumerable GetFeatureDependencies() + { + return Script.GetFeatureDependencies(); + } + + #endregion } /// diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index 42b10ccde..9bf8e94e6 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -243,4 +243,14 @@ public class DataBinding : IDataBinding } #endregion + + #region Implementation of IPluginFeatureDependent + + /// + public IEnumerable GetFeatureDependencies() + { + return Script.GetFeatureDependencies(); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs index a5856d018..e87868d52 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs @@ -8,7 +8,7 @@ namespace Artemis.Core; /// Represents a data binding that binds a certain to a value inside a /// /// -public interface IDataBinding : IStorageModel, IDisposable +public interface IDataBinding : IStorageModel, IDisposable, IPluginFeatureDependent { /// /// Gets the layer property the data binding is applied to diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs index 681ddb923..deeff25bc 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs @@ -11,7 +11,7 @@ namespace Artemis.Core; /// /// Represents a path that points to a property in data model /// -public class DataModelPath : IStorageModel, IDisposable +public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent { private readonly LinkedList _segments; private Expression>? _accessorLambda; @@ -188,6 +188,14 @@ public class DataModelPath : IStorageModel, IDisposable return string.IsNullOrWhiteSpace(Path) ? "this" : Path; } + /// + public IEnumerable GetFeatureDependencies() + { + if (Target == null) + return []; + return [Target.Module]; + } + /// /// Occurs whenever the path becomes invalid /// diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 2db77c92b..a845bd331 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -179,6 +179,14 @@ public sealed class Folder : RenderProfileElement return $"[Folder] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; } + /// + public override IEnumerable GetFeatureDependencies() + { + return LayerEffects.SelectMany(e => e.GetFeatureDependencies()) + .Concat(Children.SelectMany(c => c.GetFeatureDependencies())) + .Concat(DisplayCondition.GetFeatureDependencies()); + } + #region Rendering /// diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 66df68a05..2f7de1d48 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -160,10 +160,12 @@ public sealed class Layer : RenderProfileElement public LayerAdapter Adapter { get; } /// - public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet; + public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet && HasBounds; internal override RenderElementEntity RenderElementEntity => LayerEntity; + private bool HasBounds => Bounds.Width > 0 && Bounds.Height > 0; + /// public override List GetAllLayerProperties() { @@ -187,6 +189,16 @@ public sealed class Layer : RenderProfileElement return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; } + /// + public override IEnumerable GetFeatureDependencies() + { + return LayerEffects.SelectMany(e => e.GetFeatureDependencies()) + .Concat(LayerBrush?.GetFeatureDependencies() ?? []) + .Concat(General.GetFeatureDependencies()) + .Concat(Transform.GetFeatureDependencies()) + .Concat(DisplayCondition.GetFeatureDependencies()); + } + /// /// Occurs when a property affecting the rendering properties of this layer has been updated /// @@ -383,7 +395,7 @@ public sealed class Layer : RenderProfileElement if (ShouldBeEnabled) Enable(); - else if (Suspended || (Timeline.IsFinished && !_renderCopies.Any())) + else if (Suspended || !HasBounds || (Timeline.IsFinished && !_renderCopies.Any())) Disable(); if (!Enabled || Timeline.Delta == TimeSpan.Zero) @@ -766,7 +778,7 @@ public sealed class Layer : RenderProfileElement if (!_leds.Remove(led)) return; - + CalculateRenderProperties(); } diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs b/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs index 5ecc252c0..a926118b2 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Serialization; namespace Artemis.Core; @@ -14,12 +15,13 @@ public readonly struct FloatRange /// /// The start value of the range /// The end value of the range + [JsonConstructor] public FloatRange(float start, float end) { Start = start; End = end; - _rand = new Random(); + _rand = Random.Shared; } /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index a7b5aa8ef..e3d1f11e4 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -11,7 +11,7 @@ namespace Artemis.Core; /// initialize these for you. /// /// -public interface ILayerProperty : IStorageModel, IDisposable +public interface ILayerProperty : IStorageModel, IDisposable, IPluginFeatureDependent { /// /// Gets the description attribute applied to this property diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs b/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs index 3b0c2b5a9..a338290db 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Serialization; namespace Artemis.Core; @@ -14,12 +15,13 @@ public readonly struct IntRange /// /// The start value of the range /// The end value of the range + [JsonConstructor] public IntRange(int start, int end) { Start = start; End = end; - _rand = new Random(); + _rand = Random.Shared; } /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 5ecfeeca6..85dcba517 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -54,6 +54,12 @@ public class LayerProperty : CorePropertyChanged, ILayerProperty return $"{Path} - {CurrentValue} ({PropertyType})"; } + /// + public IEnumerable GetFeatureDependencies() + { + return DataBinding.GetFeatureDependencies(); + } + /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// diff --git a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs index 31b4af879..84de5c444 100644 --- a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs +++ b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs @@ -15,7 +15,7 @@ namespace Artemis.Core; /// initialize these for you. /// /// -public abstract class LayerPropertyGroup : IDisposable +public abstract class LayerPropertyGroup : IDisposable, IPluginFeatureDependent { private readonly List _layerProperties; private readonly List _layerPropertyGroups; @@ -343,4 +343,14 @@ public abstract class LayerPropertyGroup : IDisposable Dispose(true); GC.SuppressFinalize(this); } + + #region Implementation of IPluginFeatureDependent + + /// + public IEnumerable GetFeatureDependencies() + { + return LayerProperties.SelectMany(p => p.GetFeatureDependencies()).Concat(LayerPropertyGroups.SelectMany(g => g.GetFeatureDependencies())); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index 2a0d96a23..073afa540 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -171,6 +171,12 @@ public sealed class Profile : ProfileElement return $"[Profile] {nameof(Name)}: {Name}"; } + /// + public override IEnumerable GetFeatureDependencies() + { + return GetRootFolder().GetFeatureDependencies().Concat(Scripts.Select(c => c.ScriptingProvider)); + } + /// /// Populates all the LEDs on the elements in this profile /// diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index dbce946a8..921910db5 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -9,7 +9,7 @@ namespace Artemis.Core; /// /// Represents an element of a /// -public abstract class ProfileElement : BreakableModel, IDisposable +public abstract class ProfileElement : BreakableModel, IDisposable, IPluginFeatureDependent { internal readonly List ChildrenList; private Guid _entityId; @@ -122,6 +122,9 @@ public abstract class ProfileElement : BreakableModel, IDisposable return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}"; } + /// + public abstract IEnumerable GetFeatureDependencies(); + /// /// Occurs when a child was added to the list /// diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index 04f84c8f1..a4c1e89f4 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -9,13 +9,13 @@ namespace Artemis.Core; /// /// Represents the configuration of a profile, contained in a /// -public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable +public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable, IPluginFeatureDependent { /// /// Represents an empty profile. /// public static readonly ProfileConfiguration Empty = new(ProfileCategory.Empty, "Empty", "Empty"); - + private ActivationBehaviour _activationBehaviour; private bool _activationConditionMet; private ProfileCategory _category; @@ -146,7 +146,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable get => _activationConditionMet; private set => SetAndNotify(ref _activationConditionMet, value); } - + /// /// Gets the profile of this profile configuration /// @@ -159,8 +159,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable /// /// Gets or sets a boolean indicating whether this profile should fade in and out when enabling or disabling /// - public bool FadeInAndOut - { + public bool FadeInAndOut + { get => _fadeInAndOut; set => SetAndNotify(ref _fadeInAndOut, value); } @@ -188,7 +188,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable /// alongside any activation requirements of the , if set /// public NodeScript ActivationCondition { get; } - + /// /// Gets the entity used by this profile config /// @@ -247,6 +247,19 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable return $"[ProfileConfiguration] {nameof(Name)}: {Name}"; } + /// + public IEnumerable GetFeatureDependencies() + { + if (_disposed) + throw new ObjectDisposedException("ProfileConfiguration"); + if (Profile == null) + throw new InvalidOperationException("Cannot determine feature dependencies when the profile is not loaded."); + + return ActivationCondition.GetFeatureDependencies() + .Concat(Profile.GetFeatureDependencies()) + .Concat(Module != null ? [Module] : []); + } + internal void LoadModules(List enabledModules) { if (_disposed) diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs index cadbd5e7e..8410012a9 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs @@ -9,7 +9,7 @@ namespace Artemis.Core.LayerBrushes; /// /// For internal use only, please use or or instead /// -public abstract class BaseLayerBrush : BreakableModel, IDisposable +public abstract class BaseLayerBrush : BreakableModel, IDisposable, IPluginFeatureDependent { private LayerBrushType _brushType; private ILayerBrushConfigurationDialog? _configurationDialog; @@ -199,6 +199,20 @@ public abstract class BaseLayerBrush : BreakableModel, IDisposable Dispose(true); GC.SuppressFinalize(this); } + + #region Implementation of IPluginFeatureDependent + + /// + public IEnumerable GetFeatureDependencies() + { + IEnumerable result = [Descriptor.Provider]; + if (BaseProperties != null) + result = result.Concat(BaseProperties.GetFeatureDependencies()); + + return result; + } + + #endregion } /// diff --git a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs index 7d300077f..651df32f9 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Artemis.Storage.Entities.Profile; using SkiaSharp; @@ -7,7 +9,7 @@ namespace Artemis.Core.LayerEffects; /// /// For internal use only, please use instead /// -public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageModel +public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageModel, IPluginFeatureDependent { private ILayerEffectConfigurationDialog? _configurationDialog; private LayerEffectDescriptor _descriptor; @@ -164,7 +166,7 @@ public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageMod // Not only is this needed to initialize properties on the layer effects, it also prevents implementing anything // but LayerEffect outside the core internal abstract void Initialize(); - + internal void InternalUpdate(Timeline timeline) { BaseProperties?.Update(timeline); @@ -235,4 +237,18 @@ public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageMod BaseProperties?.ApplyToEntity(); LayerEffectEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; } + + #region Implementation of IPluginFeatureDependent + + /// + public IEnumerable GetFeatureDependencies() + { + IEnumerable result = [Descriptor.Provider]; + if (BaseProperties != null) + result = result.Concat(BaseProperties.GetFeatureDependencies()); + + return result; + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs index 4ce40a0ab..06c382749 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs @@ -8,7 +8,7 @@ namespace Artemis.Core; /// /// Represents a kind of node inside a /// -public interface INode : INotifyPropertyChanged, IBreakableModel +public interface INode : INotifyPropertyChanged, IBreakableModel, IPluginFeatureDependent { /// /// Gets or sets the ID of the node. diff --git a/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs b/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs index 456692a4d..7ed23e898 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs @@ -8,7 +8,7 @@ namespace Artemis.Core; /// /// Represents a node script /// -public interface INodeScript : INotifyPropertyChanged, IDisposable, IStorageModel +public interface INodeScript : INotifyPropertyChanged, IDisposable, IStorageModel, IPluginFeatureDependent { /// /// Gets the name of the node script. diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index e8ccddd4a..8f281f6c3 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -401,6 +401,16 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript } #endregion + + #region Implementation of IPluginFeatureDependent + + /// + public IEnumerable GetFeatureDependencies() + { + return Nodes.SelectMany(n => n.GetFeatureDependencies()); + } + + #endregion } /// diff --git a/src/Artemis.Core/VisualScripting/Nodes/Node.cs b/src/Artemis.Core/VisualScripting/Nodes/Node.cs index 8d4a40d3d..b3c4ad3ee 100644 --- a/src/Artemis.Core/VisualScripting/Nodes/Node.cs +++ b/src/Artemis.Core/VisualScripting/Nodes/Node.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.Events; -using DryIoc; namespace Artemis.Core; @@ -404,4 +403,14 @@ public abstract class Node : BreakableModel, INode } #endregion + + #region Implementation of IPluginFeatureDependent + + /// + public virtual IEnumerable GetFeatureDependencies() + { + return NodeData == null ? [] : [NodeData.Provider]; + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs index a7ba504ee..1f1c9c500 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs @@ -162,7 +162,7 @@ public partial class ProfileConfigurationEditViewModel : DialogViewModelBase m.Icon == enumValue) - : icons.ElementAt(new Random().Next(0, icons.Count - 1)); + : icons.ElementAt(Random.Shared.Next(0, icons.Count - 1)); } private async Task SaveIcon() diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs index 24bb74225..d5c4de433 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs @@ -10,10 +10,10 @@ namespace Artemis.UI.Screens.Workshop.Entries.Details; public class EntryInfoViewModel : ViewModelBase { private readonly INotificationService _notificationService; - public IGetEntryById_Entry Entry { get; } + public IEntryDetails Entry { get; } public DateTimeOffset? UpdatedAt { get; } - public EntryInfoViewModel(IGetEntryById_Entry entry, INotificationService notificationService) + public EntryInfoViewModel(IEntryDetails entry, INotificationService notificationService) { _notificationService = notificationService; Entry = entry; diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs index 21be13b28..12bd22b3a 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs @@ -20,7 +20,7 @@ public class EntryReleasesViewModel : ViewModelBase private readonly IWindowService _windowService; private readonly INotificationService _notificationService; - public EntryReleasesViewModel(IGetEntryById_Entry entry, EntryInstallationHandlerFactory factory, IWindowService windowService, INotificationService notificationService) + public EntryReleasesViewModel(IEntryDetails entry, EntryInstallationHandlerFactory factory, IWindowService windowService, INotificationService notificationService) { _factory = factory; _windowService = windowService; @@ -31,7 +31,7 @@ public class EntryReleasesViewModel : ViewModelBase OnInstallationStarted = Confirm; } - public IGetEntryById_Entry Entry { get; } + public IEntryDetails Entry { get; } public ReactiveCommand DownloadLatestRelease { get; } public Func> OnInstallationStarted { get; set; } diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml index f041d5655..91932033a 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml @@ -21,9 +21,11 @@ HorizontalContentAlignment="Stretch" Command="{CompiledBinding NavigateToEntry}" IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}"> - + - + by @@ -65,7 +67,7 @@ - + @@ -73,6 +75,18 @@ downloads + + + + + + installed + + + + update available + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs index 757396c72..d129f9be6 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs @@ -1,26 +1,39 @@ using System; using System.Reactive; +using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; using ReactiveUI; namespace Artemis.UI.Screens.Workshop.Entries.List; -public class EntryListItemViewModel : ActivatableViewModelBase +public partial class EntryListItemViewModel : ActivatableViewModelBase { private readonly IRouter _router; + [Notify] private bool _isInstalled; + [Notify] private bool _updateAvailable; - public EntryListItemViewModel(IGetEntries_Entries_Items entry, IRouter router) + public EntryListItemViewModel(IEntrySummary entry, IRouter router, IWorkshopService workshopService) { _router = router; Entry = entry; NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry); + + this.WhenActivated((CompositeDisposable _) => + { + InstalledEntry? installedEntry = workshopService.GetInstalledEntry(entry.Id); + IsInstalled = installedEntry != null; + UpdateAvailable = installedEntry != null && installedEntry.ReleaseId != entry.LatestReleaseId; + }); } - public IGetEntries_Entries_Items Entry { get; } + public IEntrySummary Entry { get; } public ReactiveCommand NavigateToEntry { get; } private async Task ExecuteNavigateToEntry() diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs index 7bbd75704..4d831ba29 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs @@ -21,7 +21,7 @@ namespace Artemis.UI.Screens.Workshop.Entries.List; public abstract partial class EntryListViewModel : RoutableScreen { - private readonly SourceList _entries = new(); + private readonly SourceList _entries = new(); private readonly ObservableAsPropertyHelper _isLoading; private readonly INotificationService _notificationService; private readonly string _route; @@ -37,13 +37,13 @@ public abstract partial class EntryListViewModel : RoutableScreen getEntryListViewModel) + Func getEntryListViewModel) { _route = route; _workshopClient = workshopClient; _notificationService = notificationService; - _showPagination = this.WhenAnyValue(vm => vm.TotalPages).Select(t => t > 1).ToProperty(this, vm => vm.ShowPagination); - _isLoading = this.WhenAnyValue(vm => vm.Page, vm => vm.LoadedPage, (p, c) => p != c).ToProperty(this, vm => vm.IsLoading); + _showPagination = this.WhenAnyValue(vm => vm.TotalPages).Select(t => t > 1).ToProperty(this, vm => vm.ShowPagination); + _isLoading = this.WhenAnyValue(vm => vm.Page, vm => vm.LoadedPage, (p, c) => p != c).ToProperty(this, vm => vm.IsLoading); CategoriesViewModel = categoriesViewModel; InputViewModel = entryListInputViewModel; diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs index ed2fa328b..ca0cdbb99 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs @@ -14,7 +14,7 @@ public class LayoutListViewModel : List.EntryListViewModel CategoriesViewModel categoriesViewModel, EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, - Func getEntryListViewModel) + Func getEntryListViewModel) : base("workshop/entries/layouts", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) { entryListInputViewModel.SearchWatermark = "Search layouts"; diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs index 206697490..7af13b4e4 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs @@ -14,7 +14,7 @@ public class PluginListViewModel : EntryListViewModel CategoriesViewModel categoriesViewModel, EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, - Func getEntryListViewModel) + Func getEntryListViewModel) : base("workshop/entries/plugins", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) { entryListInputViewModel.SearchWatermark = "Search plugins"; diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs index f251ee10d..d8ae07a17 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs @@ -14,7 +14,7 @@ public class ProfileListViewModel : List.EntryListViewModel CategoriesViewModel categoriesViewModel, EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, - Func getEntryListViewModel) + Func getEntryListViewModel) : base("workshop/entries/profiles", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) { entryListInputViewModel.SearchWatermark = "Search profiles"; diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml index cc566cbd3..3dd205871 100644 --- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml @@ -17,13 +17,17 @@ - - - - - - - + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs index 1c12e2dbf..9663f5ad4 100644 --- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs @@ -24,10 +24,10 @@ public partial class LayoutDetailsViewModel : RoutableScreen _getEntryInfoViewModel; - private readonly Func _getEntryReleasesViewModel; - private readonly Func _getEntryImagesViewModel; - [Notify] private IGetEntryById_Entry? _entry; + private readonly Func _getEntryInfoViewModel; + private readonly Func _getEntryReleasesViewModel; + private readonly Func _getEntryImagesViewModel; + [Notify] private IEntryDetails? _entry; [Notify] private EntryInfoViewModel? _entryInfoViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel; @@ -35,9 +35,9 @@ public partial class LayoutDetailsViewModel : RoutableScreen getEntryInfoViewModel, - Func getEntryReleasesViewModel, - Func getEntryImagesViewModel) + Func getEntryInfoViewModel, + Func getEntryReleasesViewModel, + Func getEntryImagesViewModel) { _client = client; _deviceService = deviceService; @@ -59,19 +59,12 @@ public partial class LayoutDetailsViewModel : RoutableScreen - - - - - - - + + + + + + + + + + + + + Used by these profiles + + + + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs index bfce68a94..be800a735 100644 --- a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDetailsViewModel.cs @@ -1,10 +1,13 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Screens.Workshop.Entries.Details; +using Artemis.UI.Screens.Workshop.Entries.List; using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Screens.Workshop.Plugins.Dialogs; using Artemis.UI.Shared.Routing; @@ -21,20 +24,23 @@ public partial class PluginDetailsViewModel : RoutableScreen _getEntryInfoViewModel; - private readonly Func _getEntryReleasesViewModel; - private readonly Func _getEntryImagesViewModel; - [Notify] private IGetEntryById_Entry? _entry; + private readonly Func _getEntryInfoViewModel; + private readonly Func _getEntryReleasesViewModel; + private readonly Func _getEntryImagesViewModel; + private readonly Func _getEntryListViewModel; + [Notify] private IEntryDetails? _entry; [Notify] private EntryInfoViewModel? _entryInfoViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel; - + [Notify] private ReadOnlyObservableCollection? _dependants; + public PluginDetailsViewModel(IWorkshopClient client, IWindowService windowService, IPluginManagementService pluginManagementService, - Func getEntryInfoViewModel, - Func getEntryReleasesViewModel, - Func getEntryImagesViewModel) + Func getEntryInfoViewModel, + Func getEntryReleasesViewModel, + Func getEntryImagesViewModel, + Func getEntryListViewModel) { _client = client; _windowService = windowService; @@ -42,6 +48,7 @@ public partial class PluginDetailsViewModel : RoutableScreen? dependants = (await _client.GetDependantEntries.ExecuteAsync(entryId, 0, 25, cancellationToken)).Data?.Entries?.Items; + Dependants = dependants != null && dependants.Any() + ? new ReadOnlyObservableCollection(new ObservableCollection(dependants.Select(_getEntryListViewModel))) + : null; } private async Task OnInstallationStarted(IEntryDetails entryDetails) diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml index 0569ee176..499a5b978 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml @@ -17,13 +17,28 @@ - - - - - - - + + + + + + + + + + + + + Required plugins + + + + + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs index 6c65b78c4..8aadc53d4 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs @@ -1,7 +1,11 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Artemis.UI.Screens.Workshop.Entries.Details; +using Artemis.UI.Screens.Workshop.Entries.List; using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Shared.Routing; using Artemis.WebClient.Workshop; @@ -13,23 +17,28 @@ namespace Artemis.UI.Screens.Workshop.Profile; public partial class ProfileDetailsViewModel : RoutableScreen { private readonly IWorkshopClient _client; - private readonly Func _getEntryInfoViewModel; - private readonly Func _getEntryReleasesViewModel; - private readonly Func _getEntryImagesViewModel; - [Notify] private IGetEntryById_Entry? _entry; + private readonly Func _getEntryInfoViewModel; + private readonly Func _getEntryReleasesViewModel; + private readonly Func _getEntryImagesViewModel; + private readonly Func _getEntryListViewModel; + + [Notify] private IEntryDetails? _entry; [Notify] private EntryInfoViewModel? _entryInfoViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel; + [Notify] private ReadOnlyObservableCollection? _dependencies; public ProfileDetailsViewModel(IWorkshopClient client, - Func getEntryInfoViewModel, - Func getEntryReleasesViewModel, - Func getEntryImagesViewModel) + Func getEntryInfoViewModel, + Func getEntryReleasesViewModel, + Func getEntryImagesViewModel, + Func getEntryListViewModel) { _client = client; _getEntryInfoViewModel = getEntryInfoViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel; + _getEntryListViewModel = getEntryListViewModel; } public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) @@ -44,16 +53,13 @@ public partial class ProfileDetailsViewModel : RoutableScreen? dependencies = (await _client.GetLatestDependencies.ExecuteAsync(entryId, cancellationToken)).Data?.Entry?.LatestRelease?.Dependencies; + Dependencies = dependencies != null && dependencies.Any() + ? new ReadOnlyObservableCollection(new ObservableCollection(dependencies.Select(_getEntryListViewModel))) + : null; } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs index e06f14ce9..c92f6290b 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using Artemis.Core; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; @@ -37,7 +38,7 @@ public class SubmissionWizardState : IDisposable public List Images { get; set; } = new(); public IEntrySource? EntrySource { get; set; } - + public void ChangeScreen() where TSubmissionViewModel : SubmissionViewModel { try 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 d52457a55..4a5b9cae9 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs @@ -65,10 +65,10 @@ public partial class ProfileSelectionStepViewModel : SubmissionViewModel private void ExecuteContinue() { - if (SelectedProfile == null) + if (SelectedProfile?.Profile == null) return; - State.EntrySource = new ProfileEntrySource(SelectedProfile); + State.EntrySource = new ProfileEntrySource(SelectedProfile, SelectedProfile.GetFeatureDependencies().Distinct().ToList()); State.Name = SelectedProfile.Name; State.Icon = SelectedProfile.Icon.GetIconStream(); diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs index aa755b8a0..3014950f7 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs @@ -41,7 +41,7 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler } // Ensure there is an installed entry - InstalledEntry installedEntry = _workshopService.GetInstalledEntry(entry) ?? new InstalledEntry(entry, release); + InstalledEntry installedEntry = _workshopService.GetInstalledEntry(entry.Id) ?? new InstalledEntry(entry, release); DirectoryInfo releaseDirectory = installedEntry.GetReleaseDirectory(release); // If the folder already exists, remove it so that if the layout now contains less files, old things dont stick around diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs index 7b9fc5539..34f0aac53 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs @@ -25,7 +25,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler public async Task InstallAsync(IEntryDetails entry, IRelease release, Progress progress, CancellationToken cancellationToken) { // Ensure there is an installed entry - InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry); + InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry.Id); if (installedEntry != null) { // If the folder already exists, we're not going to reinstall the plugin since files may be in use, consider our job done diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs index 78c31e98c..218665d05 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs @@ -36,7 +36,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler } // Find existing installation to potentially replace the profile - InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry); + InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry.Id); if (installedEntry != null && installedEntry.TryGetMetadata("ProfileId", out Guid profileId)) { ProfileConfiguration? existing = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == profileId); diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntrySource.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntrySource.cs index 739f8e28f..16235a935 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntrySource.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntrySource.cs @@ -4,10 +4,12 @@ namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers; public class ProfileEntrySource : IEntrySource { - public ProfileEntrySource(ProfileConfiguration profileConfiguration) + public ProfileEntrySource(ProfileConfiguration profileConfiguration, List dependencies) { ProfileConfiguration = profileConfiguration; + Dependencies = dependencies; } - public ProfileConfiguration ProfileConfiguration { get; set; } + public ProfileConfiguration ProfileConfiguration { get; } + public List Dependencies { get; } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs index 012491729..3dbbd68e9 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs @@ -31,6 +31,7 @@ public class ProfileEntryUploadHandler : IEntryUploadHandler MultipartFormDataContent content = new(); StreamContent streamContent = new(archiveStream); streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); + content.Add(JsonContent.Create(source.Dependencies.Select(d => new {PluginId = d.Plugin.Guid, FeatureId = d.Id}).ToList()), "ReleaseDependencies"); content.Add(streamContent, "file", "file.zip"); // Submit diff --git a/src/Artemis.WebClient.Workshop/Models/AccessToken.cs b/src/Artemis.WebClient.Workshop/Models/AccessToken.cs index 60e9f31d9..d9ed1abb6 100644 --- a/src/Artemis.WebClient.Workshop/Models/AccessToken.cs +++ b/src/Artemis.WebClient.Workshop/Models/AccessToken.cs @@ -12,14 +12,16 @@ internal class AuthenticationToken if (tokenResponse.RefreshToken == null) throw new ArtemisWebClientException("Token response contains no refresh token"); + IdentityToken = tokenResponse.IdentityToken; AccessToken = tokenResponse.AccessToken; RefreshToken = tokenResponse.RefreshToken; ExpiresAt = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn); } - + public DateTimeOffset ExpiresAt { get; private set; } public bool Expired => DateTimeOffset.UtcNow.AddSeconds(5) >= ExpiresAt; + public string? IdentityToken { get; private set; } public string AccessToken { get; private set; } public string RefreshToken { get; private set; } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql index a66c3a660..c89bf34f6 100644 --- a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql @@ -28,6 +28,20 @@ fragment submittedEntry on Entry { createdAt } +fragment entrySummary on Entry { + id + author + name + summary + entryType + downloads + createdAt + latestReleaseId + categories { + ...category + } +} + fragment entryDetails on Entry { id author diff --git a/src/Artemis.WebClient.Workshop/Queries/GetDependantEntries.graphql b/src/Artemis.WebClient.Workshop/Queries/GetDependantEntries.graphql new file mode 100644 index 000000000..551cb140e --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Queries/GetDependantEntries.graphql @@ -0,0 +1,15 @@ +query GetDependantEntries($entryId: Long! $skip: Int $take: Int) { + entries( + where: { + latestRelease: { dependencies: { some: { id: { eq: $entryId } } } } + } + skip: $skip + take: $take + order: { createdAt: DESC } + ) { + totalCount + items { + ...entrySummary + } + } +} diff --git a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql index 366e3bae4..456466a30 100644 --- a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql @@ -2,16 +2,7 @@ query GetEntries($search: String $filter: EntryFilterInput $skip: Int $take: Int entries(search: $search where: $filter skip: $skip take: $take, order: $order) { totalCount items { - id - author - name - summary - entryType - downloads - createdAt - categories { - ...category - } + ...entrySummary } } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/GetLatestDependencies.graphql b/src/Artemis.WebClient.Workshop/Queries/GetLatestDependencies.graphql new file mode 100644 index 000000000..67987cde5 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Queries/GetLatestDependencies.graphql @@ -0,0 +1,9 @@ +query GetLatestDependencies($id: Long!) { + entry(id: $id) { + latestRelease { + dependencies { + ...entrySummary + } + } + } +} diff --git a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs index 80a7bb470..c8727c361 100644 --- a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs +++ b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs @@ -258,12 +258,21 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi } /// - public void Logout() + public async Task Logout() { + DiscoveryDocumentResponse disco = await GetDiscovery(); + + // Open the web browser for the user to log out + if (disco.EndSessionEndpoint != null) + { + RequestUrl authRequestUrl = new(disco.EndSessionEndpoint); + string url = authRequestUrl.CreateEndSessionUrl(_token?.IdentityToken); + Utilities.OpenUrl(url); + } + _token = null; _claims.Clear(); SetStoredRefreshToken(null); - _isLoggedInSubject.OnNext(false); } diff --git a/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs b/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs index b806c83a9..5d8f908b3 100644 --- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs +++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs @@ -13,6 +13,6 @@ public interface IAuthenticationService : IProtectedArtemisService Task GetBearer(); Task AutoLogin(bool force = false); Task Login(CancellationToken cancellationToken); - void Logout(); + Task Logout(); bool GetIsEmailVerified(); } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs index 7bc1a04c1..ea61b3b6d 100644 --- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs @@ -14,7 +14,7 @@ public interface IWorkshopService Task NavigateToEntry(long entryId, EntryType entryType); List GetInstalledEntries(); - InstalledEntry? GetInstalledEntry(IEntryDetails entry); + InstalledEntry? GetInstalledEntry(long entryId); void RemoveInstalledEntry(InstalledEntry installedEntry); void SaveInstalledEntry(InstalledEntry entry); void Initialize(); diff --git a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs index 0721c97f0..95b90ccc5 100644 --- a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs @@ -152,9 +152,9 @@ public class WorkshopService : IWorkshopService } /// - public InstalledEntry? GetInstalledEntry(IEntryDetails entry) + public InstalledEntry? GetInstalledEntry(long entryId) { - EntryEntity? entity = _entryRepository.GetByEntryId(entry.Id); + EntryEntity? entity = _entryRepository.GetByEntryId(entryId); if (entity == null) return null; diff --git a/src/Artemis.WebClient.Workshop/graphql.config.yml b/src/Artemis.WebClient.Workshop/graphql.config.yml index a8ba99703..9662a514f 100644 --- a/src/Artemis.WebClient.Workshop/graphql.config.yml +++ b/src/Artemis.WebClient.Workshop/graphql.config.yml @@ -2,7 +2,7 @@ schema: schema.graphql extensions: endpoints: Default GraphQL Endpoint: - url: https://localhost:7281/graphql + url: https://workshop.artemis-rgb.com/graphql headers: user-agent: JS GraphQL introspect: true diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql index 959e11204..150e74f3a 100644 --- a/src/Artemis.WebClient.Workshop/schema.graphql +++ b/src/Artemis.WebClient.Workshop/schema.graphql @@ -5,8 +5,6 @@ schema { mutation: Mutation } -directive @tag(name: String!) on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION - type Category { icon: String! id: Long! @@ -35,6 +33,7 @@ type Entry { authorId: UUID! categories: [Category!]! createdAt: DateTime! + dependantReleases: [Release!]! description: String! downloads: Long! entryType: EntryType! @@ -46,6 +45,7 @@ type Entry { latestReleaseId: Long layoutInfo: [LayoutInfo!]! name: String! + pluginInfo: PluginInfo releases: [Release!]! summary: String! tags: [Tag!]! @@ -84,10 +84,33 @@ type Mutation { updateEntryImage(input: UpdateEntryImageInput!): Image } +type PluginInfo { + api: Int + entry: Entry! + entryId: Long! + helpPage: String + platforms: PluginPlatform + pluginGuid: UUID! + repository: String + requiresAdmin: Boolean! + website: String +} + +"A segment of a collection." +type PluginInfosCollectionSegment { + "A flattened list of the items." + items: [PluginInfo!] + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + totalCount: Int! +} + type Query { categories(order: [CategorySortInput!], where: CategoryFilterInput): [Category!]! entries(order: [EntrySortInput!], search: String, skip: Int, take: Int, where: EntryFilterInput): EntriesCollectionSegment entry(id: Long!): Entry + pluginInfo(pluginGuid: UUID!): PluginInfo + pluginInfos(order: [PluginInfoSortInput!], skip: Int, take: Int, where: PluginInfoFilterInput): PluginInfosCollectionSegment searchEntries(input: String!, order: [EntrySortInput!], type: EntryType, where: EntryFilterInput): [Entry!]! searchKeyboardLayout(deviceProvider: UUID!, logicalLayout: String, model: String!, physicalLayout: KeyboardLayoutType!, vendor: String!): LayoutInfo searchLayout(deviceProvider: UUID!, deviceType: RGBDeviceType!, model: String!, vendor: String!): LayoutInfo @@ -96,6 +119,7 @@ type Query { type Release { createdAt: DateTime! + dependencies: [Entry!]! downloadSize: Long! downloads: Long! entry: Entry! @@ -131,11 +155,18 @@ enum KeyboardLayoutType { UNKNOWN } +enum PluginPlatform { + LINUX + OSX + WINDOWS +} + enum RGBDeviceType { ALL COOLER DRAM FAN + GAME_CONTROLLER GRAPHICS_CARD HEADSET HEADSET_STAND @@ -166,6 +197,11 @@ scalar Long scalar UUID +input BooleanOperationFilterInput { + eq: Boolean + neq: Boolean +} + input CategoryFilterInput { and: [CategoryFilterInput!] icon: StringOperationFilterInput @@ -220,6 +256,7 @@ input EntryFilterInput { authorId: UuidOperationFilterInput categories: ListFilterInputTypeOfCategoryFilterInput createdAt: DateTimeOperationFilterInput + dependantReleases: ListFilterInputTypeOfReleaseFilterInput description: StringOperationFilterInput downloads: LongOperationFilterInput entryType: EntryTypeOperationFilterInput @@ -232,6 +269,7 @@ input EntryFilterInput { layoutInfo: ListFilterInputTypeOfLayoutInfoFilterInput name: StringOperationFilterInput or: [EntryFilterInput!] + pluginInfo: PluginInfoFilterInput releases: ListFilterInputTypeOfReleaseFilterInput summary: StringOperationFilterInput tags: ListFilterInputTypeOfTagFilterInput @@ -250,6 +288,7 @@ input EntrySortInput { latestRelease: ReleaseSortInput latestReleaseId: SortEnumType name: SortEnumType + pluginInfo: PluginInfoSortInput summary: SortEnumType } @@ -322,6 +361,13 @@ input ListFilterInputTypeOfCategoryFilterInput { some: CategoryFilterInput } +input ListFilterInputTypeOfEntryFilterInput { + all: EntryFilterInput + any: Boolean + none: EntryFilterInput + some: EntryFilterInput +} + input ListFilterInputTypeOfImageFilterInput { all: ImageFilterInput any: Boolean @@ -372,6 +418,39 @@ input NullableOfKeyboardLayoutTypeOperationFilterInput { nin: [KeyboardLayoutType] } +input NullableOfPluginPlatformOperationFilterInput { + eq: PluginPlatform + in: [PluginPlatform] + neq: PluginPlatform + nin: [PluginPlatform] +} + +input PluginInfoFilterInput { + and: [PluginInfoFilterInput!] + api: IntOperationFilterInput + entry: EntryFilterInput + entryId: LongOperationFilterInput + helpPage: StringOperationFilterInput + or: [PluginInfoFilterInput!] + platforms: NullableOfPluginPlatformOperationFilterInput + pluginGuid: UuidOperationFilterInput + repository: StringOperationFilterInput + requiresAdmin: BooleanOperationFilterInput + website: StringOperationFilterInput +} + +input PluginInfoSortInput { + api: SortEnumType + entry: EntrySortInput + entryId: SortEnumType + helpPage: SortEnumType + platforms: SortEnumType + pluginGuid: SortEnumType + repository: SortEnumType + requiresAdmin: SortEnumType + website: SortEnumType +} + input RGBDeviceTypeOperationFilterInput { eq: RGBDeviceType in: [RGBDeviceType!] @@ -382,6 +461,7 @@ input RGBDeviceTypeOperationFilterInput { input ReleaseFilterInput { and: [ReleaseFilterInput!] createdAt: DateTimeOperationFilterInput + dependencies: ListFilterInputTypeOfEntryFilterInput downloadSize: LongOperationFilterInput downloads: LongOperationFilterInput entry: EntryFilterInput