From db84f1dc7565dc8811ff0784c642203279a7bac0 Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Mon, 22 Jul 2024 23:26:53 +0200 Subject: [PATCH 1/9] Major upgrade of RGB.NET to v3 --- src/Artemis.Core/Artemis.Core.csproj | 3 +- src/Artemis.Core/RGB.NET/SKTexture.cs | 80 ++++++++-------------- src/Artemis.Core/RGB.NET/SKTextureBrush.cs | 2 +- src/Directory.Packages.props | 7 +- 4 files changed, 34 insertions(+), 58 deletions(-) diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index 42daefc59..71aeb0167 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -39,8 +39,9 @@ + - + diff --git a/src/Artemis.Core/RGB.NET/SKTexture.cs b/src/Artemis.Core/RGB.NET/SKTexture.cs index 9664318ae..14aae47e2 100644 --- a/src/Artemis.Core/RGB.NET/SKTexture.cs +++ b/src/Artemis.Core/RGB.NET/SKTexture.cs @@ -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; /// /// Represents a SkiaSharp-based RGB.NET PixelTexture /// -public sealed class SKTexture : PixelTexture, IDisposable +public sealed class SKTexture : ITexture, IDisposable { - private readonly Dictionary _ledRects; - private readonly SKPixmap _pixelData; - private readonly IntPtr _pixelDataPtr; - #region Constructors - internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int height, float scale, IReadOnlyCollection devices) : base(width, height, DATA_PER_PIXEL, - new AverageByteSampler()) + internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int height, float scale, IReadOnlyCollection 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(); 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 @@ internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int heig 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 samplerInfo = new(skRectI.Left, skRectI.Top, skRectI.Width, skRectI.Height, Stride, DataPerPixel, Data); - - Span 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(); } /// ~SKTexture() { - ReleaseUnmanagedResources(); + Dispose(); } /// public void Dispose() { Surface.Dispose(); - _pixelData.Dispose(); - - ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } - #region Constants - - private const int DATA_PER_PIXEL = 4; - - #endregion - #region Methods /// @@ -104,20 +80,23 @@ public void Invalidate() internal void CopyPixelData() { using SKImage skImage = Surface.Snapshot(); - skImage.ReadPixels(_pixelData); + _image = skImage.ToImage(); } - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override Color GetColor(in ReadOnlySpan pixel) => new(pixel[2], pixel[1], pixel[0]); - - /// - public override Color this[in Rectangle rectangle] => Color.Transparent; - #endregion #region Properties & Fields + private IImage? _image; + private readonly Dictionary _ledRects = []; + + /// + public Size Size { get; } + /// + public Color this[Point point] => Color.Transparent; + /// + public Color this[Rectangle rectangle] => Color.Transparent; + /// /// Gets the SKBitmap backing this texture /// @@ -128,11 +107,6 @@ internal void CopyPixelData() /// public SKImageInfo ImageInfo { get; } - /// - /// Gets the color data in RGB format - /// - protected override ReadOnlySpan Data => _pixelData.GetPixelSpan(); - /// /// Gets the render scale of the texture /// diff --git a/src/Artemis.Core/RGB.NET/SKTextureBrush.cs b/src/Artemis.Core/RGB.NET/SKTextureBrush.cs index 68f2308f1..d601dd905 100644 --- a/src/Artemis.Core/RGB.NET/SKTextureBrush.cs +++ b/src/Artemis.Core/RGB.NET/SKTextureBrush.cs @@ -20,7 +20,7 @@ public SKTextureBrush(SKTexture? texture) #region Methods /// - protected override Color GetColorAtPoint(in Rectangle rectangle, in RenderTarget renderTarget) + protected override Color GetColorAtPoint(Rectangle rectangle, RenderTarget renderTarget) { return Texture?.GetColorAtRenderTarget(renderTarget) ?? Color.Transparent; } diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 9c07752b6..5d70e8ae5 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -14,6 +14,7 @@ + @@ -44,9 +45,9 @@ - - - + + + From 5d82c159e15d0269d0c1f2495eb4adf5e515458d Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Mon, 22 Jul 2024 23:52:50 +0200 Subject: [PATCH 2/9] Replaced the quantization code and with the HPPH implementation --- .../ColorScience/Quantization/ColorCube.cs | 167 ------------------ .../Quantization/ColorQuantizer.cs | 31 +--- .../Quantization/QuantizerSort.cs | 121 ------------- .../ColorScience/Quantization/SortTarget.cs | 6 - 4 files changed, 7 insertions(+), 318 deletions(-) delete mode 100644 src/Artemis.Core/ColorScience/Quantization/ColorCube.cs delete mode 100644 src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs delete mode 100644 src/Artemis.Core/ColorScience/Quantization/SortTarget.cs diff --git a/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs b/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs deleted file mode 100644 index 5b7b353a0..000000000 --- a/src/Artemis.Core/ColorScience/Quantization/ColorCube.cs +++ /dev/null @@ -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.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 fullColorList, int from, int length, SortTarget preOrdered) - { - this._from = from; - this._length = length; - - if (length < 2) return; - - Span 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 colors) - { - if (Vector.IsHardwareAccelerated && (colors.Length >= Vector.Count)) - { - int chunks = colors.Length / ELEMENTS_PER_VECTOR; - int vectorElements = (chunks * ELEMENTS_PER_VECTOR); - int missingElements = colors.Length - vectorElements; - - Vector max = Vector.Zero; - Vector min = new(byte.MaxValue); - foreach (Vector currentVector in MemoryMarshal.Cast>(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 fullColorList, out ColorCube a, out ColorCube b) - { - Span 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 fullColorList) - { - ReadOnlySpan 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) - ); - } -} diff --git a/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs b/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs index 1afaa4f3c..1b6f6332d 100644 --- a/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs +++ b/src/Artemis.Core/ColorScience/Quantization/ColorQuantizer.cs @@ -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; @@ -16,7 +18,7 @@ public static class ColorQuantizer /// The colors to quantize /// How many colors to return. Must be a power of two. /// colors. - public static SKColor[] Quantize(in Span colors, int amount) + public static SKColor[] Quantize(Span colors, int amount) { if (!BitOperations.IsPow2(amount)) throw new ArgumentException("Must be power of two", nameof(amount)); @@ -31,31 +33,12 @@ public static SKColor[] Quantize(in Span colors, int amount) /// The colors to quantize /// How many splits to execute. Each split doubles the number of colors returned. /// Up to (2 ^ ) number of colors. - public static SKColor[] QuantizeSplit(in Span colors, int splits) + public static SKColor[] QuantizeSplit(Span 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 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 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(MemoryMarshal.Cast(colors).CreateSimpleColorPalette(1 << splits)).ToArray(); } /// diff --git a/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs b/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs deleted file mode 100644 index 46b593ece..000000000 --- a/src/Artemis.Core/ColorScience/Quantization/QuantizerSort.cs +++ /dev/null @@ -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 colors) - { - Span counts = stackalloc int[256]; - foreach (SKColor t in colors) - counts[t.Red]++; - - SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length); - - try - { - Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length); - Span 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.Shared.Return(bucketsArray); - } - } - - public static void SortGreen(in Span colors) - { - Span counts = stackalloc int[256]; - foreach (SKColor t in colors) - counts[t.Green]++; - - SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length); - - try - { - Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length); - Span 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.Shared.Return(bucketsArray); - } - } - - public static void SortBlue(in Span colors) - { - Span counts = stackalloc int[256]; - foreach (SKColor t in colors) - counts[t.Blue]++; - - SKColor[] bucketsArray = ArrayPool.Shared.Rent(colors.Length); - - try - { - Span buckets = bucketsArray.AsSpan().Slice(0, colors.Length); - Span 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.Shared.Return(bucketsArray); - } - } - - #endregion -} diff --git a/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs b/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs deleted file mode 100644 index 72f58517d..000000000 --- a/src/Artemis.Core/ColorScience/Quantization/SortTarget.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Artemis.Core.ColorScience; - -internal enum SortTarget -{ - None, Red, Green, Blue -} From 1e8c68bbeb5650d0ce7ce20344bafd42ac2d93a5 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 24 Jul 2024 11:39:56 +0200 Subject: [PATCH 3/9] Workshop - Avoid crashes when auto-updating without internet Workshop - Added vote based entry score system --- .../Workshop/Entries/EntryVoteView.axaml | 65 +++++++++++ .../Workshop/Entries/EntryVoteView.axaml.cs | 14 +++ .../Workshop/Entries/EntryVoteViewModel.cs | 91 +++++++++++++++ .../Entries/List/EntryListInputView.axaml | 1 + .../Entries/List/EntryListItemView.axaml | 13 ++- .../Entries/List/EntryListItemViewModel.cs | 9 +- .../Entries/List/EntryListViewModel.cs | 3 + .../Library/Tabs/InstalledTabItemView.axaml | 16 +-- .../Library/Tabs/InstalledTabItemViewModel.cs | 16 ++- .../Updating/WorkshopUpdateService.cs | 28 ++--- .../DryIoc/ContainerExtensions.cs | 1 + .../Mutations/CastVote.graphql | 16 +++ .../Mutations/CreateEntry.graphql | 2 +- .../Queries/Fragments.graphql | 4 + .../Queries/GetVotes.graphql | 6 + .../Services/VoteClient.cs | 105 ++++++++++++++++++ .../WorkshopConstants.cs | 8 +- .../graphql.config.yml | 2 +- src/Artemis.WebClient.Workshop/schema.graphql | 51 +++++++++ 19 files changed, 408 insertions(+), 43 deletions(-) create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/EntryVoteViewModel.cs create mode 100644 src/Artemis.WebClient.Workshop/Mutations/CastVote.graphql create mode 100644 src/Artemis.WebClient.Workshop/Queries/GetVotes.graphql create mode 100644 src/Artemis.WebClient.Workshop/Services/VoteClient.cs diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml new file mode 100644 index 000000000..113a21d96 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml.cs new file mode 100644 index 000000000..5c131be6a --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Entries; + +public partial class EntryVoteView : ReactiveUserControl +{ + public EntryVoteView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteViewModel.cs new file mode 100644 index 000000000..14e449974 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteViewModel.cs @@ -0,0 +1,91 @@ +using System; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Builders; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Entries; + +public partial class EntryVoteViewModel : ActivatableViewModelBase +{ + private readonly IEntrySummary _entry; + private readonly INotificationService _notificationService; + private readonly IVoteClient _voteClient; + private bool _voting; + + [Notify] private int _score; + [Notify] private bool _upvoted; + [Notify] private bool _downvoted; + + public EntryVoteViewModel(IEntrySummary entry, IAuthenticationService authenticationService, INotificationService notificationService, IVoteClient voteClient) + { + _entry = entry; + _notificationService = notificationService; + _voteClient = voteClient; + + IsLoggedIn = authenticationService.IsLoggedIn; + Score = entry.UpvoteCount - entry.DownvoteCount; + this.WhenActivated(d => IsLoggedIn.Subscribe(l => _ = GetVoteStatus(l)).DisposeWith(d)); + } + + public IObservable IsLoggedIn { get; } + + public async Task CastVote(bool upvote) + { + // Could use a ReactiveCommand to achieve the same thing but that disables the button + // while executing which grays it out for a fraction of a second and looks bad + if (_voting) + return; + + _voting = true; + try + { + IVoteCount? result; + // If the vote was removed, reset the upvote/downvote state + if ((Upvoted && upvote) || (Downvoted && !upvote)) + { + result = await _voteClient.ClearVote(_entry.Id); + Upvoted = false; + Downvoted = false; + } + else + { + result = await _voteClient.CastVote(_entry.Id, upvote); + Upvoted = upvote; + Downvoted = !upvote; + } + + if (result != null) + Score = result.UpvoteCount - result.DownvoteCount; + else + _notificationService.CreateNotification().WithTitle("Failed to cast vote").WithMessage("Please try again later.").WithSeverity(NotificationSeverity.Error).Show(); + } + finally + { + _voting = false; + } + } + + private async Task GetVoteStatus(bool isLoggedIn) + { + if (!isLoggedIn) + { + Upvoted = false; + Downvoted = false; + } + else + { + bool? vote = await _voteClient.GetVote(_entry.Id); + if (vote != null) + { + Upvoted = vote.Value; + Downvoted = !vote.Value; + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml index 8fdc91d29..476ae9bd2 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml @@ -21,6 +21,7 @@ Recently updated Recently added Download count + Score diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml index 22d723a06..7473034d3 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml @@ -21,9 +21,12 @@ HorizontalContentAlignment="Stretch" Command="{CompiledBinding NavigateToEntry}" IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}"> - + + + + - - + @@ -79,7 +82,7 @@ - + @@ -89,7 +92,7 @@ - + installed diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs index d129f9be6..a02325bc4 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Reactive; using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.UI.Shared; @@ -18,12 +17,12 @@ public partial class EntryListItemViewModel : ActivatableViewModelBase [Notify] private bool _isInstalled; [Notify] private bool _updateAvailable; - public EntryListItemViewModel(IEntrySummary entry, IRouter router, IWorkshopService workshopService) + public EntryListItemViewModel(IEntrySummary entry, IRouter router, IWorkshopService workshopService, Func getEntryVoteViewModel) { _router = router; Entry = entry; - NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry); + VoteViewModel = getEntryVoteViewModel(entry); this.WhenActivated((CompositeDisposable _) => { @@ -34,9 +33,9 @@ public EntryListItemViewModel(IEntrySummary entry, IRouter router, IWorkshopServ } public IEntrySummary Entry { get; } - public ReactiveCommand NavigateToEntry { get; } + public EntryVoteViewModel VoteViewModel { get; } - private async Task ExecuteNavigateToEntry() + public async Task NavigateToEntry() { switch (Entry.EntryType) { diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs index a6b5309c7..48388aeb5 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs @@ -137,6 +137,9 @@ private IReadOnlyList GetSort() if (InputViewModel.SortBy == 2) return new[] {new EntrySortInput {Downloads = SortEnumType.Desc}}; + // Sort by score + if (InputViewModel.SortBy == 3) + return new[] {new EntrySortInput {Score = SortEnumType.Desc}}; // Sort by latest release, then by created at return new[] diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml index 71ab5983c..b8347c3df 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml @@ -11,7 +11,6 @@ x:DataType="tabs:InstalledTabItemViewModel"> - diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs index 6307a8557..75a58ce75 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs @@ -10,6 +10,7 @@ using Artemis.UI.DryIoc.Factories; using Artemis.UI.Extensions; using Artemis.UI.Screens.Plugins; +using Artemis.UI.Screens.Workshop.Entries; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; @@ -35,6 +36,7 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase [Notify] private bool _updateAvailable; [Notify] private bool _autoUpdate; + [Notify] private EntryVoteViewModel _voteViewModel; public InstalledTabItemViewModel(InstalledEntry entry, IWorkshopClient client, @@ -43,7 +45,8 @@ public InstalledTabItemViewModel(InstalledEntry entry, IRouter router, IWindowService windowService, IPluginManagementService pluginManagementService, - ISettingsVmFactory settingsVmFactory) + ISettingsVmFactory settingsVmFactory, + Func getEntryVoteViewModel) { _client = client; _workshopService = workshopService; @@ -53,9 +56,9 @@ public InstalledTabItemViewModel(InstalledEntry entry, _pluginManagementService = pluginManagementService; _settingsVmFactory = settingsVmFactory; _autoUpdate = entry.AutoUpdate; - - Entry = entry; + Entry = entry; + this.WhenActivatedAsync(async _ => { // Grab the latest entry summary from the workshop @@ -65,6 +68,7 @@ public InstalledTabItemViewModel(InstalledEntry entry, if (entrySummary.Data?.Entry != null) { Entry.ApplyEntrySummary(entrySummary.Data.Entry); + VoteViewModel = getEntryVoteViewModel(entrySummary.Data.Entry); _workshopService.SaveInstalledEntry(Entry); } } @@ -73,7 +77,7 @@ public InstalledTabItemViewModel(InstalledEntry entry, UpdateAvailable = Entry.ReleaseId != Entry.LatestReleaseId; } }); - + this.WhenAnyValue(vm => vm.AutoUpdate).Skip(1).Subscribe(_ => AutoUpdateToggled()); } @@ -122,10 +126,10 @@ private async Task UninstallPluginPrerequisites() private void AutoUpdateToggled() { _workshopService.SetAutoUpdate(Entry, AutoUpdate); - + if (!AutoUpdate) return; - + Task.Run(async () => { await _workshopUpdateService.AutoUpdateEntry(Entry); diff --git a/src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs b/src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs index c89ff34fe..6f1f267e5 100644 --- a/src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs +++ b/src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs @@ -22,7 +22,8 @@ public class WorkshopUpdateService : IWorkshopUpdateService private readonly Lazy _updateNotificationProvider; private readonly PluginSetting _showNotifications; - public WorkshopUpdateService(ILogger logger, IWorkshopClient client, IWorkshopService workshopService, ISettingsService settingsService, Lazy updateNotificationProvider) + public WorkshopUpdateService(ILogger logger, IWorkshopClient client, IWorkshopService workshopService, ISettingsService settingsService, + Lazy updateNotificationProvider) { _logger = logger; _client = client; @@ -56,20 +57,19 @@ public async Task AutoUpdateEntries() public async Task AutoUpdateEntry(InstalledEntry installedEntry) { - // Query the latest version - IOperationResult latestReleaseResult = await _client.GetEntryLatestReleaseById.ExecuteAsync(installedEntry.Id); - IEntrySummary? entry = latestReleaseResult.Data?.Entry?.LatestRelease?.Entry; - if (entry == null) - return false; - if (latestReleaseResult.Data?.Entry?.LatestRelease is not IRelease latestRelease) - return false; - if (latestRelease.Id == installedEntry.ReleaseId) - return false; - - _logger.Information("Auto-updating entry {Entry} to version {Version}", entry, latestRelease.Version); - try { + // Query the latest version + IOperationResult latestReleaseResult = await _client.GetEntryLatestReleaseById.ExecuteAsync(installedEntry.Id); + IEntrySummary? entry = latestReleaseResult.Data?.Entry?.LatestRelease?.Entry; + if (entry == null) + return false; + if (latestReleaseResult.Data?.Entry?.LatestRelease is not IRelease latestRelease) + return false; + if (latestRelease.Id == installedEntry.ReleaseId) + return false; + + _logger.Information("Auto-updating entry {Entry} to version {Version}", entry, latestRelease.Version); EntryInstallResult updateResult = await _workshopService.InstallEntry(entry, latestRelease, new Progress(), CancellationToken.None); // This happens during installation too but not on our reference of the entry @@ -85,7 +85,7 @@ public async Task AutoUpdateEntry(InstalledEntry installedEntry) } catch (Exception e) { - _logger.Warning(e, "Auto-update failed for entry {Entry}", entry); + _logger.Warning(e, "Auto-update failed for entry {Entry}", installedEntry); } return false; diff --git a/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs b/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs index bebc8caf1..dc5a12d67 100644 --- a/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs @@ -53,6 +53,7 @@ public static void RegisterWorkshopClient(this IContainer container) container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); container.Register(); diff --git a/src/Artemis.WebClient.Workshop/Mutations/CastVote.graphql b/src/Artemis.WebClient.Workshop/Mutations/CastVote.graphql new file mode 100644 index 000000000..22dbd80d8 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Mutations/CastVote.graphql @@ -0,0 +1,16 @@ +mutation CastVote($input: CastVoteInput!) { + castVote(input: $input) { + ...voteCount + } +} + +mutation ClearVote($entryId: Long!) { + clearVote(entryId: $entryId) { + ...voteCount + } +} + +fragment voteCount on Entry { + upvoteCount + downvoteCount +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Mutations/CreateEntry.graphql b/src/Artemis.WebClient.Workshop/Mutations/CreateEntry.graphql index 7191bc704..b4b621356 100644 --- a/src/Artemis.WebClient.Workshop/Mutations/CreateEntry.graphql +++ b/src/Artemis.WebClient.Workshop/Mutations/CreateEntry.graphql @@ -2,4 +2,4 @@ mutation AddEntry ($input: CreateEntryInput!) { addEntry(input: $input) { id } -} +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql index cf0c3c779..ba23037d5 100644 --- a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql @@ -36,6 +36,8 @@ fragment entrySummary on Entry { summary entryType downloads + upvoteCount + downvoteCount createdAt latestReleaseId categories { @@ -51,6 +53,8 @@ fragment entryDetails on Entry { summary entryType downloads + upvoteCount + downvoteCount createdAt description categories { diff --git a/src/Artemis.WebClient.Workshop/Queries/GetVotes.graphql b/src/Artemis.WebClient.Workshop/Queries/GetVotes.graphql new file mode 100644 index 000000000..e671b6e7d --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Queries/GetVotes.graphql @@ -0,0 +1,6 @@ +query GetVotes { + votes { + entryId + upvote + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/VoteClient.cs b/src/Artemis.WebClient.Workshop/Services/VoteClient.cs new file mode 100644 index 000000000..7c4ba8b9f --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Services/VoteClient.cs @@ -0,0 +1,105 @@ +using StrawberryShake; + +namespace Artemis.WebClient.Workshop.Services; + +public class VoteClient : IVoteClient +{ + private readonly Dictionary _cache = new(); + private readonly IWorkshopClient _client; + private readonly SemaphoreSlim _lock = new(1, 1); + private DateTime _cacheAge = DateTime.MinValue; + + public VoteClient(IWorkshopClient client, IAuthenticationService authenticationService) + { + _client = client; + authenticationService.IsLoggedIn.Subscribe(_ => _cacheAge = DateTime.MinValue); + } + + /// + public async Task GetVote(long entryId) + { + await _lock.WaitAsync(); + + try + { + if (_cacheAge < DateTime.UtcNow.AddMinutes(-15)) + { + _cache.Clear(); + IOperationResult result = await _client.GetVotes.ExecuteAsync(); + if (result.Data?.Votes != null) + foreach (IGetVotes_Votes vote in result.Data.Votes) + _cache.Add(vote.EntryId, vote.Upvote); + _cacheAge = DateTime.UtcNow; + } + + return _cache.TryGetValue(entryId, out bool upvote) ? upvote : null; + } + finally + { + _lock.Release(); + } + } + + /// + public async Task CastVote(long entryId, bool upvote) + { + await _lock.WaitAsync(); + + try + { + IOperationResult result = await _client.CastVote.ExecuteAsync(new CastVoteInput {EntryId = entryId, Upvote = upvote}); + if (result.IsSuccessResult() && result.Data?.CastVote != null) + _cache[entryId] = upvote; + + return result.Data?.CastVote; + } + finally + { + _lock.Release(); + } + } + + /// + public async Task ClearVote(long entryId) + { + await _lock.WaitAsync(); + + try + { + IOperationResult result = await _client.ClearVote.ExecuteAsync(entryId); + if (result.IsSuccessResult() && result.Data?.ClearVote != null) + _cache.Remove(entryId); + + return result.Data?.ClearVote; + } + finally + { + _lock.Release(); + } + } +} + +public interface IVoteClient +{ + /// + /// Gets the vote status for a specific entry. + /// + /// The ID of the entry + /// A Task containing the vote status. + Task GetVote(long entryId); + + /// + /// Casts a vote for a specific entry. + /// + /// The ID of the entry. + /// A boolean indicating whether the vote is an upvote. + /// A Task containing the cast vote. + Task CastVote(long entryId, bool upvote); + + /// + /// Clears a vote for a specific entry. + /// + /// The ID of the entry + /// A Task containing the vote status. + Task ClearVote(long entryId); +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs index 10807064c..907ddb843 100644 --- a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs +++ b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs @@ -2,10 +2,10 @@ namespace Artemis.WebClient.Workshop; public static class WorkshopConstants { - // public const string AUTHORITY_URL = "https://localhost:5001"; - // public const string WORKSHOP_URL = "https://localhost:7281"; - public const string AUTHORITY_URL = "https://identity.artemis-rgb.com"; - public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com"; + public const string AUTHORITY_URL = "https://localhost:5001"; + public const string WORKSHOP_URL = "https://localhost:7281"; + // public const string AUTHORITY_URL = "https://identity.artemis-rgb.com"; + // public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com"; public const string IDENTITY_CLIENT_NAME = "IdentityApiClient"; public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient"; } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/graphql.config.yml b/src/Artemis.WebClient.Workshop/graphql.config.yml index 9662a514f..4e6e409e5 100644 --- a/src/Artemis.WebClient.Workshop/graphql.config.yml +++ b/src/Artemis.WebClient.Workshop/graphql.config.yml @@ -2,7 +2,7 @@ schema: schema.graphql extensions: endpoints: Default GraphQL Endpoint: - url: https://workshop.artemis-rgb.com/graphql + url: https://localhost:7281/graphql/ headers: user-agent: JS GraphQL introspect: true diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql index 12317cc59..e14597edf 100644 --- a/src/Artemis.WebClient.Workshop/schema.graphql +++ b/src/Artemis.WebClient.Workshop/schema.graphql @@ -56,11 +56,13 @@ type Entry { dependantReleases: [Release!]! description: String! downloads: Long! + downvoteCount: Int! entryType: EntryType! icon: Image iconId: UUID id: Long! images: [Image!]! + isDefault: Boolean! isOfficial: Boolean! latestRelease: Release latestReleaseId: Long @@ -68,8 +70,10 @@ type Entry { name: String! pluginInfo: PluginInfo releases: [Release!]! + score: Int! summary: String! tags: [Tag!]! + upvoteCount: Int! } type Image { @@ -99,6 +103,8 @@ type LayoutInfo { type Mutation { addEntry(input: CreateEntryInput!): Entry addLayoutInfo(input: CreateLayoutInfoInput!): LayoutInfo + castVote(input: CastVoteInput!): Entry + clearVote(entryId: Long!): Entry removeEntry(id: Long!): Entry removeLayoutInfo(id: Long!): LayoutInfo! removeRelease(id: Long!): Release! @@ -168,6 +174,7 @@ type Query { searchKeyboardLayout(deviceProvider: UUID!, logicalLayout: String, model: String!, physicalLayout: KeyboardLayoutType!, vendor: String!): LayoutInfo searchLayout(deviceProvider: UUID!, deviceType: RGBDeviceType!, model: String!, vendor: String!): LayoutInfo submittedEntries(order: [EntrySortInput!], where: EntryFilterInput): [Entry!]! + votes(order: [VoteSortInput!], where: VoteFilterInput): [Vote!]! } type Release { @@ -188,6 +195,15 @@ type Tag { name: String! } +type Vote { + entry: Entry! + entryId: Long! + id: Long! + upvote: Boolean! + userId: UUID! + votedAt: DateTime! +} + enum ApplyPolicy { AFTER_RESOLVER BEFORE_RESOLVER @@ -250,6 +266,11 @@ input BooleanOperationFilterInput { neq: Boolean } +input CastVoteInput { + entryId: Long! + upvote: Boolean! +} + input CategoryFilterInput { and: [CategoryFilterInput!] icon: StringOperationFilterInput @@ -268,6 +289,7 @@ input CreateEntryInput { categories: [Long!]! description: String! entryType: EntryType! + isDefault: Boolean! name: String! summary: String! tags: [String!]! @@ -307,11 +329,13 @@ input EntryFilterInput { dependantReleases: ListFilterInputTypeOfReleaseFilterInput description: StringOperationFilterInput downloads: LongOperationFilterInput + downvoteCount: IntOperationFilterInput entryType: EntryTypeOperationFilterInput icon: ImageFilterInput iconId: UuidOperationFilterInput id: LongOperationFilterInput images: ListFilterInputTypeOfImageFilterInput + isDefault: BooleanOperationFilterInput isOfficial: BooleanOperationFilterInput latestRelease: ReleaseFilterInput latestReleaseId: LongOperationFilterInput @@ -320,8 +344,10 @@ input EntryFilterInput { or: [EntryFilterInput!] pluginInfo: PluginInfoFilterInput releases: ListFilterInputTypeOfReleaseFilterInput + score: IntOperationFilterInput summary: StringOperationFilterInput tags: ListFilterInputTypeOfTagFilterInput + upvoteCount: IntOperationFilterInput } input EntrySortInput { @@ -330,16 +356,20 @@ input EntrySortInput { createdAt: SortEnumType description: SortEnumType downloads: SortEnumType + downvoteCount: SortEnumType entryType: SortEnumType icon: ImageSortInput iconId: SortEnumType id: SortEnumType + isDefault: SortEnumType isOfficial: SortEnumType latestRelease: ReleaseSortInput latestReleaseId: SortEnumType name: SortEnumType pluginInfo: PluginInfoSortInput + score: SortEnumType summary: SortEnumType + upvoteCount: SortEnumType } input EntryTypeOperationFilterInput { @@ -580,6 +610,7 @@ input UpdateEntryInput { categories: [Long!]! description: String! id: Long! + isDefault: Boolean! name: String! summary: String! tags: [String!]! @@ -604,3 +635,23 @@ input UuidOperationFilterInput { nlt: UUID nlte: UUID } + +input VoteFilterInput { + and: [VoteFilterInput!] + entry: EntryFilterInput + entryId: LongOperationFilterInput + id: LongOperationFilterInput + or: [VoteFilterInput!] + upvote: BooleanOperationFilterInput + userId: UuidOperationFilterInput + votedAt: DateTimeOperationFilterInput +} + +input VoteSortInput { + entry: EntrySortInput + entryId: SortEnumType + id: SortEnumType + upvote: SortEnumType + userId: SortEnumType + votedAt: SortEnumType +} From ec5fbba87cbc1b0700589b1780c996b4a0476a2a Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 24 Jul 2024 22:07:58 +0200 Subject: [PATCH 4/9] Workshop - Revert voting changes, was fun to try but not useful enough --- .../Workshop/Entries/EntryVoteView.axaml | 65 ----------- .../Workshop/Entries/EntryVoteView.axaml.cs | 14 --- .../Workshop/Entries/EntryVoteViewModel.cs | 91 --------------- .../Entries/List/EntryListInputView.axaml | 1 - .../Entries/List/EntryListItemView.axaml | 14 +-- .../Entries/List/EntryListItemViewModel.cs | 9 +- .../Entries/List/EntryListViewModel.cs | 3 - .../Library/Tabs/InstalledTabItemView.axaml | 16 ++- .../Library/Tabs/InstalledTabItemViewModel.cs | 16 +-- .../DryIoc/ContainerExtensions.cs | 1 - .../Mutations/CastVote.graphql | 16 --- .../Mutations/CreateEntry.graphql | 2 +- .../Queries/Fragments.graphql | 4 - .../Queries/GetVotes.graphql | 6 - .../Services/VoteClient.cs | 105 ------------------ .../WorkshopConstants.cs | 8 +- .../graphql.config.yml | 2 +- src/Artemis.WebClient.Workshop/schema.graphql | 51 --------- 18 files changed, 29 insertions(+), 395 deletions(-) delete mode 100644 src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml delete mode 100644 src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml.cs delete mode 100644 src/Artemis.UI/Screens/Workshop/Entries/EntryVoteViewModel.cs delete mode 100644 src/Artemis.WebClient.Workshop/Mutations/CastVote.graphql delete mode 100644 src/Artemis.WebClient.Workshop/Queries/GetVotes.graphql delete mode 100644 src/Artemis.WebClient.Workshop/Services/VoteClient.cs diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml deleted file mode 100644 index 113a21d96..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml.cs deleted file mode 100644 index 5c131be6a..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteView.axaml.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using Avalonia.ReactiveUI; - -namespace Artemis.UI.Screens.Workshop.Entries; - -public partial class EntryVoteView : ReactiveUserControl -{ - public EntryVoteView() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteViewModel.cs deleted file mode 100644 index 14e449974..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryVoteViewModel.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Reactive.Disposables; -using System.Threading.Tasks; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.Builders; -using Artemis.WebClient.Workshop; -using Artemis.WebClient.Workshop.Services; -using PropertyChanged.SourceGenerator; -using ReactiveUI; - -namespace Artemis.UI.Screens.Workshop.Entries; - -public partial class EntryVoteViewModel : ActivatableViewModelBase -{ - private readonly IEntrySummary _entry; - private readonly INotificationService _notificationService; - private readonly IVoteClient _voteClient; - private bool _voting; - - [Notify] private int _score; - [Notify] private bool _upvoted; - [Notify] private bool _downvoted; - - public EntryVoteViewModel(IEntrySummary entry, IAuthenticationService authenticationService, INotificationService notificationService, IVoteClient voteClient) - { - _entry = entry; - _notificationService = notificationService; - _voteClient = voteClient; - - IsLoggedIn = authenticationService.IsLoggedIn; - Score = entry.UpvoteCount - entry.DownvoteCount; - this.WhenActivated(d => IsLoggedIn.Subscribe(l => _ = GetVoteStatus(l)).DisposeWith(d)); - } - - public IObservable IsLoggedIn { get; } - - public async Task CastVote(bool upvote) - { - // Could use a ReactiveCommand to achieve the same thing but that disables the button - // while executing which grays it out for a fraction of a second and looks bad - if (_voting) - return; - - _voting = true; - try - { - IVoteCount? result; - // If the vote was removed, reset the upvote/downvote state - if ((Upvoted && upvote) || (Downvoted && !upvote)) - { - result = await _voteClient.ClearVote(_entry.Id); - Upvoted = false; - Downvoted = false; - } - else - { - result = await _voteClient.CastVote(_entry.Id, upvote); - Upvoted = upvote; - Downvoted = !upvote; - } - - if (result != null) - Score = result.UpvoteCount - result.DownvoteCount; - else - _notificationService.CreateNotification().WithTitle("Failed to cast vote").WithMessage("Please try again later.").WithSeverity(NotificationSeverity.Error).Show(); - } - finally - { - _voting = false; - } - } - - private async Task GetVoteStatus(bool isLoggedIn) - { - if (!isLoggedIn) - { - Upvoted = false; - Downvoted = false; - } - else - { - bool? vote = await _voteClient.GetVote(_entry.Id); - if (vote != null) - { - Upvoted = vote.Value; - Downvoted = !vote.Value; - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml index 476ae9bd2..8fdc91d29 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml @@ -21,7 +21,6 @@ Recently updated Recently added Download count - Score diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml index 7473034d3..cae7a1934 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml @@ -3,7 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:entries1="clr-namespace:Artemis.UI.Screens.Workshop.Entries" xmlns:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:list="clr-namespace:Artemis.UI.Screens.Workshop.Entries.List" @@ -21,12 +20,9 @@ HorizontalContentAlignment="Stretch" Command="{CompiledBinding NavigateToEntry}" IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}"> - - - - + - - + @@ -82,7 +78,7 @@ - + @@ -92,7 +88,7 @@ - + installed diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs index a02325bc4..d129f9be6 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive; using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.UI.Shared; @@ -17,12 +18,12 @@ public partial class EntryListItemViewModel : ActivatableViewModelBase [Notify] private bool _isInstalled; [Notify] private bool _updateAvailable; - public EntryListItemViewModel(IEntrySummary entry, IRouter router, IWorkshopService workshopService, Func getEntryVoteViewModel) + public EntryListItemViewModel(IEntrySummary entry, IRouter router, IWorkshopService workshopService) { _router = router; Entry = entry; - VoteViewModel = getEntryVoteViewModel(entry); + NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry); this.WhenActivated((CompositeDisposable _) => { @@ -33,9 +34,9 @@ public EntryListItemViewModel(IEntrySummary entry, IRouter router, IWorkshopServ } public IEntrySummary Entry { get; } - public EntryVoteViewModel VoteViewModel { get; } + public ReactiveCommand NavigateToEntry { get; } - public async Task NavigateToEntry() + private async Task ExecuteNavigateToEntry() { switch (Entry.EntryType) { diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs index 48388aeb5..a6b5309c7 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs @@ -137,9 +137,6 @@ private IReadOnlyList GetSort() if (InputViewModel.SortBy == 2) return new[] {new EntrySortInput {Downloads = SortEnumType.Desc}}; - // Sort by score - if (InputViewModel.SortBy == 3) - return new[] {new EntrySortInput {Score = SortEnumType.Desc}}; // Sort by latest release, then by created at return new[] diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml index b8347c3df..71ab5983c 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml @@ -11,6 +11,7 @@ x:DataType="tabs:InstalledTabItemViewModel"> + diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs index 75a58ce75..6307a8557 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs @@ -10,7 +10,6 @@ using Artemis.UI.DryIoc.Factories; using Artemis.UI.Extensions; using Artemis.UI.Screens.Plugins; -using Artemis.UI.Screens.Workshop.Entries; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; @@ -36,7 +35,6 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase [Notify] private bool _updateAvailable; [Notify] private bool _autoUpdate; - [Notify] private EntryVoteViewModel _voteViewModel; public InstalledTabItemViewModel(InstalledEntry entry, IWorkshopClient client, @@ -45,8 +43,7 @@ public InstalledTabItemViewModel(InstalledEntry entry, IRouter router, IWindowService windowService, IPluginManagementService pluginManagementService, - ISettingsVmFactory settingsVmFactory, - Func getEntryVoteViewModel) + ISettingsVmFactory settingsVmFactory) { _client = client; _workshopService = workshopService; @@ -56,9 +53,9 @@ public InstalledTabItemViewModel(InstalledEntry entry, _pluginManagementService = pluginManagementService; _settingsVmFactory = settingsVmFactory; _autoUpdate = entry.AutoUpdate; - + Entry = entry; - + this.WhenActivatedAsync(async _ => { // Grab the latest entry summary from the workshop @@ -68,7 +65,6 @@ public InstalledTabItemViewModel(InstalledEntry entry, if (entrySummary.Data?.Entry != null) { Entry.ApplyEntrySummary(entrySummary.Data.Entry); - VoteViewModel = getEntryVoteViewModel(entrySummary.Data.Entry); _workshopService.SaveInstalledEntry(Entry); } } @@ -77,7 +73,7 @@ public InstalledTabItemViewModel(InstalledEntry entry, UpdateAvailable = Entry.ReleaseId != Entry.LatestReleaseId; } }); - + this.WhenAnyValue(vm => vm.AutoUpdate).Skip(1).Subscribe(_ => AutoUpdateToggled()); } @@ -126,10 +122,10 @@ private async Task UninstallPluginPrerequisites() private void AutoUpdateToggled() { _workshopService.SetAutoUpdate(Entry, AutoUpdate); - + if (!AutoUpdate) return; - + Task.Run(async () => { await _workshopUpdateService.AutoUpdateEntry(Entry); diff --git a/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs b/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs index dc5a12d67..bebc8caf1 100644 --- a/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs @@ -53,7 +53,6 @@ public static void RegisterWorkshopClient(this IContainer container) container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); - container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); container.Register(); diff --git a/src/Artemis.WebClient.Workshop/Mutations/CastVote.graphql b/src/Artemis.WebClient.Workshop/Mutations/CastVote.graphql deleted file mode 100644 index 22dbd80d8..000000000 --- a/src/Artemis.WebClient.Workshop/Mutations/CastVote.graphql +++ /dev/null @@ -1,16 +0,0 @@ -mutation CastVote($input: CastVoteInput!) { - castVote(input: $input) { - ...voteCount - } -} - -mutation ClearVote($entryId: Long!) { - clearVote(entryId: $entryId) { - ...voteCount - } -} - -fragment voteCount on Entry { - upvoteCount - downvoteCount -} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Mutations/CreateEntry.graphql b/src/Artemis.WebClient.Workshop/Mutations/CreateEntry.graphql index b4b621356..7191bc704 100644 --- a/src/Artemis.WebClient.Workshop/Mutations/CreateEntry.graphql +++ b/src/Artemis.WebClient.Workshop/Mutations/CreateEntry.graphql @@ -2,4 +2,4 @@ mutation AddEntry ($input: CreateEntryInput!) { addEntry(input: $input) { id } -} \ No newline at end of file +} diff --git a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql index ba23037d5..cf0c3c779 100644 --- a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql @@ -36,8 +36,6 @@ fragment entrySummary on Entry { summary entryType downloads - upvoteCount - downvoteCount createdAt latestReleaseId categories { @@ -53,8 +51,6 @@ fragment entryDetails on Entry { summary entryType downloads - upvoteCount - downvoteCount createdAt description categories { diff --git a/src/Artemis.WebClient.Workshop/Queries/GetVotes.graphql b/src/Artemis.WebClient.Workshop/Queries/GetVotes.graphql deleted file mode 100644 index e671b6e7d..000000000 --- a/src/Artemis.WebClient.Workshop/Queries/GetVotes.graphql +++ /dev/null @@ -1,6 +0,0 @@ -query GetVotes { - votes { - entryId - upvote - } -} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/VoteClient.cs b/src/Artemis.WebClient.Workshop/Services/VoteClient.cs deleted file mode 100644 index 7c4ba8b9f..000000000 --- a/src/Artemis.WebClient.Workshop/Services/VoteClient.cs +++ /dev/null @@ -1,105 +0,0 @@ -using StrawberryShake; - -namespace Artemis.WebClient.Workshop.Services; - -public class VoteClient : IVoteClient -{ - private readonly Dictionary _cache = new(); - private readonly IWorkshopClient _client; - private readonly SemaphoreSlim _lock = new(1, 1); - private DateTime _cacheAge = DateTime.MinValue; - - public VoteClient(IWorkshopClient client, IAuthenticationService authenticationService) - { - _client = client; - authenticationService.IsLoggedIn.Subscribe(_ => _cacheAge = DateTime.MinValue); - } - - /// - public async Task GetVote(long entryId) - { - await _lock.WaitAsync(); - - try - { - if (_cacheAge < DateTime.UtcNow.AddMinutes(-15)) - { - _cache.Clear(); - IOperationResult result = await _client.GetVotes.ExecuteAsync(); - if (result.Data?.Votes != null) - foreach (IGetVotes_Votes vote in result.Data.Votes) - _cache.Add(vote.EntryId, vote.Upvote); - _cacheAge = DateTime.UtcNow; - } - - return _cache.TryGetValue(entryId, out bool upvote) ? upvote : null; - } - finally - { - _lock.Release(); - } - } - - /// - public async Task CastVote(long entryId, bool upvote) - { - await _lock.WaitAsync(); - - try - { - IOperationResult result = await _client.CastVote.ExecuteAsync(new CastVoteInput {EntryId = entryId, Upvote = upvote}); - if (result.IsSuccessResult() && result.Data?.CastVote != null) - _cache[entryId] = upvote; - - return result.Data?.CastVote; - } - finally - { - _lock.Release(); - } - } - - /// - public async Task ClearVote(long entryId) - { - await _lock.WaitAsync(); - - try - { - IOperationResult result = await _client.ClearVote.ExecuteAsync(entryId); - if (result.IsSuccessResult() && result.Data?.ClearVote != null) - _cache.Remove(entryId); - - return result.Data?.ClearVote; - } - finally - { - _lock.Release(); - } - } -} - -public interface IVoteClient -{ - /// - /// Gets the vote status for a specific entry. - /// - /// The ID of the entry - /// A Task containing the vote status. - Task GetVote(long entryId); - - /// - /// Casts a vote for a specific entry. - /// - /// The ID of the entry. - /// A boolean indicating whether the vote is an upvote. - /// A Task containing the cast vote. - Task CastVote(long entryId, bool upvote); - - /// - /// Clears a vote for a specific entry. - /// - /// The ID of the entry - /// A Task containing the vote status. - Task ClearVote(long entryId); -} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs index 907ddb843..10807064c 100644 --- a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs +++ b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs @@ -2,10 +2,10 @@ namespace Artemis.WebClient.Workshop; public static class WorkshopConstants { - public const string AUTHORITY_URL = "https://localhost:5001"; - public const string WORKSHOP_URL = "https://localhost:7281"; - // public const string AUTHORITY_URL = "https://identity.artemis-rgb.com"; - // public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com"; + // public const string AUTHORITY_URL = "https://localhost:5001"; + // public const string WORKSHOP_URL = "https://localhost:7281"; + public const string AUTHORITY_URL = "https://identity.artemis-rgb.com"; + public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com"; public const string IDENTITY_CLIENT_NAME = "IdentityApiClient"; public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient"; } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/graphql.config.yml b/src/Artemis.WebClient.Workshop/graphql.config.yml index 4e6e409e5..9662a514f 100644 --- a/src/Artemis.WebClient.Workshop/graphql.config.yml +++ b/src/Artemis.WebClient.Workshop/graphql.config.yml @@ -2,7 +2,7 @@ schema: schema.graphql extensions: endpoints: Default GraphQL Endpoint: - url: https://localhost:7281/graphql/ + url: https://workshop.artemis-rgb.com/graphql headers: user-agent: JS GraphQL introspect: true diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql index e14597edf..12317cc59 100644 --- a/src/Artemis.WebClient.Workshop/schema.graphql +++ b/src/Artemis.WebClient.Workshop/schema.graphql @@ -56,13 +56,11 @@ type Entry { dependantReleases: [Release!]! description: String! downloads: Long! - downvoteCount: Int! entryType: EntryType! icon: Image iconId: UUID id: Long! images: [Image!]! - isDefault: Boolean! isOfficial: Boolean! latestRelease: Release latestReleaseId: Long @@ -70,10 +68,8 @@ type Entry { name: String! pluginInfo: PluginInfo releases: [Release!]! - score: Int! summary: String! tags: [Tag!]! - upvoteCount: Int! } type Image { @@ -103,8 +99,6 @@ type LayoutInfo { type Mutation { addEntry(input: CreateEntryInput!): Entry addLayoutInfo(input: CreateLayoutInfoInput!): LayoutInfo - castVote(input: CastVoteInput!): Entry - clearVote(entryId: Long!): Entry removeEntry(id: Long!): Entry removeLayoutInfo(id: Long!): LayoutInfo! removeRelease(id: Long!): Release! @@ -174,7 +168,6 @@ type Query { searchKeyboardLayout(deviceProvider: UUID!, logicalLayout: String, model: String!, physicalLayout: KeyboardLayoutType!, vendor: String!): LayoutInfo searchLayout(deviceProvider: UUID!, deviceType: RGBDeviceType!, model: String!, vendor: String!): LayoutInfo submittedEntries(order: [EntrySortInput!], where: EntryFilterInput): [Entry!]! - votes(order: [VoteSortInput!], where: VoteFilterInput): [Vote!]! } type Release { @@ -195,15 +188,6 @@ type Tag { name: String! } -type Vote { - entry: Entry! - entryId: Long! - id: Long! - upvote: Boolean! - userId: UUID! - votedAt: DateTime! -} - enum ApplyPolicy { AFTER_RESOLVER BEFORE_RESOLVER @@ -266,11 +250,6 @@ input BooleanOperationFilterInput { neq: Boolean } -input CastVoteInput { - entryId: Long! - upvote: Boolean! -} - input CategoryFilterInput { and: [CategoryFilterInput!] icon: StringOperationFilterInput @@ -289,7 +268,6 @@ input CreateEntryInput { categories: [Long!]! description: String! entryType: EntryType! - isDefault: Boolean! name: String! summary: String! tags: [String!]! @@ -329,13 +307,11 @@ input EntryFilterInput { dependantReleases: ListFilterInputTypeOfReleaseFilterInput description: StringOperationFilterInput downloads: LongOperationFilterInput - downvoteCount: IntOperationFilterInput entryType: EntryTypeOperationFilterInput icon: ImageFilterInput iconId: UuidOperationFilterInput id: LongOperationFilterInput images: ListFilterInputTypeOfImageFilterInput - isDefault: BooleanOperationFilterInput isOfficial: BooleanOperationFilterInput latestRelease: ReleaseFilterInput latestReleaseId: LongOperationFilterInput @@ -344,10 +320,8 @@ input EntryFilterInput { or: [EntryFilterInput!] pluginInfo: PluginInfoFilterInput releases: ListFilterInputTypeOfReleaseFilterInput - score: IntOperationFilterInput summary: StringOperationFilterInput tags: ListFilterInputTypeOfTagFilterInput - upvoteCount: IntOperationFilterInput } input EntrySortInput { @@ -356,20 +330,16 @@ input EntrySortInput { createdAt: SortEnumType description: SortEnumType downloads: SortEnumType - downvoteCount: SortEnumType entryType: SortEnumType icon: ImageSortInput iconId: SortEnumType id: SortEnumType - isDefault: SortEnumType isOfficial: SortEnumType latestRelease: ReleaseSortInput latestReleaseId: SortEnumType name: SortEnumType pluginInfo: PluginInfoSortInput - score: SortEnumType summary: SortEnumType - upvoteCount: SortEnumType } input EntryTypeOperationFilterInput { @@ -610,7 +580,6 @@ input UpdateEntryInput { categories: [Long!]! description: String! id: Long! - isDefault: Boolean! name: String! summary: String! tags: [String!]! @@ -635,23 +604,3 @@ input UuidOperationFilterInput { nlt: UUID nlte: UUID } - -input VoteFilterInput { - and: [VoteFilterInput!] - entry: EntryFilterInput - entryId: LongOperationFilterInput - id: LongOperationFilterInput - or: [VoteFilterInput!] - upvote: BooleanOperationFilterInput - userId: UuidOperationFilterInput - votedAt: DateTimeOperationFilterInput -} - -input VoteSortInput { - entry: EntrySortInput - entryId: SortEnumType - id: SortEnumType - upvote: SortEnumType - userId: SortEnumType - votedAt: SortEnumType -} From bedf1f5f3857357bacad97a81dc5868a86350d9d Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Sat, 10 Aug 2024 22:49:45 +0200 Subject: [PATCH 5/9] Bumped System-Text.Json to 8.0.4 --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 9c07752b6..e56abfa8e 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -59,7 +59,7 @@ - + \ No newline at end of file From 907c758b830bc534d98b09734bdcc35968ec1b41 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 23 Sep 2024 22:17:46 +0200 Subject: [PATCH 6/9] Meta - Update packages --- .../Styles/BrokenState.axaml | 9 +-- src/Artemis.UI.Shared/Styles/Button.axaml | 50 ++++++------ .../Styles/Controls/DataModelPicker.axaml | 2 +- .../Controls/SplitMarkdownEditor.axaml | 2 +- .../Performance/PerformanceDebugView.axaml | 4 +- .../Tabs/Layout/DeviceLayoutTabView.axaml | 2 +- src/Artemis.UI/Screens/Home/HomeView.axaml | 29 ++++--- .../Plugins/PluginSettingsWindowView.axaml | 14 ++-- .../Screens/Plugins/PluginView.axaml | 16 ++-- .../Dialogs/LayerHintsDialogView.axaml | 4 +- .../ProfileTree/FolderTreeItemView.axaml | 4 +- .../ProfileTree/LayerTreeItemView.axaml | 4 +- .../DataBinding/DataBindingView.axaml | 4 +- .../Screens/Settings/Tabs/AboutTabView.axaml | 76 +++++++++---------- .../Settings/Tabs/PluginsTabView.axaml | 4 +- .../Settings/Tabs/ReleasesTabView.axaml | 4 +- .../Screens/Sidebar/SidebarView.axaml | 20 ++--- .../StartupWizard/Steps/FinishStep.axaml | 16 ++-- .../StartupWizard/Steps/WelcomeStep.axaml | 16 ++-- .../NodeScriptWindowView.axaml | 4 +- .../Screens/VisualScripting/NodeView.axaml | 8 +- .../CurrentUser/CurrentUserView.axaml | 8 +- .../List/EntryListItemVerticalView.axaml | 76 +++++++++++++++++++ .../List/EntryListItemVerticalView.axaml.cs | 14 ++++ .../List/EntryListItemVerticalViewModel.cs | 13 ++++ .../Workshop/Home/WorkshopHomeView.axaml | 32 ++++++-- .../Workshop/Home/WorkshopHomeViewModel.cs | 43 ++++++++++- .../Library/SubmissionManagementView.axaml | 4 +- .../SubmissionWizard/ReleaseWizardView.axaml | 4 +- .../Steps/Layout/LayoutInfoStepView.axaml | 4 +- .../ProfileAdaptionHintsStepView.axaml | 4 +- .../SubmissionWizardView.axaml | 4 +- src/Artemis.UI/Styles/Artemis.axaml | 2 +- .../Queries/GetEntries.graphql | 17 ++--- src/Artemis.WebClient.Workshop/schema.graphql | 27 ++++++- src/Artemis.sln.DotSettings | 1 + src/Directory.Packages.props | 42 +++++----- 37 files changed, 382 insertions(+), 205 deletions(-) create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalViewModel.cs diff --git a/src/Artemis.UI.Shared/Styles/BrokenState.axaml b/src/Artemis.UI.Shared/Styles/BrokenState.axaml index 65ef465c0..c28d4bca2 100644 --- a/src/Artemis.UI.Shared/Styles/BrokenState.axaml +++ b/src/Artemis.UI.Shared/Styles/BrokenState.axaml @@ -3,18 +3,17 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"> - - + - + - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Styles/Button.axaml b/src/Artemis.UI.Shared/Styles/Button.axaml index 12863d150..41b7529c0 100644 --- a/src/Artemis.UI.Shared/Styles/Button.axaml +++ b/src/Artemis.UI.Shared/Styles/Button.axaml @@ -25,14 +25,14 @@ HyperlinkButton.icon-button - + - + HyperlinkButton.icon-button icon-button-small - + - + Button.window-button @@ -93,7 +93,7 @@ - @@ -113,24 +113,24 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml b/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml index 18df7201a..d4cd0f731 100644 --- a/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml +++ b/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml @@ -51,7 +51,7 @@ RowDefinitions="*,*"> Welcome to the data model picker Select a value from the data model below - Learn more + Learn more diff --git a/src/Artemis.UI/Controls/SplitMarkdownEditor.axaml b/src/Artemis.UI/Controls/SplitMarkdownEditor.axaml index 50f2d0dc2..a84b7d6ed 100644 --- a/src/Artemis.UI/Controls/SplitMarkdownEditor.axaml +++ b/src/Artemis.UI/Controls/SplitMarkdownEditor.axaml @@ -17,7 +17,7 @@ - + JetBrains Profiling Guide - + diff --git a/src/Artemis.UI/Screens/Device/Tabs/Layout/DeviceLayoutTabView.axaml b/src/Artemis.UI/Screens/Device/Tabs/Layout/DeviceLayoutTabView.axaml index 5b4ffc40f..cf1a9027c 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/Layout/DeviceLayoutTabView.axaml +++ b/src/Artemis.UI/Screens/Device/Tabs/Layout/DeviceLayoutTabView.axaml @@ -104,7 +104,7 @@ - Plugins you can find your currently installed plugins, these default plugins are created by Artemis developers. We're also keeping track of a list of third-party plugins on our wiki. - - + + - + Get more plugins - + @@ -68,30 +67,30 @@ - + GitHub - - + + Website - - + + Discord - - + + E-mail - + @@ -106,7 +105,7 @@ - @@ -114,7 +113,7 @@ Donate - + - - - + - - + - + - - - - + - - + - - + - + - Learn more about adaption hints - + Add hints below to help decide where to place this layer when the profile is imported. diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml index a29cd584d..9ec7e488c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml @@ -9,14 +9,14 @@ x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.FolderTreeItemView" x:DataType="profileTree:FolderTreeItemViewModel"> - - + - - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingView.axaml index 097ca69a0..7ea182b5f 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/DataBinding/DataBindingView.axaml @@ -42,11 +42,11 @@ When you enable data bindings you can no longer use keyframes or normal values for this property. - Learn more - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml index 70b2bc390..f81713c04 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml @@ -25,15 +25,15 @@ - + - - + + - - + + - + - PolyForm Noncommercial License 1.0.0 - + @@ -65,9 +65,9 @@ Project owner, main contributor - + - + @@ -86,9 +86,9 @@ RGB.NET, main contributor - + - + @@ -107,9 +107,9 @@ Main contributor - + - + @@ -128,9 +128,9 @@ Graphics design - + - + @@ -185,39 +185,39 @@ SQLite - + https://avaloniaui.net/ - - + + https://github.com/dadhi/DryIoc - - + + https://learn.microsoft.com/en-us/ef/core/ - - + + https://github.com/amwx/FluentAvalonia - - + + https://unosquare.github.io/embedio/ - - + + https://github.com/Humanizr/Humanizer - - + + https://github.com/natemcmaster/DotNetCorePlugins - - + + https://github.com/DarthAffe/RGB.NET - - + + https://serilog.net/ - - + + https://github.com/mono/SkiaSharp - - + + https://www.sqlite.org/ - + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml index 38c86d765..b9377f61b 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml @@ -17,9 +17,9 @@ - + Get more plugins - + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml index e03339283..c4ff90b0c 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml @@ -29,10 +29,10 @@ TextWrapping="Wrap" Text="{CompiledBinding Channel, StringFormat='Found no releases for the \'{0}\' channel.'}"> - Learn more about channels on the wiki - + diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarView.axaml b/src/Artemis.UI/Screens/Sidebar/SidebarView.axaml index c02ad3d60..454becd6b 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarView.axaml +++ b/src/Artemis.UI/Screens/Sidebar/SidebarView.axaml @@ -54,7 +54,7 @@ - - - + - - + - - + - - + - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml index 1e80427c2..419e96112 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml @@ -33,18 +33,18 @@ Discord - + https://wiki.artemis-rgb.com/ - - + + https://wiki.artemis-rgb.com/en/guides/user/introduction - - + + https://github.com/Artemis-RGB/Artemis - - + + https://discord.gg/S3MVaC9 - + diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml index 52e5802dd..6baaee4f4 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml @@ -16,15 +16,15 @@ - + - - + + - - + + - + - PolyForm Noncommercial License 1.0.0 - + diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml index 8be42a735..82a8787ae 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml @@ -169,13 +169,13 @@ - Learn more about visual scripts - + diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml b/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml index ea4dc37ad..00b1402bb 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml +++ b/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml @@ -39,18 +39,18 @@ Background="{DynamicResource ContentDialogBackground}"> - - + - - + diff --git a/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserView.axaml b/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserView.axaml index eafe6dd46..eea731a15 100644 --- a/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserView.axaml +++ b/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserView.axaml @@ -43,7 +43,7 @@ - Manage account - - + Sign out - + diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalView.axaml new file mode 100644 index 000000000..c68bddd9d --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalView.axaml @@ -0,0 +1,76 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalView.axaml.cs new file mode 100644 index 000000000..9068394b0 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Entries.List; + +public partial class EntryListItemVerticalView : ReactiveUserControl +{ + public EntryListItemVerticalView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalViewModel.cs new file mode 100644 index 000000000..20e50bca4 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemVerticalViewModel.cs @@ -0,0 +1,13 @@ +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Services; + +namespace Artemis.UI.Screens.Workshop.Entries.List; + +public class EntryListItemVerticalViewModel : EntryListItemViewModel +{ + /// + public EntryListItemVerticalViewModel(IEntrySummary entry, IRouter router, IWorkshopService workshopService) : base(entry, router, workshopService) + { + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml index 4d535f0ba..d9cadbe3e 100644 --- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml @@ -10,7 +10,7 @@ - + - + - + - + - +