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

Merge branch 'development'

This commit is contained in:
RobertBeekman 2024-03-03 22:09:53 +01:00
commit 9fcd20d762
60 changed files with 525 additions and 145 deletions

View File

@ -26,7 +26,7 @@ jobs:
- name: Build DocFX - name: Build DocFX
run: docfx docfx/docfx_project/docfx.json run: docfx docfx/docfx_project/docfx.json
- name: Upload to FTP - name: Upload to FTP
uses: SamKirkland/FTP-Deploy-Action@4.3.4 uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with: with:
server: www360.your-server.de server: www360.your-server.de
protocol: ftps protocol: ftps

View File

@ -61,6 +61,9 @@ public abstract class BreakableModel : CorePropertyChanged, IBreakableModel
/// <inheritdoc /> /// <inheritdoc />
public void SetBrokenState(string state, Exception? exception = null) public void SetBrokenState(string state, Exception? exception = null)
{ {
if (state == BrokenState && BrokenStateException?.StackTrace == exception?.StackTrace)
return;
BrokenState = state ?? throw new ArgumentNullException(nameof(state)); BrokenState = state ?? throw new ArgumentNullException(nameof(state));
BrokenStateException = exception; BrokenStateException = exception;
OnBrokenStateChanged(); OnBrokenStateChanged();

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace Artemis.Core;
/// <summary>
/// Represents a class that depends on plugin features
/// </summary>
public interface IPluginFeatureDependent
{
/// <summary>
/// Gets the plugin features this class depends on, may contain the same plugin feature twice if depending on it in multiple ways.
/// </summary>
/// <returns>A <see cref="List{T}"/> of <see cref="PluginFeature"/> this class depends on.</returns>
public IEnumerable<PluginFeature> GetFeatureDependencies();
}

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions; using Artemis.Storage.Entities.Profile.Conditions;
@ -82,4 +83,14 @@ public class AlwaysOnCondition : ICondition
} }
#endregion #endregion
#region Implementation of IPluginFeatureDependent
/// <inheritdoc />
public IEnumerable<PluginFeature> GetFeatureDependencies()
{
return [];
}
#endregion
} }

View File

@ -325,4 +325,14 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
} }
#endregion #endregion
#region Implementation of IPluginFeatureDependent
/// <inheritdoc />
public IEnumerable<PluginFeature> GetFeatureDependencies()
{
return Script.GetFeatureDependencies().Concat(EventPath?.GetFeatureDependencies() ?? []);
}
#endregion
} }

View File

@ -6,7 +6,7 @@ namespace Artemis.Core;
/// <summary> /// <summary>
/// Represents a condition applied to a <see cref="ProfileElement" /> /// Represents a condition applied to a <see cref="ProfileElement" />
/// </summary> /// </summary>
public interface ICondition : IDisposable, IStorageModel public interface ICondition : IDisposable, IStorageModel, IPluginFeatureDependent
{ {
/// <summary> /// <summary>
/// Gets the entity used to store this condition /// Gets the entity used to store this condition

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions; using Artemis.Storage.Entities.Profile.Conditions;
@ -82,4 +83,14 @@ public class PlayOnceCondition : ICondition
} }
#endregion #endregion
#region Implementation of IPluginFeatureDependent
/// <inheritdoc />
public IEnumerable<PluginFeature> GetFeatureDependencies()
{
return [];
}
#endregion
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions; using Artemis.Storage.Entities.Profile.Conditions;
@ -159,6 +160,16 @@ public class StaticCondition : CorePropertyChanged, INodeScriptCondition
} }
#endregion #endregion
#region Implementation of IPluginFeatureDependent
/// <inheritdoc />
public IEnumerable<PluginFeature> GetFeatureDependencies()
{
return Script.GetFeatureDependencies();
}
#endregion
} }
/// <summary> /// <summary>

View File

@ -243,4 +243,14 @@ public class DataBinding<TLayerProperty> : IDataBinding
} }
#endregion #endregion
#region Implementation of IPluginFeatureDependent
/// <inheritdoc />
public IEnumerable<PluginFeature> GetFeatureDependencies()
{
return Script.GetFeatureDependencies();
}
#endregion
} }

View File

@ -8,7 +8,7 @@ namespace Artemis.Core;
/// Represents a data binding that binds a certain <see cref="LayerProperty{T}" /> to a value inside a /// Represents a data binding that binds a certain <see cref="LayerProperty{T}" /> to a value inside a
/// <see cref="DataModel" /> /// <see cref="DataModel" />
/// </summary> /// </summary>
public interface IDataBinding : IStorageModel, IDisposable public interface IDataBinding : IStorageModel, IDisposable, IPluginFeatureDependent
{ {
/// <summary> /// <summary>
/// Gets the layer property the data binding is applied to /// Gets the layer property the data binding is applied to

View File

@ -11,7 +11,7 @@ namespace Artemis.Core;
/// <summary> /// <summary>
/// Represents a path that points to a property in data model /// Represents a path that points to a property in data model
/// </summary> /// </summary>
public class DataModelPath : IStorageModel, IDisposable public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent
{ {
private readonly LinkedList<DataModelPathSegment> _segments; private readonly LinkedList<DataModelPathSegment> _segments;
private Expression<Func<object, object>>? _accessorLambda; private Expression<Func<object, object>>? _accessorLambda;
@ -188,6 +188,14 @@ public class DataModelPath : IStorageModel, IDisposable
return string.IsNullOrWhiteSpace(Path) ? "this" : Path; return string.IsNullOrWhiteSpace(Path) ? "this" : Path;
} }
/// <inheritdoc />
public IEnumerable<PluginFeature> GetFeatureDependencies()
{
if (Target == null)
return [];
return [Target.Module];
}
/// <summary> /// <summary>
/// Occurs whenever the path becomes invalid /// Occurs whenever the path becomes invalid
/// </summary> /// </summary>

View File

@ -179,6 +179,14 @@ public sealed class Folder : RenderProfileElement
return $"[Folder] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; return $"[Folder] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}";
} }
/// <inheritdoc />
public override IEnumerable<PluginFeature> GetFeatureDependencies()
{
return LayerEffects.SelectMany(e => e.GetFeatureDependencies())
.Concat(Children.SelectMany(c => c.GetFeatureDependencies()))
.Concat(DisplayCondition.GetFeatureDependencies());
}
#region Rendering #region Rendering
/// <inheritdoc /> /// <inheritdoc />

View File

@ -160,10 +160,12 @@ public sealed class Layer : RenderProfileElement
public LayerAdapter Adapter { get; } public LayerAdapter Adapter { get; }
/// <inheritdoc /> /// <inheritdoc />
public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet; public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet && HasBounds;
internal override RenderElementEntity RenderElementEntity => LayerEntity; internal override RenderElementEntity RenderElementEntity => LayerEntity;
private bool HasBounds => Bounds.Width > 0 && Bounds.Height > 0;
/// <inheritdoc /> /// <inheritdoc />
public override List<ILayerProperty> GetAllLayerProperties() public override List<ILayerProperty> GetAllLayerProperties()
{ {
@ -187,6 +189,16 @@ public sealed class Layer : RenderProfileElement
return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}";
} }
/// <inheritdoc />
public override IEnumerable<PluginFeature> GetFeatureDependencies()
{
return LayerEffects.SelectMany(e => e.GetFeatureDependencies())
.Concat(LayerBrush?.GetFeatureDependencies() ?? [])
.Concat(General.GetFeatureDependencies())
.Concat(Transform.GetFeatureDependencies())
.Concat(DisplayCondition.GetFeatureDependencies());
}
/// <summary> /// <summary>
/// Occurs when a property affecting the rendering properties of this layer has been updated /// Occurs when a property affecting the rendering properties of this layer has been updated
/// </summary> /// </summary>
@ -383,7 +395,7 @@ public sealed class Layer : RenderProfileElement
if (ShouldBeEnabled) if (ShouldBeEnabled)
Enable(); Enable();
else if (Suspended || (Timeline.IsFinished && !_renderCopies.Any())) else if (Suspended || !HasBounds || (Timeline.IsFinished && !_renderCopies.Any()))
Disable(); Disable();
if (!Enabled || Timeline.Delta == TimeSpan.Zero) if (!Enabled || Timeline.Delta == TimeSpan.Zero)

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Text.Json.Serialization;
namespace Artemis.Core; namespace Artemis.Core;
@ -14,12 +15,13 @@ public readonly struct FloatRange
/// </summary> /// </summary>
/// <param name="start">The start value of the range</param> /// <param name="start">The start value of the range</param>
/// <param name="end">The end value of the range</param> /// <param name="end">The end value of the range</param>
[JsonConstructor]
public FloatRange(float start, float end) public FloatRange(float start, float end)
{ {
Start = start; Start = start;
End = end; End = end;
_rand = new Random(); _rand = Random.Shared;
} }
/// <summary> /// <summary>

View File

@ -11,7 +11,7 @@ namespace Artemis.Core;
/// initialize these for you. /// initialize these for you.
/// </para> /// </para>
/// </summary> /// </summary>
public interface ILayerProperty : IStorageModel, IDisposable public interface ILayerProperty : IStorageModel, IDisposable, IPluginFeatureDependent
{ {
/// <summary> /// <summary>
/// Gets the description attribute applied to this property /// Gets the description attribute applied to this property

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Text.Json.Serialization;
namespace Artemis.Core; namespace Artemis.Core;
@ -14,12 +15,13 @@ public readonly struct IntRange
/// </summary> /// </summary>
/// <param name="start">The start value of the range</param> /// <param name="start">The start value of the range</param>
/// <param name="end">The end value of the range</param> /// <param name="end">The end value of the range</param>
[JsonConstructor]
public IntRange(int start, int end) public IntRange(int start, int end)
{ {
Start = start; Start = start;
End = end; End = end;
_rand = new Random(); _rand = Random.Shared;
} }
/// <summary> /// <summary>

View File

@ -54,6 +54,12 @@ public class LayerProperty<T> : CorePropertyChanged, ILayerProperty
return $"{Path} - {CurrentValue} ({PropertyType})"; return $"{Path} - {CurrentValue} ({PropertyType})";
} }
/// <inheritdoc />
public IEnumerable<PluginFeature> GetFeatureDependencies()
{
return DataBinding.GetFeatureDependencies();
}
/// <summary> /// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary> /// </summary>

View File

@ -15,7 +15,7 @@ namespace Artemis.Core;
/// initialize these for you. /// initialize these for you.
/// </para> /// </para>
/// </summary> /// </summary>
public abstract class LayerPropertyGroup : IDisposable public abstract class LayerPropertyGroup : IDisposable, IPluginFeatureDependent
{ {
private readonly List<ILayerProperty> _layerProperties; private readonly List<ILayerProperty> _layerProperties;
private readonly List<LayerPropertyGroup> _layerPropertyGroups; private readonly List<LayerPropertyGroup> _layerPropertyGroups;
@ -343,4 +343,14 @@ public abstract class LayerPropertyGroup : IDisposable
Dispose(true); Dispose(true);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
#region Implementation of IPluginFeatureDependent
/// <inheritdoc />
public IEnumerable<PluginFeature> GetFeatureDependencies()
{
return LayerProperties.SelectMany(p => p.GetFeatureDependencies()).Concat(LayerPropertyGroups.SelectMany(g => g.GetFeatureDependencies()));
}
#endregion
} }

View File

@ -171,6 +171,12 @@ public sealed class Profile : ProfileElement
return $"[Profile] {nameof(Name)}: {Name}"; return $"[Profile] {nameof(Name)}: {Name}";
} }
/// <inheritdoc />
public override IEnumerable<PluginFeature> GetFeatureDependencies()
{
return GetRootFolder().GetFeatureDependencies().Concat(Scripts.Select(c => c.ScriptingProvider));
}
/// <summary> /// <summary>
/// Populates all the LEDs on the elements in this profile /// Populates all the LEDs on the elements in this profile
/// </summary> /// </summary>

View File

@ -9,7 +9,7 @@ namespace Artemis.Core;
/// <summary> /// <summary>
/// Represents an element of a <see cref="Profile" /> /// Represents an element of a <see cref="Profile" />
/// </summary> /// </summary>
public abstract class ProfileElement : BreakableModel, IDisposable public abstract class ProfileElement : BreakableModel, IDisposable, IPluginFeatureDependent
{ {
internal readonly List<ProfileElement> ChildrenList; internal readonly List<ProfileElement> ChildrenList;
private Guid _entityId; private Guid _entityId;
@ -122,6 +122,9 @@ public abstract class ProfileElement : BreakableModel, IDisposable
return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}"; return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}";
} }
/// <inheritdoc />
public abstract IEnumerable<PluginFeature> GetFeatureDependencies();
/// <summary> /// <summary>
/// Occurs when a child was added to the <see cref="Children" /> list /// Occurs when a child was added to the <see cref="Children" /> list
/// </summary> /// </summary>

View File

@ -9,7 +9,7 @@ namespace Artemis.Core;
/// <summary> /// <summary>
/// Represents the configuration of a profile, contained in a <see cref="ProfileCategory" /> /// Represents the configuration of a profile, contained in a <see cref="ProfileCategory" />
/// </summary> /// </summary>
public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable, IPluginFeatureDependent
{ {
/// <summary> /// <summary>
/// Represents an empty profile. /// Represents an empty profile.
@ -247,6 +247,19 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
return $"[ProfileConfiguration] {nameof(Name)}: {Name}"; return $"[ProfileConfiguration] {nameof(Name)}: {Name}";
} }
/// <inheritdoc />
public IEnumerable<PluginFeature> 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<Module> enabledModules) internal void LoadModules(List<Module> enabledModules)
{ {
if (_disposed) if (_disposed)

View File

@ -9,7 +9,7 @@ namespace Artemis.Core.LayerBrushes;
/// <summary> /// <summary>
/// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="PerLedLayerBrush{T}" /> or instead /// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="PerLedLayerBrush{T}" /> or instead
/// </summary> /// </summary>
public abstract class BaseLayerBrush : BreakableModel, IDisposable public abstract class BaseLayerBrush : BreakableModel, IDisposable, IPluginFeatureDependent
{ {
private LayerBrushType _brushType; private LayerBrushType _brushType;
private ILayerBrushConfigurationDialog? _configurationDialog; private ILayerBrushConfigurationDialog? _configurationDialog;
@ -199,6 +199,20 @@ public abstract class BaseLayerBrush : BreakableModel, IDisposable
Dispose(true); Dispose(true);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
#region Implementation of IPluginFeatureDependent
/// <inheritdoc />
public IEnumerable<PluginFeature> GetFeatureDependencies()
{
IEnumerable<PluginFeature> result = [Descriptor.Provider];
if (BaseProperties != null)
result = result.Concat(BaseProperties.GetFeatureDependencies());
return result;
}
#endregion
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile;
using SkiaSharp; using SkiaSharp;
@ -7,7 +9,7 @@ namespace Artemis.Core.LayerEffects;
/// <summary> /// <summary>
/// For internal use only, please use <see cref="LayerEffect{T}" /> instead /// For internal use only, please use <see cref="LayerEffect{T}" /> instead
/// </summary> /// </summary>
public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageModel public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageModel, IPluginFeatureDependent
{ {
private ILayerEffectConfigurationDialog? _configurationDialog; private ILayerEffectConfigurationDialog? _configurationDialog;
private LayerEffectDescriptor _descriptor; private LayerEffectDescriptor _descriptor;
@ -235,4 +237,18 @@ public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageMod
BaseProperties?.ApplyToEntity(); BaseProperties?.ApplyToEntity();
LayerEffectEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; LayerEffectEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity;
} }
#region Implementation of IPluginFeatureDependent
/// <inheritdoc />
public IEnumerable<PluginFeature> GetFeatureDependencies()
{
IEnumerable<PluginFeature> result = [Descriptor.Provider];
if (BaseProperties != null)
result = result.Concat(BaseProperties.GetFeatureDependencies());
return result;
}
#endregion
} }

View File

@ -8,7 +8,7 @@ namespace Artemis.Core;
/// <summary> /// <summary>
/// Represents a kind of node inside a <see cref="INodeScript" /> /// Represents a kind of node inside a <see cref="INodeScript" />
/// </summary> /// </summary>
public interface INode : INotifyPropertyChanged, IBreakableModel public interface INode : INotifyPropertyChanged, IBreakableModel, IPluginFeatureDependent
{ {
/// <summary> /// <summary>
/// Gets or sets the ID of the node. /// Gets or sets the ID of the node.

View File

@ -8,7 +8,7 @@ namespace Artemis.Core;
/// <summary> /// <summary>
/// Represents a node script /// Represents a node script
/// </summary> /// </summary>
public interface INodeScript : INotifyPropertyChanged, IDisposable, IStorageModel public interface INodeScript : INotifyPropertyChanged, IDisposable, IStorageModel, IPluginFeatureDependent
{ {
/// <summary> /// <summary>
/// Gets the name of the node script. /// Gets the name of the node script.

View File

@ -401,6 +401,16 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
} }
#endregion #endregion
#region Implementation of IPluginFeatureDependent
/// <inheritdoc />
public IEnumerable<PluginFeature> GetFeatureDependencies()
{
return Nodes.SelectMany(n => n.GetFeatureDependencies());
}
#endregion
} }
/// <summary> /// <summary>

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Artemis.Core.Events; using Artemis.Core.Events;
using DryIoc;
namespace Artemis.Core; namespace Artemis.Core;
@ -404,4 +403,14 @@ public abstract class Node : BreakableModel, INode
} }
#endregion #endregion
#region Implementation of IPluginFeatureDependent
/// <inheritdoc />
public virtual IEnumerable<PluginFeature> GetFeatureDependencies()
{
return NodeData == null ? [] : [NodeData.Provider];
}
#endregion
} }

View File

@ -162,7 +162,7 @@ public partial class ProfileConfigurationEditViewModel : DialogViewModelBase<Pro
// Preselect the icon or fall back to a random one // Preselect the icon or fall back to a random one
SelectedMaterialIcon = !IsNew && Enum.TryParse(_profileConfiguration.Icon.IconName, out MaterialIconKind enumValue) SelectedMaterialIcon = !IsNew && Enum.TryParse(_profileConfiguration.Icon.IconName, out MaterialIconKind enumValue)
? icons.FirstOrDefault(m => m.Icon == enumValue) ? icons.FirstOrDefault(m => 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() private async Task SaveIcon()

View File

@ -10,10 +10,10 @@ namespace Artemis.UI.Screens.Workshop.Entries.Details;
public class EntryInfoViewModel : ViewModelBase public class EntryInfoViewModel : ViewModelBase
{ {
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
public IGetEntryById_Entry Entry { get; } public IEntryDetails Entry { get; }
public DateTimeOffset? UpdatedAt { get; } public DateTimeOffset? UpdatedAt { get; }
public EntryInfoViewModel(IGetEntryById_Entry entry, INotificationService notificationService) public EntryInfoViewModel(IEntryDetails entry, INotificationService notificationService)
{ {
_notificationService = notificationService; _notificationService = notificationService;
Entry = entry; Entry = entry;

View File

@ -20,7 +20,7 @@ public class EntryReleasesViewModel : ViewModelBase
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly INotificationService _notificationService; 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; _factory = factory;
_windowService = windowService; _windowService = windowService;
@ -31,7 +31,7 @@ public class EntryReleasesViewModel : ViewModelBase
OnInstallationStarted = Confirm; OnInstallationStarted = Confirm;
} }
public IGetEntryById_Entry Entry { get; } public IEntryDetails Entry { get; }
public ReactiveCommand<Unit, Unit> DownloadLatestRelease { get; } public ReactiveCommand<Unit, Unit> DownloadLatestRelease { get; }
public Func<IEntryDetails, Task<bool>> OnInstallationStarted { get; set; } public Func<IEntryDetails, Task<bool>> OnInstallationStarted { get; set; }

View File

@ -21,9 +21,11 @@
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
Command="{CompiledBinding NavigateToEntry}" Command="{CompiledBinding NavigateToEntry}"
IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}"> IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="*, Auto">
<!-- Icon --> <!-- Icon -->
<Border Grid.Column="0" <Border Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
CornerRadius="6" CornerRadius="6"
VerticalAlignment="Center" VerticalAlignment="Center"
Margin="0 0 10 0" Margin="0 0 10 0"
@ -34,7 +36,7 @@
</Border> </Border>
<!-- Body --> <!-- Body -->
<Grid Grid.Column="1" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto"> <Grid Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
<TextBlock Grid.Row="0" Margin="0 0 0 5" TextTrimming="CharacterEllipsis"> <TextBlock Grid.Row="0" Margin="0 0 0 5" TextTrimming="CharacterEllipsis">
<Run Classes="h5" Text="{CompiledBinding Entry.Name, FallbackValue=Title}" /> <Run Classes="h5" Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
<Run Classes="subtitle">by</Run> <Run Classes="subtitle">by</Run>
@ -65,7 +67,7 @@
</Grid> </Grid>
<!-- Info --> <!-- Info -->
<StackPanel Grid.Column="2" Margin="0 0 4 0"> <StackPanel Grid.Column="2" Grid.Row="0" Margin="0 0 4 0" HorizontalAlignment="Right">
<TextBlock TextAlignment="Right" Text="{CompiledBinding Entry.CreatedAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" /> <TextBlock TextAlignment="Right" Text="{CompiledBinding Entry.CreatedAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" />
<TextBlock TextAlignment="Right"> <TextBlock TextAlignment="Right">
<avalonia:MaterialIcon Kind="Downloads" /> <avalonia:MaterialIcon Kind="Downloads" />
@ -73,6 +75,18 @@
<Run>downloads</Run> <Run>downloads</Run>
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
<!-- Install state -->
<StackPanel Grid.Column="2" Grid.Row="1" Margin="0 0 4 0" HorizontalAlignment="Right" VerticalAlignment="Bottom" IsVisible="{CompiledBinding IsInstalled}">
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding !UpdateAvailable}">
<avalonia:MaterialIcon Kind="CheckCircle" Foreground="{DynamicResource SystemAccentColorLight1}"/>
<Run>installed</Run>
</TextBlock>
<TextBlock TextAlignment="Right" IsVisible="{CompiledBinding UpdateAvailable}">
<avalonia:MaterialIcon Kind="Update" Foreground="{DynamicResource SystemAccentColorLight1}"/>
<Run>update available</Run>
</TextBlock>
</StackPanel>
</Grid> </Grid>
</Button> </Button>
</UserControl> </UserControl>

View File

@ -1,26 +1,39 @@
using System; using System;
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.List; namespace Artemis.UI.Screens.Workshop.Entries.List;
public class EntryListItemViewModel : ActivatableViewModelBase public partial class EntryListItemViewModel : ActivatableViewModelBase
{ {
private readonly IRouter _router; 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; _router = router;
Entry = entry; Entry = entry;
NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry); 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<Unit, Unit> NavigateToEntry { get; } public ReactiveCommand<Unit, Unit> NavigateToEntry { get; }
private async Task ExecuteNavigateToEntry() private async Task ExecuteNavigateToEntry()

View File

@ -21,7 +21,7 @@ namespace Artemis.UI.Screens.Workshop.Entries.List;
public abstract partial class EntryListViewModel : RoutableScreen<WorkshopListParameters> public abstract partial class EntryListViewModel : RoutableScreen<WorkshopListParameters>
{ {
private readonly SourceList<IGetEntries_Entries_Items> _entries = new(); private readonly SourceList<IEntrySummary> _entries = new();
private readonly ObservableAsPropertyHelper<bool> _isLoading; private readonly ObservableAsPropertyHelper<bool> _isLoading;
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly string _route; private readonly string _route;
@ -37,13 +37,13 @@ public abstract partial class EntryListViewModel : RoutableScreen<WorkshopListPa
CategoriesViewModel categoriesViewModel, CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel, EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService, INotificationService notificationService,
Func<IGetEntries_Entries_Items, EntryListItemViewModel> getEntryListViewModel) Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
{ {
_route = route; _route = route;
_workshopClient = workshopClient; _workshopClient = workshopClient;
_notificationService = notificationService; _notificationService = notificationService;
_showPagination = this.WhenAnyValue<EntryListViewModel, int>(vm => vm.TotalPages).Select(t => t > 1).ToProperty(this, vm => vm.ShowPagination); _showPagination = this.WhenAnyValue(vm => vm.TotalPages).Select(t => t > 1).ToProperty(this, vm => vm.ShowPagination);
_isLoading = this.WhenAnyValue<EntryListViewModel, bool, int, int>(vm => vm.Page, vm => vm.LoadedPage, (p, c) => p != c).ToProperty(this, vm => vm.IsLoading); _isLoading = this.WhenAnyValue(vm => vm.Page, vm => vm.LoadedPage, (p, c) => p != c).ToProperty(this, vm => vm.IsLoading);
CategoriesViewModel = categoriesViewModel; CategoriesViewModel = categoriesViewModel;
InputViewModel = entryListInputViewModel; InputViewModel = entryListInputViewModel;

View File

@ -14,7 +14,7 @@ public class LayoutListViewModel : List.EntryListViewModel
CategoriesViewModel categoriesViewModel, CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel, EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService, INotificationService notificationService,
Func<IGetEntries_Entries_Items, EntryListItemViewModel> getEntryListViewModel) Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
: base("workshop/entries/layouts", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) : base("workshop/entries/layouts", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
{ {
entryListInputViewModel.SearchWatermark = "Search layouts"; entryListInputViewModel.SearchWatermark = "Search layouts";

View File

@ -14,7 +14,7 @@ public class PluginListViewModel : EntryListViewModel
CategoriesViewModel categoriesViewModel, CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel, EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService, INotificationService notificationService,
Func<IGetEntries_Entries_Items, EntryListItemViewModel> getEntryListViewModel) Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
: base("workshop/entries/plugins", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) : base("workshop/entries/plugins", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
{ {
entryListInputViewModel.SearchWatermark = "Search plugins"; entryListInputViewModel.SearchWatermark = "Search plugins";

View File

@ -14,7 +14,7 @@ public class ProfileListViewModel : List.EntryListViewModel
CategoriesViewModel categoriesViewModel, CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel, EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService, INotificationService notificationService,
Func<IGetEntries_Entries_Items, EntryListItemViewModel> getEntryListViewModel) Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
: base("workshop/entries/profiles", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) : base("workshop/entries/profiles", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
{ {
entryListInputViewModel.SearchWatermark = "Search profiles"; entryListInputViewModel.SearchWatermark = "Search profiles";

View File

@ -17,13 +17,17 @@
</Border> </Border>
</StackPanel> </StackPanel>
<Border Classes="card" Grid.Row="1" Grid.Column="1" Margin="10 0"> <ScrollViewer Grid.Row="1" Grid.Column="1">
<StackPanel Margin="10 0" Spacing="10">
<Border Classes="card">
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia"> <mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
<mdxaml:MarkdownScrollViewer.Styles> <mdxaml:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml" /> <StyleInclude Source="/Styles/Markdown.axaml" />
</mdxaml:MarkdownScrollViewer.Styles> </mdxaml:MarkdownScrollViewer.Styles>
</mdxaml:MarkdownScrollViewer> </mdxaml:MarkdownScrollViewer>
</Border> </Border>
</StackPanel>
</ScrollViewer>
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" /> <ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />
</Grid> </Grid>

View File

@ -24,10 +24,10 @@ public partial class LayoutDetailsViewModel : RoutableScreen<WorkshopDetailParam
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly IDeviceService _deviceService; private readonly IDeviceService _deviceService;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly Func<IGetEntryById_Entry, EntryInfoViewModel> _getEntryInfoViewModel; private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
private readonly Func<IGetEntryById_Entry, EntryReleasesViewModel> _getEntryReleasesViewModel; private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IGetEntryById_Entry, EntryImagesViewModel> _getEntryImagesViewModel; private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
[Notify] private IGetEntryById_Entry? _entry; [Notify] private IEntryDetails? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel; [Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel;
@ -35,9 +35,9 @@ public partial class LayoutDetailsViewModel : RoutableScreen<WorkshopDetailParam
public LayoutDetailsViewModel(IWorkshopClient client, public LayoutDetailsViewModel(IWorkshopClient client,
IDeviceService deviceService, IDeviceService deviceService,
IWindowService windowService, IWindowService windowService,
Func<IGetEntryById_Entry, EntryInfoViewModel> getEntryInfoViewModel, Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
Func<IGetEntryById_Entry, EntryReleasesViewModel> getEntryReleasesViewModel, Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IGetEntryById_Entry, EntryImagesViewModel> getEntryImagesViewModel) Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
{ {
_client = client; _client = client;
_deviceService = deviceService; _deviceService = deviceService;
@ -59,20 +59,13 @@ public partial class LayoutDetailsViewModel : RoutableScreen<WorkshopDetailParam
return; return;
Entry = result.Data?.Entry; Entry = result.Data?.Entry;
if (Entry == null) EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
{ EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
EntryInfoViewModel = null; EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
EntryReleasesViewModel = null;
}
else
{
EntryInfoViewModel = _getEntryInfoViewModel(Entry);
EntryReleasesViewModel = _getEntryReleasesViewModel(Entry);
EntryImagesViewModel = _getEntryImagesViewModel(Entry);
if (EntryReleasesViewModel != null)
EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished; EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished;
} }
}
private async Task OnInstallationFinished(InstalledEntry installedEntry) private async Task OnInstallationFinished(InstalledEntry installedEntry)
{ {

View File

@ -17,7 +17,9 @@
</Border> </Border>
</StackPanel> </StackPanel>
<Border Classes="card" Grid.Row="1" Grid.Column="1" Margin="10 0"> <ScrollViewer Grid.Row="1" Grid.Column="1">
<StackPanel Margin="10 0" Spacing="10">
<Border Classes="card">
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia"> <mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
<mdxaml:MarkdownScrollViewer.Styles> <mdxaml:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml" /> <StyleInclude Source="/Styles/Markdown.axaml" />
@ -25,6 +27,18 @@
</mdxaml:MarkdownScrollViewer> </mdxaml:MarkdownScrollViewer>
</Border> </Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Dependants, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Used by these profiles</TextBlock>
<Border Classes="card-separator" />
<ScrollViewer>
<ItemsControl ItemsSource="{CompiledBinding Dependants}"></ItemsControl>
</ScrollViewer>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" /> <ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -1,10 +1,13 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Screens.Workshop.Entries.Details; 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.Parameters;
using Artemis.UI.Screens.Workshop.Plugins.Dialogs; using Artemis.UI.Screens.Workshop.Plugins.Dialogs;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
@ -21,20 +24,23 @@ public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParam
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;
private readonly Func<IGetEntryById_Entry, EntryInfoViewModel> _getEntryInfoViewModel; private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
private readonly Func<IGetEntryById_Entry, EntryReleasesViewModel> _getEntryReleasesViewModel; private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IGetEntryById_Entry, EntryImagesViewModel> _getEntryImagesViewModel; private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
[Notify] private IGetEntryById_Entry? _entry; private readonly Func<IEntrySummary, EntryListItemViewModel> _getEntryListViewModel;
[Notify] private IEntryDetails? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel; [Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel;
[Notify] private ReadOnlyObservableCollection<EntryListItemViewModel>? _dependants;
public PluginDetailsViewModel(IWorkshopClient client, public PluginDetailsViewModel(IWorkshopClient client,
IWindowService windowService, IWindowService windowService,
IPluginManagementService pluginManagementService, IPluginManagementService pluginManagementService,
Func<IGetEntryById_Entry, EntryInfoViewModel> getEntryInfoViewModel, Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
Func<IGetEntryById_Entry, EntryReleasesViewModel> getEntryReleasesViewModel, Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IGetEntryById_Entry, EntryImagesViewModel> getEntryImagesViewModel) Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel,
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
{ {
_client = client; _client = client;
_windowService = windowService; _windowService = windowService;
@ -42,6 +48,7 @@ public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParam
_getEntryInfoViewModel = getEntryInfoViewModel; _getEntryInfoViewModel = getEntryInfoViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel;
_getEntryListViewModel = getEntryListViewModel;
} }
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
@ -56,20 +63,20 @@ public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParam
return; return;
Entry = result.Data?.Entry; Entry = result.Data?.Entry;
if (Entry == null) EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
{ EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
EntryInfoViewModel = null; EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
EntryReleasesViewModel = null;
}
else
{
EntryInfoViewModel = _getEntryInfoViewModel(Entry);
EntryReleasesViewModel = _getEntryReleasesViewModel(Entry);
EntryImagesViewModel = _getEntryImagesViewModel(Entry);
if (EntryReleasesViewModel != null)
{
EntryReleasesViewModel.OnInstallationStarted = OnInstallationStarted; EntryReleasesViewModel.OnInstallationStarted = OnInstallationStarted;
EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished; EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished;
} }
IReadOnlyList<IEntrySummary>? dependants = (await _client.GetDependantEntries.ExecuteAsync(entryId, 0, 25, cancellationToken)).Data?.Entries?.Items;
Dependants = dependants != null && dependants.Any()
? new ReadOnlyObservableCollection<EntryListItemViewModel>(new ObservableCollection<EntryListItemViewModel>(dependants.Select(_getEntryListViewModel)))
: null;
} }
private async Task<bool> OnInstallationStarted(IEntryDetails entryDetails) private async Task<bool> OnInstallationStarted(IEntryDetails entryDetails)

View File

@ -17,7 +17,9 @@
</Border> </Border>
</StackPanel> </StackPanel>
<Border Classes="card" Grid.Row="1" Grid.Column="1" Margin="10 0"> <ScrollViewer Grid.Row="1" Grid.Column="1">
<StackPanel Margin="10 0" Spacing="10">
<Border Classes="card">
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia"> <mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
<mdxaml:MarkdownScrollViewer.Styles> <mdxaml:MarkdownScrollViewer.Styles>
<StyleInclude Source="/Styles/Markdown.axaml" /> <StyleInclude Source="/Styles/Markdown.axaml" />
@ -25,6 +27,19 @@
</mdxaml:MarkdownScrollViewer> </mdxaml:MarkdownScrollViewer>
</Border> </Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Dependencies, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Required plugins</TextBlock>
<Border Classes="card-separator" />
<ScrollViewer>
<ItemsControl ItemsSource="{CompiledBinding Dependencies}"></ItemsControl>
</ScrollViewer>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" /> <ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -1,7 +1,11 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Entries.Details; 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.Parameters;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop;
@ -13,23 +17,28 @@ namespace Artemis.UI.Screens.Workshop.Profile;
public partial class ProfileDetailsViewModel : RoutableScreen<WorkshopDetailParameters> public partial class ProfileDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
{ {
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly Func<IGetEntryById_Entry, EntryInfoViewModel> _getEntryInfoViewModel; private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
private readonly Func<IGetEntryById_Entry, EntryReleasesViewModel> _getEntryReleasesViewModel; private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IGetEntryById_Entry, EntryImagesViewModel> _getEntryImagesViewModel; private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
[Notify] private IGetEntryById_Entry? _entry; private readonly Func<IEntrySummary, EntryListItemViewModel> _getEntryListViewModel;
[Notify] private IEntryDetails? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel; [Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel;
[Notify] private ReadOnlyObservableCollection<EntryListItemViewModel>? _dependencies;
public ProfileDetailsViewModel(IWorkshopClient client, public ProfileDetailsViewModel(IWorkshopClient client,
Func<IGetEntryById_Entry, EntryInfoViewModel> getEntryInfoViewModel, Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
Func<IGetEntryById_Entry, EntryReleasesViewModel> getEntryReleasesViewModel, Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IGetEntryById_Entry, EntryImagesViewModel> getEntryImagesViewModel) Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel,
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
{ {
_client = client; _client = client;
_getEntryInfoViewModel = getEntryInfoViewModel; _getEntryInfoViewModel = getEntryInfoViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel;
_getEntryListViewModel = getEntryListViewModel;
} }
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
@ -44,16 +53,13 @@ public partial class ProfileDetailsViewModel : RoutableScreen<WorkshopDetailPara
return; return;
Entry = result.Data?.Entry; Entry = result.Data?.Entry;
if (Entry == null) EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
{ EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
EntryInfoViewModel = null; EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
EntryReleasesViewModel = null;
} IReadOnlyList<IEntrySummary>? dependencies = (await _client.GetLatestDependencies.ExecuteAsync(entryId, cancellationToken)).Data?.Entry?.LatestRelease?.Dependencies;
else Dependencies = dependencies != null && dependencies.Any()
{ ? new ReadOnlyObservableCollection<EntryListItemViewModel>(new ObservableCollection<EntryListItemViewModel>(dependencies.Select(_getEntryListViewModel)))
EntryInfoViewModel = _getEntryInfoViewModel(Entry); : null;
EntryReleasesViewModel = _getEntryReleasesViewModel(Entry);
EntryImagesViewModel = _getEntryImagesViewModel(Entry);
}
} }
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Artemis.Core;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;

View File

@ -65,10 +65,10 @@ public partial class ProfileSelectionStepViewModel : SubmissionViewModel
private void ExecuteContinue() private void ExecuteContinue()
{ {
if (SelectedProfile == null) if (SelectedProfile?.Profile == null)
return; return;
State.EntrySource = new ProfileEntrySource(SelectedProfile); State.EntrySource = new ProfileEntrySource(SelectedProfile, SelectedProfile.GetFeatureDependencies().Distinct().ToList());
State.Name = SelectedProfile.Name; State.Name = SelectedProfile.Name;
State.Icon = SelectedProfile.Icon.GetIconStream(); State.Icon = SelectedProfile.Icon.GetIconStream();

View File

@ -41,7 +41,7 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler
} }
// Ensure there is an installed entry // 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); 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 // If the folder already exists, remove it so that if the layout now contains less files, old things dont stick around

View File

@ -25,7 +25,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler
public async Task<EntryInstallResult> InstallAsync(IEntryDetails entry, IRelease release, Progress<StreamProgress> progress, CancellationToken cancellationToken) public async Task<EntryInstallResult> InstallAsync(IEntryDetails entry, IRelease release, Progress<StreamProgress> progress, CancellationToken cancellationToken)
{ {
// Ensure there is an installed entry // Ensure there is an installed entry
InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry); InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry.Id);
if (installedEntry != null) 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 // If the folder already exists, we're not going to reinstall the plugin since files may be in use, consider our job done

View File

@ -36,7 +36,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
} }
// Find existing installation to potentially replace the profile // 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)) if (installedEntry != null && installedEntry.TryGetMetadata("ProfileId", out Guid profileId))
{ {
ProfileConfiguration? existing = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == profileId); ProfileConfiguration? existing = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == profileId);

View File

@ -4,10 +4,12 @@ namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
public class ProfileEntrySource : IEntrySource public class ProfileEntrySource : IEntrySource
{ {
public ProfileEntrySource(ProfileConfiguration profileConfiguration) public ProfileEntrySource(ProfileConfiguration profileConfiguration, List<PluginFeature> dependencies)
{ {
ProfileConfiguration = profileConfiguration; ProfileConfiguration = profileConfiguration;
Dependencies = dependencies;
} }
public ProfileConfiguration ProfileConfiguration { get; set; } public ProfileConfiguration ProfileConfiguration { get; }
public List<PluginFeature> Dependencies { get; }
} }

View File

@ -31,6 +31,7 @@ public class ProfileEntryUploadHandler : IEntryUploadHandler
MultipartFormDataContent content = new(); MultipartFormDataContent content = new();
StreamContent streamContent = new(archiveStream); StreamContent streamContent = new(archiveStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); 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"); content.Add(streamContent, "file", "file.zip");
// Submit // Submit

View File

@ -12,6 +12,7 @@ internal class AuthenticationToken
if (tokenResponse.RefreshToken == null) if (tokenResponse.RefreshToken == null)
throw new ArtemisWebClientException("Token response contains no refresh token"); throw new ArtemisWebClientException("Token response contains no refresh token");
IdentityToken = tokenResponse.IdentityToken;
AccessToken = tokenResponse.AccessToken; AccessToken = tokenResponse.AccessToken;
RefreshToken = tokenResponse.RefreshToken; RefreshToken = tokenResponse.RefreshToken;
ExpiresAt = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn); ExpiresAt = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn);
@ -20,6 +21,7 @@ internal class AuthenticationToken
public DateTimeOffset ExpiresAt { get; private set; } public DateTimeOffset ExpiresAt { get; private set; }
public bool Expired => DateTimeOffset.UtcNow.AddSeconds(5) >= ExpiresAt; public bool Expired => DateTimeOffset.UtcNow.AddSeconds(5) >= ExpiresAt;
public string? IdentityToken { get; private set; }
public string AccessToken { get; private set; } public string AccessToken { get; private set; }
public string RefreshToken { get; private set; } public string RefreshToken { get; private set; }
} }

View File

@ -28,6 +28,20 @@ fragment submittedEntry on Entry {
createdAt createdAt
} }
fragment entrySummary on Entry {
id
author
name
summary
entryType
downloads
createdAt
latestReleaseId
categories {
...category
}
}
fragment entryDetails on Entry { fragment entryDetails on Entry {
id id
author author

View File

@ -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
}
}
}

View File

@ -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) { entries(search: $search where: $filter skip: $skip take: $take, order: $order) {
totalCount totalCount
items { items {
id ...entrySummary
author
name
summary
entryType
downloads
createdAt
categories {
...category
}
} }
} }
} }

View File

@ -0,0 +1,9 @@
query GetLatestDependencies($id: Long!) {
entry(id: $id) {
latestRelease {
dependencies {
...entrySummary
}
}
}
}

View File

@ -258,12 +258,21 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
} }
/// <inheritdoc /> /// <inheritdoc />
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; _token = null;
_claims.Clear(); _claims.Clear();
SetStoredRefreshToken(null); SetStoredRefreshToken(null);
_isLoggedInSubject.OnNext(false); _isLoggedInSubject.OnNext(false);
} }

View File

@ -13,6 +13,6 @@ public interface IAuthenticationService : IProtectedArtemisService
Task<string?> GetBearer(); Task<string?> GetBearer();
Task<bool> AutoLogin(bool force = false); Task<bool> AutoLogin(bool force = false);
Task Login(CancellationToken cancellationToken); Task Login(CancellationToken cancellationToken);
void Logout(); Task Logout();
bool GetIsEmailVerified(); bool GetIsEmailVerified();
} }

View File

@ -14,7 +14,7 @@ public interface IWorkshopService
Task NavigateToEntry(long entryId, EntryType entryType); Task NavigateToEntry(long entryId, EntryType entryType);
List<InstalledEntry> GetInstalledEntries(); List<InstalledEntry> GetInstalledEntries();
InstalledEntry? GetInstalledEntry(IEntryDetails entry); InstalledEntry? GetInstalledEntry(long entryId);
void RemoveInstalledEntry(InstalledEntry installedEntry); void RemoveInstalledEntry(InstalledEntry installedEntry);
void SaveInstalledEntry(InstalledEntry entry); void SaveInstalledEntry(InstalledEntry entry);
void Initialize(); void Initialize();

View File

@ -152,9 +152,9 @@ public class WorkshopService : IWorkshopService
} }
/// <inheritdoc /> /// <inheritdoc />
public InstalledEntry? GetInstalledEntry(IEntryDetails entry) public InstalledEntry? GetInstalledEntry(long entryId)
{ {
EntryEntity? entity = _entryRepository.GetByEntryId(entry.Id); EntryEntity? entity = _entryRepository.GetByEntryId(entryId);
if (entity == null) if (entity == null)
return null; return null;

View File

@ -2,7 +2,7 @@ schema: schema.graphql
extensions: extensions:
endpoints: endpoints:
Default GraphQL Endpoint: Default GraphQL Endpoint:
url: https://localhost:7281/graphql url: https://workshop.artemis-rgb.com/graphql
headers: headers:
user-agent: JS GraphQL user-agent: JS GraphQL
introspect: true introspect: true

View File

@ -5,8 +5,6 @@ schema {
mutation: Mutation 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 { type Category {
icon: String! icon: String!
id: Long! id: Long!
@ -35,6 +33,7 @@ type Entry {
authorId: UUID! authorId: UUID!
categories: [Category!]! categories: [Category!]!
createdAt: DateTime! createdAt: DateTime!
dependantReleases: [Release!]!
description: String! description: String!
downloads: Long! downloads: Long!
entryType: EntryType! entryType: EntryType!
@ -46,6 +45,7 @@ type Entry {
latestReleaseId: Long latestReleaseId: Long
layoutInfo: [LayoutInfo!]! layoutInfo: [LayoutInfo!]!
name: String! name: String!
pluginInfo: PluginInfo
releases: [Release!]! releases: [Release!]!
summary: String! summary: String!
tags: [Tag!]! tags: [Tag!]!
@ -84,10 +84,33 @@ type Mutation {
updateEntryImage(input: UpdateEntryImageInput!): Image 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 { type Query {
categories(order: [CategorySortInput!], where: CategoryFilterInput): [Category!]! categories(order: [CategorySortInput!], where: CategoryFilterInput): [Category!]!
entries(order: [EntrySortInput!], search: String, skip: Int, take: Int, where: EntryFilterInput): EntriesCollectionSegment entries(order: [EntrySortInput!], search: String, skip: Int, take: Int, where: EntryFilterInput): EntriesCollectionSegment
entry(id: Long!): Entry 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!]! searchEntries(input: String!, order: [EntrySortInput!], type: EntryType, where: EntryFilterInput): [Entry!]!
searchKeyboardLayout(deviceProvider: UUID!, logicalLayout: String, model: String!, physicalLayout: KeyboardLayoutType!, vendor: String!): LayoutInfo searchKeyboardLayout(deviceProvider: UUID!, logicalLayout: String, model: String!, physicalLayout: KeyboardLayoutType!, vendor: String!): LayoutInfo
searchLayout(deviceProvider: UUID!, deviceType: RGBDeviceType!, model: String!, vendor: String!): LayoutInfo searchLayout(deviceProvider: UUID!, deviceType: RGBDeviceType!, model: String!, vendor: String!): LayoutInfo
@ -96,6 +119,7 @@ type Query {
type Release { type Release {
createdAt: DateTime! createdAt: DateTime!
dependencies: [Entry!]!
downloadSize: Long! downloadSize: Long!
downloads: Long! downloads: Long!
entry: Entry! entry: Entry!
@ -131,11 +155,18 @@ enum KeyboardLayoutType {
UNKNOWN UNKNOWN
} }
enum PluginPlatform {
LINUX
OSX
WINDOWS
}
enum RGBDeviceType { enum RGBDeviceType {
ALL ALL
COOLER COOLER
DRAM DRAM
FAN FAN
GAME_CONTROLLER
GRAPHICS_CARD GRAPHICS_CARD
HEADSET HEADSET
HEADSET_STAND HEADSET_STAND
@ -166,6 +197,11 @@ scalar Long
scalar UUID scalar UUID
input BooleanOperationFilterInput {
eq: Boolean
neq: Boolean
}
input CategoryFilterInput { input CategoryFilterInput {
and: [CategoryFilterInput!] and: [CategoryFilterInput!]
icon: StringOperationFilterInput icon: StringOperationFilterInput
@ -220,6 +256,7 @@ input EntryFilterInput {
authorId: UuidOperationFilterInput authorId: UuidOperationFilterInput
categories: ListFilterInputTypeOfCategoryFilterInput categories: ListFilterInputTypeOfCategoryFilterInput
createdAt: DateTimeOperationFilterInput createdAt: DateTimeOperationFilterInput
dependantReleases: ListFilterInputTypeOfReleaseFilterInput
description: StringOperationFilterInput description: StringOperationFilterInput
downloads: LongOperationFilterInput downloads: LongOperationFilterInput
entryType: EntryTypeOperationFilterInput entryType: EntryTypeOperationFilterInput
@ -232,6 +269,7 @@ input EntryFilterInput {
layoutInfo: ListFilterInputTypeOfLayoutInfoFilterInput layoutInfo: ListFilterInputTypeOfLayoutInfoFilterInput
name: StringOperationFilterInput name: StringOperationFilterInput
or: [EntryFilterInput!] or: [EntryFilterInput!]
pluginInfo: PluginInfoFilterInput
releases: ListFilterInputTypeOfReleaseFilterInput releases: ListFilterInputTypeOfReleaseFilterInput
summary: StringOperationFilterInput summary: StringOperationFilterInput
tags: ListFilterInputTypeOfTagFilterInput tags: ListFilterInputTypeOfTagFilterInput
@ -250,6 +288,7 @@ input EntrySortInput {
latestRelease: ReleaseSortInput latestRelease: ReleaseSortInput
latestReleaseId: SortEnumType latestReleaseId: SortEnumType
name: SortEnumType name: SortEnumType
pluginInfo: PluginInfoSortInput
summary: SortEnumType summary: SortEnumType
} }
@ -322,6 +361,13 @@ input ListFilterInputTypeOfCategoryFilterInput {
some: CategoryFilterInput some: CategoryFilterInput
} }
input ListFilterInputTypeOfEntryFilterInput {
all: EntryFilterInput
any: Boolean
none: EntryFilterInput
some: EntryFilterInput
}
input ListFilterInputTypeOfImageFilterInput { input ListFilterInputTypeOfImageFilterInput {
all: ImageFilterInput all: ImageFilterInput
any: Boolean any: Boolean
@ -372,6 +418,39 @@ input NullableOfKeyboardLayoutTypeOperationFilterInput {
nin: [KeyboardLayoutType] 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 { input RGBDeviceTypeOperationFilterInput {
eq: RGBDeviceType eq: RGBDeviceType
in: [RGBDeviceType!] in: [RGBDeviceType!]
@ -382,6 +461,7 @@ input RGBDeviceTypeOperationFilterInput {
input ReleaseFilterInput { input ReleaseFilterInput {
and: [ReleaseFilterInput!] and: [ReleaseFilterInput!]
createdAt: DateTimeOperationFilterInput createdAt: DateTimeOperationFilterInput
dependencies: ListFilterInputTypeOfEntryFilterInput
downloadSize: LongOperationFilterInput downloadSize: LongOperationFilterInput
downloads: LongOperationFilterInput downloads: LongOperationFilterInput
entry: EntryFilterInput entry: EntryFilterInput