12 Commits

Author SHA1 Message Date
BlossomiShymae
309ac8e8f0 Merge branch 'main' into release 2024-08-23 22:53:40 -05:00
BlossomiShymae
943234464e Merge branch 'main' into release 2024-08-20 06:09:29 -05:00
BlossomiShymae
8681cd0b39 Merge branch 'main' into release 2024-08-19 05:15:51 -05:00
BlossomiShymae
e2bfac458d Merge branch 'main' into release 2024-08-17 16:26:58 -05:00
BlossomiShymae
9ba36a5d7e Merge branch 'main' into release 2024-08-16 15:05:02 -05:00
BlossomiShymae
9c0bbc2be0 Merge branch 'main' into release 2024-08-16 01:30:48 -05:00
BlossomiShymae
0d077e351d Merge branch 'main' into release 2024-08-15 17:32:45 -05:00
BlossomiShymae
1f20c5a286 Merge branch 'main' into release 2024-08-15 17:20:14 -05:00
BlossomiShymae
41e76c5d10 Merge branch 'main' into release 2024-08-13 02:46:50 -05:00
BlossomiShymae
38ebed976d Merge branch 'main' into release 2024-08-13 02:43:55 -05:00
BlossomiShymae
6875fbaa66 Merge branch 'main' into release 2024-08-13 02:41:11 -05:00
BlossomiShymae
0ddcfb47a6 Add update checker 2024-08-13 02:37:46 -05:00
154 changed files with 2011 additions and 6000 deletions

View File

@@ -1,12 +1,9 @@
# .github/workflows/release.yml # .github/workflows/release.yml
on: on:
release: push:
types: [created] branches:
- release
env:
PROJECT_NAME: Needlework.Net
ASSEMBLY_NAME: NeedleworkDotNet
jobs: jobs:
build: build:
@@ -18,15 +15,30 @@ jobs:
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.x
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Build
run: dotnet build ${{env.PROJECT_NAME}} -c Release
- name: Publish
run: dotnet publish ${{env.PROJECT_NAME}} -c Release -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -o publish -r win-x64 --self-contained=false
- name: Zip files
run: 7z a -tzip ${{env.ASSEMBLY_NAME}}-win-x64.zip ./Publish/* README.md LICENSE
- name: Upload to release
uses: softprops/action-gh-release@v1
if: ${{startsWith(github.ref, 'refs/tags/') }}
with: with:
files: ${{env.ASSEMBLY_NAME}}-win-x64.zip fetch-depth: 0
ref: release
- name: Build
run: dotnet build Needlework.Net -c Release
- name: Publish
run: dotnet publish Needlework.Net -c Release -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -o publish -r win-x64 --self-contained=false
- name: Get Version
id: version
shell: powershell
run: |
$xml=[xml](Get-Content .\Needlework.Net\Needlework.Net.csproj)
$ver=($xml.Project.PropertyGroup).AssemblyVersion
$ver="VERSION=$ver"
$ver=$ver -replace '\s',''
echo $ver >> $env:GITHUB_OUTPUT
- name: Zip Files
run: 7z a -tzip NeedleworkDotNet-win-x64.zip ./Publish/* README.md LICENSE
- name: Release
uses: softprops/action-gh-release@v1
with:
name: "Needlework.Net v${{ steps.version.outputs.VERSION }}"
prerelease: false
tag_name: "${{ steps.version.outputs.VERSION }}"
files: |
NeedleworkDotNet-win-x64.zip

5
.gitignore vendored
View File

@@ -34,7 +34,6 @@ bld/
[Oo]bj/ [Oo]bj/
[Ll]og/ [Ll]og/
[Ll]ogs/ [Ll]ogs/
[Dd]ata/
# Visual Studio 2015/2017 cache/options directory # Visual Studio 2015/2017 cache/options directory
.vs/ .vs/
@@ -483,7 +482,3 @@ $RECYCLE.BIN/
# Vim temporary swap files # Vim temporary swap files
*.swp *.swp
*.sqlite
*.sqlite-shm
*.sqlite-wal

View File

@@ -7,17 +7,20 @@
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
RequestedThemeVariant="Dark"> RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles> <Application.Styles>
<sty:FluentAvaloniaTheme PreferSystemTheme="False" PreferUserAccentColor="False" /> <sty:FluentAvaloniaTheme PreferSystemTheme="False" PreferUserAccentColor="False" />
<materialIcons:MaterialIconStyles /> <materialIcons:MaterialIconStyles />
<StyleInclude Source="Controls/Card.axaml"/> <StyleInclude Source="Controls/Card.axaml"/>
<StyleInclude Source="Controls/UserCard.axaml"/>
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" /> <StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/> <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
</Application.Styles> </Application.Styles>
<Application.Resources> <Application.Resources>
<converters:EnumerableToVisibility x:Key="EnumerableToVisibilityConverter"/> <converters:EnumerableBoolConverter x:Key="EnumerableBoolConverter"/>
<converters:NullableToVisibility x:Key="NullableToVisibilityConverter"/> <converters:NullBoolConverter x:Key="NullBoolConverter"/>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@@ -1,43 +1,18 @@
using Akavache;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Templates;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Needlework.Net.Constants; using Needlework.Net.ViewModels;
using Needlework.Net.Extensions; using Needlework.Net.Views;
using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages;
using Needlework.Net.Views.MainWindow;
using System; using System;
using System.Reactive.Linq;
using System.Text.Json; using System.Text.Json;
namespace Needlework.Net; namespace Needlework.Net;
public partial class App : Application, IEnableLogger public partial class App(IServiceProvider serviceProvider) : Application
{ {
private readonly IDataTemplate _viewLocator; private readonly IServiceProvider _serviceProvider = serviceProvider;
private readonly IBlobCache _blobCache;
private readonly PageFactory _pageFactory;
private readonly MainWindowViewModel _mainWindowViewModel;
public App(IServiceProvider serviceProvider)
{
_viewLocator = serviceProvider.GetRequiredService<IDataTemplate>();
_blobCache = serviceProvider.GetRequiredService<IBlobCache>();
_pageFactory = serviceProvider.GetRequiredService<PageFactory>();
_mainWindowViewModel = serviceProvider.GetRequiredService<MainWindowViewModel>();
this.Log()
.Debug("NeedleworkDotNet version: {Version}", AppInfo.Version);
this.Log()
.Debug("OS description: {Description}", System.Runtime.InteropServices.RuntimeInformation.OSDescription);
}
public static JsonSerializerOptions JsonSerializerOptions { get; } = new() public static JsonSerializerOptions JsonSerializerOptions { get; } = new()
{ {
@@ -51,7 +26,6 @@ public partial class App : Application, IEnableLogger
public override void Initialize() public override void Initialize()
{ {
DataTemplates.Add(_viewLocator);
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
@@ -59,15 +33,15 @@ public partial class App : Application, IEnableLogger
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow = new MainWindowView(_mainWindowViewModel, _pageFactory); desktop.MainWindow = new MainWindow()
MainWindow = desktop.MainWindow;
desktop.ShutdownRequested += (_, _) =>
{ {
_blobCache.Flush().Wait(); DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
_blobCache.Dispose();
}; };
MainWindow = desktop.MainWindow;
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
using System.Reflection;
namespace Needlework.Net.Constants
{
public static class AppInfo
{
public static readonly string Name = "Needlework.Net";
public static readonly string Version = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
}
}

View File

@@ -1,9 +0,0 @@
namespace Needlework.Net.Constants
{
public static class BlobCacheKeys
{
public static readonly string GithubLatestRelease = nameof(GithubLatestRelease);
public static readonly string AppSettings = nameof(AppSettings);
}
}

View File

@@ -1,11 +0,0 @@
namespace Needlework.Net.Constants
{
public static class FlurlClientKeys
{
public static readonly string GithubClient = nameof(GithubClient);
public static readonly string GithubUserContentClient = nameof(GithubUserContentClient);
public static readonly string Client = nameof(Client);
}
}

View File

@@ -1,9 +0,0 @@
using System;
namespace Needlework.Net.Constants
{
public static class Intervals
{
public static readonly TimeSpan CheckForUpdates = TimeSpan.FromMinutes(60);
}
}

View File

@@ -9,8 +9,8 @@
<!-- Set Defaults --> <!-- Set Defaults -->
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Padding="12" <Border Padding="16"
CornerRadius="4" CornerRadius="16,16,16,16"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"> Background="{DynamicResource CardBackgroundFillColorDefaultBrush}">
<ContentPresenter Content="{TemplateBinding Content}"/> <ContentPresenter Content="{TemplateBinding Content}"/>
</Border> </Border>

View File

@@ -1,71 +0,0 @@
<Styles 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:i="https://github.com/projektanker/icons.avalonia"
xmlns:controls="using:Needlework.Net.Controls">
<Design.PreviewWith>
<WrapPanel>
<controls:UserCard
Width="300"
Height="400"
UserImage="/Assets/Users/blossomishymae.png"
UserName="estrogen elf"
UserGithub="BlossomiShymae">
Needlework.Net is the .NET rewrite of Needlework. This tool was made to help others with LCU and Game Client development. Feel free to ask any questions
or help contribute to the project! Made with love. 💜
</controls:UserCard>
</WrapPanel>
</Design.PreviewWith>
<Style Selector="controls|UserCard">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<Border CornerRadius="4"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
Margin="0 50 0 0"
Padding="16 66 16 16">
<Grid RowDefinitions="auto,auto,auto"
ColumnDefinitions="*">
<TextBlock Grid.Row="0"
Grid.Column="0"
Theme="{StaticResource SubtitleTextBlockStyle}"
Margin="0 0 0 4"
Text="{TemplateBinding UserName}"/>
<Grid Grid.Row="1"
Grid.Column="0"
RowDefinitions="*"
ColumnDefinitions="auto,auto"
Margin="0 0 0 16">
<Button Grid.Row="0"
Grid.Column="0"
Theme="{StaticResource TransparentButton}"
FontSize="20"
Name="PART_GithubButton">
<i:Icon Value="fa-github"/>
</Button>
<TextBlock Grid.Row="0"
Grid.Column="1"
Margin="8 0 0 0"
VerticalAlignment="Center"
Text="{TemplateBinding UserGithub}"/>
</Grid>
<TextBlock Grid.Row="2"
Grid.Column="0"
TextWrapping="WrapWithOverflow"
Text="{TemplateBinding Content}"/>
</Grid>
</Border>
<Border CornerRadius="100"
Width="100"
Height="100"
Margin="{TemplateBinding UserImageMargin}"
ClipToBounds="True">
<Image Source="{TemplateBinding UserImage}"
RenderOptions.BitmapInterpolationMode="HighQuality"/>
</Border>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

View File

@@ -1,91 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Media;
using System.Diagnostics;
namespace Needlework.Net.Controls;
[TemplatePart("PART_GithubButton", typeof(Button))]
public partial class UserCard : ContentControl
{
private Button? _githubButton;
public UserCard()
{
UserImageMargin = new(0, !double.IsNaN(Height) ? 100 - Height : 0, 0, 0);
}
public static readonly StyledProperty<IImage?> UserImageProperty =
AvaloniaProperty.Register<UserCard, IImage?>(nameof(UserImage), defaultValue: null);
public IImage? UserImage
{
get { return GetValue(UserImageProperty); }
set { SetValue(UserImageProperty, value); }
}
public static readonly StyledProperty<string?> UserNameProperty =
AvaloniaProperty.Register<UserCard, string?>(nameof(UserName), defaultValue: null);
public string? UserName
{
get { return GetValue(UserNameProperty); }
set { SetValue(UserNameProperty, value); }
}
public static readonly StyledProperty<string?> UserGithubProperty =
AvaloniaProperty.Register<UserCard, string?>(nameof(UserGithub), defaultValue: null);
public string? UserGithub
{
get { return GetValue(UserGithubProperty); }
set { SetValue(UserGithubProperty, value); }
}
public static readonly DirectProperty<UserCard, Thickness> UserImageMarginProperty =
AvaloniaProperty.RegisterDirect<UserCard, Thickness>(nameof(UserImageMargin), o => o.UserImageMargin);
private Thickness _userImageMargin = new(0, 0, 0, 0);
public Thickness UserImageMargin
{
get { return _userImageMargin; }
private set { SetAndRaise(UserImageMarginProperty, ref _userImageMargin, value); }
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
SizeChanged += UserCard_SizeChanged;
if (_githubButton != null)
{
_githubButton.Click -= GithubButton_Click;
}
_githubButton = e.NameScope.Find("PART_GithubButton") as Button;
if (_githubButton != null)
{
_githubButton.Click += GithubButton_Click;
}
}
private void UserCard_SizeChanged(object? sender, SizeChangedEventArgs e)
{
UserImageMargin = new(0, !double.IsNaN(e.NewSize.Height) ? 100 - e.NewSize.Height : 0, 0, 0);
}
private void GithubButton_Click(object? sender, RoutedEventArgs e)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo($"https://github.com/{UserGithub}") { UseShellExecute = true }
};
process.Start();
}
}

View File

@@ -6,7 +6,7 @@ using System.Linq;
namespace Needlework.Net.Converters namespace Needlework.Net.Converters
{ {
public class EnumerableToVisibility : IValueConverter public class EnumerableBoolConverter : IValueConverter
{ {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {

View File

@@ -4,7 +4,7 @@ using System.Globalization;
namespace Needlework.Net.Converters namespace Needlework.Net.Converters
{ {
public class NullableToVisibility : IValueConverter public class NullBoolConverter : IValueConverter
{ {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {

View File

@@ -1,13 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.DataModels
{
public partial class AppSettings : ObservableObject
{
[ObservableProperty]
private bool _isCheckForUpdates = true;
[ObservableProperty]
private bool _isCheckForSchema = true;
}
}

View File

@@ -1,13 +0,0 @@
namespace Needlework.Net.DataModels
{
public class HextechDocsPost
{
public required string Path { get; init; }
public required string Title { get; init; }
public required string Excerpt { get; init; }
public string Url => $"https://hextechdocs.dev{Path}";
}
}

View File

@@ -1,16 +0,0 @@
using Serilog;
namespace Needlework.Net.Extensions
{
public static class EnableLoggerExtensions
{
private static readonly ILogger _logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}", path: "Logs/debug-.log", rollingInterval: RollingInterval.Day, shared: true)
.CreateLogger();
public static ILogger Log(this IEnableLogger? context) => _logger.ForContext(context?.GetType() ?? typeof(Program));
}
public interface IEnableLogger;
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Needlework.Net.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSingletonsFromAssemblies<T>(this ServiceCollection services)
{
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => !p.IsAbstract && typeof(T).IsAssignableFrom(p));
foreach (var type in types) services.AddSingleton(typeof(T), type);
return services;
}
}
}

View File

@@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Messages
{
public class ContentRequestMessage : RequestMessage<string>
{
}
}

View File

@@ -0,0 +1,9 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using Needlework.Net.Models;
namespace Needlework.Net.Messages
{
public class DataReadyMessage(OpenApiDocumentWrapper wrapper) : ValueChangedMessage<OpenApiDocumentWrapper>(wrapper)
{
}
}

View File

@@ -0,0 +1,9 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using Needlework.Net.Models;
namespace Needlework.Net.Messages
{
public class DataRequestMessage : RequestMessage<OpenApiDocumentWrapper>
{
}
}

View File

@@ -0,0 +1,20 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Messages
{
public class EditorUpdateMessage(EditorUpdate editorUpdate) : ValueChangedMessage<EditorUpdate>(editorUpdate)
{
}
public class EditorUpdate
{
public string Text { get; }
public string Key { get; }
public EditorUpdate(string text, string key)
{
Text = text;
Key = key;
}
}
}

View File

@@ -0,0 +1,9 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.OpenApi.Models;
namespace Needlework.Net.Messages
{
public class HostDocumentRequestMessage : RequestMessage<OpenApiDocument>
{
}
}

View File

@@ -0,0 +1,9 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using Needlework.Net.ViewModels;
namespace Needlework.Net.Messages
{
public class InfoBarUpdateMessage(InfoBarViewModel vm) : ValueChangedMessage<InfoBarViewModel>(vm)
{
}
}

View File

@@ -1,12 +1,12 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Needlework.Net.DataModels namespace Needlework.Net.Models
{ {
public class GithubRelease public class GithubRelease
{ {
[JsonPropertyName("tag_name")] [JsonPropertyName("tag_name")]
public string TagName { get; set; } = string.Empty; public string TagName { get; set; } = string.Empty;
public bool IsLatest(string assemblyVersion) => int.Parse(TagName.Replace(".", "")) > int.Parse(assemblyVersion.ToString().Replace(".", "")); public bool IsLatest(int version) => int.Parse(TagName.Replace(".", "")) > version;
} }
} }

View File

@@ -1,31 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Needlework.Net.Models;
public class Library
{
[JsonPropertyName("repo")]
public required string Repo { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("language")]
public required string Language { get; init; }
[JsonPropertyName("owner")]
public required string Owner { get; init; }
[JsonPropertyName("tags")]
public List<string> Tags { get; init; } = [];
public string Link
{
get
{
if (Owner.Equals("jellies")) return $"https://github.com/elliejs/{Repo}";
return $"https://github.com/{Owner}/{Repo}";
}
}
}

View File

@@ -1,9 +0,0 @@
using FluentAvalonia.UI.Controls;
using System;
namespace Needlework.Net.Models
{
public record Notification(string Title, string Message, InfoBarSeverity InfoBarSeverity, TimeSpan? Duration = null, string? Url = null)
{
}
}

View File

@@ -1,10 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
namespace Needlework.Net.Models; namespace Needlework.Net.Models;
public class Document public class OpenApiDocumentWrapper
{ {
internal OpenApiDocument OpenApiDocument { get; } internal OpenApiDocument OpenApiDocument { get; }
@@ -14,7 +13,7 @@ public class Document
public List<string> Paths => [.. OpenApiDocument.Paths.Keys]; public List<string> Paths => [.. OpenApiDocument.Paths.Keys];
public Document(OpenApiDocument openApiDocument) public OpenApiDocumentWrapper(OpenApiDocument openApiDocument)
{ {
OpenApiDocument = openApiDocument; OpenApiDocument = openApiDocument;
var plugins = new SortedDictionary<string, List<PathOperation>>(); var plugins = new SortedDictionary<string, List<PathOperation>>();
@@ -36,10 +35,10 @@ public class Document
{ {
pluginsKey = "default"; pluginsKey = "default";
if (plugins.TryGetValue(pluginsKey, out var p)) if (plugins.TryGetValue(pluginsKey, out var p))
p.Add(new(method.ToString(), path, pluginsKey, operation)); p.Add(new(method.ToString(), path, operation));
else else
{ {
operations.Add(new(method.ToString(), path, pluginsKey, operation)); operations.Add(new(method.ToString(), path, operation));
plugins[pluginsKey] = operations; plugins[pluginsKey] = operations;
} }
} }
@@ -47,16 +46,19 @@ public class Document
{ {
foreach (var tag in operation.Tags) foreach (var tag in operation.Tags)
{ {
if (tag.Name == "plugins") var lowercaseTag = tag.Name.ToLower();
if (lowercaseTag == "plugins")
continue; continue;
else if (lowercaseTag.Contains("plugin "))
pluginsKey = lowercaseTag.Replace("plugin ", "");
else else
pluginsKey = tag.Name; pluginsKey = lowercaseTag;
if (plugins.TryGetValue(pluginsKey, out var p)) if (plugins.TryGetValue(pluginsKey, out var p))
p.Add(new(method.ToString(), path, tag.Name, operation)); p.Add(new(method.ToString(), path, operation));
else else
{ {
operations.Add(new(method.ToString(), path, tag.Name, operation)); operations.Add(new(method.ToString(), path, operation));
plugins[pluginsKey] = operations; plugins[pluginsKey] = operations;
} }
} }
@@ -64,10 +66,6 @@ public class Document
} }
} }
plugins = new(plugins.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.OrderBy(x => x.Path).ToList()));
Plugins = plugins; Plugins = plugins;
} }
} }

View File

@@ -2,4 +2,4 @@ using Microsoft.OpenApi.Models;
namespace Needlework.Net.Models; namespace Needlework.Net.Models;
public record PathOperation(string Method, string Path, string Tag, OpenApiOperation Operation); public record PathOperation(string Method, string Path, OpenApiOperation Operation);

View File

@@ -0,0 +1,23 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
namespace Needlework.Net.Models;
public static class Resources
{
/// <summary>
/// Get the OpenApi document of the LCU schema. Provided by dysolix.
/// </summary>
/// <param name="httpClient"></param>
/// <returns></returns>
public static async Task<OpenApiDocument> GetOpenApiDocumentAsync(HttpClient httpClient)
{
var stream = await httpClient.GetStreamAsync("https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json");
var document = new OpenApiStreamReader().Read(stream, out var _);
return document;
}
}

View File

@@ -1,8 +0,0 @@
using Needlework.Net.ViewModels.Pages.Endpoints;
namespace Needlework.Net.Models
{
public record SchemaPaneItem(string Key, Tab Tab)
{
}
}

View File

@@ -1,10 +0,0 @@
namespace Needlework.Net.Models
{
public class SystemBuild
{
public string Branch { get; set; } = string.Empty;
public string Patchline { get; set; } = string.Empty;
public string PatchlineVisibleName { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
@@ -11,57 +11,39 @@
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch> <AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
<ApplicationIcon>app.ico</ApplicationIcon> <ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>NeedleworkDotNet</AssemblyName> <AssemblyName>NeedleworkDotNet</AssemblyName>
<AssemblyVersion>0.13.1.0</AssemblyVersion> <AssemblyVersion>0.7.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion> <FileVersion>$(AssemblyVersion)</FileVersion>
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions> <AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="akavache" Version="10.2.41" /> <PackageReference Include="Avalonia" Version="11.1.3" />
<PackageReference Include="AngleSharp" Version="1.3.0" /> <PackageReference Include="Avalonia.AvaloniaEdit" Version="11.1.0" />
<PackageReference Include="Avalonia" Version="11.2.8" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.1.3" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.3.0" /> <PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.3" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.8" /> <PackageReference Include="Avalonia.Desktop" Version="11.1.3" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.8" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.8" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.8" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.8" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.3.0" /> <PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
<PackageReference Include="BlossomiShymae.Briar" Version="0.2.3" /> <PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.13.1" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="DebounceThrottle" Version="3.0.1" /> <PackageReference Include="FluentAvaloniaUI" Version="2.1.0" />
<PackageReference Include="FastCache.Cached" Version="1.8.2" /> <PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
<PackageReference Include="FluentAvaloniaUI" Version="2.3.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Flurl" Version="4.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Flurl.Http" Version="4.0.2" /> <PackageReference Include="Microsoft.OpenApi" Version="1.6.17" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.4.1" /> <PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.17" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" /> <PackageReference Include="Projektanker.Icons.Avalonia" Version="9.4.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" /> <PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> <PackageReference Include="TextMateSharp.Grammars" Version="1.0.62" />
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="9.0.3" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.24" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.24" />
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.6.2" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.6.2" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.69" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="Assets\**" /> <AvaloniaResource Include="Assets\**" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<AvaloniaXaml Remove="Utilities\**" />
<Compile Remove="Utilities\**" />
<EmbeddedResource Remove="Utilities\**" />
<None Remove="Utilities\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<UpToDateCheckInput Remove="Views\AboutView.axaml" /> <UpToDateCheckInput Remove="Views\AboutView.axaml" />
</ItemGroup> </ItemGroup>
@@ -70,31 +52,13 @@
<Compile Update="Controls\BusyArea.axaml.cs"> <Compile Update="Controls\BusyArea.axaml.cs">
<DependentUpon>BusyArea.axaml</DependentUpon> <DependentUpon>BusyArea.axaml</DependentUpon>
</Compile> </Compile>
<Compile Update="Views\MainWindow\MainWindowView.axaml.cs"> <Compile Update="Views\EndpointView.axaml.cs">
<DependentUpon>MainWindowView.axaml</DependentUpon> <DependentUpon>EndpointView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\EndpointListView.axaml.cs">
<DependentUpon>EndpointListView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\EndpointsView.axaml.cs">
<DependentUpon>EndpointsView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\PluginView.axaml.cs">
<DependentUpon>PluginView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Schemas\SchemaSearchDetailsView.axaml.cs">
<DependentUpon>SchemaSearchDetailsView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\WebSocket\WebsocketView.axaml.cs">
<DependentUpon>WebSocketView.axaml</DependentUpon>
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Assets\Users\" /> <Folder Include="Assets\Users\" />
</ItemGroup> <Folder Include="Utilities\" />
<ItemGroup>
<None Include="ViewModels\Pages\Schemas\SchemaSearchDetailsViewModel.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,32 +1,10 @@
using Akavache; using Avalonia;
using Akavache.Sqlite3;
using Avalonia;
using Avalonia.Controls.Templates;
using Flurl.Http.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Needlework.Net.Constants;
using Needlework.Net.Extensions; using Needlework.Net.Extensions;
using Needlework.Net.Services; using Needlework.Net.Services;
using Needlework.Net.ViewModels.MainWindow; using Needlework.Net.ViewModels;
using Needlework.Net.ViewModels.Pages;
using Needlework.Net.ViewModels.Pages.About;
using Needlework.Net.ViewModels.Pages.Console;
using Needlework.Net.ViewModels.Pages.Endpoints;
using Needlework.Net.ViewModels.Pages.Home;
using Needlework.Net.ViewModels.Pages.Schemas;
using Needlework.Net.ViewModels.Pages.Settings;
using Needlework.Net.ViewModels.Pages.WebSocket;
using Needlework.Net.Views.MainWindow;
using Needlework.Net.Views.Pages.About;
using Needlework.Net.Views.Pages.Console;
using Needlework.Net.Views.Pages.Endpoints;
using Needlework.Net.Views.Pages.Home;
using Needlework.Net.Views.Pages.Schemas;
using Needlework.Net.Views.Pages.Settings;
using Needlework.Net.Views.Pages.WebSocket;
using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome; using Projektanker.Icons.Avalonia.FontAwesome;
using Serilog;
using System; using System;
using System.IO; using System.IO;
@@ -43,19 +21,18 @@ class Program
AppDomain.CurrentDomain.UnhandledException += Program_UnhandledException; AppDomain.CurrentDomain.UnhandledException += Program_UnhandledException;
BuildAvaloniaApp() BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args); .StartWithClassicDesktopLifetime(args);
} }
// Avalonia configuration, don't remove; also used by visual designer. // Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
{ {
IconProvider.Current.Register<FontAwesomeIconProvider>(); IconProvider.Current
.Register<FontAwesomeIconProvider>();
return AppBuilder.Configure(() => new App(BuildServices())) return AppBuilder.Configure(() => new App(BuildServices()))
.UsePlatformDetect() .UsePlatformDetect()
.WithInterFont() .WithInterFont()
.With(new Win32PlatformOptions { CompositionMode = [Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition] })
.With(new MacOSPlatformOptions { ShowInDock = true, })
.LogToTrace(); .LogToTrace();
} }
@@ -63,90 +40,18 @@ class Program
{ {
var builder = new ServiceCollection(); var builder = new ServiceCollection();
AddViews(builder);
AddViewModels(builder);
AddServices(builder);
return builder.BuildServiceProvider();
}
private static void AddViews(ServiceCollection builder)
{
var locator = new ViewLocator();
// MAIN WINDOW
locator.Register<NotificationViewModel>(() => new NotificationView());
locator.Register<ViewModels.MainWindow.SchemaSearchDetailsViewModel>(() => new Views.MainWindow.SchemaSearchDetailsView());
locator.Register<SchemaViewModel>(() => new SchemaView());
// ABOUT
locator.Register<AboutViewModel>(() => new AboutView());
// CONSOLE
locator.Register<ConsoleViewModel>(() => new ConsoleView());
// ENDPOINTS
locator.Register<EndpointListViewModel>(() => new EndpointListView());
locator.Register<EndpointSearchDetailsViewModel>(() => new EndpointSearchDetailsView());
locator.Register<EndpointsViewModel>(() => new EndpointsView());
locator.Register<EndpointTabItemContentViewModel>(() => new EndpointTabItemContentView());
locator.Register<PathOperationViewModel>(() => new PathOperationView());
locator.Register<PluginViewModel>(() => new PluginView());
locator.Register<PropertyClassViewModel>(() => new PropertyClassView());
// HOME
locator.Register<HomeViewModel>(() => new HomeView());
locator.Register<LibraryViewModel>(() => new LibraryView());
locator.Register<HextechDocsPostViewModel>(() => new HextechDocsPostView());
// SCHEMAS
locator.Register<SchemasViewModel>(() => new SchemasView());
locator.Register<ViewModels.Pages.Schemas.SchemaSearchDetailsViewModel>(() => new Views.Pages.Schemas.SchemaSearchDetailsView());
// WEBSOCKET
locator.Register<WebSocketViewModel>(() => new WebSocketView());
locator.Register<EventViewModel>(() => new EventView());
// SETTINGS
locator.Register<SettingsViewModel>(() => new SettingsView());
builder.AddSingleton<IDataTemplate>(locator);
}
private static void AddServices(ServiceCollection builder)
{
builder.AddSingleton<DialogService>();
builder.AddSingleton<DocumentService>();
builder.AddSingleton<NotificationService>();
builder.AddSingleton<SchemaPaneService>();
builder.AddSingleton<HextechDocsService>();
builder.AddSingleton<GithubService>();
builder.AddSingleton<IBlobCache>((_) =>
{
var appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
appDataFolder = string.IsNullOrEmpty(appDataFolder) ? "AppData" : appDataFolder;
var appFolder = Path.Join(appDataFolder, AppInfo.Name);
Directory.CreateDirectory(appFolder);
var filePath = Path.Join(appFolder, "cache.sqlite");
return new SqlRawPersistentBlobCache(filePath);
});
builder.AddSingleton<IFlurlClientCache>(new FlurlClientCache()
.Add(FlurlClientKeys.GithubClient, "https://api.github.com")
.Add(FlurlClientKeys.GithubUserContentClient, "https://raw.githubusercontent.com")
.Add(FlurlClientKeys.Client));
builder.AddLogging((builder) => builder.AddSerilog(EnableLoggerExtensions.Log(null)));
}
private static void AddViewModels(ServiceCollection builder)
{
builder.AddSingleton<MainWindowViewModel>(); builder.AddSingleton<MainWindowViewModel>();
builder.AddSingleton<DialogService>();
builder.AddSingletonsFromAssemblies<PageBase>();
builder.AddSingleton<PageBase, HomeViewModel>(); builder.AddHttpClient();
builder.AddSingleton<PageBase, ConsoleViewModel>();
builder.AddSingleton<PageBase, EndpointsViewModel>();
builder.AddSingleton<PageBase, WebSocketViewModel>();
builder.AddSingleton<PageBase, SchemasViewModel>();
builder.AddSingleton<PageBase, AboutViewModel>();
builder.AddSingleton<PageBase, SettingsViewModel>();
builder.AddSingleton<PageFactory>(); var services = builder.BuildServiceProvider();
return services;
} }
private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e) private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{ {
File.WriteAllText($"Logs/fatal-{DateTime.Now:yyyyMMdd}.log", e.ExceptionObject.ToString()); File.WriteAllText($"errorlog-{DateTime.Now:HHmmssfff}", e.ExceptionObject.ToString());
} }
} }

View File

@@ -1,65 +0,0 @@
using FastCache;
using Flurl.Http;
using Flurl.Http.Configuration;
using Microsoft.OpenApi.Readers;
using Needlework.Net.Constants;
using Needlework.Net.Extensions;
using Needlework.Net.Models;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Needlework.Net
{
public class DocumentService : IEnableLogger
{
private readonly OpenApiStreamReader _reader = new();
private readonly IFlurlClient _githubUserContentClient;
public DocumentService(IFlurlClientCache clients)
{
_githubUserContentClient = clients.Get(FlurlClientKeys.GithubUserContentClient);
}
public async Task<Document> GetLcuSchemaDocumentAsync(CancellationToken cancellationToken = default)
{
if (Cached<Document>.TryGet(nameof(GetLcuSchemaDocumentAsync), out var cached))
{
return cached;
}
var lcuSchemaStream = await _githubUserContentClient.Request("/dysolix/hasagi-types/main/swagger.json")
.GetStreamAsync(cancellationToken: cancellationToken);
var lcuSchemaRaw = _reader.Read(lcuSchemaStream, out var diagnostic);
foreach (var error in diagnostic.Errors)
{
this.Log()
.Warning("Diagnostic error: {Message}", error);
}
var document = new Document(lcuSchemaRaw);
return cached.Save(document, TimeSpan.FromMinutes(60));
}
public async Task<Document> GetLolClientDocumentAsync(CancellationToken cancellationToken = default)
{
if (Cached<Document>.TryGet(nameof(GetLolClientDocumentAsync), out var cached))
{
return cached;
}
var lolClientStream = await _githubUserContentClient.Request("/AlsoSylv/Irelia/refs/heads/master/schemas/game_schema.json")
.GetStreamAsync(cancellationToken: cancellationToken);
var lolClientRaw = _reader.Read(lolClientStream, out var diagnostic);
foreach (var error in diagnostic.Errors)
{
this.Log()
.Warning("Diagnostic error: {Message}", error);
}
var document = new Document(lolClientRaw);
return cached.Save(document, TimeSpan.FromMinutes(60));
}
}
}

View File

@@ -1,42 +0,0 @@
using Akavache;
using Flurl.Http;
using Flurl.Http.Configuration;
using Needlework.Net.Constants;
using Needlework.Net.DataModels;
using Needlework.Net.Extensions;
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.Services
{
public class GithubService : IEnableLogger
{
private readonly IFlurlClient _githubClient;
private readonly IFlurlClient _githubUserContentClient;
private readonly IBlobCache _blobCache;
public GithubService(IBlobCache blobCache, IFlurlClientCache clients)
{
_githubClient = clients.Get(FlurlClientKeys.GithubClient);
_githubUserContentClient = clients.Get(FlurlClientKeys.GithubUserContentClient);
_blobCache = blobCache;
}
public async Task<GithubRelease> GetLatestReleaseAsync()
{
return await _blobCache.GetOrFetchObject(BlobCacheKeys.GithubLatestRelease, async () =>
{
this.Log()
.Debug("Downloading latest release info from GitHub...");
var release = await _githubClient
.Request("/repos/BlossomiShymae/Needlework.Net/releases/latest")
.WithHeader("User-Agent", $"{AppInfo.Name}/{AppInfo.Version}")
.GetJsonAsync<GithubRelease>();
return release;
}, DateTimeOffset.Now + Intervals.CheckForUpdates);
}
}
}

View File

@@ -1,49 +0,0 @@
using Akavache;
using AngleSharp;
using Needlework.Net.DataModels;
using Needlework.Net.Extensions;
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.Services
{
public class HextechDocsService : IEnableLogger
{
private readonly IBrowsingContext _context = BrowsingContext.New(Configuration.Default.WithDefaultLoader());
private readonly IBlobCache _blobCache;
public HextechDocsService(IBlobCache blobCache)
{
_blobCache = blobCache;
}
public async Task<List<HextechDocsPost>> GetPostsAsync()
{
return await _blobCache.GetOrFetchObject("HextechDocsPosts", async () =>
{
this.Log()
.Debug("Downloading HextechDocs posts...");
var document = await _context.OpenAsync("https://hextechdocs.dev/tag/lcu/");
var elements = document.QuerySelectorAll("article.post-card");
var posts = new List<HextechDocsPost>();
foreach (var element in elements)
{
var path = element.QuerySelector("a.post-card-content-link")!.GetAttribute("href")!;
var title = element.QuerySelector(".post-card-title")!.TextContent;
var excerpt = element.QuerySelector(".post-card-excerpt > p")!.TextContent;
var post = new HextechDocsPost()
{
Path = path,
Title = title,
Excerpt = excerpt,
};
posts.Add(post);
}
return posts;
}, DateTimeOffset.Now + TimeSpan.FromHours(12));
}
}
}

View File

@@ -1,20 +0,0 @@
using FluentAvalonia.UI.Controls;
using Needlework.Net.Models;
using System;
using System.Reactive.Subjects;
namespace Needlework.Net.Services
{
public class NotificationService
{
private readonly Subject<Notification> _notificationSubject = new();
public IObservable<Notification> Notifications { get { return _notificationSubject; } }
public void Notify(string title, string message, InfoBarSeverity severity, TimeSpan? duration = null, string? url = null)
{
var notification = new Notification(title, message, severity, duration, url);
_notificationSubject.OnNext(notification);
}
}
}

View File

@@ -1,20 +0,0 @@
using Needlework.Net.Models;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System;
using System.Reactive.Subjects;
namespace Needlework.Net.Services
{
public class SchemaPaneService
{
private readonly Subject<SchemaPaneItem> _schemaPaneItemsSubject = new();
public IObservable<SchemaPaneItem> SchemaPaneItems { get { return _schemaPaneItemsSubject; } }
public void Add(string key, Tab tab)
{
var schemaPaneItem = new SchemaPaneItem(key, tab);
_schemaPaneItemsSubject.OnNext(schemaPaneItem);
}
}
}

View File

@@ -8,23 +8,30 @@ namespace Needlework.Net
{ {
public class ViewLocator : IDataTemplate public class ViewLocator : IDataTemplate
{ {
private readonly Dictionary<Type, Func<Control>> _viewRegister = []; private readonly Dictionary<object, Control> _controlCache = [];
public void Register<T>(Func<Control> viewActivator)
where T : INotifyPropertyChanged
{
_viewRegister[typeof(T)] = viewActivator;
}
public Control Build(object? data) public Control Build(object? data)
{ {
if (!_viewRegister.TryGetValue(data!.GetType(), out var activator)) var fullName = data?.GetType().FullName;
if (fullName is null)
{ {
throw new Exception("Data type has no registered view activator."); return new TextBlock { Text = "Data is null or has no name." };
} }
var res = activator(); var name = fullName.Replace("ViewModel", "View");
res!.DataContext = data; var type = Type.GetType(name);
if (type is null)
{
return new TextBlock { Text = $"No View For {name}." };
}
if (!_controlCache.TryGetValue(data!, out var res))
{
res ??= (Control)Activator.CreateInstance(type)!;
_controlCache[data!] = res;
}
res.DataContext = data;
return res; return res;
} }

View File

@@ -0,0 +1,26 @@
using CommunityToolkit.Mvvm.Input;
using System.Diagnostics;
using System.Net.Http;
namespace Needlework.Net.ViewModels
{
public partial class AboutViewModel : PageBase
{
public HttpClient HttpClient { get; }
public AboutViewModel(HttpClient httpClient) : base("About", "info-circle")
{
HttpClient = httpClient;
}
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo(url) { UseShellExecute = true }
};
process.Start();
}
}
}

View File

@@ -0,0 +1,98 @@
using Avalonia.Collections;
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels
{
public partial class ConsoleViewModel : PageBase, IRecipient<DataReadyMessage>
{
public IAvaloniaReadOnlyList<string> RequestMethods { get; } = new AvaloniaList<string>(["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE"]);
public IAvaloniaList<string> RequestPaths { get; } = new AvaloniaList<string>();
[ObservableProperty] private bool _isBusy = true;
[ObservableProperty] private bool _isRequestBusy = false;
[ObservableProperty] private string? _requestMethodSelected = "GET";
[ObservableProperty] private string? _requestPath = null;
[ObservableProperty] private string? _requestBody = null;
[ObservableProperty] private string? _responsePath = null;
[ObservableProperty] private string? _responseStatus = null;
[ObservableProperty] private string? _responseAuthorization = null;
public ConsoleViewModel() : base("Console", "terminal", -200)
{
WeakReferenceMessenger.Default.Register<DataReadyMessage>(this);
}
[RelayCommand]
private async Task SendRequest()
{
try
{
IsRequestBusy = true;
if (string.IsNullOrEmpty(RequestPath)) throw new Exception("Path is empty.");
var method = RequestMethodSelected switch
{
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
"DELETE" => HttpMethod.Delete,
"HEAD" => HttpMethod.Head,
"PATCH" => HttpMethod.Patch,
"OPTIONS" => HttpMethod.Options,
"TRACE" => HttpMethod.Trace,
_ => throw new Exception("Method is not selected."),
};
var processInfo = ProcessFinder.Get();
var requestBody = WeakReferenceMessenger.Default.Send(new ContentRequestMessage(), "ConsoleRequestEditor").Response;
var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var client = Connector.GetLcuHttpClientInstance();
var response = await client.SendAsync(new(method, RequestPath) { Content = content });
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
var responseBody = await response.Content.ReadAsByteArrayAsync();
var body = responseBody.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), App.JsonSerializerOptions) : string.Empty;
if (body.Length >= App.MaxCharacters)
{
WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(body));
WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(string.Empty), nameof(ConsoleViewModel));
}
else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(body), nameof(ConsoleViewModel));
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}";
ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{RequestPath}";
ResponseAuthorization = $"Basic {riotAuthentication.Value}";
}
catch (Exception ex)
{
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5))));
ResponseStatus = null;
ResponsePath = null;
ResponseAuthorization = null;
WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(string.Empty), nameof(ConsoleViewModel));
}
finally
{
IsRequestBusy = false;
}
}
public void Receive(DataReadyMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(() =>
{
RequestPaths.Clear();
RequestPaths.AddRange(message.Value.Paths);
IsBusy = false;
});
}
}
}

View File

@@ -0,0 +1,49 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using System;
using System.Linq;
namespace Needlework.Net.ViewModels
{
public partial class EndpointViewModel : ObservableObject
{
public string Endpoint { get; }
public string Title => Endpoint;
public IAvaloniaReadOnlyList<PathOperationViewModel> PathOperations { get; }
[ObservableProperty] private PathOperationViewModel? _selectedPathOperation;
[ObservableProperty] private string? _search;
public IAvaloniaList<PathOperationViewModel> FilteredPathOperations { get; }
public EndpointViewModel(string endpoint)
{
Endpoint = endpoint;
var handler = WeakReferenceMessenger.Default.Send<DataRequestMessage>().Response;
PathOperations = new AvaloniaList<PathOperationViewModel>(handler.Plugins[endpoint].Select(x => new PathOperationViewModel(x)));
FilteredPathOperations = new AvaloniaList<PathOperationViewModel>(PathOperations);
}
partial void OnSearchChanged(string? value)
{
FilteredPathOperations.Clear();
if (string.IsNullOrWhiteSpace(value))
{
FilteredPathOperations.AddRange(PathOperations);
return;
}
FilteredPathOperations.AddRange(PathOperations.Where(o => o.Path.Contains(value, StringComparison.InvariantCultureIgnoreCase)));
}
partial void OnSelectedPathOperationChanged(PathOperationViewModel? value)
{
if (value == null) return;
WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(value.Operation.RequestTemplate ?? string.Empty, "EndpointRequestEditor")));
}
}
}

View File

@@ -0,0 +1,31 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Net.Http;
namespace Needlework.Net.ViewModels
{
public partial class EndpointsContainerViewModel : PageBase
{
[ObservableProperty] private ObservableObject _activeViewModel;
[ObservableProperty] private ObservableObject _endpointsViewModel;
[ObservableProperty] private string _title = string.Empty;
public EndpointsContainerViewModel(HttpClient httpClient) : base("Endpoints", "list-alt", -500)
{
_activeViewModel = _endpointsViewModel = new EndpointsViewModel(httpClient, OnClicked);
}
private void OnClicked(ObservableObject viewModel)
{
ActiveViewModel = viewModel;
if (viewModel is EndpointViewModel endpoint) Title = endpoint.Title;
}
[RelayCommand]
private void GoBack()
{
ActiveViewModel = EndpointsViewModel;
Title = string.Empty;
}
}
}

View File

@@ -0,0 +1,59 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using System;
using System.Linq;
using System.Net.Http;
namespace Needlework.Net.ViewModels
{
public partial class EndpointsViewModel : ObservableObject, IRecipient<DataReadyMessage>
{
public HttpClient HttpClient { get; }
public string Title => "Endpoints";
public Action<ObservableObject> OnClicked;
public IAvaloniaList<string> Plugins { get; } = new AvaloniaList<string>();
public IAvaloniaList<string> Query { get; } = new AvaloniaList<string>();
[ObservableProperty] private bool _isBusy = true;
[ObservableProperty] private string _search = string.Empty;
[ObservableProperty] private string? _selectedQuery = string.Empty;
public EndpointsViewModel(HttpClient httpClient, Action<ObservableObject> onClicked)
{
HttpClient = httpClient;
OnClicked = onClicked;
WeakReferenceMessenger.Default.Register(this);
}
public void Receive(DataReadyMessage message)
{
IsBusy = false;
Plugins.Clear();
Plugins.AddRange(message.Value.Plugins.Keys);
Query.Clear();
Query.AddRange(Plugins);
}
partial void OnSearchChanged(string value)
{
Query.Clear();
if (!string.IsNullOrEmpty(Search))
Query.AddRange(Plugins.Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase)));
else
Query.AddRange(Plugins);
}
[RelayCommand]
private void OpenEndpoint(string? value)
{
if (string.IsNullOrEmpty(value)) return;
OnClicked.Invoke(new EndpointViewModel(value));
}
}
}

View File

@@ -0,0 +1,22 @@
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
namespace Needlework.Net.ViewModels
{
public class EventViewModel : ObservableObject
{
public string Time { get; }
public string Type { get; }
public string Uri { get; }
public string Key => $"{Time} {Type} {Uri}";
public EventViewModel(EventData eventData)
{
Time = $"{DateTime.Now:HH:mm:ss.fff}";
Type = eventData?.EventType.ToUpper() ?? string.Empty;
Uri = eventData?.Uri ?? string.Empty;
}
}
}

View File

@@ -0,0 +1,20 @@
using CommunityToolkit.Mvvm.Input;
using System.Diagnostics;
namespace Needlework.Net.ViewModels
{
public partial class HomeViewModel : PageBase
{
public HomeViewModel() : base("Home", "home", int.MinValue) { }
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo(url) { UseShellExecute = true }
};
process.Start();
}
}
}

View File

@@ -0,0 +1,27 @@
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using System;
namespace Needlework.Net.ViewModels
{
public partial class InfoBarViewModel : ObservableObject
{
[ObservableProperty] private string _title;
[ObservableProperty] private bool _isOpen;
[ObservableProperty] private string _message;
[ObservableProperty] private InfoBarSeverity _severity;
[ObservableProperty] private TimeSpan _duration;
[ObservableProperty] private Control? _actionButton;
public InfoBarViewModel(string title, bool isOpen, string message, InfoBarSeverity severity, TimeSpan duration, Control? actionButton = null)
{
_title = title;
_isOpen = isOpen;
_message = message;
_severity = severity;
_duration = duration;
_actionButton = actionButton;
}
}
}

View File

@@ -1,201 +0,0 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Constants;
using Needlework.Net.Extensions;
using Needlework.Net.Helpers;
using Needlework.Net.Messages;
using Needlework.Net.Services;
using Needlework.Net.Views.MainWindow;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.MainWindow;
public partial class MainWindowViewModel
: ObservableObject, IRecipient<OopsiesDialogRequestedMessage>, IEnableLogger
{
private readonly DocumentService _documentService;
private readonly NotificationService _notificationService;
private readonly DialogService _dialogService;
private readonly SchemaPaneService _schemaPaneService;
public MainWindowViewModel(DialogService dialogService, DocumentService documentService, NotificationService notificationService, SchemaPaneService schemaPaneService)
{
_dialogService = dialogService;
_documentService = documentService;
_notificationService = notificationService;
_schemaPaneService = schemaPaneService;
_notificationService.Notifications.Subscribe(async notification =>
{
var vm = new NotificationViewModel(notification);
Notifications.Add(vm);
await Task.Delay(notification.Duration ?? TimeSpan.FromSeconds(10));
Notifications.Remove(vm);
});
_schemaPaneService.SchemaPaneItems.Subscribe(async item =>
{
var document = item.Tab switch
{
Pages.Endpoints.Tab.LCU => await documentService.GetLcuSchemaDocumentAsync(),
Pages.Endpoints.Tab.GameClient => await documentService.GetLolClientDocumentAsync(),
_ => throw new NotImplementedException()
};
var propertyClassViewModel = OpenApiHelpers.WalkSchema(document.OpenApiDocument.Components.Schemas[item.Key], document.OpenApiDocument);
var schemaViewModel = new SchemaViewModel(propertyClassViewModel);
if (Schemas.ToList().Find(schema => schema.Id == schemaViewModel.Id) == null)
{
Schemas.Add(schemaViewModel);
IsPaneOpen = true;
OpenSchemaPaneCommand.NotifyCanExecuteChanged();
CloseSchemaAllCommand.NotifyCanExecuteChanged();
}
});
WeakReferenceMessenger.Default.RegisterAll(this);
}
[ObservableProperty]
private bool _isPaneOpen;
[ObservableProperty]
private ObservableCollection<SchemaViewModel> _schemas = [];
[ObservableProperty]
private SchemaViewModel? _selectedSchema;
[ObservableProperty]
private ObservableCollection<NotificationViewModel> _notifications = [];
[ObservableProperty]
private SchemaSearchDetailsViewModel? _selectedSchemaSearchDetails;
public string AppName => AppInfo.Name;
public string Title => $"{AppInfo.Name} {AppInfo.Version}";
partial void OnSelectedSchemaSearchDetailsChanged(SchemaSearchDetailsViewModel? value)
{
if (value == null) return;
Task.Run(async () =>
{
var document = value.Tab switch
{
Pages.Endpoints.Tab.LCU => await _documentService.GetLcuSchemaDocumentAsync(),
Pages.Endpoints.Tab.GameClient => await _documentService.GetLolClientDocumentAsync(),
_ => throw new NotImplementedException()
};
var propertyClassViewModel = OpenApiHelpers.WalkSchema(document.OpenApiDocument.Components.Schemas[value.Key], document.OpenApiDocument);
var schemaViewModel = new SchemaViewModel(propertyClassViewModel);
Dispatcher.UIThread.Post(() =>
{
if (Schemas.ToList().Find(schema => schema.Id == schemaViewModel.Id) == null)
{
Schemas.Add(schemaViewModel);
IsPaneOpen = true;
OpenSchemaPaneCommand.NotifyCanExecuteChanged();
CloseSchemaAllCommand.NotifyCanExecuteChanged();
}
});
});
}
partial void OnSelectedSchemaChanged(SchemaViewModel? value)
{
CloseSchemaCommand.NotifyCanExecuteChanged();
}
partial void OnSchemasChanged(ObservableCollection<SchemaViewModel> value)
{
if (!value.Any())
{
IsPaneOpen = false;
}
}
public async Task<IEnumerable<object>> PopulateAsync(string? searchText, CancellationToken cancellationToken)
{
if (searchText == null) return [];
var lcuSchemaDocument = await _documentService.GetLcuSchemaDocumentAsync(cancellationToken);
var gameClientDocument = await _documentService.GetLolClientDocumentAsync(cancellationToken);
var lcuResults = lcuSchemaDocument.OpenApiDocument.Components.Schemas.Keys.Where(key => key.Contains(searchText, StringComparison.OrdinalIgnoreCase))
.Select(key => new SchemaSearchDetailsViewModel(key, Pages.Endpoints.Tab.LCU));
var gameClientResults = gameClientDocument.OpenApiDocument.Components.Schemas.Keys.Where(key => key.Contains(searchText, StringComparison.OrdinalIgnoreCase))
.Select(key => new SchemaSearchDetailsViewModel(key, Pages.Endpoints.Tab.GameClient));
return Enumerable.Concat(lcuResults, gameClientResults);
}
[RelayCommand(CanExecute = nameof(CanOpenSchemaPane))]
private void OpenSchemaPane()
{
IsPaneOpen = !IsPaneOpen;
}
private bool CanOpenSchemaPane()
{
return Schemas.Any();
}
[RelayCommand(CanExecute = nameof(CanCloseSchema))]
private void CloseSchema()
{
if (SelectedSchema is SchemaViewModel selection)
{
SelectedSchema = null;
Schemas = new ObservableCollection<SchemaViewModel>(Schemas.Where(schema => schema != selection));
OpenSchemaPaneCommand.NotifyCanExecuteChanged();
CloseSchemaCommand.NotifyCanExecuteChanged();
CloseSchemaAllCommand.NotifyCanExecuteChanged();
}
}
private bool CanCloseSchema()
{
return SelectedSchema != null;
}
[RelayCommand(CanExecute = nameof(CanCloseSchemaAll))]
private void CloseSchemaAll()
{
SelectedSchema = null;
Schemas = [];
OpenSchemaPaneCommand.NotifyCanExecuteChanged();
CloseSchemaCommand.NotifyCanExecuteChanged();
CloseSchemaAllCommand.NotifyCanExecuteChanged();
}
private bool CanCloseSchemaAll()
{
return Schemas.Any();
}
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process() { StartInfo = new ProcessStartInfo(url) { UseShellExecute = true } };
process.Start();
}
public void Receive(OopsiesDialogRequestedMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(async () => await _dialogService.ShowAsync<OopsiesDialog>(message.Value));
}
}

View File

@@ -1,27 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Models;
using System.Diagnostics;
namespace Needlework.Net.ViewModels.MainWindow
{
public partial class NotificationViewModel : ObservableObject
{
public NotificationViewModel(Notification notification)
{
Notification = notification;
IsButtonVisible = !string.IsNullOrEmpty(notification.Url);
}
public bool IsButtonVisible { get; }
public Notification Notification { get; }
[RelayCommand]
public void OpenUrl()
{
var process = new Process() { StartInfo = new() { UseShellExecute = true } };
process.Start();
}
}
}

View File

@@ -1,25 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.ViewModels.Pages.Endpoints;
namespace Needlework.Net.ViewModels.MainWindow
{
public partial class SchemaSearchDetailsViewModel : ObservableObject
{
public SchemaSearchDetailsViewModel(string key, Tab tab)
{
Tab = tab;
Key = key;
}
public string Key { get; }
public Tab Tab { get; }
public string Document => Tab switch
{
Tab.LCU => "LCU",
Tab.GameClient => "Game Client",
_ => throw new System.NotImplementedException()
};
}
}

View File

@@ -1,22 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System.Collections.Generic;
namespace Needlework.Net.ViewModels.MainWindow
{
public partial class SchemaViewModel : ObservableObject
{
public SchemaViewModel(PropertyClassViewModel vm)
{
Id = vm.Id;
PropertyFields = vm.PropertyFields;
PropertyEnums = vm.PropertyEnums;
}
public string Id { get; }
public List<PropertyFieldViewModel> PropertyFields { get; } = [];
public List<PropertyEnumViewModel> PropertyEnums { get; } = [];
}
}

View File

@@ -0,0 +1,157 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using FluentAvalonia.UI.Controls;
using Microsoft.OpenApi.Models;
using Needlework.Net.Messages;
using Needlework.Net.Models;
using Needlework.Net.Services;
using Needlework.Net.Views;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels
{
public partial class MainWindowViewModel
: ObservableObject, IRecipient<DataRequestMessage>, IRecipient<HostDocumentRequestMessage>, IRecipient<InfoBarUpdateMessage>, IRecipient<OopsiesDialogRequestedMessage>
{
public IAvaloniaReadOnlyList<NavigationViewItem> MenuItems { get; }
[NotifyPropertyChangedFor(nameof(CurrentPage))]
[ObservableProperty] private NavigationViewItem _selectedMenuItem;
public PageBase CurrentPage => (PageBase)SelectedMenuItem.Tag!;
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
[ObservableProperty] private bool _isUpdateShown = false;
public HttpClient HttpClient { get; }
public DialogService DialogService { get; }
public OpenApiDocumentWrapper? OpenApiDocumentWrapper { get; set; }
public OpenApiDocument? HostDocument { get; set; }
[ObservableProperty] private bool _isBusy = true;
[ObservableProperty] private ObservableCollection<InfoBarViewModel> _infoBarItems = [];
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, DialogService dialogService)
{
MenuItems = new AvaloniaList<NavigationViewItem>(pages
.OrderBy(p => p.Index)
.ThenBy(p => p.DisplayName)
.Select(p => new NavigationViewItem()
{
Content = p.DisplayName,
Tag = p,
IconSource = new BitmapIconSource() { UriSource = new Uri($"avares://NeedleworkDotNet/Assets/Icons/{p.Icon}.png") }
}));
SelectedMenuItem = MenuItems[0];
HttpClient = httpClient;
DialogService = dialogService;
WeakReferenceMessenger.Default.RegisterAll(this);
Task.Run(FetchDataAsync);
new Thread(ProcessEvents) { IsBackground = true }.Start();
}
private void ProcessEvents(object? obj)
{
while (!IsUpdateShown)
{
Task.Run(CheckLatestVersionAsync);
Thread.Sleep(TimeSpan.FromSeconds(60));
}
}
private async Task CheckLatestVersionAsync()
{
try
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/BlossomiShymae/Needlework.Net/releases/latest");
request.Headers.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("Needlework.Net", Version));
var response = await HttpClient.SendAsync(request);
var release = await response.Content.ReadFromJsonAsync<GithubRelease>();
if (release == null) return;
var currentVersion = int.Parse(Version.Replace(".", ""));
if (release.IsLatest(currentVersion))
{
Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
{
await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(10), new Avalonia.Controls.Button()
{
Command = OpenUrlCommand,
CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases",
Content = "Download"
}));
IsUpdateShown = true;
});
}
}
catch (Exception) { }
}
private async Task FetchDataAsync()
{
var document = await Resources.GetOpenApiDocumentAsync(HttpClient);
HostDocument = document;
var handler = new OpenApiDocumentWrapper(document);
OpenApiDocumentWrapper = handler;
WeakReferenceMessenger.Default.Send(new DataReadyMessage(handler));
IsBusy = false;
}
public void Receive(DataRequestMessage message)
{
message.Reply(OpenApiDocumentWrapper!);
}
public void Receive(HostDocumentRequestMessage message)
{
message.Reply(HostDocument!);
}
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo(url)
{
UseShellExecute = true
}
};
process.Start();
}
public void Receive(InfoBarUpdateMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Post(async () => await ShowInfoBarAsync(message.Value));
}
private async Task ShowInfoBarAsync(InfoBarViewModel vm)
{
InfoBarItems.Add(vm);
await Task.Delay(vm.Duration);
InfoBarItems.Remove(vm);
}
public void Receive(OopsiesDialogRequestedMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(async () => await DialogService.ShowAsync<OopsiesDialog>(message.Value));
}
}
}

View File

@@ -1,88 +1,56 @@
using Microsoft.OpenApi.Models; using Avalonia.Collections;
using Needlework.Net.Models; using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.ViewModels.Pages.Endpoints; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.OpenApi.Models;
using Needlework.Net.Messages;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
namespace Needlework.Net.Helpers namespace Needlework.Net.ViewModels
{ {
public static class OpenApiHelpers public partial class OperationViewModel : ObservableObject
{ {
public static string GetReturnType(OpenApiResponses responses) public string Summary { get; }
public string Description { get; }
public string ReturnType { get; }
public bool IsRequestBody { get; }
public string? RequestBodyType { get; }
public IAvaloniaReadOnlyList<PropertyClassViewModel> RequestClasses { get; }
public IAvaloniaReadOnlyList<PropertyClassViewModel> ResponseClasses { get; }
public IAvaloniaReadOnlyList<ParameterViewModel> PathParameters { get; }
public IAvaloniaReadOnlyList<ParameterViewModel> QueryParameters { get; }
public string? RequestTemplate { get; }
public OperationViewModel(OpenApiOperation operation)
{ {
if (!TryGetResponse(responses, out var response)) Summary = operation.Summary ?? string.Empty;
return "none"; Description = operation.Description ?? string.Empty;
IsRequestBody = operation.RequestBody != null;
if (TryGetApplicationJsonMedia(response, out var media)) ReturnType = GetReturnType(operation.Responses);
{ RequestClasses = GetRequestClasses(operation.RequestBody);
var schema = media.Schema; ResponseClasses = GetResponseClasses(operation.Responses);
return GetSchemaType(schema); PathParameters = GetParameters(operation.Parameters, ParameterLocation.Path);
} QueryParameters = GetParameters(operation.Parameters, ParameterLocation.Query);
RequestBodyType = GetRequestBodyType(operation.RequestBody);
return "none"; RequestTemplate = GetRequestTemplate(operation.RequestBody);
} }
public static bool TryGetApplicationJsonMedia(OpenApiResponse response, [NotNullWhen(true)] out OpenApiMediaType? media) // Because GetLolGameflowV1SpectateDelayedLaunch has an empty schema with no type... private string? GetRequestTemplate(OpenApiRequestBody? requestBody)
{ {
var flag = false; var requestClasses = GetRequestClasses(requestBody);
if (response.Content.TryGetValue("application/json", out var _media)) if (requestClasses.Count == 0)
{ {
if (_media?.Schema?.Type != null) var type = GetRequestBodyType(requestBody);
{ if (type == null) return null;
media = _media; return GetRequestDefaultValue(type);
flag = true;
}
else
{
media = null;
}
} }
else
{ var template = CreateTemplate(requestClasses);
media = null; return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(string.Join(string.Empty, template)), App.JsonSerializerOptions);
}
return flag;
} }
public static bool TryGetApplicationJsonMedia(OpenApiRequestBody requestBody, [NotNullWhen(true)] out OpenApiMediaType? media) private List<string> CreateTemplate(AvaloniaList<PropertyClassViewModel> requestClasses)
{
var flag = false;
if (requestBody.Content.TryGetValue("application/json", out var _media))
{
if (_media?.Schema?.Type != null)
{
media = _media;
flag = true;
}
else
{
media = null;
}
}
else
{
media = null;
}
return flag;
}
public static string GetSchemaType(OpenApiSchema? schema)
{
if (schema == null) return "object"; // Because GetLolVanguardV1Notification exists where it has a required parameter without a type...
if (schema.Reference != null) return schema.Reference.Id;
if (schema.Type == "object" && schema.AdditionalProperties?.Reference != null) return schema.AdditionalProperties.Reference.Id;
if (schema.Type == "integer" || schema.Type == "number") return $"{schema.Type}:{schema.Format}";
if (schema.Type == "array" && schema.AdditionalProperties?.Reference != null) return $"{schema.AdditionalProperties.Reference.Id}[]";
if (schema.Type == "array" && schema.AdditionalProperties?.Type != null) return $"{schema.AdditionalProperties.Type}[]";
if (schema.Type == "array" && schema.Items.Reference != null) return $"{schema.Items.Reference.Id}[]";
if (schema.Type == "array" && (schema.Items.Type == "integer" || schema.Items.Type == "number")) return $"{schema.Items.Type}:{schema.Items.Format}[]";
if (schema.Type == "array") return $"{schema.Items.Type}[]";
return schema.Type;
}
public static List<string> CreateTemplate(List<PropertyClassViewModel> requestClasses)
{ {
if (requestClasses.Count == 0) return []; if (requestClasses.Count == 0) return [];
List<string> template = []; List<string> template = [];
@@ -115,7 +83,7 @@ namespace Needlework.Net.Helpers
} }
else else
{ {
List<PropertyClassViewModel> classes = [.. requestClasses]; AvaloniaList<PropertyClassViewModel> classes = [.. requestClasses];
classes.Remove(rootClass); classes.Remove(rootClass);
template[i] = string.Join(string.Empty, CreateTemplate(classes)); template[i] = string.Join(string.Empty, CreateTemplate(classes));
} }
@@ -129,62 +97,7 @@ namespace Needlework.Net.Helpers
return template; return template;
} }
public static string GetComponentId(OpenApiSchema schema) private static string GetRequestDefaultValue(string type)
{
string componentId;
if (schema.Reference != null) componentId = schema.Reference.Id;
else if (schema.Items != null) componentId = schema.Items.Reference.Id;
else componentId = schema.AdditionalProperties.Reference.Id;
return componentId;
}
public static List<ParameterViewModel> GetParameters(List<OpenApiParameter> parameters, ParameterLocation location)
{
var pathParameters = new List<ParameterViewModel>();
foreach (var parameter in parameters)
{
if (parameter.In != location) continue;
pathParameters.Add(new ParameterViewModel(parameter.Name, GetSchemaType(parameter.Schema), parameter.Required));
}
return pathParameters;
}
public static string? GetRequestBodyType(OpenApiRequestBody? requestBody)
{
if (requestBody == null) return null;
if (requestBody.Content.TryGetValue("application/json", out var media))
{
var schema = media.Schema;
if (schema == null) return null; // Because "PostLolAccountVerificationV1SendDeactivationPin" exists where the media body is empty...
return GetSchemaType(schema);
}
return null;
}
public static List<PropertyClassViewModel> GetRequestClasses(OpenApiRequestBody? requestBody, Document document)
{
if (requestBody == null) return [];
if (TryGetApplicationJsonMedia(requestBody, out var media))
{
var rawDocument = document.OpenApiDocument;
var schema = media.Schema;
if (schema == null) return [];
var type = GetSchemaType(media.Schema);
if (IsComponent(type))
{
var componentId = GetComponentId(schema);
var componentSchema = rawDocument.Components.Schemas[componentId];
List<PropertyClassViewModel> propertyClasses = [];
WalkSchema(componentSchema, propertyClasses, rawDocument);
return propertyClasses;
}
}
return [];
}
public static string GetRequestDefaultValue(string type)
{ {
var defaultValue = string.Empty; var defaultValue = string.Empty;
if (type.Contains("[]")) defaultValue = "[]"; if (type.Contains("[]")) defaultValue = "[]";
@@ -196,68 +109,45 @@ namespace Needlework.Net.Helpers
return defaultValue; return defaultValue;
} }
public static string? GetRequestTemplate(OpenApiRequestBody? requestBody, Document document) private string? GetRequestBodyType(OpenApiRequestBody? requestBody)
{ {
var requestClasses = GetRequestClasses(requestBody, document); if (requestBody == null) return null;
if (requestClasses.Count == 0) if (requestBody.Content.TryGetValue("application/json", out var media))
{ {
var type = GetRequestBodyType(requestBody); var schema = media.Schema;
if (type == null) return null; if (schema == null) return null; // Because "PostLolAccountVerificationV1SendDeactivationPin" exists where the media body is empty...
return GetRequestDefaultValue(type); return GetSchemaType(schema);
} }
return null;
var template = CreateTemplate(requestClasses);
return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(string.Join(string.Empty, template)), App.JsonSerializerOptions);
} }
public static List<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses, Document document) private AvaloniaList<ParameterViewModel> GetParameters(IList<OpenApiParameter> parameters, ParameterLocation location)
{ {
if (!TryGetResponse(responses, out var response)) var pathParameters = new AvaloniaList<ParameterViewModel>();
return []; foreach (var parameter in parameters)
if (TryGetApplicationJsonMedia(response, out var media))
{ {
var rawDocument = document.OpenApiDocument; if (parameter.In != location) continue;
var schema = media.Schema; pathParameters.Add(new ParameterViewModel(parameter.Name, GetSchemaType(parameter.Schema), parameter.Required));
if (schema == null) return [];
List<PropertyClassViewModel> propertyClasses = [];
WalkSchema(schema, propertyClasses, rawDocument);
return propertyClasses;
} }
return pathParameters;
}
private AvaloniaList<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses)
{
if (responses.TryGetValue("2XX", out var response)
&& response.Content.TryGetValue("application/json", out var media))
{
var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response;
var schema = media.Schema;
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
WalkSchema(schema, propertyClasses, document);
return propertyClasses;
}
return []; return [];
} }
public static bool IsComponent(string type) private void WalkSchema(OpenApiSchema schema, AvaloniaList<PropertyClassViewModel> propertyClasses, OpenApiDocument document)
{
return !(type.Contains("object")
|| type.Contains("array")
|| type.Contains("bool")
|| type.Contains("string")
|| type.Contains("integer")
|| type.Contains("number"));
}
public static bool TryGetResponse(OpenApiResponses responses, [NotNullWhen(true)] out OpenApiResponse? response)
{
response = null;
var flag = false;
if (responses.TryGetValue("2XX", out var x))
{
response = x;
flag = true;
}
else if (responses.TryGetValue("200", out var y))
{
response = y;
flag = true;
}
return flag;
}
public static void WalkSchema(OpenApiSchema schema, List<PropertyClassViewModel> propertyClasses, OpenApiDocument document)
{ {
var type = GetSchemaType(schema); var type = GetSchemaType(schema);
if (IsComponent(type)) if (IsComponent(type))
@@ -277,12 +167,67 @@ namespace Needlework.Net.Helpers
} }
} }
public static PropertyClassViewModel WalkSchema(OpenApiSchema schema, OpenApiDocument document) private static string GetComponentId(OpenApiSchema schema)
{ {
string componentId = GetComponentId(schema); string componentId;
var componentSchema = document.Components.Schemas[componentId]; if (schema.Reference != null) componentId = schema.Reference.Id;
var propertyClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum); else if (schema.Items != null) componentId = schema.Items.Reference.Id;
return propertyClass; else componentId = schema.AdditionalProperties.Reference.Id;
return componentId;
}
private static bool IsComponent(string type)
{
return !(type.Contains("object")
|| type.Contains("array")
|| type.Contains("bool")
|| type.Contains("string")
|| type.Contains("integer")
|| type.Contains("number"));
}
private AvaloniaList<PropertyClassViewModel> GetRequestClasses(OpenApiRequestBody? requestBody)
{
if (requestBody == null) return [];
if (requestBody.Content.TryGetValue("application/json", out var media))
{
var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response;
var schema = media.Schema;
if (schema == null) return [];
var type = GetSchemaType(media.Schema);
if (IsComponent(type))
{
var componentId = GetComponentId(schema);
var componentSchema = document.Components.Schemas[componentId];
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
WalkSchema(componentSchema, propertyClasses, document);
return propertyClasses;
}
}
return [];
}
private string GetReturnType(OpenApiResponses responses)
{
if (responses.TryGetValue("2XX", out var response)
&& response.Content.TryGetValue("application/json", out var media))
{
var schema = media.Schema;
return GetSchemaType(schema);
}
return "none";
}
public static string GetSchemaType(OpenApiSchema schema)
{
if (schema.Reference != null) return schema.Reference.Id;
if (schema.Type == "object" && schema.AdditionalProperties?.Reference != null) return schema.AdditionalProperties.Reference.Id;
if (schema.Type == "integer" || schema.Type == "number") return $"{schema.Type}:{schema.Format}";
if (schema.Type == "array" && schema.Items.Reference != null) return $"{schema.Items.Reference.Id}[]";
if (schema.Type == "array" && (schema.Items.Type == "integer" || schema.Items.Type == "number")) return $"{schema.Items.Type}:{schema.Items.Format}[]";
if (schema.Type == "array") return $"{schema.Items.Type}[]";
return schema.Type;
} }
} }
} }

View File

@@ -0,0 +1,12 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.ViewModels
{
public abstract partial class PageBase(string displayName, string icon, int index = 0) : ObservableValidator
{
[ObservableProperty] private string _displayName = displayName;
[ObservableProperty] private string _icon = icon;
[ObservableProperty] private int _index = index;
}
}

View File

@@ -1,24 +0,0 @@
using CommunityToolkit.Mvvm.Input;
using System.Diagnostics;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.About;
public partial class AboutViewModel : PageBase
{
public AboutViewModel() : base("About", "fa-solid fa-circle-info")
{
}
public override Task InitializeAsync()
{
return Task.CompletedTask;
}
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process() { StartInfo = new ProcessStartInfo(url) { UseShellExecute = true } };
process.Start();
}
}

View File

@@ -1,45 +0,0 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Services;
using Needlework.Net.ViewModels.Shared;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Console;
public partial class ConsoleViewModel : PageBase
{
private readonly DocumentService _documentService;
public ConsoleViewModel(DocumentService documentService, NotificationService notificationService) : base("Console", "fa-solid fa-terminal")
{
_request = new(notificationService, Endpoints.Tab.LCU);
_documentService = documentService;
}
public List<string> RequestMethods { get; } = ["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE"];
public List<string> RequestPaths { get; } = [];
[ObservableProperty] private bool _isBusy = true;
[ObservableProperty] private RequestViewModel _request;
public override async Task InitializeAsync()
{
var document = await _documentService.GetLcuSchemaDocumentAsync();
Dispatcher.UIThread.Invoke(() =>
{
RequestPaths.Clear();
RequestPaths.AddRange(document.Paths);
});
IsBusy = false;
}
[RelayCommand]
private async Task SendRequest()
{
await Request.ExecuteAsync();
}
}

View File

@@ -1,68 +0,0 @@
using Avalonia;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Models;
using Needlework.Net.Services;
using System;
using System.Collections.ObjectModel;
using System.Linq;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class EndpointListViewModel : ObservableObject
{
private readonly Document _document;
private readonly Tab _tab;
private readonly Action<ObservableObject> _onClicked;
private readonly ObservableCollection<string> _plugins;
private readonly NotificationService _notificationService;
public EndpointListViewModel(NotificationService notificationService, ObservableCollection<string> plugins, Action<ObservableObject> onClicked, Models.Document document, Tab tab)
{
_plugins = new ObservableCollection<string>(plugins);
_document = document;
_tab = tab;
_onClicked = onClicked;
_notificationService = notificationService;
Plugins = EndpointSearchDetails = new ObservableCollection<EndpointSearchDetailsViewModel>(plugins.Select(plugin => new EndpointSearchDetailsViewModel(notificationService, document, tab, onClicked, plugin)));
}
public ObservableCollection<EndpointSearchDetailsViewModel> Plugins { get; }
[ObservableProperty]
private ObservableCollection<EndpointSearchDetailsViewModel> _endpointSearchDetails = [];
[ObservableProperty]
private string _search = string.Empty;
[ObservableProperty]
private Vector _offset = new();
partial void OnSearchChanged(string value)
{
EndpointSearchDetails.Clear();
if (!string.IsNullOrEmpty(Search))
{
EndpointSearchDetails.AddRange(_plugins.Where(plugin => plugin.Contains(value, StringComparison.InvariantCultureIgnoreCase))
.Select(plugin => new EndpointSearchDetailsViewModel(_notificationService, _document, _tab, _onClicked, plugin)));
}
else
{
EndpointSearchDetails.AddRange(
_plugins.Select(plugin => new EndpointSearchDetailsViewModel(_notificationService, _document, _tab, _onClicked, plugin)));
}
}
[RelayCommand]
private void OpenEndpoint(string? value)
{
if (string.IsNullOrEmpty(value)) return;
_onClicked.Invoke(new PluginViewModel(_notificationService, value, _document, _tab));
}
}

View File

@@ -1,38 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Models;
using Needlework.Net.Services;
using System;
namespace Needlework.Net.ViewModels.Pages.Endpoints
{
public partial class EndpointSearchDetailsViewModel : ObservableObject
{
private readonly Document _document;
private readonly Tab _tab;
private readonly Action<ObservableObject> _onClicked;
private readonly NotificationService _notificationService;
public EndpointSearchDetailsViewModel(Services.NotificationService notificationService, Document document, Tab tab, Action<ObservableObject> onClicked, string? plugin)
{
_document = document;
_tab = tab;
_onClicked = onClicked;
_plugin = plugin;
_notificationService = notificationService;
}
[ObservableProperty]
private string? _plugin;
[RelayCommand]
private void OpenEndpoint()
{
if (string.IsNullOrEmpty(Plugin)) return;
_onClicked.Invoke(new PluginViewModel(_notificationService, Plugin, _document, _tab));
}
}
}

View File

@@ -1,61 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.ObjectModel;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class EndpointTabItemContentViewModel : ObservableObject
{
private readonly Action<string?, Guid> _onEndpointNavigation;
private readonly Tab _tab;
public EndpointTabItemContentViewModel(Services.NotificationService notificationService, ObservableCollection<string> plugins, Action<string?, Guid> onEndpointNavigation, IAsyncRelayCommand addEndpointCommand, Models.Document document, Tab tab)
{
_activeViewModel = _endpointsViewModel = new EndpointListViewModel(notificationService, new ObservableCollection<string>(plugins), OnClicked, document, tab);
_onEndpointNavigation = onEndpointNavigation;
_tab = tab;
_title = GetTitle(tab);
AddEndpointCommand = addEndpointCommand;
}
public Guid Guid { get; } = Guid.NewGuid();
public IAsyncRelayCommand AddEndpointCommand { get; }
[ObservableProperty] private ObservableObject _activeViewModel;
[ObservableProperty] private ObservableObject _endpointsViewModel;
[ObservableProperty] private string _title;
private string GetTitle(Tab tab)
{
return tab switch
{
Tab.LCU => "LCU",
Tab.GameClient => "Game Client",
_ => string.Empty,
};
}
private void OnClicked(ObservableObject viewModel)
{
ActiveViewModel = viewModel;
if (viewModel is PluginViewModel endpoint)
{
Title = $"{GetTitle(_tab)} - {endpoint.Title}";
_onEndpointNavigation.Invoke(endpoint.Title, Guid);
}
}
[RelayCommand]
private void GoBack()
{
ActiveViewModel = EndpointsViewModel;
Title = GetTitle(_tab);
_onEndpointNavigation.Invoke(null, Guid);
}
}

View File

@@ -1,12 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class EndpointTabItemViewModel : ObservableObject
{
[ObservableProperty] private string _header = string.Empty;
public IconSource IconSource { get; set; } = new SymbolIconSource() { Symbol = Symbol.Document, FontSize = 20.0, Foreground = Avalonia.Media.Brushes.White };
public bool Selected { get; set; } = false;
public required EndpointTabItemContentViewModel Content { get; init; }
}

View File

@@ -1,78 +0,0 @@
using Avalonia.Threading;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Models;
using Needlework.Net.Services;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public enum Tab
{
LCU,
GameClient
}
public partial class EndpointsViewModel : PageBase
{
private readonly DocumentService _documentService;
private readonly NotificationService _notificationService;
public EndpointsViewModel(DocumentService documentService, NotificationService notificationService) : base("Endpoints", "fa-solid fa-rectangle-list")
{
_documentService = documentService;
_notificationService = notificationService;
}
public ObservableCollection<string> Plugins { get; } = [];
public ObservableCollection<EndpointTabItemViewModel> Endpoints { get; } = [];
[ObservableProperty] private bool _isBusy = true;
public override async Task InitializeAsync()
{
await AddEndpoint(Tab.LCU);
IsBusy = false;
}
[RelayCommand]
private async Task AddEndpoint(Tab tab)
{
Document document = tab switch
{
Tab.LCU => await _documentService.GetLcuSchemaDocumentAsync(),
Tab.GameClient => await _documentService.GetLolClientDocumentAsync(),
_ => throw new NotImplementedException(),
};
await Dispatcher.UIThread.InvokeAsync(() =>
{
Plugins.Clear();
Plugins.AddRange(document.Plugins.Keys);
var vm = new EndpointTabItemContentViewModel(_notificationService, Plugins, OnEndpointNavigation, AddEndpointCommand, document, tab);
Endpoints.Add(new()
{
Content = vm,
Header = vm.Title,
Selected = true
});
});
}
private void OnEndpointNavigation(string? title, Guid guid)
{
foreach (var endpoint in Endpoints)
{
if (endpoint.Content.Guid.Equals(guid))
{
endpoint.Header = endpoint.Content.Title;
break;
}
}
}
}

View File

@@ -1,44 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.OpenApi.Models;
using Needlework.Net.Helpers;
using System.Collections.Generic;
using System.Linq;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class OperationViewModel : ObservableObject
{
public OperationViewModel(OpenApiOperation operation, Models.Document document)
{
Summary = operation.Summary ?? string.Empty;
Description = operation.Description ?? string.Empty;
IsRequestBody = operation.RequestBody != null;
ReturnType = OpenApiHelpers.GetReturnType(operation.Responses);
RequestClasses = OpenApiHelpers.GetRequestClasses(operation.RequestBody, document);
ResponseClasses = OpenApiHelpers.GetResponseClasses(operation.Responses, document);
PathParameters = OpenApiHelpers.GetParameters(operation.Parameters.ToList(), ParameterLocation.Path);
QueryParameters = OpenApiHelpers.GetParameters(operation.Parameters.ToList(), ParameterLocation.Query);
RequestBodyType = OpenApiHelpers.GetRequestBodyType(operation.RequestBody);
RequestTemplate = OpenApiHelpers.GetRequestTemplate(operation.RequestBody, document);
}
public string Summary { get; }
public string Description { get; }
public string ReturnType { get; }
public bool IsRequestBody { get; }
public string? RequestBodyType { get; }
public List<PropertyClassViewModel> RequestClasses { get; }
public List<PropertyClassViewModel> ResponseClasses { get; }
public List<ParameterViewModel> PathParameters { get; }
public List<ParameterViewModel> QueryParameters { get; }
public string? RequestTemplate { get; }
}

View File

@@ -1,24 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class ParameterViewModel : ObservableObject
{
public ParameterViewModel(string name, string type, bool isRequired, string? value = null)
{
Name = name;
Type = type;
IsRequired = isRequired;
Value = value;
}
public string Name { get; }
public string Type { get; }
public bool IsRequired { get; }
[ObservableProperty]
private string? _value = null;
}

View File

@@ -1,73 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Models;
using Needlework.Net.ViewModels.Shared;
using System;
using System.Text;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class PathOperationViewModel : ObservableObject
{
public PathOperationViewModel(Services.NotificationService notificationService, PathOperation pathOperation, Document document, Tab tab)
{
Path = pathOperation.Path;
Operation = new OperationViewModel(pathOperation.Operation, document);
Request = new(() => new RequestViewModel(notificationService, tab)
{
Method = pathOperation.Method.ToUpper(),
RequestDocument = new(Operation.RequestTemplate ?? string.Empty)
});
Url = $"https://swagger.dysolix.dev/lcu/#/{Uri.EscapeDataString(pathOperation.Tag)}/{pathOperation.Operation.OperationId}";
Markdown = $"[{pathOperation.Method.ToUpper()} {Path}]({Url})";
}
public string Path { get; }
public OperationViewModel Operation { get; }
public string Url { get; }
public string Markdown { get; }
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private Lazy<RequestViewModel> _request;
[RelayCommand]
private async Task SendRequest()
{
var sb = new StringBuilder(Path);
foreach (var pathParameter in Operation.PathParameters)
{
sb.Replace($"{{{pathParameter.Name}}}", pathParameter.Value);
}
var firstQueryAdded = false;
foreach (var queryParameter in Operation.QueryParameters)
{
if (!string.IsNullOrWhiteSpace(queryParameter.Value))
{
sb.Append(firstQueryAdded ? '&' : '?');
firstQueryAdded = true;
sb.Append($"{queryParameter.Name}={Uri.EscapeDataString(queryParameter.Value)}");
}
}
Request.Value.RequestPath = sb.ToString();
await Request.Value.ExecuteAsync();
}
[RelayCommand]
private void CopyUrl()
{
App.MainWindow?.Clipboard?.SetTextAsync(Url);
}
[RelayCommand]
private void CopyMarkdown()
{
App.MainWindow?.Clipboard?.SetTextAsync(Markdown);
}
}

View File

@@ -1,55 +0,0 @@
using Avalonia;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class PluginViewModel : ObservableObject
{
public PluginViewModel(Services.NotificationService notificationService, string endpoint, Models.Document document, Tab tab)
{
Endpoint = endpoint;
PathOperations = document.Plugins[endpoint].Select(x => new PathOperationViewModel(notificationService, x, document, tab)).ToList();
FilteredPathOperations = new ObservableCollection<PathOperationViewModel>(PathOperations);
}
public string Endpoint { get; }
public string Title => Endpoint;
public List<PathOperationViewModel> PathOperations { get; }
[ObservableProperty]
private ObservableCollection<PathOperationViewModel> _filteredPathOperations;
[ObservableProperty]
private PathOperationViewModel? _selectedPathOperation;
[ObservableProperty]
private string? _search;
[ObservableProperty]
private Vector _offset = new();
[ObservableProperty]
private Vector _paramsOffset = new();
[ObservableProperty]
private Vector _schemasOffset = new();
partial void OnSearchChanged(string? value)
{
FilteredPathOperations.Clear();
if (string.IsNullOrWhiteSpace(value))
{
FilteredPathOperations.AddRange(PathOperations);
return;
}
FilteredPathOperations.AddRange(PathOperations.Where(o => o.Path.Contains(value, StringComparison.InvariantCultureIgnoreCase)));
}
}

View File

@@ -1,37 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Needlework.Net.Helpers;
using System.Collections.Generic;
using System.Linq;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public class PropertyClassViewModel : ObservableObject
{
public PropertyClassViewModel(string id, IDictionary<string, OpenApiSchema> properties, IList<IOpenApiAny> enumValue)
{
List<PropertyFieldViewModel> propertyFields = [];
List<PropertyEnumViewModel> propertyEnums = [];
foreach ((var propertyName, var propertySchema) in properties)
{
var type = OpenApiHelpers.GetSchemaType(propertySchema);
var field = new PropertyFieldViewModel(propertyName, type);
propertyFields.Add(field);
}
if (enumValue != null && enumValue.Any())
{
var propertyEnum = new PropertyEnumViewModel(enumValue);
propertyEnums.Add(propertyEnum);
}
PropertyFields = propertyFields;
PropertyEnums = propertyEnums;
Id = id;
}
public string Id { get; }
public List<PropertyFieldViewModel> PropertyFields { get; } = [];
public List<PropertyEnumViewModel> PropertyEnums { get; } = [];
}

View File

@@ -1,16 +0,0 @@
using Microsoft.OpenApi.Any;
using System.Collections.Generic;
using System.Linq;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public class PropertyEnumViewModel
{
public PropertyEnumViewModel(IList<IOpenApiAny> enumValue)
{
Values = $"[{string.Join(", ", enumValue.Select(x => $"\"{((OpenApiString)x).Value}\"").ToList())}]";
}
public string Type { get; } = "Enum";
public string Values { get; }
}

View File

@@ -1,14 +0,0 @@
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public class PropertyFieldViewModel
{
public PropertyFieldViewModel(string name, string type)
{
Name = name;
Type = type;
}
public string Name { get; }
public string Type { get; }
}

View File

@@ -1,45 +0,0 @@
using BlossomiShymae.Briar.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class ResponseViewModel : ObservableObject
{
public ResponseViewModel(string path)
{
Path = path;
var processInfo = GetProcessInfo();
if (processInfo != null)
{
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
Path = $"https://127.0.0.1:{processInfo.AppPort}{path}";
Username = riotAuthentication.Username;
Password = riotAuthentication.Password;
Authorization = $"Basic {riotAuthentication.RawValue}";
}
}
[ObservableProperty]
private string? _path;
[ObservableProperty]
private string? _status;
[ObservableProperty]
private string? _authentication;
[ObservableProperty]
private string? _username;
[ObservableProperty]
private string? _password;
[ObservableProperty]
private string? _authorization;
private static ProcessInfo? GetProcessInfo()
{
if (ProcessFinder.IsActive()) return ProcessFinder.GetProcessInfo();
return null;
}
}

View File

@@ -1,15 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.DataModels;
namespace Needlework.Net.ViewModels.Pages.Home
{
public partial class HextechDocsPostViewModel : ObservableObject
{
public HextechDocsPostViewModel(HextechDocsPost hextechDocsPost)
{
HextechDocsPost = hextechDocsPost;
}
public HextechDocsPost HextechDocsPost { get; }
}
}

View File

@@ -1,80 +0,0 @@
using Avalonia;
using Avalonia.Platform;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.Extensions;
using Needlework.Net.Models;
using Needlework.Net.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Text.Json;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Home;
public partial class HomeViewModel : PageBase, IEnableLogger
{
private readonly HextechDocsService _hextechDocsService;
private readonly IDisposable _carouselNextDisposable;
public HomeViewModel(HextechDocsService hextechDocsService) : base("Home", "fa-solid fa-house")
{
_hextechDocsService = hextechDocsService;
_carouselNextDisposable = Observable.Timer(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5))
.Select(time => Unit.Default)
.Subscribe(_ =>
{
if (SelectedHextechDocsPost is HextechDocsPostViewModel vm)
{
var index = HextechDocsPosts.IndexOf(vm);
if (index == HextechDocsPosts.Count - 1)
{
index = 0;
}
else
{
index += 1;
}
SelectedHextechDocsPost = HextechDocsPosts.ElementAt(index);
}
});
}
public List<LibraryViewModel> Libraries { get; } = JsonSerializer.Deserialize<List<Library>>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json")))
!.Where(library => library.Tags.Contains("lcu") || library.Tags.Contains("ingame"))
.Select(library => new LibraryViewModel(library))
.ToList();
[ObservableProperty]
private Vector _librariesOffset = new();
[ObservableProperty]
private List<HextechDocsPostViewModel> _hextechDocsPosts = [];
[ObservableProperty]
private HextechDocsPostViewModel? _selectedHextechDocsPost;
public override async Task InitializeAsync()
{
try
{
var posts = await _hextechDocsService.GetPostsAsync();
var hextechDocsPosts = posts.Select(post => new HextechDocsPostViewModel(post)).ToList();
Dispatcher.UIThread.Invoke(() =>
{
HextechDocsPosts = hextechDocsPosts;
SelectedHextechDocsPost = HextechDocsPosts.First();
});
}
catch (Exception ex)
{
this.Log()
.Error(ex, "Failed to get posts from HextechDocs.");
}
}
}

View File

@@ -1,15 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.Models;
namespace Needlework.Net.ViewModels.Pages.Home
{
public partial class LibraryViewModel : ObservableObject
{
public LibraryViewModel(Library library)
{
Library = library;
}
public Library Library { get; }
}
}

View File

@@ -1,14 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages;
public abstract partial class PageBase(string displayName, string icon) : ObservableValidator
{
public string DisplayName { get; } = displayName;
public string Icon { get; } = icon;
public abstract Task InitializeAsync();
}

View File

@@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages
{
public class PageFactory
{
private readonly IEnumerable<PageBase> _pages;
public PageFactory(IEnumerable<PageBase> pages)
{
_pages = pages;
}
public PageBase GetPage<T>() where T : PageBase
{
var page = _pages.Where(page => typeof(T) == page.GetType())
.FirstOrDefault() ?? throw new NotSupportedException(typeof(T).FullName);
Task.Run(page.InitializeAsync);
return page;
}
}
}

View File

@@ -1,38 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Services;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System;
namespace Needlework.Net.ViewModels.Pages.Schemas
{
public partial class SchemaSearchDetailsViewModel : ObservableObject
{
private readonly SchemaPaneService _schemaPaneService;
public SchemaSearchDetailsViewModel(Tab tab, PropertyClassViewModel vm, SchemaPaneService schemaPaneService)
{
_schemaPaneService = schemaPaneService;
Tab = tab;
Id = vm.Id;
}
public string Id { get; }
public Tab Tab { get; }
public string Document => Tab switch
{
Tab.LCU => "LCU",
Tab.GameClient => "Game Client",
_ => throw new NotImplementedException()
};
[RelayCommand]
private void Display()
{
_schemaPaneService.Add(Id, Tab);
}
}
}

View File

@@ -1,77 +0,0 @@
using Avalonia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using DebounceThrottle;
using Needlework.Net.Helpers;
using Needlework.Net.Services;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Schemas
{
public partial class SchemasViewModel : PageBase
{
private readonly DebounceDispatcher _debounceDispatcher = new(TimeSpan.FromMilliseconds(500));
private readonly DocumentService _documentService;
private readonly SchemaPaneService _schemaPaneService;
private List<SchemaSearchDetailsViewModel> _schemas = [];
public SchemasViewModel(DocumentService documentService, SchemaPaneService schemaPaneService) : base("Schemas", "fa-solid fa-file-lines")
{
_documentService = documentService;
_schemaPaneService = schemaPaneService;
}
[ObservableProperty]
private bool _isBusy = true;
[ObservableProperty]
private string? _search;
[ObservableProperty]
private List<SchemaSearchDetailsViewModel> _schemaItems = [];
[ObservableProperty]
private Vector _offset = new();
partial void OnSearchChanged(string? value)
{
_debounceDispatcher.Debounce(() =>
{
if (string.IsNullOrEmpty(value))
{
Dispatcher.UIThread.Invoke(() =>
{
SchemaItems = _schemas.ToList();
});
return;
}
var items = _schemas.Where(schema => schema.Id.Contains(value, StringComparison.OrdinalIgnoreCase))
.ToList();
Dispatcher.UIThread.Invoke(() => { SchemaItems = items; });
});
}
public override async Task InitializeAsync()
{
var lcuSchemaDocument = await _documentService.GetLcuSchemaDocumentAsync();
var lolClientDocument = await _documentService.GetLolClientDocumentAsync();
Dispatcher.UIThread.Invoke(() =>
{
var schemas = Enumerable.Concat(
lcuSchemaDocument.OpenApiDocument.Components.Schemas.Values.Select(schema => new SchemaSearchDetailsViewModel(Tab.LCU, OpenApiHelpers.WalkSchema(schema, lcuSchemaDocument.OpenApiDocument), _schemaPaneService)),
lolClientDocument.OpenApiDocument.Components.Schemas.Values.Select(schema => new SchemaSearchDetailsViewModel(Tab.GameClient, OpenApiHelpers.WalkSchema(schema, lolClientDocument.OpenApiDocument), _schemaPaneService))
).ToList();
_schemas = schemas;
SchemaItems = schemas.ToList();
IsBusy = false;
});
}
}
}

View File

@@ -1,197 +0,0 @@
using Akavache;
using Avalonia.Threading;
using BlossomiShymae.Briar;
using BlossomiShymae.Briar.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Needlework.Net.Constants;
using Needlework.Net.DataModels;
using Needlework.Net.Extensions;
using Needlework.Net.Models;
using Needlework.Net.Services;
using System;
using System.Net.Http.Json;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Settings
{
public partial class SettingsViewModel : PageBase, IEnableLogger
{
private readonly IBlobCache _blobCache;
private readonly IDisposable _checkForUpdatesDisposable;
private readonly IDisposable _checkForSchemaVersionDisposable;
private readonly GithubService _githubService;
private readonly DocumentService _documentService;
private readonly NotificationService _notificationService;
private readonly TaskCompletionSource<bool> _initializeTaskCompletionSource = new();
public SettingsViewModel(IBlobCache blobCache, GithubService githubService, DocumentService documentService, NotificationService notificationService) : base("Settings", "fa-solid fa-gear")
{
_blobCache = blobCache;
_githubService = githubService;
_documentService = documentService;
_notificationService = notificationService;
_checkForUpdatesDisposable = Observable.Timer(TimeSpan.Zero, Intervals.CheckForUpdates)
.Select(time => Unit.Default)
.Subscribe(async _ =>
{
try
{
await _initializeTaskCompletionSource.Task;
if (AppSettings!.IsCheckForUpdates)
{
await CheckForUpdatesAsync();
}
}
catch (Exception ex)
{
var message = "Failed to check for updates. Please check your internet connection or try again later.";
this.Log()
.Error(ex, message);
_notificationService.Notify(AppInfo.Name, message, InfoBarSeverity.Error);
_checkForUpdatesDisposable?.Dispose();
}
});
_checkForSchemaVersionDisposable = Observable.Timer(TimeSpan.Zero, TimeSpan.FromMinutes(5))
.Select(time => Unit.Default)
.Subscribe(async _ =>
{
try
{
await _initializeTaskCompletionSource.Task;
if (AppSettings!.IsCheckForSchema)
{
await CheckForSchemaVersionAsync();
}
}
catch (Exception ex)
{
var message = "Failed to check for schema version. Please check your internet connection or try again later.";
this.Log()
.Error(ex, message);
_notificationService.Notify(AppInfo.Name, message, InfoBarSeverity.Error);
_checkForSchemaVersionDisposable?.Dispose();
}
});
}
[ObservableProperty]
private bool _isBusy = true;
[ObservableProperty]
private AppSettings? _appSettings;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(UpdateCheckTitle), nameof(UpdateCheckIconValue), nameof(UpdateCheckLastChecked))]
private Guid _upToDateGuid = Guid.Empty;
public bool IsUpToDate { get; private set; }
public bool IsSchemaVersionChecked { get; private set; }
public string UpdateCheckTitle => IsUpToDate switch
{
true => "You're up to date",
false => "You're out of date"
};
public string UpdateCheckIconValue => IsUpToDate switch
{
true => "fa-heart-circle-check",
false => "fa-heart-circle-exclamation"
};
public string UpdateCheckLastChecked => $"Last checked: {DateTime.Now:dddd}, {DateTime.Now:T}";
partial void OnAppSettingsChanged(AppSettings? value)
{
if (AppSettings is AppSettings appSettings)
{
_blobCache.InsertObject(BlobCacheKeys.AppSettings, appSettings);
}
}
public override async Task InitializeAsync()
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
try
{
AppSettings = await _blobCache.GetObject<AppSettings>(BlobCacheKeys.AppSettings);
}
catch (Exception ex)
{
this.Log()
.Warning(ex, "Failed to get application settings.");
AppSettings = new();
}
finally
{
AppSettings!.PropertyChanged += (s, e) => OnAppSettingsChanged((AppSettings?)s);
IsBusy = false;
_initializeTaskCompletionSource.SetResult(true);
}
});
}
[RelayCommand]
private async Task CheckForUpdatesAsync()
{
var release = await _githubService.GetLatestReleaseAsync();
if (release.IsLatest(AppInfo.Version))
{
this.Log()
.Information("New version available: {TagName}", release.TagName);
_notificationService.Notify(AppInfo.Name, $"New version available: {release.TagName}", InfoBarSeverity.Informational, null, "https://github.com/BlossomiShymae/Needlework.Net/releases/latest");
_checkForUpdatesDisposable?.Dispose();
IsUpToDate = false;
}
else
{
IsUpToDate = true;
}
UpToDateGuid = Guid.NewGuid();
}
private async Task CheckForSchemaVersionAsync()
{
if (!ProcessFinder.IsPortOpen()) return;
var lcuSchemaDocument = await _documentService.GetLcuSchemaDocumentAsync();
var client = Connector.GetLcuHttpClientInstance();
var currentSemVer = lcuSchemaDocument.Info.Version.Split('.');
var systemBuild = await client.GetFromJsonAsync<SystemBuild>("/system/v1/builds") ?? throw new NullReferenceException();
var latestSemVer = systemBuild.Version.Split('.');
if (!IsSchemaVersionChecked)
{
this.Log()
.Information("LCU Schema (current): {Version}", lcuSchemaDocument.Info.Version);
this.Log()
.Information("LCU Schema (latest): {Version}", systemBuild.Version);
IsSchemaVersionChecked = true;
}
bool isVersionMatching = currentSemVer[0] == latestSemVer[0] && currentSemVer[1] == latestSemVer[1]; // Compare major and minor versions
if (!isVersionMatching)
{
this.Log()
.Warning("LCU Schema outdated: Current {CurrentVersion}, Latest {LatestVersion}", lcuSchemaDocument.Info.Version, systemBuild.Version);
_notificationService.Notify(AppInfo.Name, $"LCU Schema is outdated compared to latest system build. Consider submitting a pull request on dysolix/hasagi-types.\nCurrent: {string.Join(".", currentSemVer)}\nLatest: {string.Join(".", latestSemVer)}", InfoBarSeverity.Warning, null, "https://github.com/dysolix/hasagi-types#updating-the-types");
_checkForSchemaVersionDisposable?.Dispose();
}
}
}
}

View File

@@ -1,23 +0,0 @@
using BlossomiShymae.Briar.WebSocket.Events;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
namespace Needlework.Net.ViewModels.Pages.WebSocket;
public class EventViewModel : ObservableObject
{
public EventViewModel(EventData eventData)
{
Time = $"{DateTime.Now:HH:mm:ss.fff}";
Type = eventData?.EventType?.ToUpper() ?? string.Empty;
Uri = eventData?.Uri ?? string.Empty;
}
public string Time { get; }
public string Type { get; }
public string Uri { get; }
public string Key => $"{Time} {Type} {Uri}";
}

View File

@@ -1,225 +0,0 @@
using Avalonia;
using Avalonia.Collections;
using AvaloniaEdit.Document;
using BlossomiShymae.Briar;
using BlossomiShymae.Briar.WebSocket.Events;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Flurl.Http;
using Flurl.Http.Configuration;
using Needlework.Net.Constants;
using Needlework.Net.Extensions;
using Needlework.Net.Messages;
using Needlework.Net.Services;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Websocket.Client;
namespace Needlework.Net.ViewModels.Pages.WebSocket;
public partial class WebSocketViewModel : PageBase, IEnableLogger
{
private Dictionary<string, EventMessage> _events = [];
private readonly IFlurlClient _githubUserContentClient;
private readonly NotificationService _notificationService;
private readonly object _tokenLock = new();
public WebSocketViewModel(IFlurlClientCache clients, NotificationService notificationService) : base("Event Viewer", "fa-solid fa-plug")
{
_githubUserContentClient = clients.Get(FlurlClientKeys.GithubUserContentClient);
_notificationService = notificationService;
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
}
public ObservableCollection<EventViewModel> EventLog { get; } = [];
public SemaphoreSlim EventLogLock { get; } = new(1, 1);
public WebsocketClient? Client { get; set; }
public List<IDisposable> ClientDisposables = [];
public CancellationTokenSource TokenSource { get; set; } = new();
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
[ObservableProperty]
private Vector _eventLogOffset = new();
[NotifyPropertyChangedFor(nameof(FilteredEventLog))]
[ObservableProperty]
private string _search = string.Empty;
[ObservableProperty]
private bool _isAttach = true;
[ObservableProperty]
private bool _isTail = false;
[ObservableProperty]
private EventViewModel? _selectedEventLog = null;
[ObservableProperty]
private IAvaloniaList<string> _eventTypes = new AvaloniaList<string>();
[ObservableProperty]
private string _eventType = "OnJsonApiEvent";
[ObservableProperty]
private TextDocument _document = new();
[ObservableProperty]
private Vector _documentOffset = new();
public override async Task InitializeAsync()
{
await InitializeEventTypes();
InitializeWebsocket();
}
private async Task InitializeEventTypes()
{
try
{
var file = await _githubUserContentClient.Request("/dysolix/hasagi-types/refs/heads/main/dist/lcu-events.d.ts")
.GetStringAsync();
var matches = EventTypesRegex().Matches(file);
Avalonia.Threading.Dispatcher.UIThread.Invoke(() => EventTypes.AddRange(matches.Select(m => m.Groups[1].Value)));
}
catch (HttpRequestException ex)
{
var message = "Failed to get event types from GitHub. Please check your internet connection or try again later.";
this.Log()
.Error(ex, message);
_notificationService.Notify(AppInfo.Name, message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error);
}
}
private void InitializeWebsocket()
{
lock (_tokenLock)
{
if (Client != null)
{
this.Log()
.Debug("Disposing old connection");
foreach (var disposable in ClientDisposables)
disposable.Dispose();
ClientDisposables.Clear();
Client.Dispose();
}
TokenSource.Cancel();
var tokenSource = new CancellationTokenSource();
var thread = new Thread(() =>
{
while (!tokenSource.IsCancellationRequested)
{
try
{
var client = Connector.CreateLcuWebsocketClient();
ClientDisposables.Add(client.EventReceived.Subscribe(OnMessage));
ClientDisposables.Add(client.DisconnectionHappened.Subscribe(OnDisconnection));
ClientDisposables.Add(client.ReconnectionHappened.Subscribe(OnReconnection));
client.Start();
client.Send(new EventMessage(EventRequestType.Subscribe, new EventKind() { Prefix = EventType }));
Client = client;
return;
}
catch (Exception) { }
Thread.Sleep(TimeSpan.FromSeconds(5));
}
})
{ IsBackground = true };
thread.Start();
this.Log()
.Debug("Initialized new connection: {EventType}", EventType);
TokenSource = tokenSource;
}
}
partial void OnSelectedEventLogChanged(EventViewModel? value)
{
if (value == null) return;
if (_events.TryGetValue(value.Key, out var message))
{
var text = JsonSerializer.Serialize(message, App.JsonSerializerOptions);
if (text.Length >= App.MaxCharacters) WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(text));
else Document = new(text);
}
}
[RelayCommand]
private void Clear()
{
_events.Clear();
EventLog.Clear();
Document = new();
}
private void OnReconnection(ReconnectionInfo info)
{
this.Log()
.Debug("Reconnected: {Type}", info.Type);
}
private void OnDisconnection(DisconnectionInfo info)
{
this.Log()
.Debug("Disconnected: {Type}", info.Type);
InitializeWebsocket();
}
partial void OnEventTypeChanged(string value)
{
InitializeWebsocket();
}
private void OnMessage(EventMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(async () =>
{
if (!IsAttach) return;
var line = new EventViewModel(message.Data!);
await EventLogLock.WaitAsync();
try
{
if (EventLog.Count < 1000)
{
EventLog.Add(line);
_events[line.Key] = message;
}
else
{
var _event = EventLog[0];
EventLog.RemoveAt(0);
_events.Remove(_event.Key);
EventLog.Add(line);
_events[line.Key] = message;
}
}
finally
{
EventLogLock.Release();
}
});
}
[GeneratedRegex("\"(.*?)\":")]
public static partial Regex EventTypesRegex();
}

View File

@@ -0,0 +1,20 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.ViewModels
{
public partial class ParameterViewModel : ObservableObject
{
public string Name { get; }
public string Type { get; }
public bool IsRequired { get; }
[ObservableProperty] private string? _value = null;
public ParameterViewModel(string name, string type, bool isRequired, string? value = null)
{
Name = name;
Type = type;
IsRequired = isRequired;
Value = value;
}
}
}

View File

@@ -0,0 +1,121 @@
using Avalonia.Media;
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using Needlework.Net.Models;
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels
{
public partial class PathOperationViewModel : ObservableObject
{
public string Method { get; }
public SolidColorBrush Color { get; }
public string Path { get; }
public OperationViewModel Operation { get; }
public ProcessInfo? ProcessInfo { get; }
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private Lazy<ResponseViewModel> _response;
public PathOperationViewModel(PathOperation pathOperation)
{
Method = pathOperation.Method.ToUpper();
Color = new SolidColorBrush(GetColor(Method));
Path = pathOperation.Path;
Operation = new OperationViewModel(pathOperation.Operation);
Response = new(() => new ResponseViewModel(pathOperation.Path));
}
[RelayCommand]
public async Task SendRequest()
{
try
{
IsBusy = true;
var method = Method switch
{
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
"DELETE" => HttpMethod.Delete,
"HEAD" => HttpMethod.Head,
"PATCH" => HttpMethod.Patch,
"OPTIONS" => HttpMethod.Options,
"TRACE" => HttpMethod.Trace,
_ => throw new Exception("Method is missing.")
};
var processInfo = ProcessFinder.Get();
var sb = new StringBuilder(Path);
foreach (var pathParameter in Operation.PathParameters)
{
sb.Replace($"{{{pathParameter.Name}}}", pathParameter.Value);
}
var firstQueryAdded = false;
foreach (var queryParameter in Operation.QueryParameters)
{
if (!string.IsNullOrWhiteSpace(queryParameter.Value))
{
sb.Append(firstQueryAdded ? '&' : '?');
firstQueryAdded = true;
sb.Append($"{queryParameter.Name}={Uri.EscapeDataString(queryParameter.Value)}");
}
}
var uri = sb.ToString();
var requestBody = WeakReferenceMessenger.Default.Send(new ContentRequestMessage(), "EndpointRequestEditor").Response;
var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var client = Connector.GetLcuHttpClientInstance();
var response = await client.SendAsync(new(method, uri) { Content = content });
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
var responseBytes = await response.Content.ReadAsByteArrayAsync();
var responseBody = responseBytes.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBytes), App.JsonSerializerOptions) : string.Empty;
if (responseBody.Length >= App.MaxCharacters)
{
WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(responseBody));
WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(string.Empty, "EndpointResponseEditor")));
}
else WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(responseBody, "EndpointResponseEditor")));
Response.Value.Status = $"{(int)response.StatusCode} {response.StatusCode}";
Response.Value.Path = $"https://127.0.0.1:{processInfo.AppPort}{uri}";
Response.Value.Authentication = Response.Value.Authorization = $"Basic {riotAuthentication.Value}";
Response.Value.Username = riotAuthentication.Username;
Response.Value.Password = riotAuthentication.Password;
}
catch (Exception ex)
{
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5))));
WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(string.Empty, "EndpointResponseEditor")));
}
finally
{
IsBusy = false;
}
}
public static Color GetColor(string method) => method switch
{
"GET" => Avalonia.Media.Color.FromRgb(95, 99, 186),
"POST" => Avalonia.Media.Color.FromRgb(103, 186, 95),
"PUT" => Avalonia.Media.Color.FromRgb(186, 139, 95),
"DELETE" => Avalonia.Media.Color.FromRgb(186, 95, 95),
"HEAD" => Avalonia.Media.Color.FromRgb(136, 95, 186),
"PATCH" => Avalonia.Media.Color.FromRgb(95, 186, 139),
_ => throw new InvalidOperationException("Method does not have assigned color.")
};
}
}

View File

@@ -0,0 +1,36 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using System.Collections.Generic;
using System.Linq;
namespace Needlework.Net.ViewModels
{
public class PropertyClassViewModel : ObservableObject
{
public string Id { get; }
public IAvaloniaReadOnlyList<PropertyFieldViewModel> PropertyFields { get; } = new AvaloniaList<PropertyFieldViewModel>();
public IAvaloniaReadOnlyList<PropertyEnumViewModel> PropertyEnums { get; } = new AvaloniaList<PropertyEnumViewModel>();
public PropertyClassViewModel(string id, IDictionary<string, OpenApiSchema> properties, IList<IOpenApiAny> enumValue)
{
AvaloniaList<PropertyFieldViewModel> propertyFields = [];
AvaloniaList<PropertyEnumViewModel> propertyEnums = [];
foreach ((var propertyName, var propertySchema) in properties)
{
var type = OperationViewModel.GetSchemaType(propertySchema);
var field = new PropertyFieldViewModel(propertyName, type);
propertyFields.Add(field);
}
if (enumValue != null && enumValue.Any())
{
var propertyEnum = new PropertyEnumViewModel(enumValue);
propertyEnums.Add(propertyEnum);
}
PropertyFields = propertyFields;
PropertyEnums = propertyEnums;
Id = id;
}
}
}

View File

@@ -0,0 +1,17 @@
using Microsoft.OpenApi.Any;
using System.Collections.Generic;
using System.Linq;
namespace Needlework.Net.ViewModels
{
public class PropertyEnumViewModel
{
public string Type { get; } = "Enum";
public string Values { get; }
public PropertyEnumViewModel(IList<IOpenApiAny> enumValue)
{
Values = $"[{string.Join(", ", enumValue.Select(x => $"\"{((OpenApiString)x).Value}\"").ToList())}]";
}
}
}

View File

@@ -0,0 +1,14 @@
namespace Needlework.Net.ViewModels
{
public class PropertyFieldViewModel
{
public string Name { get; }
public string Type { get; }
public PropertyFieldViewModel(string name, string type)
{
Name = name;
Type = type;
}
}
}

View File

@@ -0,0 +1,35 @@
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.ViewModels
{
public partial class ResponseViewModel : ObservableObject
{
[ObservableProperty] private string? _path;
[ObservableProperty] private string? _status;
[ObservableProperty] private string? _authentication;
[ObservableProperty] private string? _username;
[ObservableProperty] private string? _password;
[ObservableProperty] private string? _authorization;
public ResponseViewModel(string path)
{
Path = path;
var processInfo = GetProcessInfo();
if (processInfo != null)
{
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
Path = $"https://127.0.0.1:{processInfo.AppPort}{path}";
Username = riotAuthentication.Username;
Password = riotAuthentication.Password;
Authorization = $"Basic {riotAuthentication.RawValue}";
}
}
private static ProcessInfo? GetProcessInfo()
{
if (ProcessFinder.IsActive()) return ProcessFinder.Get();
return null;
}
}
}

View File

@@ -1,237 +0,0 @@
using Avalonia;
using Avalonia.Media;
using AvaloniaEdit.Document;
using BlossomiShymae.Briar;
using BlossomiShymae.Briar.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Extensions;
using Needlework.Net.Messages;
using Needlework.Net.Services;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Shared;
public partial class RequestViewModel : ObservableObject, IEnableLogger
{
private readonly NotificationService _notificationService;
private readonly Tab _tab;
public RequestViewModel(NotificationService notificationService, Tab tab)
{
_tab = tab;
_notificationService = notificationService;
}
[ObservableProperty]
private string? _method = "GET";
[ObservableProperty]
private SolidColorBrush _color = new(GetColor("GET"));
[ObservableProperty]
private bool _isRequestBusy;
[ObservableProperty]
private string? _requestPath;
[ObservableProperty]
private TextDocument _requestDocument = new();
[ObservableProperty]
private Vector _requestDocumentOffset = new();
[ObservableProperty]
private TextDocument _responseDocument = new();
[ObservableProperty]
private Vector _responseDocumentOffset = new();
[ObservableProperty]
private double _responseDocumentHorizontalScrollBar;
[ObservableProperty]
private double _responseDocumentVerticalScrollBar;
[ObservableProperty]
private string? _responsePath;
[ObservableProperty]
private string? _responseStatus;
[ObservableProperty]
private string? _responseAuthentication;
[ObservableProperty]
private string? _responseUsername;
[ObservableProperty]
private string? _responsePassword;
[ObservableProperty]
private string? _responseAuthorization;
partial void OnMethodChanged(string? oldValue, string? newValue)
{
if (newValue == null) return;
Color = new(GetColor(newValue));
}
public async Task ExecuteAsync()
{
switch (_tab)
{
case Tab.LCU:
await ExecuteLcuAsync();
break;
case Tab.GameClient:
await ExecuteGameClientAsync();
break;
default:
break;
}
}
private async Task ExecuteGameClientAsync()
{
try
{
IsRequestBusy = true;
if (string.IsNullOrEmpty(RequestPath))
throw new Exception("Path is empty.");
var method = GetMethod();
this.Log()
.Debug("Sending request: {Tuple}", (Method, RequestPath));
var requestBody = RequestDocument.Text;
var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var client = Connector.GetGameHttpClientInstance();
var response = await client.SendAsync(new HttpRequestMessage(method, RequestPath) { Content = content });
var responseBody = await response.Content.ReadAsByteArrayAsync();
var body = responseBody.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), App.JsonSerializerOptions) : string.Empty;
if (body.Length > App.MaxCharacters)
{
WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(body));
ResponseDocument = new();
}
else
{
ResponseDocument = new(body);
}
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}";
ResponsePath = $"https://127.0.0.1:2999{RequestPath}";
}
catch (Exception ex)
{
this.Log()
.Error(ex, "Request failed: {Tuple}", (Method, RequestPath));
_notificationService.Notify("Request failed", ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error);
ResponseStatus = null;
ResponsePath = null;
ResponseAuthentication = null;
ResponseAuthorization = null;
ResponseUsername = null;
ResponsePassword = null;
ResponseDocument = new();
}
finally
{
IsRequestBusy = false;
}
}
private async Task ExecuteLcuAsync()
{
try
{
IsRequestBusy = true;
if (string.IsNullOrEmpty(RequestPath))
throw new Exception("Path is empty.");
var method = GetMethod();
this.Log()
.Debug("Sending request: {Tuple}", (Method, RequestPath));
var processInfo = ProcessFinder.GetProcessInfo();
var requestBody = RequestDocument.Text;
var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var client = Connector.GetLcuHttpClientInstance();
var response = await client.SendAsync(new(method, RequestPath) { Content = content });
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
var responseBody = await response.Content.ReadAsByteArrayAsync();
var body = responseBody.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), App.JsonSerializerOptions) : string.Empty;
if (body.Length >= App.MaxCharacters)
{
WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(body));
ResponseDocument = new();
}
else
{
ResponseDocument = new(body);
}
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}";
ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{RequestPath}";
ResponseAuthentication = riotAuthentication.Value;
ResponseAuthorization = $"Basic {riotAuthentication.Value}";
ResponseUsername = riotAuthentication.Username;
ResponsePassword = riotAuthentication.Password;
}
catch (Exception ex)
{
this.Log()
.Error(ex, "Request failed: {Tuple}", (Method, RequestPath));
_notificationService.Notify("Request failed", ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error);
ResponseStatus = null;
ResponsePath = null;
ResponseAuthentication = null;
ResponseAuthorization = null;
ResponseUsername = null;
ResponsePassword = null;
ResponseDocument = new();
}
finally
{
IsRequestBusy = false;
}
}
private HttpMethod GetMethod()
{
return Method switch
{
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
"DELETE" => HttpMethod.Delete,
"HEAD" => HttpMethod.Head,
"PATCH" => HttpMethod.Patch,
"OPTIONS" => HttpMethod.Options,
"TRACE" => HttpMethod.Trace,
_ => throw new Exception("Method is not selected or missing."),
};
}
private static Color GetColor(string method) => method switch
{
"GET" => Avalonia.Media.Color.FromRgb(95, 99, 186),
"POST" => Avalonia.Media.Color.FromRgb(103, 186, 95),
"PUT" => Avalonia.Media.Color.FromRgb(186, 139, 95),
"DELETE" => Avalonia.Media.Color.FromRgb(186, 95, 95),
"HEAD" => Avalonia.Media.Color.FromRgb(136, 95, 186),
"PATCH" => Avalonia.Media.Color.FromRgb(95, 186, 139),
_ => throw new InvalidOperationException("Method does not have assigned color.")
};
}

View File

@@ -0,0 +1,126 @@
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading;
using Websocket.Client;
namespace Needlework.Net.ViewModels
{
public partial class WebsocketViewModel : PageBase
{
public ObservableCollection<EventViewModel> EventLog { get; } = [];
public SemaphoreSlim EventLogLock { get; } = new(1, 1);
[NotifyPropertyChangedFor(nameof(FilteredEventLog))]
[ObservableProperty] private string _search = string.Empty;
[ObservableProperty] private bool _isAttach = true;
[ObservableProperty] private bool _isTail = false;
[ObservableProperty] private EventViewModel? _selectedEventLog = null;
private Dictionary<string, EventMessage> _events = [];
public WebsocketClient? Client { get; set; }
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
public WebsocketViewModel() : base("Event Viewer", "plug", -100)
{
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
thread.Start();
}
private void InitializeWebsocket()
{
while (true)
{
try
{
var client = Connector.CreateLcuWebsocketClient();
client.EventReceived.Subscribe(OnMessage);
client.DisconnectionHappened.Subscribe(OnDisconnection);
client.ReconnectionHappened.Subscribe(OnReconnection);
client.Start();
client.Send(new EventMessage(RequestType.Subscribe, EventMessage.Kinds.OnJsonApiEvent));
Client = client;
return;
}
catch (Exception) { }
Thread.Sleep(TimeSpan.FromSeconds(5));
}
}
partial void OnSelectedEventLogChanged(EventViewModel? value)
{
if (value == null) return;
if (_events.TryGetValue(value.Key, out var message))
{
var text = JsonSerializer.Serialize(message, App.JsonSerializerOptions);
if (text.Length >= App.MaxCharacters) WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(text));
else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebsocketViewModel));
}
}
[RelayCommand]
private void Clear()
{
_events.Clear();
EventLog.Clear();
}
private void OnReconnection(ReconnectionInfo info)
{
Trace.WriteLine($"-- Reconnection --\nType{info.Type}");
}
private void OnDisconnection(DisconnectionInfo info)
{
Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}");
Client?.Dispose();
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
thread.Start();
}
private void OnMessage(EventMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(async () =>
{
if (!IsAttach) return;
var line = new EventViewModel(message.Data!);
await EventLogLock.WaitAsync();
try
{
if (EventLog.Count < 1000)
{
EventLog.Add(line);
_events[line.Key] = message;
}
else
{
var _event = EventLog[0];
EventLog.RemoveAt(0);
_events.Remove(_event.Key);
EventLog.Add(line);
_events[line.Key] = message;
}
}
finally
{
EventLogLock.Release();
}
});
}
}
}

View File

@@ -0,0 +1,167 @@
<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:vm="using:Needlework.Net.ViewModels"
xmlns:controls="using:Needlework.Net.Controls"
xmlns:i="https://github.com/projektanker/icons.avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.AboutView"
x:DataType="vm:AboutViewModel">
<UserControl.Styles>
<Style Selector="Button">
<Setter Property="Theme" Value="{StaticResource TransparentButton}"/>
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
</Style>
<Style Selector="i|Icon">
<Setter Property="FontSize" Value="20" />
</Style>
</UserControl.Styles>
<Grid Margin="8"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<StackPanel Spacing="8">
<Grid HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal">
<controls:Card Margin="8">
<Image Source="/Assets/Users/blossomishymae.png"
RenderOptions.BitmapInterpolationMode="MediumQuality"
Width="200"
Height="200"/>
</controls:Card>
<StackPanel Margin="8 0 0 0">
<controls:Card Width="400" Margin="8">
<StackPanel Orientation="Horizontal">
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
Margin="0 0 8 0">Blossomi Shymae</TextBlock>
<Button CommandParameter="https://github.com/BlossomiShymae">
<i:Icon Value="fa-github"/>
</Button>
</StackPanel>
</controls:Card>
<controls:Card Width="400" Margin="8">
<StackPanel >
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">About</TextBlock>
<TextBlock TextWrapping="Wrap">
Needlework.Net is the .NET rewrite of Needlework. This tool was made to help others with LCU development. Feel free to ask any questions
or help contribute to the project! Made with love. 💜
</TextBlock>
</StackPanel>
</controls:Card>
</StackPanel>
</StackPanel>
</Grid>
<Border Width="800">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Thanks to the friends and people who made this tool possible...</TextBlock>
</Border>
<WrapPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal"
Margin="8">
<controls:Card>
<Image Source="/Assets/Users/dysolix.png"
RenderOptions.BitmapInterpolationMode="MediumQuality"
Width="100"
Height="100"/>
</controls:Card>
<StackPanel Margin="2 0 0 0">
<controls:Card Width="250" Margin="2">
<StackPanel Orientation="Horizontal">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
Margin="0 0 8 0">dysolix</TextBlock>
<Button CommandParameter="https://github.com/dysolix">
<i:Icon Value="fa-github"/>
</Button>
</StackPanel>
</controls:Card>
<controls:Card Width="250" Margin="2">
<StackPanel >
<TextBlock TextWrapping="Wrap">
For providing and hosting an auto-generated OpenAPI document of the LCU.
</TextBlock>
</StackPanel>
</controls:Card>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal"
Margin="8">
<controls:Card>
<Image Source="/Assets/Users/ray.png"
RenderOptions.BitmapInterpolationMode="MediumQuality"
Width="100"
Height="100"/>
</controls:Card>
<StackPanel Margin="2 0 0 0">
<controls:Card Width="250" Margin="2">
<StackPanel Orientation="Horizontal">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
Margin="0 0 8 0">Ray</TextBlock>
<Button CommandParameter="https://github.com/Hi-Ray">
<i:Icon Value="fa-github"/>
</Button>
</StackPanel>
</controls:Card>
<controls:Card Width="250" Margin="2">
<StackPanel >
<TextBlock TextWrapping="Wrap">
For guidance, advice, or providing help via HextechDocs.
</TextBlock>
</StackPanel>
</controls:Card>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal"
Margin="8">
<controls:Card>
<Image Source="/Assets/Users/dubble.png"
RenderOptions.BitmapInterpolationMode="MediumQuality"
Width="100"
Height="100"/>
</controls:Card>
<StackPanel Margin="4 0 0 0">
<controls:Card Width="250" Margin="2">
<StackPanel Orientation="Horizontal">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
Margin="0 0 8 0">dubble</TextBlock>
<Button CommandParameter="https://github.com/cuppachino">
<i:Icon Value="fa-github"/>
</Button>
</StackPanel>
</controls:Card>
<controls:Card Width="250" Margin="2">
<StackPanel >
<TextBlock TextWrapping="Wrap">
For encouraging me to publish Needlework. This project may never have seen the light of day without him.
</TextBlock>
</StackPanel>
</controls:Card>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal"
Margin="8">
<controls:Card>
<Image Source="/Assets/Users/community.png"
RenderOptions.BitmapInterpolationMode="MediumQuality"
Width="100"
Height="100"/>
</controls:Card>
<StackPanel Margin="4 0 0 0">
<controls:Card Width="250" Margin="2">
<StackPanel Orientation="Horizontal">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
Width="250"
TextWrapping="Wrap">Third Party Developer Community</TextBlock>
</StackPanel>
</controls:Card>
<controls:Card Width="250" Margin="2">
<StackPanel >
<TextBlock TextWrapping="Wrap">
For providing numerous documentation on the LCU.
</TextBlock>
</StackPanel>
</controls:Card>
</StackPanel>
</StackPanel>
</WrapPanel>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,6 +1,6 @@
using Avalonia.Controls; using Avalonia.Controls;
namespace Needlework.Net.Views.Pages.About; namespace Needlework.Net.Views;
public partial class AboutView : UserControl public partial class AboutView : UserControl
{ {

View File

@@ -5,10 +5,10 @@
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit" xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives" xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Console" xmlns:vm="using:Needlework.Net.ViewModels"
xmlns:controls="using:Needlework.Net.Controls" xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Console.ConsoleView" x:Class="Needlework.Net.Views.ConsoleView"
x:DataType="vm:ConsoleViewModel"> x:DataType="vm:ConsoleViewModel">
<controls:BusyArea IsBusy="{Binding IsBusy}" <controls:BusyArea IsBusy="{Binding IsBusy}"
BusyText="Loading..."> BusyText="Loading...">
@@ -16,16 +16,16 @@
<Grid Grid.Row="0" <Grid Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="2"> Grid.ColumnSpan="2">
<StackPanel Margin="0 0 0 8"> <StackPanel Margin="0 0 0 16">
<Grid RowDefinitions="auto" ColumnDefinitions="auto,*,auto"> <Grid RowDefinitions="auto" ColumnDefinitions="auto,*,auto">
<ComboBox ItemsSource="{Binding RequestMethods}" <ComboBox ItemsSource="{Binding RequestMethods}"
SelectedItem="{Binding Request.Method}" SelectedItem="{Binding RequestMethodSelected}"
Margin="0 0 8 0" Margin="0 0 8 0"
Grid.Row="0" Grid.Row="0"
Grid.Column="0"/> Grid.Column="0"/>
<AutoCompleteBox <AutoCompleteBox
ItemsSource="{Binding RequestPaths}" ItemsSource="{Binding RequestPaths}"
Text="{Binding Request.RequestPath}" Text="{Binding RequestPath}"
MaxDropDownHeight="400" MaxDropDownHeight="400"
FilterMode="StartsWith" FilterMode="StartsWith"
Grid.Row="0" Grid.Row="0"
@@ -49,23 +49,17 @@
<TextBox IsReadOnly="True" <TextBox IsReadOnly="True"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Text="{Binding Request.ResponsePath}"/> Text="{Binding ResponsePath}"/>
<avaloniaEdit:TextEditor <avaloniaEdit:TextEditor
Name="RequestEditor" Name="RequestEditor"
Document="{Binding Request.RequestDocument}" Text=""
ShowLineNumbers="True" ShowLineNumbers="True"
HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"
Margin="0 8 0 0" Margin="0 8 0 0"
FontSize="12" FontSize="12"
Grid.Row="1" Grid.Row="1"
Grid.Column="0"> Grid.Column="0"/>
<avaloniaEdit:TextEditor.Styles>
<Style Selector="ScrollViewer#PART_ScrollViewer">
<Setter Property="Offset" Value="{Binding Request.RequestDocumentOffset, Mode=TwoWay}"/>
</Style>
</avaloniaEdit:TextEditor.Styles>
</avaloniaEdit:TextEditor>
</Grid> </Grid>
<Grid RowDefinitions="35,*" <Grid RowDefinitions="35,*"
ColumnDefinitions="*" ColumnDefinitions="*"
@@ -75,7 +69,7 @@
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Grid.Row="0" Grid.Row="0"
Grid.Column="0"> Grid.Column="0">
<Button Content="{Binding Request.ResponseStatus}" <Button Content="{Binding ResponseStatus}"
FontSize="12" FontSize="12"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
</StackPanel> </StackPanel>
@@ -83,19 +77,12 @@
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
Name="ResponseEditor" Name="ResponseEditor"
Document="{Binding Request.ResponseDocument}"
HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"
ShowLineNumbers="True" ShowLineNumbers="True"
IsReadOnly="True" IsReadOnly="True"
Text="" Text=""
FontSize="12"> FontSize="12"/>
<avaloniaEdit:TextEditor.Styles>
<Style Selector="ScrollViewer#PART_ScrollViewer">
<Setter Property="Offset" Value="{Binding Request.ResponseDocumentOffset, Mode=TwoWay}"/>
</Style>
</avaloniaEdit:TextEditor.Styles>
</avaloniaEdit:TextEditor>
</Grid> </Grid>
</Grid> </Grid>
</controls:BusyArea> </controls:BusyArea>

Some files were not shown because too many files have changed in this diff Show More