1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 13:28:33 +00:00

Compare commits

...

6 Commits

Author SHA1 Message Date
Robert
25786c6951 Workshop - Limit home page submissions 2024-11-14 15:03:14 +01:00
Robert
d725234d56 Storage - Downgrade EF for now to latest 8.x
Core - Include backwards compatible color quantizer methods
2024-11-14 14:58:54 +01:00
Robert
37f973b093 Core - Removed scripting providers
Meta - Updated packages
2024-11-14 14:09:51 +01:00
Robert
7691af95b9 Merge remote-tracking branch 'origin/RGB.NET_v3' into development 2024-11-14 13:30:08 +01:00
5d82c159e1 Replaced the quantization code and with the HPPH implementation 2024-07-22 23:52:50 +02:00
db84f1dc75 Major upgrade of RGB.NET to v3 2024-07-22 23:26:53 +02:00
38 changed files with 91 additions and 1818 deletions

View File

@ -39,8 +39,9 @@
<PackageReference Include="DryIoc.dll" />
<PackageReference Include="EmbedIO" />
<PackageReference Include="HidSharp" />
<PackageReference Include="HPPH.SkiaSharp" />
<PackageReference Include="Humanizer.Core" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All"/>
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="McMaster.NETCore.Plugins" />
<PackageReference Include="RGB.NET.Core" />
<PackageReference Include="RGB.NET.Layout" />

View File

@ -1,167 +0,0 @@
using SkiaSharp;
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Artemis.Core.ColorScience;
internal readonly struct ColorRanges
{
public readonly byte RedRange;
public readonly byte GreenRange;
public readonly byte BlueRange;
public ColorRanges(byte redRange, byte greenRange, byte blueRange)
{
this.RedRange = redRange;
this.GreenRange = greenRange;
this.BlueRange = blueRange;
}
}
internal readonly struct ColorCube
{
private const int BYTES_PER_COLOR = 4;
private static readonly int ELEMENTS_PER_VECTOR = Vector<byte>.Count / BYTES_PER_COLOR;
private static readonly int BYTES_PER_VECTOR = ELEMENTS_PER_VECTOR * BYTES_PER_COLOR;
private readonly int _from;
private readonly int _length;
private readonly SortTarget _currentOrder = SortTarget.None;
public ColorCube(in Span<SKColor> fullColorList, int from, int length, SortTarget preOrdered)
{
this._from = from;
this._length = length;
if (length < 2) return;
Span<SKColor> colors = fullColorList.Slice(from, length);
ColorRanges colorRanges = GetColorRanges(colors);
if ((colorRanges.RedRange > colorRanges.GreenRange) && (colorRanges.RedRange > colorRanges.BlueRange))
{
if (preOrdered != SortTarget.Red)
QuantizerSort.SortRed(colors);
_currentOrder = SortTarget.Red;
}
else if (colorRanges.GreenRange > colorRanges.BlueRange)
{
if (preOrdered != SortTarget.Green)
QuantizerSort.SortGreen(colors);
_currentOrder = SortTarget.Green;
}
else
{
if (preOrdered != SortTarget.Blue)
QuantizerSort.SortBlue(colors);
_currentOrder = SortTarget.Blue;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ColorRanges GetColorRanges(in ReadOnlySpan<SKColor> colors)
{
if (Vector.IsHardwareAccelerated && (colors.Length >= Vector<byte>.Count))
{
int chunks = colors.Length / ELEMENTS_PER_VECTOR;
int vectorElements = (chunks * ELEMENTS_PER_VECTOR);
int missingElements = colors.Length - vectorElements;
Vector<byte> max = Vector<byte>.Zero;
Vector<byte> min = new(byte.MaxValue);
foreach (Vector<byte> currentVector in MemoryMarshal.Cast<SKColor, Vector<byte>>(colors[..vectorElements]))
{
max = Vector.Max(max, currentVector);
min = Vector.Min(min, currentVector);
}
byte redMin = byte.MaxValue;
byte redMax = byte.MinValue;
byte greenMin = byte.MaxValue;
byte greenMax = byte.MinValue;
byte blueMin = byte.MaxValue;
byte blueMax = byte.MinValue;
for (int i = 0; i < BYTES_PER_VECTOR; i += BYTES_PER_COLOR)
{
if (min[i + 2] < redMin) redMin = min[i + 2];
if (max[i + 2] > redMax) redMax = max[i + 2];
if (min[i + 1] < greenMin) greenMin = min[i + 1];
if (max[i + 1] > greenMax) greenMax = max[i + 1];
if (min[i] < blueMin) blueMin = min[i];
if (max[i] > blueMax) blueMax = max[i];
}
for (int i = 0; i < missingElements; i++)
{
SKColor color = colors[^(i + 1)];
if (color.Red < redMin) redMin = color.Red;
if (color.Red > redMax) redMax = color.Red;
if (color.Green < greenMin) greenMin = color.Green;
if (color.Green > greenMax) greenMax = color.Green;
if (color.Blue < blueMin) blueMin = color.Blue;
if (color.Blue > blueMax) blueMax = color.Blue;
}
return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin));
}
else
{
byte redMin = byte.MaxValue;
byte redMax = byte.MinValue;
byte greenMin = byte.MaxValue;
byte greenMax = byte.MinValue;
byte blueMin = byte.MaxValue;
byte blueMax = byte.MinValue;
foreach (SKColor color in colors)
{
if (color.Red < redMin) redMin = color.Red;
if (color.Red > redMax) redMax = color.Red;
if (color.Green < greenMin) greenMin = color.Green;
if (color.Green > greenMax) greenMax = color.Green;
if (color.Blue < blueMin) blueMin = color.Blue;
if (color.Blue > blueMax) blueMax = color.Blue;
}
return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Split(in Span<SKColor> fullColorList, out ColorCube a, out ColorCube b)
{
Span<SKColor> colors = fullColorList.Slice(_from, _length);
int median = colors.Length / 2;
a = new ColorCube(fullColorList, _from, median, _currentOrder);
b = new ColorCube(fullColorList, _from + median, colors.Length - median, _currentOrder);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal SKColor GetAverageColor(in ReadOnlySpan<SKColor> fullColorList)
{
ReadOnlySpan<SKColor> colors = fullColorList.Slice(_from, _length);
int r = 0, g = 0, b = 0;
foreach (SKColor color in colors)
{
r += color.Red;
g += color.Green;
b += color.Blue;
}
return new SKColor(
(byte)(r / colors.Length),
(byte)(g / colors.Length),
(byte)(b / colors.Length)
);
}
}

View File

@ -1,7 +1,9 @@
using SkiaSharp;
using HPPH;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
namespace Artemis.Core.ColorScience;
@ -10,13 +12,27 @@ namespace Artemis.Core.ColorScience;
/// </summary>
public static class ColorQuantizer
{
/// <inheritdoc cref="Quantize(Span{SKColor}, int)"/>
[Obsolete("Use Quantize(Span<SKColor> colors, int amount) in-parameter instead")]
public static SKColor[] Quantize(in Span<SKColor> colors, int amount)
{
return Quantize(colors, amount);
}
/// <inheritdoc cref="QuantizeSplit(Span{SKColor}, int)"/>
[Obsolete("Use QuantizeSplit(Span<SKColor> colors, int splits) without the in-parameter instead")]
public static SKColor[] QuantizeSplit(in Span<SKColor> colors, int splits)
{
return QuantizeSplit(colors, splits);
}
/// <summary>
/// Quantizes a span of colors into the desired amount of representative colors.
/// </summary>
/// <param name="colors">The colors to quantize</param>
/// <param name="amount">How many colors to return. Must be a power of two.</param>
/// <returns><paramref name="amount"/> colors.</returns>
public static SKColor[] Quantize(in Span<SKColor> colors, int amount)
public static SKColor[] Quantize(Span<SKColor> colors, int amount)
{
if (!BitOperations.IsPow2(amount))
throw new ArgumentException("Must be power of two", nameof(amount));
@ -24,38 +40,19 @@ public static class ColorQuantizer
int splits = BitOperations.Log2((uint)amount);
return QuantizeSplit(colors, splits);
}
/// <summary>
/// Quantizes a span of colors, splitting the average <paramref name="splits"/> number of times.
/// </summary>
/// <param name="colors">The colors to quantize</param>
/// <param name="splits">How many splits to execute. Each split doubles the number of colors returned.</param>
/// <returns>Up to (2 ^ <paramref name="splits"/>) number of colors.</returns>
public static SKColor[] QuantizeSplit(in Span<SKColor> colors, int splits)
public static SKColor[] QuantizeSplit(Span<SKColor> colors, int splits)
{
if (colors.Length < (1 << splits)) throw new ArgumentException($"The color array must at least contain ({(1 << splits)}) to perform {splits} splits.");
Span<ColorCube> cubes = new ColorCube[1 << splits];
cubes[0] = new ColorCube(colors, 0, colors.Length, SortTarget.None);
int currentIndex = 0;
for (int i = 0; i < splits; i++)
{
int currentCubeCount = 1 << i;
Span<ColorCube> currentCubes = cubes.Slice(0, currentCubeCount);
for (int j = 0; j < currentCubes.Length; j++)
{
currentCubes[j].Split(colors, out ColorCube a, out ColorCube b);
currentCubes[j] = a;
cubes[++currentIndex] = b;
}
}
SKColor[] result = new SKColor[cubes.Length];
for (int i = 0; i < cubes.Length; i++)
result[i] = cubes[i].GetAverageColor(colors);
return result;
// DarthAffe 22.07.2024: This is not ideal as it allocates an additional array, but i don't see a way to get SKColors out here
return MemoryMarshal.Cast<ColorBGRA, SKColor>(MemoryMarshal.Cast<SKColor, ColorBGRA>(colors).CreateSimpleColorPalette(1 << splits)).ToArray();
}
/// <summary>

View File

@ -1,121 +0,0 @@
using SkiaSharp;
using System;
using System.Buffers;
namespace Artemis.Core.ColorScience;
//HACK DarthAffe 17.11.2022: Sorting is a really hot path in the quantizer, therefore abstracting this into cleaner code (one method with parameter or something like that) sadly has a well measurable performance impact.
internal static class QuantizerSort
{
#region Methods
public static void SortRed(in Span<SKColor> colors)
{
Span<int> counts = stackalloc int[256];
foreach (SKColor t in colors)
counts[t.Red]++;
SKColor[] bucketsArray = ArrayPool<SKColor>.Shared.Rent(colors.Length);
try
{
Span<SKColor> buckets = bucketsArray.AsSpan().Slice(0, colors.Length);
Span<int> currentBucketIndex = stackalloc int[256];
int offset = 0;
for (int i = 0; i < counts.Length; i++)
{
currentBucketIndex[i] = offset;
offset += counts[i];
}
foreach (SKColor color in colors)
{
int index = color.Red;
int bucketIndex = currentBucketIndex[index];
currentBucketIndex[index]++;
buckets[bucketIndex] = color;
}
buckets.CopyTo(colors);
}
finally
{
ArrayPool<SKColor>.Shared.Return(bucketsArray);
}
}
public static void SortGreen(in Span<SKColor> colors)
{
Span<int> counts = stackalloc int[256];
foreach (SKColor t in colors)
counts[t.Green]++;
SKColor[] bucketsArray = ArrayPool<SKColor>.Shared.Rent(colors.Length);
try
{
Span<SKColor> buckets = bucketsArray.AsSpan().Slice(0, colors.Length);
Span<int> currentBucketIndex = stackalloc int[256];
int offset = 0;
for (int i = 0; i < counts.Length; i++)
{
currentBucketIndex[i] = offset;
offset += counts[i];
}
foreach (SKColor color in colors)
{
int index = color.Green;
int bucketIndex = currentBucketIndex[index];
currentBucketIndex[index]++;
buckets[bucketIndex] = color;
}
buckets.CopyTo(colors);
}
finally
{
ArrayPool<SKColor>.Shared.Return(bucketsArray);
}
}
public static void SortBlue(in Span<SKColor> colors)
{
Span<int> counts = stackalloc int[256];
foreach (SKColor t in colors)
counts[t.Blue]++;
SKColor[] bucketsArray = ArrayPool<SKColor>.Shared.Rent(colors.Length);
try
{
Span<SKColor> buckets = bucketsArray.AsSpan().Slice(0, colors.Length);
Span<int> currentBucketIndex = stackalloc int[256];
int offset = 0;
for (int i = 0; i < counts.Length; i++)
{
currentBucketIndex[i] = offset;
offset += counts[i];
}
foreach (SKColor color in colors)
{
int index = color.Blue;
int bucketIndex = currentBucketIndex[index];
currentBucketIndex[index]++;
buckets[bucketIndex] = color;
}
buckets.CopyTo(colors);
}
finally
{
ArrayPool<SKColor>.Shared.Return(bucketsArray);
}
}
#endregion
}

View File

@ -1,6 +0,0 @@
namespace Artemis.Core.ColorScience;
internal enum SortTarget
{
None, Red, Green, Blue
}

View File

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.ScriptingProviders;
using Artemis.Storage.Entities.Profile;
using SkiaSharp;
@ -14,15 +12,10 @@ namespace Artemis.Core;
public sealed class Profile : ProfileElement
{
private readonly object _lock = new();
private readonly ObservableCollection<ScriptConfiguration> _scriptConfigurations;
private readonly ObservableCollection<ProfileScript> _scripts;
private bool _isFreshImport;
internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
{
_scripts = new ObservableCollection<ProfileScript>();
_scriptConfigurations = new ObservableCollection<ScriptConfiguration>();
Opacity = 0d;
ShouldDisplay = true;
Configuration = configuration;
@ -31,8 +24,6 @@ public sealed class Profile : ProfileElement
EntityId = profileEntity.Id;
Exceptions = new List<Exception>();
Scripts = new ReadOnlyObservableCollection<ProfileScript>(_scripts);
ScriptConfigurations = new ReadOnlyObservableCollection<ScriptConfiguration>(_scriptConfigurations);
Load();
}
@ -41,17 +32,7 @@ public sealed class Profile : ProfileElement
/// Gets the profile configuration of this profile
/// </summary>
public ProfileConfiguration Configuration { get; }
/// <summary>
/// Gets a collection of all active scripts assigned to this profile
/// </summary>
public ReadOnlyObservableCollection<ProfileScript> Scripts { get; }
/// <summary>
/// Gets a collection of all script configurations assigned to this profile
/// </summary>
public ReadOnlyObservableCollection<ScriptConfiguration> ScriptConfigurations { get; }
/// <summary>
/// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it
/// since import
@ -85,15 +66,9 @@ public sealed class Profile : ProfileElement
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdating(deltaTime);
foreach (ProfileElement profileElement in Children)
profileElement.Update(deltaTime);
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdated(deltaTime);
const double OPACITY_PER_SECOND = 1;
if (ShouldDisplay && Opacity < 1)
@ -111,9 +86,6 @@ public sealed class Profile : ProfileElement
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds);
SKPaint? opacityPaint = null;
bool applyOpacityLayer = Configuration.FadeInAndOut && Opacity < 1;
@ -133,9 +105,6 @@ public sealed class Profile : ProfileElement
opacityPaint?.Dispose();
}
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds);
if (!Exceptions.Any())
return;
@ -174,7 +143,7 @@ public sealed class Profile : ProfileElement
/// <inheritdoc />
public override IEnumerable<PluginFeature> GetFeatureDependencies()
{
return GetRootFolder().GetFeatureDependencies().Concat(Scripts.Select(c => c.ScriptingProvider));
return GetRootFolder().GetFeatureDependencies();
}
/// <summary>
@ -205,10 +174,7 @@ public sealed class Profile : ProfileElement
{
if (!disposing)
return;
while (Scripts.Count > 0)
RemoveScript(Scripts[0]);
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
ChildrenList.Clear();
@ -238,61 +204,11 @@ public sealed class Profile : ProfileElement
AddChild(new Folder(this, this, rootFolder));
}
while (_scriptConfigurations.Any())
RemoveScriptConfiguration(_scriptConfigurations[0]);
foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)))
AddScriptConfiguration(scriptConfiguration);
// Load node scripts last since they may rely on the profile structure being in place
foreach (RenderProfileElement renderProfileElement in GetAllRenderElements())
renderProfileElement.LoadNodeScript();
}
/// <summary>
/// Removes a script configuration from the profile, if the configuration has an active script it is also removed.
/// </summary>
internal void RemoveScriptConfiguration(ScriptConfiguration scriptConfiguration)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
return;
Script? script = scriptConfiguration.Script;
if (script != null)
RemoveScript((ProfileScript) script);
_scriptConfigurations.Remove(scriptConfiguration);
}
/// <summary>
/// Adds a script configuration to the profile but does not instantiate it's script.
/// </summary>
internal void AddScriptConfiguration(ScriptConfiguration scriptConfiguration)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
_scriptConfigurations.Add(scriptConfiguration);
}
/// <summary>
/// Adds a script that has a script configuration belonging to this profile.
/// </summary>
internal void AddScript(ProfileScript script)
{
if (!_scriptConfigurations.Contains(script.ScriptConfiguration))
throw new ArtemisCoreException("Cannot add a script to a profile whose script configuration doesn't belong to the same profile.");
if (!_scripts.Contains(script))
_scripts.Add(script);
}
/// <summary>
/// Removes a script from the profile and disposes it.
/// </summary>
internal void RemoveScript(ProfileScript script)
{
_scripts.Remove(script);
script.Dispose();
}
internal override void Save()
{
if (Disposed)
@ -310,12 +226,5 @@ public sealed class Profile : ProfileElement
ProfileEntity.Layers.Clear();
ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity));
ProfileEntity.ScriptConfigurations.Clear();
foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
{
scriptConfiguration.Save();
ProfileEntity.ScriptConfigurations.Add(scriptConfiguration.Entity);
}
}
}

View File

@ -1,23 +0,0 @@
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Represents a view model containing a script editor
/// </summary>
public interface IScriptEditorViewModel
{
/// <summary>
/// Gets the script type this view model was created for
/// </summary>
ScriptType ScriptType { get; }
/// <summary>
/// Gets the script this editor is editing
/// </summary>
Script? Script { get; }
/// <summary>
/// Called whenever the view model must display a different script
/// </summary>
/// <param name="script">The script to display or <see langword="null" /> if no script is to be displayed</param>
void ChangeScript(Script? script);
}

View File

@ -1,163 +0,0 @@
using System;
using Artemis.Storage.Entities.General;
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Represents the configuration of a script
/// </summary>
public class ScriptConfiguration : CorePropertyChanged, IStorageModel
{
private bool _hasChanges;
private bool _isSuspended;
private string _name;
private string? _pendingScriptContent;
private string? _scriptContent;
private string _scriptingProviderId;
/// <summary>
/// Creates a new instance of the <see cref="ScriptConfiguration" /> class
/// </summary>
public ScriptConfiguration(ScriptingProvider provider, string name, ScriptType scriptType)
{
_scriptingProviderId = provider.Id;
_name = name;
Entity = new ScriptConfigurationEntity();
PendingScriptContent = provider.GetDefaultScriptContent(scriptType);
ScriptContent = PendingScriptContent;
}
internal ScriptConfiguration(ScriptConfigurationEntity entity)
{
_scriptingProviderId = null!;
_name = null!;
Entity = entity;
Load();
}
/// <summary>
/// Gets or sets the ID of the scripting provider
/// </summary>
public string ScriptingProviderId
{
get => _scriptingProviderId;
set => SetAndNotify(ref _scriptingProviderId, value);
}
/// <summary>
/// Gets or sets the name of the script
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// Gets or sets the script's content
/// </summary>
public string? ScriptContent
{
get => _scriptContent;
private set
{
if (!SetAndNotify(ref _scriptContent, value)) return;
OnScriptContentChanged();
}
}
/// <summary>
/// Gets or sets the pending changes to the script's content
/// </summary>
public string? PendingScriptContent
{
get => _pendingScriptContent;
set
{
if (string.IsNullOrWhiteSpace(value))
value = null;
if (!SetAndNotify(ref _pendingScriptContent, value)) return;
HasChanges = ScriptContent != PendingScriptContent;
}
}
// TODO: Implement suspension
/// <summary>
/// [NYI] Gets or sets a boolean indicating whether this configuration is suspended
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this configuration has pending changes to it's
/// <see cref="ScriptContent" />
/// </summary>
public bool HasChanges
{
get => _hasChanges;
set => SetAndNotify(ref _hasChanges, value);
}
/// <summary>
/// If active, gets the script
/// </summary>
public Script? Script { get; internal set; }
internal ScriptConfigurationEntity Entity { get; }
/// <summary>
/// Applies the <see cref="PendingScriptContent" /> to the <see cref="ScriptContent" />
/// </summary>
public void ApplyPendingChanges()
{
ScriptContent = PendingScriptContent;
HasChanges = false;
}
/// <summary>
/// Discards the <see cref="PendingScriptContent" />
/// </summary>
public void DiscardPendingChanges()
{
PendingScriptContent = ScriptContent;
HasChanges = false;
}
/// <summary>
/// Occurs whenever the contents of the script have changed
/// </summary>
public event EventHandler? ScriptContentChanged;
/// <summary>
/// Invokes the <see cref="ScriptContentChanged" /> event
/// </summary>
protected virtual void OnScriptContentChanged()
{
ScriptContentChanged?.Invoke(this, EventArgs.Empty);
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
ScriptingProviderId = Entity.ScriptingProviderId;
ScriptContent = Entity.ScriptContent;
PendingScriptContent = Entity.ScriptContent;
Name = Entity.Name;
}
/// <inheritdoc />
public void Save()
{
Entity.ScriptingProviderId = ScriptingProviderId;
Entity.ScriptContent = ScriptContent;
Entity.Name = Name;
}
#endregion
}

View File

@ -1,80 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Allows you to implement and register your own scripting provider.
/// </summary>
public abstract class ScriptingProvider<TGlobalScript, TProfileScript> : ScriptingProvider
where TGlobalScript : GlobalScript
where TProfileScript : ProfileScript
{
#region Overrides of PluginFeature
/// <inheritdoc />
internal override void InternalDisable()
{
base.InternalDisable();
while (Scripts.Count > 0)
Scripts[0].Dispose();
}
#endregion
#region Overrides of ScriptingProvider
/// <inheritdoc />
internal override Type ProfileScriptType => typeof(TProfileScript);
/// <inheritdoc />
internal override Type GlobalScriptType => typeof(TGlobalScript);
#endregion
}
/// <summary>
/// Allows you to implement and register your own scripting provider.
/// <para>
/// Note: You can't implement this, implement
/// <see cref="ScriptingProvider{TGlobalScript, TProfileScript}" /> instead.
/// </para>
/// </summary>
public abstract class ScriptingProvider : PluginFeature
{
/// <summary>
/// The base constructor of the <see cref="ScriptingProvider" /> class
/// </summary>
protected ScriptingProvider()
{
Scripts = new ReadOnlyCollection<Script>(InternalScripts);
}
/// <summary>
/// Gets the name of the scripting language this provider provides
/// </summary>
public abstract string LanguageName { get; }
/// <summary>
/// Gets a list of all active scripts belonging to this scripting provider
/// </summary>
public ReadOnlyCollection<Script> Scripts { get; }
internal abstract Type GlobalScriptType { get; }
internal abstract Type ProfileScriptType { get; }
internal List<Script> InternalScripts { get; } = new();
/// <summary>
/// Called when the UI needs a script editor for the specified <paramref name="scriptType" />
/// </summary>
/// <param name="scriptType">The type of script the editor will host</param>
public abstract IScriptEditorViewModel CreateScriptEditor(ScriptType scriptType);
/// <summary>
/// Called when a script for a certain type needs default content.
/// </summary>
/// <param name="scriptType">The type of script the default content is for.</param>
public abstract string GetDefaultScriptContent(ScriptType scriptType);
}

View File

@ -1,45 +0,0 @@
using Artemis.Core.Services;
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Represents a script running globally
/// </summary>
public abstract class GlobalScript : Script
{
/// <inheritdoc />
protected GlobalScript(ScriptConfiguration configuration) : base(configuration)
{
}
internal ScriptingService? ScriptingService { get; set; }
/// <summary>
/// Called whenever the Artemis Core is about to update
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnCoreUpdating(double deltaTime)
{
}
/// <summary>
/// Called whenever the Artemis Core has been updated
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnCoreUpdated(double deltaTime)
{
}
#region Overrides of Script
/// <inheritdoc />
public override ScriptType ScriptType => ScriptType.Global;
/// <inheritdoc />
internal override void InternalCleanup()
{
ScriptingService?.RemoveScript(ScriptConfiguration);
}
#endregion
}

View File

@ -1,67 +0,0 @@
using SkiaSharp;
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Represents a script bound to a specific <see cref="Profile" /> processed by a <see cref="ScriptingProvider" />.
/// </summary>
public abstract class ProfileScript : Script
{
/// <inheritdoc />
protected ProfileScript(Profile profile, ScriptConfiguration configuration) : base(configuration)
{
Profile = profile;
}
/// <summary>
/// Gets the profile this script is bound to
/// </summary>
public Profile Profile { get; }
/// <summary>
/// Called whenever the profile is about to update
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnProfileUpdating(double deltaTime)
{
}
/// <summary>
/// Called whenever the profile has been updated
/// </summary>
/// <param name="deltaTime">Seconds passed since last update</param>
public virtual void OnProfileUpdated(double deltaTime)
{
}
/// <summary>
/// Called whenever the profile is about to render
/// </summary>
/// <param name="canvas">The profile canvas</param>
/// <param name="bounds">The area to be filled, covers the entire canvas</param>
public virtual void OnProfileRendering(SKCanvas canvas, SKRect bounds)
{
}
/// <summary>
/// Called whenever the profile has been rendered
/// </summary>
/// <param name="canvas">The profile canvas</param>
/// <param name="bounds">The area to be filled, covers the entire canvas</param>
public virtual void OnProfileRendered(SKCanvas canvas, SKRect bounds)
{
}
#region Overrides of Script
/// <inheritdoc />
public override ScriptType ScriptType => ScriptType.Profile;
/// <inheritdoc />
internal override void InternalCleanup()
{
Profile.RemoveScript(this);
}
#endregion
}

View File

@ -1,114 +0,0 @@
using System;
using System.ComponentModel;
namespace Artemis.Core.ScriptingProviders;
/// <summary>
/// Represents a script processed by a <see cref="ScriptingProviders.ScriptingProvider" />.
/// </summary>
public abstract class Script : CorePropertyChanged, IDisposable
{
private bool _disposed;
private ScriptingProvider _scriptingProvider = null!;
/// <summary>
/// The base constructor of any script
/// </summary>
/// <param name="configuration">The script configuration this script belongs to</param>
protected Script(ScriptConfiguration configuration)
{
if (configuration.Script != null)
throw new ArtemisCoreException("The provided script configuration already has an active script");
ScriptConfiguration = configuration;
ScriptConfiguration.PropertyChanged += ScriptConfigurationOnPropertyChanged;
}
/// <summary>
/// Gets the scripting provider this script belongs to
/// </summary>
public ScriptingProvider ScriptingProvider
{
get => _scriptingProvider;
internal set => SetAndNotify(ref _scriptingProvider, value);
}
/// <summary>
/// Gets the script configuration this script belongs to
/// </summary>
public ScriptConfiguration ScriptConfiguration { get; }
/// <summary>
/// Gets the script type of this script
/// </summary>
public abstract ScriptType ScriptType { get; }
/// <summary>
/// Occurs when the contents of the script have changed
/// </summary>
public event EventHandler? ScriptContentChanged;
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
}
}
/// <summary>
/// Invokes the <see cref="ScriptContentChanged" /> event
/// </summary>
protected virtual void OnScriptContentChanged()
{
ScriptContentChanged?.Invoke(this, EventArgs.Empty);
}
internal abstract void InternalCleanup();
private void ScriptConfigurationOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ScriptConfiguration.ScriptContent))
OnScriptContentChanged();
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
ScriptConfiguration.PropertyChanged -= ScriptConfigurationOnPropertyChanged;
ScriptConfiguration.Script = null;
ScriptingProvider.InternalScripts.Remove(this);
// Can't trust those pesky plugin devs!
InternalCleanup();
Dispose(true);
GC.SuppressFinalize(this);
}
}
/// <summary>
/// Represents a type of script
/// </summary>
public enum ScriptType
{
/// <summary>
/// A global script that's always active
/// </summary>
Global,
/// <summary>
/// A script tied to a <see cref="Profile" />
/// </summary>
Profile
}

View File

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Artemis.Core.SkiaSharp;
using HPPH;
using HPPH.SkiaSharp;
using RGB.NET.Core;
using RGB.NET.Presets.Textures.Sampler;
using RGB.NET.Presets.Extensions;
using SkiaSharp;
namespace Artemis.Core;
@ -12,35 +12,29 @@ namespace Artemis.Core;
/// <summary>
/// Represents a SkiaSharp-based RGB.NET PixelTexture
/// </summary>
public sealed class SKTexture : PixelTexture<byte>, IDisposable
public sealed class SKTexture : ITexture, IDisposable
{
private readonly Dictionary<Led, SKRectI> _ledRects;
private readonly SKPixmap _pixelData;
private readonly IntPtr _pixelDataPtr;
#region Constructors
internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int height, float scale, IReadOnlyCollection<ArtemisDevice> devices) : base(width, height, DATA_PER_PIXEL,
new AverageByteSampler())
internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int height, float scale, IReadOnlyCollection<ArtemisDevice> devices)
{
RenderScale = scale;
Size = new Size(width, height);
ImageInfo = new SKImageInfo(width, height);
Surface = graphicsContext == null
? SKSurface.Create(ImageInfo)
: SKSurface.Create(graphicsContext.GraphicsContext, true, ImageInfo);
RenderScale = scale;
_pixelDataPtr = Marshal.AllocHGlobal(ImageInfo.BytesSize);
_pixelData = new SKPixmap(ImageInfo, _pixelDataPtr, ImageInfo.RowBytes);
_ledRects = new Dictionary<Led, SKRectI>();
foreach (ArtemisDevice artemisDevice in devices)
{
foreach (ArtemisLed artemisLed in artemisDevice.Leds)
{
_ledRects[artemisLed.RgbLed] = SKRectI.Create(
(int) (artemisLed.AbsoluteRectangle.Left * RenderScale),
(int) (artemisLed.AbsoluteRectangle.Top * RenderScale),
(int) (artemisLed.AbsoluteRectangle.Width * RenderScale),
(int) (artemisLed.AbsoluteRectangle.Height * RenderScale)
(int)(artemisLed.AbsoluteRectangle.Left * RenderScale),
(int)(artemisLed.AbsoluteRectangle.Top * RenderScale),
(int)(artemisLed.AbsoluteRectangle.Width * RenderScale),
(int)(artemisLed.AbsoluteRectangle.Height * RenderScale)
);
}
}
@ -50,47 +44,29 @@ public sealed class SKTexture : PixelTexture<byte>, IDisposable
internal Color GetColorAtRenderTarget(in RenderTarget renderTarget)
{
if (Data.Length == 0) return Color.Transparent;
if (_image == null) return Color.Transparent;
SKRectI skRectI = _ledRects[renderTarget.Led];
if (skRectI.Width <= 0 || skRectI.Height <= 0)
return Color.Transparent;
SamplerInfo<byte> samplerInfo = new(skRectI.Left, skRectI.Top, skRectI.Width, skRectI.Height, Stride, DataPerPixel, Data);
Span<byte> pixelData = stackalloc byte[DATA_PER_PIXEL];
Sampler.Sample(samplerInfo, pixelData);
return GetColor(pixelData);
}
private void ReleaseUnmanagedResources()
{
Marshal.FreeHGlobal(_pixelDataPtr);
return _image[skRectI.Left, skRectI.Top, skRectI.Width, skRectI.Height].Average().ToColor();
}
/// <inheritdoc />
~SKTexture()
{
ReleaseUnmanagedResources();
Dispose();
}
/// <inheritdoc />
public void Dispose()
{
Surface.Dispose();
_pixelData.Dispose();
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
#region Constants
private const int DATA_PER_PIXEL = 4;
#endregion
#region Methods
/// <summary>
@ -104,20 +80,23 @@ public sealed class SKTexture : PixelTexture<byte>, IDisposable
internal void CopyPixelData()
{
using SKImage skImage = Surface.Snapshot();
skImage.ReadPixels(_pixelData);
_image = skImage.ToImage();
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override Color GetColor(in ReadOnlySpan<byte> pixel) => new(pixel[2], pixel[1], pixel[0]);
/// <inheritdoc />
public override Color this[in Rectangle rectangle] => Color.Transparent;
#endregion
#region Properties & Fields
private IImage? _image;
private readonly Dictionary<Led, SKRectI> _ledRects = [];
/// <inheritdoc />
public Size Size { get; }
/// <inheritdoc />
public Color this[Point point] => Color.Transparent;
/// <inheritdoc />
public Color this[Rectangle rectangle] => Color.Transparent;
/// <summary>
/// Gets the SKBitmap backing this texture
/// </summary>
@ -128,11 +107,6 @@ public sealed class SKTexture : PixelTexture<byte>, IDisposable
/// </summary>
public SKImageInfo ImageInfo { get; }
/// <summary>
/// Gets the color data in RGB format
/// </summary>
protected override ReadOnlySpan<byte> Data => _pixelData.GetPixelSpan();
/// <summary>
/// Gets the render scale of the texture
/// </summary>

View File

@ -20,7 +20,7 @@ internal class SKTextureBrush : AbstractBrush
#region Methods
/// <inheritdoc />
protected override Color GetColorAtPoint(in Rectangle rectangle, in RenderTarget renderTarget)
protected override Color GetColorAtPoint(Rectangle rectangle, RenderTarget renderTarget)
{
return Texture?.GetColorAtRenderTarget(renderTarget) ?? Color.Transparent;
}

View File

@ -1,4 +1,3 @@
using Artemis.Core.ScriptingProviders;
using Artemis.Core.Services.Core;
using SkiaSharp;
@ -7,22 +6,17 @@ namespace Artemis.Core.Services;
internal class CoreRenderer : IRenderer
{
private readonly IModuleService _moduleService;
private readonly IScriptingService _scriptingService;
private readonly IProfileService _profileService;
public CoreRenderer(IModuleService moduleService, IScriptingService scriptingService, IProfileService profileService)
public CoreRenderer(IModuleService moduleService, IProfileService profileService)
{
_moduleService = moduleService;
_scriptingService = scriptingService;
_profileService = profileService;
}
/// <inheritdoc />
public void Render(SKCanvas canvas, double delta)
{
foreach (GlobalScript scriptingServiceGlobalScript in _scriptingService.GlobalScripts)
scriptingServiceGlobalScript.OnCoreUpdating(delta);
_moduleService.UpdateActiveModules(delta);
if (!_profileService.ProfileRenderingDisabled)
@ -30,9 +24,6 @@ internal class CoreRenderer : IRenderer
_profileService.UpdateProfiles(delta);
_profileService.RenderProfiles(canvas);
}
foreach (GlobalScript scriptingServiceGlobalScript in _scriptingService.GlobalScripts)
scriptingServiceGlobalScript.OnCoreUpdated(delta);
}
/// <inheritdoc />

View File

@ -1,17 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Artemis.Core.DryIoc.Factories;
using Artemis.Core.ScriptingProviders;
using Artemis.Storage;
using DryIoc;
using HidSharp;
using RGB.NET.Core;
using Serilog;
using Serilog.Events;
using SkiaSharp;
namespace Artemis.Core.Services;
@ -33,7 +28,6 @@ internal class CoreService : ICoreService
IPluginManagementService pluginManagementService,
IProfileService profileService,
IModuleService moduleService,
IScriptingService scriptingService,
IRenderService renderService)
{
Constants.CorePlugin.Container = container;

View File

@ -1,46 +0,0 @@
using System.Collections.ObjectModel;
using Artemis.Core.ScriptingProviders;
namespace Artemis.Core.Services;
/// <summary>
/// A service that allows you to manage various types of <see cref="NodeScript" /> instances
/// </summary>
public interface IScriptingService : IArtemisService
{
/// <summary>
/// Gets a list of all available scripting providers
/// </summary>
ReadOnlyCollection<ScriptingProvider> ScriptingProviders { get; }
/// <summary>
/// Gets a list of all currently active global scripts
/// </summary>
ReadOnlyCollection<GlobalScript> GlobalScripts { get; }
/// <summary>
/// Adds a script by the provided script configuration to the provided profile and instantiates it.
/// </summary>
/// <param name="scriptConfiguration">The script configuration whose script to add.</param>
/// <param name="profile">The profile to add the script to.</param>
ProfileScript AddScript(ScriptConfiguration scriptConfiguration, Profile profile);
/// <summary>
/// Removes a script by the provided script configuration from the provided profile and disposes it.
/// </summary>
/// <param name="scriptConfiguration">The script configuration whose script to remove.</param>
/// <param name="profile">The profile to remove the script from.</param>
void RemoveScript(ScriptConfiguration scriptConfiguration, Profile profile);
/// <summary>
/// Adds a script by the provided script configuration to the global collection and instantiates it.
/// </summary>
/// <param name="scriptConfiguration">The script configuration whose script to add.</param>
GlobalScript AddScript(ScriptConfiguration scriptConfiguration);
/// <summary>
/// Removes a script by the provided script configuration from the global collection and disposes it.
/// </summary>
/// <param name="scriptConfiguration">The script configuration whose script to remove.</param>
void RemoveScript(ScriptConfiguration scriptConfiguration);
}

View File

@ -1,165 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.ScriptingProviders;
namespace Artemis.Core.Services;
internal class ScriptingService : IScriptingService
{
private readonly List<GlobalScript> _globalScripts;
private readonly IPluginManagementService _pluginManagementService;
private readonly IProfileService _profileService;
private readonly List<ScriptingProvider> _scriptingProviders;
public ScriptingService(IPluginManagementService pluginManagementService, IProfileService profileService)
{
_pluginManagementService = pluginManagementService;
_profileService = profileService;
_pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled;
_pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureToggled;
_scriptingProviders = _pluginManagementService.GetFeaturesOfType<ScriptingProvider>();
_globalScripts = new List<GlobalScript>();
ScriptingProviders = new ReadOnlyCollection<ScriptingProvider>(_scriptingProviders);
GlobalScripts = new ReadOnlyCollection<GlobalScript>(_globalScripts);
// No need to sub to Deactivated, scripts will deactivate themselves
profileService.ProfileActivated += ProfileServiceOnProfileActivated;
foreach (ProfileCategory profileCategory in _profileService.ProfileCategories)
{
foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations)
{
if (profileConfiguration.Profile != null)
InitializeProfileScripts(profileConfiguration.Profile);
}
}
}
private GlobalScript CreateScriptInstance(ScriptConfiguration scriptConfiguration)
{
GlobalScript? script = null;
try
{
if (scriptConfiguration.Script != null)
throw new ArtemisCoreException("The provided script configuration already has an active script");
ScriptingProvider? provider = _scriptingProviders.FirstOrDefault(p => p.Id == scriptConfiguration.ScriptingProviderId);
if (provider == null)
throw new ArtemisCoreException($"Can't create script instance as there is no matching scripting provider found for the script ({scriptConfiguration.ScriptingProviderId}).");
script = (GlobalScript) provider.Plugin.Resolve(provider.GlobalScriptType, scriptConfiguration);
script.ScriptingProvider = provider;
script.ScriptingService = this;
scriptConfiguration.Script = script;
provider.InternalScripts.Add(script);
return script;
}
catch (Exception e)
{
script?.Dispose();
throw new ArtemisCoreException("Failed to initialize global script", e);
}
}
private ProfileScript CreateScriptInstance(ScriptConfiguration scriptConfiguration, Profile profile)
{
ProfileScript? script = null;
try
{
if (scriptConfiguration.Script != null)
throw new ArtemisCoreException("The provided script configuration already has an active script");
ScriptingProvider? provider = _scriptingProviders.FirstOrDefault(p => p.Id == scriptConfiguration.ScriptingProviderId);
if (provider == null)
throw new ArtemisCoreException($"Can't create script instance as there is no matching scripting provider found for the script ({scriptConfiguration.ScriptingProviderId}).");
script = (ProfileScript) provider.Plugin.Resolve(provider.ProfileScriptType, profile, scriptConfiguration);
script.ScriptingProvider = provider;
scriptConfiguration.Script = script;
provider.InternalScripts.Add(script);
lock (profile)
{
profile.AddScript(script);
}
return script;
}
catch (Exception e)
{
// If something went wrong but the script was created, clean up as best we can
if (script != null)
{
if (profile.Scripts.Contains(script))
profile.RemoveScript(script);
else
script.Dispose();
}
throw new ArtemisCoreException("Failed to initialize profile script", e);
}
}
private void InitializeProfileScripts(Profile profile)
{
// Initialize the scripts on the profile
foreach (ScriptConfiguration scriptConfiguration in profile.ScriptConfigurations.Where(c => c.Script == null && _scriptingProviders.Any(p => p.Id == c.ScriptingProviderId)))
CreateScriptInstance(scriptConfiguration, profile);
}
private void PluginManagementServiceOnPluginFeatureToggled(object? sender, PluginFeatureEventArgs e)
{
_scriptingProviders.Clear();
_scriptingProviders.AddRange(_pluginManagementService.GetFeaturesOfType<ScriptingProvider>());
foreach (ProfileCategory profileCategory in _profileService.ProfileCategories)
{
foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations)
{
if (profileConfiguration.Profile != null)
InitializeProfileScripts(profileConfiguration.Profile);
}
}
}
private void ProfileServiceOnProfileActivated(object? sender, ProfileConfigurationEventArgs e)
{
if (e.ProfileConfiguration.Profile != null)
InitializeProfileScripts(e.ProfileConfiguration.Profile);
}
/// <inheritdoc />
public ReadOnlyCollection<ScriptingProvider> ScriptingProviders { get; }
/// <inheritdoc />
public ReadOnlyCollection<GlobalScript> GlobalScripts { get; }
/// <inheritdoc />
public ProfileScript AddScript(ScriptConfiguration scriptConfiguration, Profile profile)
{
profile.AddScriptConfiguration(scriptConfiguration);
return CreateScriptInstance(scriptConfiguration, profile);
}
/// <inheritdoc />
public void RemoveScript(ScriptConfiguration scriptConfiguration, Profile profile)
{
profile.RemoveScriptConfiguration(scriptConfiguration);
}
/// <inheritdoc />
public GlobalScript AddScript(ScriptConfiguration scriptConfiguration)
{
throw new NotImplementedException("Global scripts are not yet implemented.");
}
/// <inheritdoc />
public void RemoveScript(ScriptConfiguration scriptConfiguration)
{
throw new NotImplementedException("Global scripts are not yet implemented.");
}
}

View File

@ -1,12 +0,0 @@
using System;
namespace Artemis.Storage.Entities.General;
public class ScriptConfigurationEntity
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string ScriptingProviderId { get; set; } = string.Empty;
public string? ScriptContent { get; set; }
}

View File

@ -11,7 +11,6 @@ public class ProfileEntity
{
Folders = new List<FolderEntity>();
Layers = new List<LayerEntity>();
ScriptConfigurations = new List<ScriptConfigurationEntity>();
}
public Guid Id { get; set; }
@ -21,7 +20,6 @@ public class ProfileEntity
public List<FolderEntity> Folders { get; set; }
public List<LayerEntity> Layers { get; set; }
public List<ScriptConfigurationEntity> ScriptConfigurations { get; set; }
public void UpdateGuid(Guid guid)
{

View File

@ -1,54 +0,0 @@
using Artemis.Core.ScriptingProviders;
namespace Artemis.UI.Shared.ScriptingProviders;
/// <summary>
/// Represents a Stylet view model containing a script editor
/// </summary>
public class ScriptEditorViewModel : ActivatableViewModelBase, IScriptEditorViewModel
{
private Script? _script;
/// <summary>
/// Creates a new instance of <see cref="ScriptEditorViewModel" />
/// </summary>
/// <param name="scriptType">The script type this view model was created for</param>
public ScriptEditorViewModel(ScriptType scriptType)
{
ScriptType = scriptType;
}
/// <summary>
/// Called just before the script is changed to a different one
/// </summary>
/// <param name="script">The script to display or <see langword="null" /> if no script is to be displayed</param>
protected virtual void OnScriptChanging(Script? script)
{
}
/// <summary>
/// Called after the script was changed to a different one
/// </summary>
/// <param name="script">The script to display or <see langword="null" /> if no script is to be displayed</param>
protected virtual void OnScriptChanged(Script? script)
{
}
/// <inheritdoc />
public ScriptType ScriptType { get; }
/// <inheritdoc />
public Script? Script
{
get => _script;
internal set => RaiseAndSetIfChanged(ref _script, value);
}
/// <inheritdoc />
public void ChangeScript(Script? script)
{
OnScriptChanging(script);
Script = script;
OnScriptChanged(script);
}
}

View File

@ -67,5 +67,8 @@
<UpToDateCheckInput Remove="Screens\Workshop\Entries\Tabs\PluginListView.axaml" />
<UpToDateCheckInput Remove="Screens\Workshop\Entries\Tabs\ProfileListView.axaml" />
<UpToDateCheckInput Remove="Screens\Workshop\Plugins\Dialogs\PluginDialogView.axaml" />
<UpToDateCheckInput Remove="Screens\Scripting\Dialogs\ScriptConfigurationCreateView.axaml" />
<UpToDateCheckInput Remove="Screens\Scripting\Dialogs\ScriptConfigurationEditView.axaml" />
<UpToDateCheckInput Remove="Screens\Scripting\ScriptsDialogView.axaml" />
</ItemGroup>
</Project>

View File

@ -3,7 +3,6 @@ using System.Reactive;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Core.ScriptingProviders;
using Artemis.UI.Screens.Device;
using Artemis.UI.Screens.Device.General;
using Artemis.UI.Screens.Device.InputMappings;
@ -21,7 +20,6 @@ using Artemis.UI.Screens.ProfileEditor.Properties.DataBinding;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers;
using Artemis.UI.Screens.Scripting;
using Artemis.UI.Screens.Settings;
using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Screens.Sidebar;
@ -470,32 +468,6 @@ public class LayerHintVmFactory : ILayerHintVmFactory
}
}
public interface IScriptVmFactory : IVmFactory
{
ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration);
ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration);
}
public class ScriptVmFactory : IScriptVmFactory
{
private readonly IContainer _container;
public ScriptVmFactory(IContainer container)
{
_container = container;
}
public ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration)
{
return _container.Resolve<ScriptConfigurationViewModel>(new object[] {scriptConfiguration});
}
public ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration)
{
return _container.Resolve<ScriptConfigurationViewModel>(new object[] {profile, scriptConfiguration});
}
}
public interface IReleaseVmFactory : IVmFactory
{
ReleaseViewModel ReleaseListViewModel(IGetReleases_PublishedReleases_Nodes release);

View File

@ -9,7 +9,6 @@ using Artemis.Core.DeviceProviders;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Core.Modules;
using Artemis.Core.ScriptingProviders;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
@ -99,8 +98,6 @@ public partial class PluginFeatureViewModel : ActivatableViewModelBase
return MaterialIconKind.Brush;
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(LayerEffectProvider)))
return MaterialIconKind.AutoAwesome;
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(ScriptingProvider)))
return MaterialIconKind.Code;
return MaterialIconKind.Extension;
}
}
@ -117,8 +114,6 @@ public partial class PluginFeatureViewModel : ActivatableViewModelBase
return "Layer Brush";
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(LayerEffectProvider)))
return "Layer Effect";
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(ScriptingProvider)))
return "Scripting Provider";
return "Miscellaneous feature";
}
}

View File

@ -31,11 +31,6 @@
<avalonia:MaterialIcon Kind="Settings" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_View Scripts" Command="{CompiledBinding ViewScripts}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="BookEdit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Adapt Profile" Command="{CompiledBinding AdaptProfile}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Magic" />

View File

@ -8,7 +8,6 @@ using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Screens.Scripting;
using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
@ -64,7 +63,6 @@ public partial class MenuBarViewModel : ActivatableViewModelBase
AddFolder = ReactiveCommand.Create(ExecuteAddFolder);
AddLayer = ReactiveCommand.Create(ExecuteAddLayer);
ViewProperties = ReactiveCommand.CreateFromTask(ExecuteViewProperties, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null));
ViewScripts = ReactiveCommand.CreateFromTask(ExecuteViewScripts, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null));
AdaptProfile = ReactiveCommand.CreateFromTask(ExecuteAdaptProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null));
ToggleSuspended = ReactiveCommand.Create(ExecuteToggleSuspended, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null));
DeleteProfile = ReactiveCommand.CreateFromTask(ExecuteDeleteProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null));
@ -81,7 +79,6 @@ public partial class MenuBarViewModel : ActivatableViewModelBase
public ReactiveCommand<Unit, Unit> AddLayer { get; }
public ReactiveCommand<Unit, Unit> ToggleSuspended { get; }
public ReactiveCommand<Unit, Unit> ViewProperties { get; }
public ReactiveCommand<Unit, Unit> ViewScripts { get; }
public ReactiveCommand<Unit, Unit> AdaptProfile { get; }
public ReactiveCommand<Unit, Unit> DeleteProfile { get; }
public ReactiveCommand<Unit, Unit> ExportProfile { get; }
@ -133,16 +130,7 @@ public partial class MenuBarViewModel : ActivatableViewModelBase
await _windowService.ShowDialogAsync<ProfileConfigurationEditViewModel, ProfileConfiguration?>(ProfileConfiguration.Category, ProfileConfiguration);
}
private async Task ExecuteViewScripts()
{
if (ProfileConfiguration?.Profile == null)
return;
await _windowService.ShowDialogAsync<ScriptsDialogViewModel, object?>(ProfileConfiguration.Profile);
await _profileEditorService.SaveProfileAsync();
}
private async Task ExecuteAdaptProfile()
{
if (ProfileConfiguration?.Profile == null)

View File

@ -1,36 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dialogs="clr-namespace:Artemis.UI.Screens.Scripting.Dialogs"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:scriptingProviders="clr-namespace:Artemis.Core.ScriptingProviders;assembly=Artemis.Core"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Scripting.Dialogs.ScriptConfigurationCreateView"
x:DataType="dialogs:ScriptConfigurationCreateViewModel">
<Panel>
<StackPanel IsVisible="{CompiledBinding ScriptingProviders.Count}">
<TextBlock Classes="label" Margin="0 5">Script name</TextBlock>
<TextBox Watermark="Name" Text="{CompiledBinding ScriptName}" />
<TextBlock Classes="label" Margin="0 5">Script type</TextBlock>
<ComboBox SelectedItem="{CompiledBinding SelectedScriptingProvider}" ItemsSource="{CompiledBinding ScriptingProviders}" HorizontalAlignment="Stretch">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="scriptingProviders:ScriptingProvider">
<StackPanel Orientation="Horizontal">
<shared:ArtemisIcon Icon="{CompiledBinding Plugin.Info.ResolvedIcon}" Width="16" Height="16" Margin="0 0 5 0" />
<TextBlock Text="{CompiledBinding LanguageName}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<TextBlock IsVisible="{CompiledBinding !ScriptingProviders.Count}">
You don't have any scripting providers installed or enabled, therefore you cannot use scripts.
</TextBlock>
</Panel>
</UserControl>

View File

@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Scripting.Dialogs;
public partial class ScriptConfigurationCreateView : ReactiveUserControl<ScriptConfigurationCreateViewModel>
{
public ScriptConfigurationCreateView()
{
InitializeComponent();
}
}

View File

@ -1,40 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using Artemis.Core.ScriptingProviders;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
namespace Artemis.UI.Screens.Scripting.Dialogs;
public partial class ScriptConfigurationCreateViewModel : ContentDialogViewModelBase
{
[Notify] private string? _scriptName;
[Notify] private ScriptingProvider _selectedScriptingProvider;
public ScriptConfigurationCreateViewModel(IScriptingService scriptingService)
{
ScriptingProviders = new List<ScriptingProvider>(scriptingService.ScriptingProviders);
Submit = ReactiveCommand.Create(ExecuteSubmit, ValidationContext.Valid);
_selectedScriptingProvider = ScriptingProviders.First();
this.ValidationRule(vm => vm.ScriptName, s => !string.IsNullOrWhiteSpace(s), "Script name cannot be empty.");
}
public ScriptConfiguration? ScriptConfiguration { get; private set; }
public List<ScriptingProvider> ScriptingProviders { get; }
public ReactiveCommand<Unit, Unit> Submit { get; }
private void ExecuteSubmit()
{
if (ScriptName == null)
return;
ScriptConfiguration = new ScriptConfiguration(SelectedScriptingProvider, ScriptName, ScriptType.Profile);
ContentDialog?.Hide(ContentDialogResult.Primary);
}
}

View File

@ -1,10 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dialogs="clr-namespace:Artemis.UI.Screens.Scripting.Dialogs"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Scripting.Dialogs.ScriptConfigurationEditView"
x:DataType="dialogs:ScriptConfigurationEditViewModel">
<TextBox Watermark="Name" Name="Input" Text="{CompiledBinding ScriptName}" />
</UserControl>

View File

@ -1,19 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.Scripting.Dialogs;
public partial class ScriptConfigurationEditView : ReactiveUserControl<ScriptConfigurationEditViewModel>
{
public ScriptConfigurationEditView()
{
InitializeComponent();
this.WhenActivated(_ =>
{
Input.Focus();
Input.SelectAll();
});
}
}

View File

@ -1,35 +0,0 @@
using System.Reactive;
using Artemis.Core.ScriptingProviders;
using Artemis.UI.Shared;
using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ReactiveUI.Validation.Extensions;
namespace Artemis.UI.Screens.Scripting.Dialogs;
public partial class ScriptConfigurationEditViewModel : ContentDialogViewModelBase
{
[Notify] private string? _scriptName;
public ScriptConfigurationEditViewModel(ScriptConfiguration scriptConfiguration)
{
ScriptConfiguration = scriptConfiguration;
Submit = ReactiveCommand.Create(ExecuteSubmit, ValidationContext.Valid);
ScriptName = ScriptConfiguration.Name;
this.ValidationRule(vm => vm.ScriptName, s => !string.IsNullOrWhiteSpace(s), "Script name cannot be empty.");
}
public ScriptConfiguration ScriptConfiguration { get; }
public ReactiveCommand<Unit, Unit> Submit { get; }
private void ExecuteSubmit()
{
if (ScriptName == null)
return;
ScriptConfiguration.Name = ScriptName;
ContentDialog?.Hide(ContentDialogResult.Primary);
}
}

View File

@ -1,63 +0,0 @@
using System.Reactive;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.ScriptingProviders;
using Artemis.Core.Services;
using Artemis.UI.Screens.Scripting.Dialogs;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using FluentAvalonia.UI.Controls;
using ReactiveUI;
using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton;
namespace Artemis.UI.Screens.Scripting;
public class ScriptConfigurationViewModel : ActivatableViewModelBase
{
private readonly IScriptingService _scriptingService;
private readonly IWindowService _windowService;
public ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration, IScriptingService scriptingService, IWindowService windowService)
{
_scriptingService = scriptingService;
_windowService = windowService;
ScriptConfiguration = scriptConfiguration;
Script = ScriptConfiguration.Script;
EditScriptConfiguration = ReactiveCommand.CreateFromTask<ScriptConfiguration>(ExecuteEditScriptConfiguration);
ToggleSuspended = ReactiveCommand.Create(() => ScriptConfiguration.IsSuspended = !ScriptConfiguration.IsSuspended);
}
public ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration, IScriptingService scriptingService, IWindowService windowService)
: this(scriptConfiguration, scriptingService, windowService)
{
Profile = profile;
}
public Profile? Profile { get; }
public ScriptConfiguration ScriptConfiguration { get; }
public Script? Script { get; }
public ReactiveCommand<ScriptConfiguration, Unit> EditScriptConfiguration { get; }
public ReactiveCommand<Unit, bool> ToggleSuspended { get; }
private async Task ExecuteEditScriptConfiguration(ScriptConfiguration scriptConfiguration)
{
ContentDialogResult contentDialogResult = await _windowService.CreateContentDialog()
.WithTitle("Edit script")
.WithViewModel(out ScriptConfigurationEditViewModel vm, scriptConfiguration)
.WithCloseButtonText("Cancel")
.HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Submit))
.HavingSecondaryButton(b => b.WithText("Delete"))
.WithDefaultButton(ContentDialogButton.Primary)
.ShowAsync();
// Remove the script if the delete button was pressed
if (contentDialogResult == ContentDialogResult.Secondary)
{
if (Profile != null)
_scriptingService.RemoveScript(scriptConfiguration, Profile);
else
_scriptingService.RemoveScript(scriptConfiguration);
}
}
}

View File

@ -1,90 +0,0 @@
<windowing:AppWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:scripting="clr-namespace:Artemis.UI.Screens.Scripting"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Scripting.ScriptsDialogView"
x:DataType="scripting:ScriptsDialogViewModel"
Icon="/Assets/Images/Logo/application.ico"
Title="Artemis | Scripts"
Width="1200"
Height="750">
<DockPanel>
<ScrollViewer DockPanel.Dock="Left" VerticalScrollBarVisibility="Auto" Width="300" Margin="10">
<StackPanel>
<ListBox ItemsSource="{CompiledBinding ScriptConfigurations}" SelectedItem="{CompiledBinding SelectedScript}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type scripting:ScriptConfigurationViewModel}">
<Grid ColumnDefinitions="Auto,*,Auto,Auto" RowDefinitions="*,*" Margin="4">
<shared:ArtemisIcon Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2"
Icon="{CompiledBinding ScriptConfiguration.Script.ScriptingProvider.Plugin.Info.ResolvedIcon, FallbackValue=QuestionMark}"
Width="32 "
Height="32"
Margin="0 0 10 0"
VerticalAlignment="Center" />
<TextBlock Grid.Row="0"
Grid.Column="1"
VerticalAlignment="Center"
Text="{CompiledBinding ScriptConfiguration.Name}"
IsVisible="{CompiledBinding !ScriptConfiguration.HasChanges}"
TextTrimming="CharacterEllipsis" />
<StackPanel Grid.Row="0"
Grid.Column="1"
VerticalAlignment="Center"
Orientation="Horizontal"
IsVisible="{CompiledBinding ScriptConfiguration.HasChanges}">
<TextBlock Text="{CompiledBinding ScriptConfiguration.Name}" FontWeight="Bold"></TextBlock>
<TextBlock Text="*"></TextBlock>
</StackPanel>
<TextBlock Grid.Row="1"
Grid.Column="1"
Text="{CompiledBinding ScriptConfiguration.Script.ScriptingProvider.LanguageName, FallbackValue='Unknown scripting provider'}"
Classes="subtitle"
FontSize="11"
VerticalAlignment="Center" />
<Button Classes="icon-button icon-button-small"
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="2"
ToolTip.Tip="Edit script"
HorizontalAlignment="Right"
Command="{CompiledBinding EditScriptConfiguration}"
CommandParameter="{CompiledBinding ScriptConfiguration}"
Margin="0 0 2 0">
<avalonia:MaterialIcon Kind="Cog" />
</Button>
<Button Classes="icon-button icon-button-small"
Command="{CompiledBinding ToggleSuspended}"
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="3"
ToolTip.Tip="Suspend/resume script">
<Panel>
<avalonia:MaterialIcon Kind="EyeOff" IsVisible="{CompiledBinding ScriptConfiguration.IsSuspended}" />
<avalonia:MaterialIcon Kind="Eye" IsVisible="{CompiledBinding !ScriptConfiguration.IsSuspended}" />
</Panel>
</Button>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add new script"
Margin="10"
HorizontalAlignment="Stretch"
Command="{CompiledBinding AddScriptConfiguration}" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Top" Classes="router-container" Margin="0 10 0 0">
<ContentControl Content="{CompiledBinding ScriptEditorViewModel}" />
</Border>
</DockPanel>
</windowing:AppWindow>

View File

@ -1,34 +0,0 @@
using System.ComponentModel;
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Scripting;
public partial class ScriptsDialogView : ReactiveAppWindow<ScriptsDialogViewModel>
{
private bool _canClose;
public ScriptsDialogView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
Closing += OnClosing;
}
private async void OnClosing(object? sender, CancelEventArgs e)
{
if (_canClose)
return;
e.Cancel = true;
if (ViewModel == null || await ViewModel.CanClose())
{
_canClose = true;
Close();
}
}
}

View File

@ -1,142 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.ScriptingProviders;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.Scripting.Dialogs;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using DynamicData;
using DynamicData.Binding;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.Scripting;
public partial class ScriptsDialogViewModel : DialogViewModelBase<object?>
{
private readonly Dictionary<ScriptingProvider, IScriptEditorViewModel> _providerViewModels = new();
private readonly IScriptingService _scriptingService;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<bool>? _hasScripts;
[Notify] private ReadOnlyObservableCollection<ScriptConfigurationViewModel> _scriptConfigurations;
[Notify] private IScriptEditorViewModel? _scriptEditorViewModel;
[Notify] private ScriptConfigurationViewModel? _selectedScript;
public ScriptsDialogViewModel(IScriptingService scriptingService, IWindowService windowService, IProfileService profileService, IScriptVmFactory scriptVmFactory)
{
_scriptingService = scriptingService;
_windowService = windowService;
ScriptType = ScriptType.Global;
ScriptingProviders = new List<ScriptingProvider>(scriptingService.ScriptingProviders);
AddScriptConfiguration = ReactiveCommand.CreateFromTask(ExecuteAddScriptConfiguration, Observable.Return(ScriptingProviders.Any()));
this.WhenAnyValue(vm => vm.SelectedScript).Subscribe(s => SetupScriptEditor(s?.ScriptConfiguration));
_scriptConfigurations = new ReadOnlyObservableCollection<ScriptConfigurationViewModel>(new ObservableCollection<ScriptConfigurationViewModel>());
// TODO: When not bound to a profile, base the contents of the UI on the ScriptingService
}
public ScriptsDialogViewModel(Profile profile, IScriptingService scriptingService, IWindowService windowService, IProfileService profileService, IScriptVmFactory scriptVmFactory)
: this(scriptingService, windowService, profileService, scriptVmFactory)
{
ScriptType = ScriptType.Profile;
Profile = profile;
this.WhenActivated(d =>
{
_hasScripts = Profile.ScriptConfigurations.ToObservableChangeSet()
.Count()
.Select(c => c > 0)
.ToProperty(this, vm => vm.HasScripts)
.DisposeWith(d);
Profile.ScriptConfigurations.ToObservableChangeSet()
.Transform(c => scriptVmFactory.ScriptConfigurationViewModel(Profile, c))
.Bind(out ReadOnlyObservableCollection<ScriptConfigurationViewModel> scriptConfigurationViewModels)
.Subscribe()
.DisposeWith(d);
ScriptConfigurations = scriptConfigurationViewModels;
SelectedScript = ScriptConfigurations.FirstOrDefault();
Disposable.Create(() => profileService.SaveProfile(Profile, false)).DisposeWith(d);
});
}
public ScriptType ScriptType { get; }
public List<ScriptingProvider> ScriptingProviders { get; }
public Profile? Profile { get; }
public bool HasScripts => _hasScripts?.Value ?? false;
public ReactiveCommand<Unit, Unit> AddScriptConfiguration { get; }
public async Task<bool> CanClose()
{
if (!ScriptConfigurations.Any(s => s.ScriptConfiguration.HasChanges))
return true;
bool result = await _windowService.ShowConfirmContentDialog("Discard changes", "One or more scripts still have pending changes, do you want to discard them?");
if (!result)
return false;
foreach (ScriptConfigurationViewModel scriptConfigurationViewModel in ScriptConfigurations)
scriptConfigurationViewModel.ScriptConfiguration.DiscardPendingChanges();
return true;
}
private void SetupScriptEditor(ScriptConfiguration? scriptConfiguration)
{
if (scriptConfiguration == null)
{
ScriptEditorViewModel = null;
return;
}
// The script is null if the provider is missing
if (scriptConfiguration.Script == null)
{
ScriptEditorViewModel = null;
return;
}
if (!_providerViewModels.TryGetValue(scriptConfiguration.Script.ScriptingProvider, out IScriptEditorViewModel? viewModel))
{
viewModel = scriptConfiguration.Script.ScriptingProvider.CreateScriptEditor(ScriptType);
_providerViewModels.Add(scriptConfiguration.Script.ScriptingProvider, viewModel);
}
ScriptEditorViewModel = viewModel;
ScriptEditorViewModel.ChangeScript(scriptConfiguration.Script);
}
private async Task ExecuteAddScriptConfiguration()
{
await _windowService.CreateContentDialog()
.WithTitle("Add script")
.WithViewModel(out ScriptConfigurationCreateViewModel vm)
.WithCloseButtonText("Cancel")
.HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Submit))
.WithDefaultButton(ContentDialogButton.Primary)
.ShowAsync();
if (vm.ScriptConfiguration == null)
return;
// Add the script to the profile and instantiate it
if (Profile != null)
_scriptingService.AddScript(vm.ScriptConfiguration, Profile);
else
_scriptingService.AddScript(vm.ScriptConfiguration);
// Select the new script
SelectedScript = ScriptConfigurations.LastOrDefault();
}
}

View File

@ -48,10 +48,10 @@ public partial class WorkshopHomeViewModel : RoutableScreen
{
p.Clear();
if (popularResult.Data?.PopularEntries != null)
p.AddRange(popularResult.Data.PopularEntries);
p.AddRange(popularResult.Data.PopularEntries.Take(8));
});
IOperationResult<IGetEntriesv2Result> latestResult = await client.GetEntriesv2.ExecuteAsync(null, null, [new EntrySortInput {CreatedAt = SortEnumType.Desc}], 25, null);
IOperationResult<IGetEntriesv2Result> latestResult = await client.GetEntriesv2.ExecuteAsync(null, null, [new EntrySortInput {CreatedAt = SortEnumType.Desc}], 8, null);
latest.Edit(l =>
{
l.Clear();

View File

@ -5,61 +5,62 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.3.0" />
<PackageVersion Include="Avalonia" Version="11.1.3" />
<PackageVersion Include="Avalonia" Version="11.2.0" />
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.1.0" />
<PackageVersion Include="Avalonia.Controls.ItemsRepeater" Version="11.1.3" />
<PackageVersion Include="Avalonia.Controls.PanAndZoom" Version="11.1.0.1" />
<PackageVersion Include="Avalonia.Desktop" Version="11.1.3" />
<PackageVersion Include="Avalonia.Controls.ItemsRepeater" Version="11.1.4" />
<PackageVersion Include="Avalonia.Controls.PanAndZoom" Version="11.2.0" />
<PackageVersion Include="Avalonia.Desktop" Version="11.2.0" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.9" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.1.3" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.2.0" />
<PackageVersion Include="Avalonia.Skia.Lottie" Version="11.0.0" />
<PackageVersion Include="Avalonia.Win32" Version="11.1.3" />
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="8.0.0" />
<PackageVersion Include="Avalonia.Xaml.Behaviors" Version="11.1.0.4" />
<PackageVersion Include="Avalonia.Win32" Version="11.2.0" />
<PackageVersion Include="HPPH.SkiaSharp" Version="1.0.0" />
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.0" />
<PackageVersion Include="Avalonia.Xaml.Behaviors" Version="11.2.0" />
<PackageVersion Include="AvaloniaEdit.TextMate" Version="11.1.0" />
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
<PackageVersion Include="DryIoc.dll" Version="5.4.3" />
<PackageVersion Include="DynamicData" Version="8.4.1" />
<PackageVersion Include="DynamicData" Version="9.0.4" />
<PackageVersion Include="EmbedIO" Version="3.5.2" />
<PackageVersion Include="FluentAvalonia.ProgressRing" Version="1.69.2" />
<PackageVersion Include="FluentAvaloniaUI" Version="2.1.0" />
<PackageVersion Include="HidSharp" Version="2.1.0" />
<PackageVersion Include="Humanizer.Core" Version="2.14.1" />
<PackageVersion Include="IdentityModel" Version="7.0.0" />
<PackageVersion Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageVersion Include="JetBrains.Annotations" Version="2024.3.0" />
<PackageVersion Include="LiteDB" Version="5.0.21" />
<PackageVersion Include="Markdown.Avalonia.Tight" Version="11.0.2" />
<PackageVersion Include="Material.Icons.Avalonia" Version="2.1.10" />
<PackageVersion Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageVersion Include="Microsoft.Win32" Version="2.0.1" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="8.0.8" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.0" />
<PackageVersion Include="NoStringEvaluating" Version="2.5.2" />
<PackageVersion Include="Octopus.Octodiff" Version="2.0.546" />
<PackageVersion Include="Octopus.Octodiff" Version="2.0.547" />
<PackageVersion Include="PropertyChanged.SourceGenerator" Version="1.1.0" />
<PackageVersion Include="RGB.NET.Core" Version="2.1.0" />
<PackageVersion Include="RGB.NET.Layout" Version="2.1.0" />
<PackageVersion Include="RGB.NET.Presets" Version="2.1.0" />
<PackageVersion Include="RGB.NET.Core" Version="3.0.0-prerelease.3" />
<PackageVersion Include="RGB.NET.Layout" Version="3.0.0-prerelease.3" />
<PackageVersion Include="RGB.NET.Presets" Version="3.0.0-prerelease.3" />
<PackageVersion Include="RawInput.Sharp" Version="0.1.3" />
<PackageVersion Include="ReactiveUI" Version="20.1.1" />
<PackageVersion Include="ReactiveUI.Validation" Version="4.0.9" />
<PackageVersion Include="Serilog" Version="4.0.1" />
<PackageVersion Include="ReactiveUI" Version="20.1.63" />
<PackageVersion Include="ReactiveUI.Validation" Version="4.1.1" />
<PackageVersion Include="Serilog" Version="4.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="SkiaSharp" Version="2.88.8" />
<PackageVersion Include="SkiaSharp.Vulkan.SharpVk" Version="2.88.8" />
<PackageVersion Include="SkiaSharp" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.Vulkan.SharpVk" Version="2.88.9" />
<PackageVersion Include="Splat.DryIoc" Version="15.2.22" />
<PackageVersion Include="StrawberryShake.Server" Version="13.9.12" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="TextMateSharp.Grammars" Version="1.0.63" />
<PackageVersion Include="StrawberryShake.Server" Version="14.1.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
<PackageVersion Include="System.Text.Json" Version="9.0.0" />
<PackageVersion Include="TextMateSharp.Grammars" Version="1.0.64" />
</ItemGroup>
</Project>