mirror of
https://github.com/BlossomiShymae/Needlework.Net.git
synced 2025-12-06 10:10:48 +01:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adc8b0c0f1 | ||
|
|
be7d575b48 | ||
|
|
f9dd654b6a | ||
|
|
57d3eb4172 | ||
|
|
ce2336ab4d | ||
|
|
9a76e1af4a | ||
|
|
6f0126863b | ||
|
|
826134888e | ||
|
|
ef16642c04 | ||
|
|
a5f49c48b8 | ||
|
|
1364cdc38c | ||
|
|
c51f20a324 | ||
|
|
6d1acee8df | ||
|
|
375d5a2ff8 | ||
|
|
2aa77f3e02 | ||
|
|
576863bd72 | ||
|
|
68e5abd1d1 | ||
|
|
b18f425257 | ||
|
|
5ebed22ae3 | ||
|
|
dc44cf72df | ||
|
|
01cb8886c6 | ||
|
|
38e4a64bb8 | ||
|
|
b63713f054 | ||
|
|
6a776dfd5f | ||
|
|
9270c6d1f1 | ||
|
|
f65c6f1b09 | ||
|
|
bd6589c310 | ||
|
|
cf947f3af4 | ||
|
|
2e4637f533 | ||
|
|
7aaa79956c | ||
|
|
e9d4615ecf | ||
|
|
fb63adc1b7 | ||
|
|
b41be19cd9 | ||
|
|
38e1ea2301 | ||
|
|
30451b8c8c | ||
|
|
05927030eb | ||
|
|
e9f99a9e28 | ||
|
|
1e838abdbf | ||
|
|
dede2e909c | ||
|
|
58556283f0 | ||
|
|
16781a4df4 | ||
|
|
569f49d484 | ||
|
|
8eabd64911 | ||
|
|
02e739e1a3 | ||
|
|
c253d00ff1 | ||
|
|
4edd71a04a | ||
|
|
a4fe10157f | ||
|
|
bc4ed78767 | ||
|
|
375285067d | ||
|
|
3ec277bdd3 | ||
|
|
c097890588 | ||
|
|
3352740733 | ||
|
|
48751efc28 | ||
|
|
b6f713c675 | ||
|
|
59619764c2 | ||
|
|
de6f9f64dd | ||
|
|
4eae0bd913 |
40
.github/workflows/release.yml
vendored
40
.github/workflows/release.yml
vendored
@@ -1,9 +1,12 @@
|
||||
# .github/workflows/release.yml
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
env:
|
||||
PROJECT_NAME: Needlework.Net
|
||||
ASSEMBLY_NAME: NeedleworkDotNet
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -15,30 +18,15 @@ jobs:
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: release
|
||||
uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: dotnet build Needlework.Net -c Release
|
||||
run: dotnet build ${{env.PROJECT_NAME}} -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
|
||||
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:
|
||||
name: "Needlework.Net v${{ steps.version.outputs.VERSION }}"
|
||||
prerelease: false
|
||||
tag_name: "${{ steps.version.outputs.VERSION }}"
|
||||
files: |
|
||||
NeedleworkDotNet-win-x64.zip
|
||||
files: ${{env.ASSEMBLY_NAME}}-win-x64.zip
|
||||
@@ -15,6 +15,7 @@
|
||||
<sty:FluentAvaloniaTheme PreferSystemTheme="False" PreferUserAccentColor="False" />
|
||||
<materialIcons:MaterialIconStyles />
|
||||
<StyleInclude Source="Controls/Card.axaml"/>
|
||||
<StyleInclude Source="Controls/UserCard.axaml"/>
|
||||
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
</Application.Styles>
|
||||
|
||||
@@ -3,8 +3,8 @@ using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Needlework.Net.ViewModels;
|
||||
using Needlework.Net.Views;
|
||||
using Needlework.Net.ViewModels.MainWindow;
|
||||
using Needlework.Net.Views.MainWindow;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -33,15 +33,13 @@ public partial class App(IServiceProvider serviceProvider) : Application
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow()
|
||||
desktop.MainWindow = new MainWindowView()
|
||||
{
|
||||
DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
|
||||
};
|
||||
MainWindow = desktop.MainWindow;
|
||||
}
|
||||
|
||||
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
BIN
Needlework.Net/Assets/Users/aoshiw.png
Normal file
BIN
Needlework.Net/Assets/Users/aoshiw.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 5.1 KiB |
BIN
Needlework.Net/Assets/Users/sylv.jpg
Normal file
BIN
Needlework.Net/Assets/Users/sylv.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
1
Needlework.Net/Assets/libraries.json
Normal file
1
Needlework.Net/Assets/libraries.json
Normal file
@@ -0,0 +1 @@
|
||||
[{"Repo":"GrrrLCU","Description":"A simple wrapper for the LCU. Grrr. x3","Language":"C#","Link":"https://github.com/BlossomiShymae/GrrrLCU"},{"Repo":"Kunc.RiotGames","Description":null,"Language":"C#","Link":"https://github.com/AoshiW/Kunc.RiotGames"},{"Repo":"rito","Description":"Rito is a simple, crossplatform (Windows and Linux) C++20 library interfacing with Riot services (i.e. Riot REST API and League of Legends client).","Language":"cpp","Link":"https://github.com/bartekprtc/rito"},{"Repo":"R4J","Description":"A Java library containing the API for every Riot game","Language":"Java","Link":"https://github.com/stelar7/R4J"},{"Repo":"hasagi-core","Description":"LCU library with auto-generated types for request parameters and responses","Language":"JavaScript","Link":"https://github.com/dysolix/hasagi-core"},{"Repo":"lcu-driver","Description":"Python3 helper for the League of Legends LCU API.","Language":"Python","Link":"https://github.com/sousa-andre/lcu-driver"},{"Repo":"willump","Description":"Python3 helper for the League of Legends LCU API.","Language":"Python","Link":"https://github.com/elliejs/Willump"},{"Repo":"Irelia","Description":"LoL LCU Wrapper for Rust, built on top of hyper!","Language":"Rust","Link":"https://github.com/AlsoSylv/Irelia"},{"Repo":"Shaco","Description":"League of Legends LCU wrapper for rust","Language":"Rust","Link":"https://github.com/Leastrio/Shaco"},{"Repo":"hasagi-core","Description":"LCU library with auto-generated types for request parameters and responses","Language":"TypeScript","Link":"https://github.com/dysolix/hasagi-core"},{"Repo":"hexgate","Description":"LCU API wrapper for League of Legends","Language":"TypeScript","Link":"https://github.com/cuppachino/hexgate"}]
|
||||
71
Needlework.Net/Controls/UserCard.axaml
Normal file
71
Needlework.Net/Controls/UserCard.axaml
Normal file
@@ -0,0 +1,71 @@
|
||||
<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="16,16,16,16"
|
||||
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>
|
||||
91
Needlework.Net/Controls/UserCard.axaml.cs
Normal file
91
Needlework.Net/Controls/UserCard.axaml.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
60
Needlework.Net/DataSource.cs
Normal file
60
Needlework.Net/DataSource.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.OpenApi.Readers;
|
||||
using Needlework.Net.Models;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Needlework.Net
|
||||
{
|
||||
public class DataSource
|
||||
{
|
||||
private readonly ILogger<DataSource> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
private Document? _lcuSchemaDocument;
|
||||
private Document? _lolClientDocument;
|
||||
private readonly TaskCompletionSource<bool> _taskCompletionSource = new();
|
||||
|
||||
|
||||
public DataSource(HttpClient httpClient, ILogger<DataSource> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Document> GetLcuSchemaDocumentAsync()
|
||||
{
|
||||
await _taskCompletionSource.Task;
|
||||
return _lcuSchemaDocument ?? throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
public async Task<Document> GetLolClientDocumentAsync()
|
||||
{
|
||||
await _taskCompletionSource.Task;
|
||||
return _lolClientDocument ?? throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var reader = new OpenApiStreamReader();
|
||||
var lcuSchemaStream = await _httpClient.GetStreamAsync("https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json");
|
||||
var lcuSchemaRaw = reader.Read(lcuSchemaStream, out var _);
|
||||
_lcuSchemaDocument = new Document(lcuSchemaRaw);
|
||||
|
||||
var lolClientStream = await _httpClient.GetStreamAsync("https://raw.githubusercontent.com/AlsoSylv/Irelia/refs/heads/master/schemas/game_schema.json");
|
||||
var lolClientRaw = reader.Read(lolClientStream, out var _);
|
||||
_lolClientDocument = new Document(lolClientRaw);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to initialize DataSource");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_taskCompletionSource.SetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Needlework.Net/Logger.cs
Normal file
27
Needlework.Net/Logger.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Needlework.Net
|
||||
{
|
||||
public static class Logger
|
||||
{
|
||||
public static void Setup(ILoggingBuilder builder)
|
||||
{
|
||||
var logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.File("Logs/debug-", rollingInterval: RollingInterval.Day, shared: true)
|
||||
.CreateLogger();
|
||||
logger.Debug("NeedleworkDotNet version: {Version}", Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0");
|
||||
logger.Debug("OS description: {Description}", System.Runtime.InteropServices.RuntimeInformation.OSDescription);
|
||||
builder.AddSerilog(logger);
|
||||
}
|
||||
|
||||
public static void LogFatal(UnhandledExceptionEventArgs e)
|
||||
{
|
||||
File.WriteAllText($"Logs/fatal-{DateTime.Now:HHmmssfff}", e.ExceptionObject.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Needlework.Net.Messages
|
||||
{
|
||||
public class ContentRequestMessage : RequestMessage<string>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using Needlework.Net.Models;
|
||||
|
||||
namespace Needlework.Net.Messages
|
||||
{
|
||||
public class DataReadyMessage(OpenApiDocumentWrapper wrapper) : ValueChangedMessage<OpenApiDocumentWrapper>(wrapper)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using Needlework.Net.Models;
|
||||
|
||||
namespace Needlework.Net.Messages
|
||||
{
|
||||
public class DataRequestMessage : RequestMessage<OpenApiDocumentWrapper>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Needlework.Net.Messages
|
||||
{
|
||||
public class HostDocumentRequestMessage : RequestMessage<OpenApiDocument>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using Needlework.Net.ViewModels;
|
||||
using Needlework.Net.ViewModels.MainWindow;
|
||||
|
||||
namespace Needlework.Net.Messages
|
||||
{
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Needlework.Net.Models;
|
||||
|
||||
public class OpenApiDocumentWrapper
|
||||
public class Document
|
||||
{
|
||||
internal OpenApiDocument OpenApiDocument { get; }
|
||||
|
||||
@@ -13,7 +14,7 @@ public class OpenApiDocumentWrapper
|
||||
|
||||
public List<string> Paths => [.. OpenApiDocument.Paths.Keys];
|
||||
|
||||
public OpenApiDocumentWrapper(OpenApiDocument openApiDocument)
|
||||
public Document(OpenApiDocument openApiDocument)
|
||||
{
|
||||
OpenApiDocument = openApiDocument;
|
||||
var plugins = new SortedDictionary<string, List<PathOperation>>();
|
||||
@@ -35,10 +36,10 @@ public class OpenApiDocumentWrapper
|
||||
{
|
||||
pluginsKey = "default";
|
||||
if (plugins.TryGetValue(pluginsKey, out var p))
|
||||
p.Add(new(method.ToString(), path, operation));
|
||||
p.Add(new(method.ToString(), path, pluginsKey, operation));
|
||||
else
|
||||
{
|
||||
operations.Add(new(method.ToString(), path, operation));
|
||||
operations.Add(new(method.ToString(), path, pluginsKey, operation));
|
||||
plugins[pluginsKey] = operations;
|
||||
}
|
||||
}
|
||||
@@ -46,19 +47,16 @@ public class OpenApiDocumentWrapper
|
||||
{
|
||||
foreach (var tag in operation.Tags)
|
||||
{
|
||||
var lowercaseTag = tag.Name.ToLower();
|
||||
if (lowercaseTag == "plugins")
|
||||
if (tag.Name == "plugins")
|
||||
continue;
|
||||
else if (lowercaseTag.Contains("plugin "))
|
||||
pluginsKey = lowercaseTag.Replace("plugin ", "");
|
||||
else
|
||||
pluginsKey = lowercaseTag;
|
||||
pluginsKey = tag.Name;
|
||||
|
||||
if (plugins.TryGetValue(pluginsKey, out var p))
|
||||
p.Add(new(method.ToString(), path, operation));
|
||||
p.Add(new(method.ToString(), path, tag.Name, operation));
|
||||
else
|
||||
{
|
||||
operations.Add(new(method.ToString(), path, operation));
|
||||
operations.Add(new(method.ToString(), path, tag.Name, operation));
|
||||
plugins[pluginsKey] = operations;
|
||||
}
|
||||
}
|
||||
@@ -66,6 +64,10 @@ public class OpenApiDocumentWrapper
|
||||
}
|
||||
}
|
||||
|
||||
plugins = new(plugins.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value.OrderBy(x => x.Path).ToList()));
|
||||
|
||||
Plugins = plugins;
|
||||
}
|
||||
}
|
||||
9
Needlework.Net/Models/Library.cs
Normal file
9
Needlework.Net/Models/Library.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Needlework.Net.Models;
|
||||
|
||||
public class Library
|
||||
{
|
||||
public required string Repo { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public required string Language { get; init; }
|
||||
public required string Link { get; init; }
|
||||
}
|
||||
@@ -2,4 +2,4 @@ using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Needlework.Net.Models;
|
||||
|
||||
public record PathOperation(string Method, string Path, OpenApiOperation Operation);
|
||||
public record PathOperation(string Method, string Path, string Tag, OpenApiOperation Operation);
|
||||
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
10
Needlework.Net/Models/SystemBuild.cs
Normal file
10
Needlework.Net/Models/SystemBuild.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
|
||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||
<AssemblyName>NeedleworkDotNet</AssemblyName>
|
||||
<AssemblyVersion>0.6.1.0</AssemblyVersion>
|
||||
<AssemblyVersion>0.12.0.0</AssemblyVersion>
|
||||
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
|
||||
</PropertyGroup>
|
||||
@@ -27,22 +27,33 @@
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
|
||||
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
|
||||
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.11.1" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.14.0" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0" />
|
||||
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="1.6.17" />
|
||||
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.17" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="1.6.22" />
|
||||
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.22" />
|
||||
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.4.0" />
|
||||
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
|
||||
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.62" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.65" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaXaml Remove="Utilities\**" />
|
||||
<Compile Remove="Utilities\**" />
|
||||
<EmbeddedResource Remove="Utilities\**" />
|
||||
<None Remove="Utilities\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<UpToDateCheckInput Remove="Views\AboutView.axaml" />
|
||||
@@ -52,13 +63,18 @@
|
||||
<Compile Update="Controls\BusyArea.axaml.cs">
|
||||
<DependentUpon>BusyArea.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\EndpointView.axaml.cs">
|
||||
<Compile Update="Views\MainWindow\MainWindowView.axaml.cs">
|
||||
<DependentUpon>MainWindowView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\Pages\Endpoints\EndpointsNavigationView.axaml.cs">
|
||||
<DependentUpon>EndpointsNavigationView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\Pages\Endpoints\EndpointView.axaml.cs">
|
||||
<DependentUpon>EndpointView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Assets\Users\" />
|
||||
<Folder Include="Utilities\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Needlework.Net.Extensions;
|
||||
using Needlework.Net.Services;
|
||||
using Needlework.Net.ViewModels;
|
||||
using Needlework.Net.ViewModels.MainWindow;
|
||||
using Needlework.Net.ViewModels.Pages;
|
||||
using Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
using Projektanker.Icons.Avalonia;
|
||||
using Projektanker.Icons.Avalonia.FontAwesome;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Needlework.Net;
|
||||
|
||||
@@ -29,11 +32,23 @@ class Program
|
||||
{
|
||||
IconProvider.Current
|
||||
.Register<FontAwesomeIconProvider>();
|
||||
var services = BuildServices();
|
||||
Task.Run(async () => await InitializeDataSourceAsync(services));
|
||||
|
||||
return AppBuilder.Configure(() => new App(BuildServices()))
|
||||
return AppBuilder.Configure(() => new App(services))
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
.LogToTrace()
|
||||
.With(new Win32PlatformOptions
|
||||
{
|
||||
CompositionMode = [Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition]
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task InitializeDataSourceAsync(IServiceProvider services)
|
||||
{
|
||||
var dataSource = services.GetRequiredService<DataSource>();
|
||||
await dataSource.InitializeAsync();
|
||||
}
|
||||
|
||||
private static IServiceProvider BuildServices()
|
||||
@@ -42,9 +57,18 @@ class Program
|
||||
|
||||
builder.AddSingleton<MainWindowViewModel>();
|
||||
builder.AddSingleton<DialogService>();
|
||||
builder.AddSingleton<DataSource>();
|
||||
builder.AddSingletonsFromAssemblies<PageBase>();
|
||||
|
||||
builder.AddHttpClient();
|
||||
builder.AddHttpClient(nameof(EndpointsTabViewModel)).ConfigurePrimaryHttpMessageHandler(() => // Insecure SSL for Game Client API
|
||||
{
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
};
|
||||
return handler;
|
||||
});
|
||||
builder.AddLogging(Logger.Setup);
|
||||
|
||||
var services = builder.BuildServiceProvider();
|
||||
return services;
|
||||
@@ -52,6 +76,6 @@ class Program
|
||||
|
||||
private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
File.WriteAllText($"errorlog-{DateTime.Now:HHmmssfff}", e.ExceptionObject.ToString());
|
||||
Logger.LogFatal(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,50 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Needlework.Net
|
||||
{
|
||||
public class ViewLocator : IDataTemplate
|
||||
{
|
||||
public Control? Build(object? param)
|
||||
private readonly Dictionary<object, Control> _controlCache = [];
|
||||
|
||||
public Control Build(object? data)
|
||||
{
|
||||
if (param is null) return new TextBlock { Text = "data was null" };
|
||||
var name = data?.GetType().Name;
|
||||
if (name is null)
|
||||
{
|
||||
return new TextBlock { Text = "Data is null or has no name." };
|
||||
}
|
||||
if (!name.Contains("ViewModel"))
|
||||
{
|
||||
return new TextBlock { Text = "Data name must end with ViewModel." };
|
||||
}
|
||||
|
||||
var name = param.GetType().FullName!
|
||||
.Replace("ViewModels", "Views")
|
||||
.Replace("ViewModel", "View");
|
||||
var type = Type.GetType(name);
|
||||
name = name.Replace("ViewModel", "View");
|
||||
var type = Assembly.GetExecutingAssembly()
|
||||
.GetTypes()
|
||||
.Where(t => t.Name == name)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (type != null) return (Control)Activator.CreateInstance(type)!;
|
||||
else return new TextBlock { Text = "Not Found: " + 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;
|
||||
}
|
||||
|
||||
public bool Match(object? data)
|
||||
{
|
||||
return data is INotifyPropertyChanged;
|
||||
}
|
||||
public bool Match(object? data) => data is INotifyPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
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 = Connector.GetProcessInfo();
|
||||
var requestBody = WeakReferenceMessenger.Default.Send(new ContentRequestMessage(), "ConsoleRequestEditor").Response;
|
||||
var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
|
||||
var response = await Connector.SendAsync(method, RequestPath, 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
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")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Needlework.Net/ViewModels/MainWindow/InfoBarViewModel.cs
Normal file
26
Needlework.Net/ViewModels/MainWindow/InfoBarViewModel.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Avalonia.Controls;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using System;
|
||||
|
||||
namespace Needlework.Net.ViewModels.MainWindow;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
217
Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs
Normal file
217
Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
using Avalonia.Collections;
|
||||
using BlossomiShymae.GrrrLCU;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.Messages;
|
||||
using Needlework.Net.Models;
|
||||
using Needlework.Net.Services;
|
||||
using Needlework.Net.ViewModels.Pages;
|
||||
using Needlework.Net.Views.MainWindow;
|
||||
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.Tasks;
|
||||
using System.Timers;
|
||||
|
||||
namespace Needlework.Net.ViewModels.MainWindow;
|
||||
|
||||
public partial class MainWindowViewModel
|
||||
: ObservableObject, IRecipient<InfoBarUpdateMessage>, IRecipient<OopsiesDialogRequestedMessage>
|
||||
{
|
||||
public IAvaloniaReadOnlyList<NavigationViewItem> MenuItems { get; }
|
||||
[ObservableProperty] private NavigationViewItem _selectedMenuItem;
|
||||
[ObservableProperty] private PageBase _currentPage;
|
||||
|
||||
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
|
||||
[ObservableProperty] private bool _isUpdateShown = false;
|
||||
|
||||
[ObservableProperty] private string _schemaVersion = "N/A";
|
||||
[ObservableProperty] private string _schemaVersionLatest = "N/A";
|
||||
|
||||
public HttpClient HttpClient { get; }
|
||||
public DialogService DialogService { get; }
|
||||
|
||||
private readonly DataSource _dataSource;
|
||||
|
||||
[ObservableProperty] private bool _isBusy = true;
|
||||
|
||||
[ObservableProperty] private ObservableCollection<InfoBarViewModel> _infoBarItems = [];
|
||||
|
||||
private readonly ILogger<MainWindowViewModel> _logger;
|
||||
|
||||
private readonly System.Timers.Timer _latestUpdateTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMinutes(10).TotalMilliseconds,
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
private readonly System.Timers.Timer _schemaVersionTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(5).TotalMilliseconds,
|
||||
Enabled = true
|
||||
};
|
||||
private bool _isSchemaVersionChecked = false;
|
||||
|
||||
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, DialogService dialogService, ILogger<MainWindowViewModel> logger, DataSource dataSource)
|
||||
{
|
||||
_logger = logger;
|
||||
_dataSource = dataSource;
|
||||
|
||||
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];
|
||||
CurrentPage = (PageBase)MenuItems[0].Tag!;
|
||||
|
||||
HttpClient = httpClient;
|
||||
DialogService = dialogService;
|
||||
|
||||
WeakReferenceMessenger.Default.RegisterAll(this);
|
||||
|
||||
_latestUpdateTimer.Elapsed += OnLatestUpdateTimerElapsed;
|
||||
_schemaVersionTimer.Elapsed += OnSchemaVersionTimerElapsed;
|
||||
_latestUpdateTimer.Start();
|
||||
_schemaVersionTimer.Start();
|
||||
OnLatestUpdateTimerElapsed(null, null);
|
||||
OnSchemaVersionTimerElapsed(null, null);
|
||||
|
||||
}
|
||||
|
||||
partial void OnSelectedMenuItemChanged(NavigationViewItem value)
|
||||
{
|
||||
if (value.Tag is PageBase page)
|
||||
{
|
||||
CurrentPage = page;
|
||||
if (!page.IsInitialized)
|
||||
{
|
||||
Task.Run(page.InitializeAsync);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnSchemaVersionTimerElapsed(object? sender, ElapsedEventArgs? e)
|
||||
{
|
||||
if (!ProcessFinder.IsPortOpen()) return;
|
||||
var lcuSchemaDocument = await _dataSource.GetLcuSchemaDocumentAsync();
|
||||
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
_logger.LogInformation("LCU Schema (current): {Version}", lcuSchemaDocument.Info.Version);
|
||||
_logger.LogInformation("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)
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ShowInfoBarAsync(new("Newer System Build", true, $"LCU Schema is possibly 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, TimeSpan.FromSeconds(60), new Avalonia.Controls.Button()
|
||||
{
|
||||
Command = OpenUrlCommand,
|
||||
CommandParameter = "https://github.com/dysolix/hasagi-types#updating-the-types",
|
||||
Content = "Submit PR"
|
||||
}));
|
||||
});
|
||||
|
||||
_schemaVersionTimer.Elapsed -= OnSchemaVersionTimerElapsed;
|
||||
_schemaVersionTimer.Stop();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Schema version check failed");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnLatestUpdateTimerElapsed(object? sender, ElapsedEventArgs? e)
|
||||
{
|
||||
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)
|
||||
{
|
||||
_logger.LogWarning("Release response is 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(30), new Avalonia.Controls.Button()
|
||||
{
|
||||
Command = OpenUrlCommand,
|
||||
CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases",
|
||||
Content = "Download"
|
||||
}));
|
||||
});
|
||||
|
||||
_latestUpdateTimer.Elapsed -= OnLatestUpdateTimerElapsed;
|
||||
_latestUpdateTimer.Stop();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to check for latest version");
|
||||
}
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Needlework.Net.Messages;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Needlework.Net.ViewModels
|
||||
{
|
||||
public partial class OperationViewModel : ObservableObject
|
||||
{
|
||||
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)
|
||||
{
|
||||
Summary = operation.Summary ?? string.Empty;
|
||||
Description = operation.Description ?? string.Empty;
|
||||
IsRequestBody = operation.RequestBody != null;
|
||||
ReturnType = GetReturnType(operation.Responses);
|
||||
RequestClasses = GetRequestClasses(operation.RequestBody);
|
||||
ResponseClasses = GetResponseClasses(operation.Responses);
|
||||
PathParameters = GetParameters(operation.Parameters, ParameterLocation.Path);
|
||||
QueryParameters = GetParameters(operation.Parameters, ParameterLocation.Query);
|
||||
RequestBodyType = GetRequestBodyType(operation.RequestBody);
|
||||
RequestTemplate = GetRequestTemplate(operation.RequestBody);
|
||||
}
|
||||
|
||||
private string? GetRequestTemplate(OpenApiRequestBody? requestBody)
|
||||
{
|
||||
var requestClasses = GetRequestClasses(requestBody);
|
||||
if (requestClasses.Count == 0)
|
||||
{
|
||||
var type = GetRequestBodyType(requestBody);
|
||||
if (type == null) return null;
|
||||
return GetRequestDefaultValue(type);
|
||||
}
|
||||
|
||||
var template = CreateTemplate(requestClasses);
|
||||
return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(string.Join(string.Empty, template)), App.JsonSerializerOptions);
|
||||
}
|
||||
|
||||
private List<string> CreateTemplate(AvaloniaList<PropertyClassViewModel> requestClasses)
|
||||
{
|
||||
if (requestClasses.Count == 0) return [];
|
||||
List<string> template = [];
|
||||
template.Add("{");
|
||||
|
||||
var rootClass = requestClasses.First();
|
||||
if (rootClass.PropertyEnums.Any()) return [rootClass.PropertyEnums.First().Values];
|
||||
var propertyFields = rootClass.PropertyFields;
|
||||
for (int i = 0; i < propertyFields.Count; i++)
|
||||
{
|
||||
template.Add($"\"{propertyFields[i].Name}\"");
|
||||
template.Add(":");
|
||||
template.Add($"#{propertyFields[i].Type}");
|
||||
|
||||
if (i == propertyFields.Count - 1) template.Add("}");
|
||||
else template.Add(",");
|
||||
}
|
||||
|
||||
for (int i = 0; i < template.Count; i++)
|
||||
{
|
||||
var type = template[i];
|
||||
if (!type.Contains("#")) continue;
|
||||
|
||||
var foundClass = requestClasses.Where(c => c.Id == type.Replace("#", string.Empty));
|
||||
if (foundClass.Any())
|
||||
{
|
||||
if (foundClass.First().PropertyEnums.Any())
|
||||
{
|
||||
template[i] = string.Join(string.Empty, CreateTemplate([.. foundClass]));
|
||||
}
|
||||
else
|
||||
{
|
||||
AvaloniaList<PropertyClassViewModel> classes = [.. requestClasses];
|
||||
classes.Remove(rootClass);
|
||||
template[i] = string.Join(string.Empty, CreateTemplate(classes));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
template[i] = GetRequestDefaultValue(type);
|
||||
}
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private static string GetRequestDefaultValue(string type)
|
||||
{
|
||||
var defaultValue = string.Empty;
|
||||
if (type.Contains("[]")) defaultValue = "[]";
|
||||
else if (type.Contains("string")) defaultValue = "\"\"";
|
||||
else if (type.Contains("boolean")) defaultValue = "false";
|
||||
else if (type.Contains("integer")) defaultValue = "0";
|
||||
else if (type.Contains("double") || type.Contains("float")) defaultValue = "0.0";
|
||||
else if (type.Contains("object")) defaultValue = "{}";
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
private AvaloniaList<ParameterViewModel> GetParameters(IList<OpenApiParameter> parameters, ParameterLocation location)
|
||||
{
|
||||
var pathParameters = new AvaloniaList<ParameterViewModel>();
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
if (parameter.In != location) continue;
|
||||
pathParameters.Add(new ParameterViewModel(parameter.Name, GetSchemaType(parameter.Schema), parameter.Required));
|
||||
}
|
||||
|
||||
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 [];
|
||||
}
|
||||
|
||||
private void WalkSchema(OpenApiSchema schema, AvaloniaList<PropertyClassViewModel> propertyClasses, OpenApiDocument document)
|
||||
{
|
||||
var type = GetSchemaType(schema);
|
||||
if (IsComponent(type))
|
||||
{
|
||||
string componentId = GetComponentId(schema);
|
||||
var componentSchema = document.Components.Schemas[componentId];
|
||||
var responseClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum);
|
||||
|
||||
if (propertyClasses.Where(c => c.Id == componentId).Any()) return; // Avoid adding duplicate schemas in classes
|
||||
propertyClasses.Add(responseClass);
|
||||
|
||||
foreach ((var _, var property) in componentSchema.Properties)
|
||||
// Check for self-references like "LolLootLootOddsResponse"
|
||||
// I blame dubble
|
||||
if (IsComponent(GetSchemaType(property)) && componentId != GetComponentId(property))
|
||||
WalkSchema(property, propertyClasses, document);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetComponentId(OpenApiSchema schema)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
32
Needlework.Net/ViewModels/Pages/AboutViewModel.cs
Normal file
32
Needlework.Net/ViewModels/Pages/AboutViewModel.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages;
|
||||
|
||||
public partial class AboutViewModel : PageBase
|
||||
{
|
||||
public HttpClient HttpClient { get; }
|
||||
|
||||
public AboutViewModel(HttpClient httpClient) : base("About", "info-circle")
|
||||
{
|
||||
HttpClient = httpClient;
|
||||
}
|
||||
|
||||
public override Task InitializeAsync()
|
||||
{
|
||||
IsInitialized = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenUrl(string url)
|
||||
{
|
||||
var process = new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo(url) { UseShellExecute = true }
|
||||
};
|
||||
process.Start();
|
||||
}
|
||||
}
|
||||
45
Needlework.Net/ViewModels/Pages/ConsoleViewModel.cs
Normal file
45
Needlework.Net/ViewModels/Pages/ConsoleViewModel.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages;
|
||||
|
||||
public partial class ConsoleViewModel : PageBase
|
||||
{
|
||||
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 RequestViewModel _request;
|
||||
|
||||
private readonly DataSource _dataSource;
|
||||
|
||||
public ConsoleViewModel(ILogger<RequestViewModel> requestViewModelLogger, DataSource dataSource, HttpClient httpClient) : base("Console", "terminal", -200)
|
||||
{
|
||||
_request = new(requestViewModelLogger, Endpoints.Tab.LCU, httpClient);
|
||||
_dataSource = dataSource;
|
||||
}
|
||||
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
var document = await _dataSource.GetLcuSchemaDocumentAsync();
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
RequestPaths.Clear();
|
||||
RequestPaths.AddRange(document.Paths);
|
||||
});
|
||||
IsBusy = false;
|
||||
IsInitialized = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SendRequest()
|
||||
{
|
||||
await Request.ExecuteAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
|
||||
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 event EventHandler<string>? PathOperationSelected;
|
||||
|
||||
public EndpointViewModel(string endpoint, ILogger<RequestViewModel> requestViewModelLogger, Models.Document document, Tab tab, System.Net.Http.HttpClient httpClient)
|
||||
{
|
||||
Endpoint = endpoint;
|
||||
PathOperations = new AvaloniaList<PathOperationViewModel>(document.Plugins[endpoint].Select(x => new PathOperationViewModel(x, requestViewModelLogger, document, tab, httpClient)));
|
||||
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;
|
||||
PathOperationSelected?.Invoke(this, value.Operation.RequestTemplate ?? string.Empty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using System;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
|
||||
public partial class EndpointsNavigationViewModel : ObservableObject
|
||||
{
|
||||
public Guid Guid { get; } = Guid.NewGuid();
|
||||
|
||||
[ObservableProperty] private ObservableObject _activeViewModel;
|
||||
[ObservableProperty] private ObservableObject _endpointsViewModel;
|
||||
[ObservableProperty] private string _title;
|
||||
|
||||
private readonly Action<string?, Guid> _onEndpointNavigation;
|
||||
private readonly Tab _tab;
|
||||
|
||||
public EndpointsNavigationViewModel(IAvaloniaList<string> plugins, Action<string?, Guid> onEndpointNavigation, ILogger<RequestViewModel> requestViewModelLogger, Models.Document document, Tab tab, System.Net.Http.HttpClient httpClient)
|
||||
{
|
||||
_activeViewModel = _endpointsViewModel = new EndpointsViewModel(plugins, OnClicked, requestViewModelLogger, document, tab, httpClient);
|
||||
_onEndpointNavigation = onEndpointNavigation;
|
||||
_tab = tab;
|
||||
_title = GetTitle(tab);
|
||||
}
|
||||
|
||||
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 EndpointViewModel endpoint)
|
||||
{
|
||||
Title = $"{GetTitle(_tab)} - {endpoint.Title}";
|
||||
_onEndpointNavigation.Invoke(endpoint.Title, Guid);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GoBack()
|
||||
{
|
||||
ActiveViewModel = EndpointsViewModel;
|
||||
Title = GetTitle(_tab);
|
||||
_onEndpointNavigation.Invoke(null, Guid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.Models;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
|
||||
public enum Tab
|
||||
{
|
||||
LCU,
|
||||
GameClient
|
||||
}
|
||||
|
||||
public partial class EndpointsTabViewModel : PageBase
|
||||
{
|
||||
public IAvaloniaList<string> Plugins { get; } = new AvaloniaList<string>();
|
||||
public IAvaloniaList<EndpointItem> Endpoints { get; } = new AvaloniaList<EndpointItem>();
|
||||
|
||||
[ObservableProperty] private bool _isBusy = true;
|
||||
|
||||
private readonly ILogger<RequestViewModel> _requestViewModelLogger;
|
||||
private readonly DataSource _dataSource;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public EndpointsTabViewModel(ILogger<RequestViewModel> requestViewModelLogger, DataSource dataSource, IHttpClientFactory httpClientFactory) : base("Endpoints", "list-alt", -500)
|
||||
{
|
||||
_requestViewModelLogger = requestViewModelLogger;
|
||||
_dataSource = dataSource;
|
||||
_httpClient = httpClientFactory.CreateClient(nameof(EndpointsTabViewModel));
|
||||
}
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
await Dispatcher.UIThread.Invoke(async () => await AddEndpoint(Tab.LCU));
|
||||
IsBusy = false;
|
||||
IsInitialized = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddEndpoint(Tab tab)
|
||||
{
|
||||
Document document = tab switch
|
||||
{
|
||||
Tab.LCU => await _dataSource.GetLcuSchemaDocumentAsync(),
|
||||
Tab.GameClient => await _dataSource.GetLolClientDocumentAsync(),
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
Plugins.Clear();
|
||||
Plugins.AddRange(document.Plugins.Keys);
|
||||
|
||||
var vm = new EndpointsNavigationViewModel(Plugins, OnEndpointNavigation, _requestViewModelLogger, document, tab, _httpClient);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class EndpointItem : 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 EndpointsNavigationViewModel Content { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.Models;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
|
||||
public partial class EndpointsViewModel : ObservableObject
|
||||
{
|
||||
public IAvaloniaList<string> Plugins { get; }
|
||||
public IAvaloniaList<string> Query { get; }
|
||||
|
||||
[ObservableProperty] private string _search = string.Empty;
|
||||
[ObservableProperty] private string? _selectedQuery = string.Empty;
|
||||
|
||||
public Action<ObservableObject> OnClicked { get; }
|
||||
|
||||
private readonly ILogger<RequestViewModel> _requestViewModelLogger;
|
||||
private readonly Document _document;
|
||||
private readonly Tab _tab;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public EndpointsViewModel(IAvaloniaList<string> plugins, Action<ObservableObject> onClicked, ILogger<RequestViewModel> requestViewModelLogger, Models.Document document, Tab tab, System.Net.Http.HttpClient httpClient)
|
||||
{
|
||||
Plugins = new AvaloniaList<string>(plugins);
|
||||
Query = new AvaloniaList<string>(plugins);
|
||||
OnClicked = onClicked;
|
||||
_requestViewModelLogger = requestViewModelLogger;
|
||||
_document = document;
|
||||
_tab = tab;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
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, _requestViewModelLogger, _document, _tab, _httpClient));
|
||||
}
|
||||
}
|
||||
259
Needlework.Net/ViewModels/Pages/Endpoints/OperationViewModel.cs
Normal file
259
Needlework.Net/ViewModels/Pages/Endpoints/OperationViewModel.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Needlework.Net.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
|
||||
public partial class OperationViewModel : ObservableObject
|
||||
{
|
||||
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, Models.Document document)
|
||||
{
|
||||
Summary = operation.Summary ?? string.Empty;
|
||||
Description = operation.Description ?? string.Empty;
|
||||
IsRequestBody = operation.RequestBody != null;
|
||||
ReturnType = GetReturnType(operation.Responses);
|
||||
RequestClasses = GetRequestClasses(operation.RequestBody, document);
|
||||
ResponseClasses = GetResponseClasses(operation.Responses, document);
|
||||
PathParameters = GetParameters(operation.Parameters, ParameterLocation.Path);
|
||||
QueryParameters = GetParameters(operation.Parameters, ParameterLocation.Query);
|
||||
RequestBodyType = GetRequestBodyType(operation.RequestBody);
|
||||
RequestTemplate = GetRequestTemplate(operation.RequestBody, document);
|
||||
}
|
||||
|
||||
private string? GetRequestTemplate(OpenApiRequestBody? requestBody, Document document)
|
||||
{
|
||||
var requestClasses = GetRequestClasses(requestBody, document);
|
||||
if (requestClasses.Count == 0)
|
||||
{
|
||||
var type = GetRequestBodyType(requestBody);
|
||||
if (type == null) return null;
|
||||
return GetRequestDefaultValue(type);
|
||||
}
|
||||
|
||||
var template = CreateTemplate(requestClasses);
|
||||
return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(string.Join(string.Empty, template)), App.JsonSerializerOptions);
|
||||
}
|
||||
|
||||
private List<string> CreateTemplate(AvaloniaList<PropertyClassViewModel> requestClasses)
|
||||
{
|
||||
if (requestClasses.Count == 0) return [];
|
||||
List<string> template = [];
|
||||
template.Add("{");
|
||||
|
||||
var rootClass = requestClasses.First();
|
||||
if (rootClass.PropertyEnums.Any()) return [rootClass.PropertyEnums.First().Values];
|
||||
var propertyFields = rootClass.PropertyFields;
|
||||
for (int i = 0; i < propertyFields.Count; i++)
|
||||
{
|
||||
template.Add($"\"{propertyFields[i].Name}\"");
|
||||
template.Add(":");
|
||||
template.Add($"#{propertyFields[i].Type}");
|
||||
|
||||
if (i == propertyFields.Count - 1) template.Add("}");
|
||||
else template.Add(",");
|
||||
}
|
||||
|
||||
for (int i = 0; i < template.Count; i++)
|
||||
{
|
||||
var type = template[i];
|
||||
if (!type.Contains("#")) continue;
|
||||
|
||||
var foundClass = requestClasses.Where(c => c.Id == type.Replace("#", string.Empty));
|
||||
if (foundClass.Any())
|
||||
{
|
||||
if (foundClass.First().PropertyEnums.Any())
|
||||
{
|
||||
template[i] = string.Join(string.Empty, CreateTemplate([.. foundClass]));
|
||||
}
|
||||
else
|
||||
{
|
||||
AvaloniaList<PropertyClassViewModel> classes = [.. requestClasses];
|
||||
classes.Remove(rootClass);
|
||||
template[i] = string.Join(string.Empty, CreateTemplate(classes));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
template[i] = GetRequestDefaultValue(type);
|
||||
}
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
private static string GetRequestDefaultValue(string type)
|
||||
{
|
||||
var defaultValue = string.Empty;
|
||||
if (type.Contains("[]")) defaultValue = "[]";
|
||||
else if (type.Contains("string")) defaultValue = "\"\"";
|
||||
else if (type.Contains("boolean")) defaultValue = "false";
|
||||
else if (type.Contains("integer")) defaultValue = "0";
|
||||
else if (type.Contains("double") || type.Contains("float")) defaultValue = "0.0";
|
||||
else if (type.Contains("object")) defaultValue = "{}";
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
private AvaloniaList<ParameterViewModel> GetParameters(IList<OpenApiParameter> parameters, ParameterLocation location)
|
||||
{
|
||||
var pathParameters = new AvaloniaList<ParameterViewModel>();
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
if (parameter.In != location) continue;
|
||||
pathParameters.Add(new ParameterViewModel(parameter.Name, GetSchemaType(parameter.Schema), parameter.Required));
|
||||
}
|
||||
|
||||
return pathParameters;
|
||||
}
|
||||
|
||||
private 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;
|
||||
|
||||
}
|
||||
|
||||
private AvaloniaList<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses, Document document)
|
||||
{
|
||||
if (!TryGetResponse(responses, out var response))
|
||||
return [];
|
||||
|
||||
if (response.Content.TryGetValue("application/json", out var media))
|
||||
{
|
||||
var rawDocument = document.OpenApiDocument;
|
||||
var schema = media.Schema;
|
||||
if (schema == null) return [];
|
||||
|
||||
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
|
||||
WalkSchema(schema, propertyClasses, rawDocument);
|
||||
return propertyClasses;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private void WalkSchema(OpenApiSchema schema, AvaloniaList<PropertyClassViewModel> propertyClasses, OpenApiDocument document)
|
||||
{
|
||||
var type = GetSchemaType(schema);
|
||||
if (IsComponent(type))
|
||||
{
|
||||
string componentId = GetComponentId(schema);
|
||||
var componentSchema = document.Components.Schemas[componentId];
|
||||
var responseClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum);
|
||||
|
||||
if (propertyClasses.Where(c => c.Id == componentId).Any()) return; // Avoid adding duplicate schemas in classes
|
||||
propertyClasses.Add(responseClass);
|
||||
|
||||
foreach ((var _, var property) in componentSchema.Properties)
|
||||
// Check for self-references like "LolLootLootOddsResponse"
|
||||
// I blame dubble
|
||||
if (IsComponent(GetSchemaType(property)) && componentId != GetComponentId(property))
|
||||
WalkSchema(property, propertyClasses, document);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetComponentId(OpenApiSchema schema)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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, Document document)
|
||||
{
|
||||
if (requestBody == null) return [];
|
||||
if (requestBody.Content.TryGetValue("application/json", 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];
|
||||
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
|
||||
WalkSchema(componentSchema, propertyClasses, rawDocument);
|
||||
return propertyClasses;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private string GetReturnType(OpenApiResponses responses)
|
||||
{
|
||||
if (!TryGetResponse(responses, out var response))
|
||||
return "none";
|
||||
|
||||
if (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.AdditionalProperties?.Reference != null) return schema.AdditionalProperties.Reference.Id;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
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 string Path { get; }
|
||||
public OperationViewModel Operation { get; }
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
[ObservableProperty] private Lazy<RequestViewModel> _request;
|
||||
|
||||
public PathOperationViewModel(PathOperation pathOperation, ILogger<RequestViewModel> requestViewModelLogger, Document document, Tab tab, System.Net.Http.HttpClient httpClient)
|
||||
{
|
||||
Path = pathOperation.Path;
|
||||
Operation = new OperationViewModel(pathOperation.Operation, document);
|
||||
Request = new(() => new RequestViewModel(requestViewModelLogger, tab, httpClient)
|
||||
{
|
||||
Method = pathOperation.Method.ToUpper()
|
||||
});
|
||||
Url = $"https://swagger.dysolix.dev/lcu/#/{pathOperation.Tag}/{pathOperation.Operation.OperationId}";
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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.Pages.Endpoints;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.OpenApi.Any;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
|
||||
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())}]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
|
||||
public class PropertyFieldViewModel
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Type { get; }
|
||||
|
||||
public PropertyFieldViewModel(string name, string type)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using BlossomiShymae.GrrrLCU;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
|
||||
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.GetProcessInfo();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
34
Needlework.Net/ViewModels/Pages/HomeViewModel.cs
Normal file
34
Needlework.Net/ViewModels/Pages/HomeViewModel.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Avalonia.Platform;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Needlework.Net.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages;
|
||||
|
||||
public partial class HomeViewModel : PageBase
|
||||
{
|
||||
public List<Library> Libraries { get; } = JsonSerializer.Deserialize<List<Library>>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json")))!;
|
||||
|
||||
public HomeViewModel() : base("Home", "home", int.MinValue) { }
|
||||
|
||||
public override Task InitializeAsync()
|
||||
{
|
||||
IsInitialized = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenUrl(string url)
|
||||
{
|
||||
var process = new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo(url) { UseShellExecute = true }
|
||||
};
|
||||
process.Start();
|
||||
}
|
||||
|
||||
}
|
||||
15
Needlework.Net/ViewModels/Pages/PageBase.cs
Normal file
15
Needlework.Net/ViewModels/Pages/PageBase.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages;
|
||||
|
||||
|
||||
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;
|
||||
[ObservableProperty] private bool _isInitialized;
|
||||
|
||||
public abstract Task InitializeAsync();
|
||||
}
|
||||
21
Needlework.Net/ViewModels/Pages/Websocket/EventViewModel.cs
Normal file
21
Needlework.Net/ViewModels/Pages/Websocket/EventViewModel.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using BlossomiShymae.GrrrLCU;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages.Websocket;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
193
Needlework.Net/ViewModels/Pages/Websocket/WebsocketViewModel.cs
Normal file
193
Needlework.Net/ViewModels/Pages/Websocket/WebsocketViewModel.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using Avalonia.Collections;
|
||||
using BlossomiShymae.GrrrLCU;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.Messages;
|
||||
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
|
||||
{
|
||||
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;
|
||||
|
||||
[ObservableProperty] private IAvaloniaList<string> _eventTypes = new AvaloniaList<string>();
|
||||
[ObservableProperty] private string _eventType = "OnJsonApiEvent";
|
||||
|
||||
private Dictionary<string, EventMessage> _events = [];
|
||||
|
||||
public WebsocketClient? Client { get; set; }
|
||||
|
||||
public List<IDisposable> ClientDisposables = [];
|
||||
|
||||
private readonly object _tokenLock = new();
|
||||
public CancellationTokenSource TokenSource { get; set; } = new();
|
||||
|
||||
public HttpClient HttpClient { get; }
|
||||
|
||||
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
|
||||
|
||||
private readonly ILogger<WebsocketViewModel> _logger;
|
||||
|
||||
public WebsocketViewModel(HttpClient httpClient, ILogger<WebsocketViewModel> logger) : base("Event Viewer", "plug", -100)
|
||||
{
|
||||
_logger = logger;
|
||||
HttpClient = httpClient;
|
||||
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await InitializeEventTypes();
|
||||
InitializeWebsocket();
|
||||
});
|
||||
}
|
||||
|
||||
public override Task InitializeAsync()
|
||||
{
|
||||
IsInitialized = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task InitializeEventTypes()
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = await HttpClient.GetStringAsync("https://raw.githubusercontent.com/dysolix/hasagi-types/refs/heads/main/dist/lcu-events.d.ts");
|
||||
var matches = EventTypesRegex().Matches(file);
|
||||
Avalonia.Threading.Dispatcher.UIThread.Invoke(() => EventTypes.AddRange(matches.Select(m => m.Groups[1].Value)));
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get event types");
|
||||
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new("Failed to get event types", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(10))));
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeWebsocket()
|
||||
{
|
||||
lock (_tokenLock)
|
||||
{
|
||||
if (Client != null)
|
||||
{
|
||||
_logger.LogDebug("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();
|
||||
_logger.LogDebug("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 WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebsocketViewModel));
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Clear()
|
||||
{
|
||||
_events.Clear();
|
||||
EventLog.Clear();
|
||||
}
|
||||
|
||||
private void OnReconnection(ReconnectionInfo info)
|
||||
{
|
||||
_logger.LogTrace("Reconnected: {Type}", info.Type);
|
||||
}
|
||||
|
||||
private void OnDisconnection(DisconnectionInfo info)
|
||||
{
|
||||
_logger.LogTrace("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();
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
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 string? _responsePath;
|
||||
[ObservableProperty] private string? _responseStatus;
|
||||
[ObservableProperty] private string? _responseAuthentication;
|
||||
[ObservableProperty] private string? _responseUsername;
|
||||
[ObservableProperty] private string? _responsePassword;
|
||||
[ObservableProperty] private string? _responseAuthorization;
|
||||
|
||||
public PathOperationViewModel(PathOperation pathOperation)
|
||||
{
|
||||
Method = pathOperation.Method.ToUpper();
|
||||
Color = new SolidColorBrush(GetColor(Method));
|
||||
Path = pathOperation.Path;
|
||||
Operation = new OperationViewModel(pathOperation.Operation);
|
||||
ProcessInfo = GetProcessInfo();
|
||||
if (ProcessInfo != null)
|
||||
{
|
||||
ResponsePath = $"https://127.0.0.1:{ProcessInfo.AppPort}{Path}";
|
||||
var riotAuth = new RiotAuthentication(ProcessInfo.RemotingAuthToken);
|
||||
ResponseUsername = riotAuth.Username;
|
||||
ResponsePassword = riotAuth.Password;
|
||||
ResponseAuthorization = $"Basic {riotAuth.Value}";
|
||||
}
|
||||
}
|
||||
|
||||
private ProcessInfo? GetProcessInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
var processInfo = Connector.GetProcessInfo();
|
||||
return processInfo;
|
||||
}
|
||||
catch (Exception) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
[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 = Connector.GetProcessInfo();
|
||||
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 response = await Connector.SendAsync(method, uri, 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")));
|
||||
|
||||
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode}";
|
||||
ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{uri}";
|
||||
ResponseAuthentication = $"Basic {riotAuthentication.Value}";
|
||||
ResponseUsername = riotAuthentication.Username;
|
||||
ResponsePassword = 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.")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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())}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
205
Needlework.Net/ViewModels/Shared/RequestViewModel.cs
Normal file
205
Needlework.Net/ViewModels/Shared/RequestViewModel.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
using Avalonia.Media;
|
||||
using BlossomiShymae.GrrrLCU;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.Messages;
|
||||
using Needlework.Net.ViewModels.MainWindow;
|
||||
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
|
||||
{
|
||||
[ObservableProperty] private string? _method = "GET";
|
||||
[ObservableProperty] private SolidColorBrush _color = new(GetColor("GET"));
|
||||
|
||||
[ObservableProperty] private bool _isRequestBusy = false;
|
||||
[ObservableProperty] private string? _requestPath = null;
|
||||
[ObservableProperty] private string? _requestBody = null;
|
||||
|
||||
[ObservableProperty] private string? _responsePath = null;
|
||||
[ObservableProperty] private string? _responseStatus = null;
|
||||
[ObservableProperty] private string? _responseAuthentication = null;
|
||||
[ObservableProperty] private string? _responseUsername = null;
|
||||
[ObservableProperty] private string? _responsePassword = null;
|
||||
[ObservableProperty] private string? _responseAuthorization = null;
|
||||
[ObservableProperty] private string? _responseBody = null;
|
||||
|
||||
public event EventHandler<RequestViewModel>? RequestText;
|
||||
public event EventHandler<string>? UpdateText;
|
||||
|
||||
private readonly ILogger<RequestViewModel> _logger;
|
||||
private readonly Tab _tab;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public RequestViewModel(ILogger<RequestViewModel> logger, Pages.Endpoints.Tab tab, HttpClient httpClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_tab = tab;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
_logger.LogDebug("Sending request: {Tuple}", (Method, RequestPath));
|
||||
RequestText?.Invoke(this, this);
|
||||
var content = new StringContent(RequestBody ?? string.Empty, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
|
||||
var responsePath = $"https://127.0.0.1:2999{RequestPath}";
|
||||
var response = await _httpClient.SendAsync(new HttpRequestMessage(method, responsePath) { 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));
|
||||
UpdateText?.Invoke(this, string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
ResponseBody = body;
|
||||
UpdateText?.Invoke(this, body);
|
||||
}
|
||||
|
||||
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}";
|
||||
ResponsePath = responsePath;
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Request failed: {Tuple}", (Method, RequestPath));
|
||||
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5))));
|
||||
UpdateText?.Invoke(this, string.Empty);
|
||||
|
||||
ResponseStatus = null;
|
||||
ResponsePath = null;
|
||||
ResponseAuthentication = null;
|
||||
ResponseAuthorization = null;
|
||||
ResponseUsername = null;
|
||||
ResponsePassword = null;
|
||||
ResponseBody = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRequestBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteLcuAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsRequestBusy = true;
|
||||
if (string.IsNullOrEmpty(RequestPath))
|
||||
throw new Exception("Path is empty.");
|
||||
var method = GetMethod();
|
||||
|
||||
_logger.LogDebug("Sending request: {Tuple}", (Method, RequestPath));
|
||||
|
||||
var processInfo = ProcessFinder.GetProcessInfo();
|
||||
RequestText?.Invoke(this, this);
|
||||
var content = new StringContent(RequestBody ?? string.Empty, 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));
|
||||
UpdateText?.Invoke(this, string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
ResponseBody = body;
|
||||
UpdateText?.Invoke(this, 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)
|
||||
{
|
||||
_logger.LogError(ex, "Request failed: {Tuple}", (Method, RequestPath));
|
||||
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5))));
|
||||
UpdateText?.Invoke(this, string.Empty);
|
||||
|
||||
ResponseStatus = null;
|
||||
ResponsePath = null;
|
||||
ResponseAuthentication = null;
|
||||
ResponseAuthorization = null;
|
||||
ResponseUsername = null;
|
||||
ResponsePassword = null;
|
||||
ResponseBody = null;
|
||||
}
|
||||
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.")
|
||||
};
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns: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>
|
||||
@@ -1,72 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Styling;
|
||||
using AvaloniaEdit;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Needlework.Net.Extensions;
|
||||
using Needlework.Net.Messages;
|
||||
using Needlework.Net.ViewModels;
|
||||
using TextMateSharp.Grammars;
|
||||
|
||||
namespace Needlework.Net.Views;
|
||||
|
||||
public partial class EndpointView : UserControl, IRecipient<EditorUpdateMessage>, IRecipient<ContentRequestMessage>
|
||||
{
|
||||
private TextEditor? _requestEditor;
|
||||
private TextEditor? _responseEditor;
|
||||
|
||||
public EndpointView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
|
||||
var vm = (EndpointViewModel)DataContext!;
|
||||
_requestEditor = this.FindControl<TextEditor>("EndpointRequestEditor");
|
||||
_responseEditor = this.FindControl<TextEditor>("EndpointResponseEditor");
|
||||
_requestEditor?.ApplyJsonEditorSettings();
|
||||
_responseEditor?.ApplyJsonEditorSettings();
|
||||
|
||||
WeakReferenceMessenger.Default.Register<EditorUpdateMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ContentRequestMessage, string>(this, "EndpointRequestEditor");
|
||||
|
||||
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
|
||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||
}
|
||||
|
||||
private void OnBaseThemeChanged(ThemeVariant currentTheme)
|
||||
{
|
||||
var registryOptions = new RegistryOptions(
|
||||
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
|
||||
}
|
||||
|
||||
public void Receive(EditorUpdateMessage message)
|
||||
{
|
||||
switch (message.Value.Key)
|
||||
{
|
||||
case "EndpointRequestEditor":
|
||||
_requestEditor!.Text = message.Value.Text;
|
||||
break;
|
||||
case "EndpointResponseEditor":
|
||||
_responseEditor!.Text = message.Value.Text;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ContentRequestMessage message)
|
||||
{
|
||||
message.Reply(_requestEditor!.Text);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Needlework.Net.Views;
|
||||
|
||||
public partial class EndpointsContainerView : UserControl
|
||||
{
|
||||
public EndpointsContainerView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Needlework.Net.Views
|
||||
{
|
||||
public partial class EndpointsView : UserControl
|
||||
{
|
||||
public EndpointsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels"
|
||||
xmlns:controls="using:Needlework.Net.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Needlework.Net.Views.HomeView"
|
||||
x:DataType="vm:HomeViewModel">
|
||||
<!-- TOP LEVEL -->
|
||||
<ScrollViewer>
|
||||
<WrapPanel Margin="8"
|
||||
Orientation="Horizontal">
|
||||
<!-- WELCOME -->
|
||||
<StackPanel>
|
||||
<Border Margin="12">
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">
|
||||
Welcome to Needlework.Net
|
||||
</TextBlock>
|
||||
<TextBlock>Get started with LCU development by clicking on the endpoints tab in the left panel.</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<controls:Card Margin="12">
|
||||
<TextBlock TextWrapping="Wrap">THE PROGRAM IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF MERCHANTABILITY, NONINFRINGMENT, OR OF FITNESS FOR A PARTICULAR PURPOSE. LICENSOR DOES NOT WARRANT THAT THE FUNCTIONS CONTAINED IN THE PROGRAM WILL MEET YOUR REQUIREMENTS OR THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. LICENSOR MAKES NO WARRANTIES RESPECTING ANY HARM THAT MAY BE CAUSED BY MALICIOUS USE OF THIS SOFTWARE. LICENSOR FURTHER EXPRESSLY DISCLAIMS ANY WARRANTY OR REPRESENTATION TO AUTHORIZED USERS OR TO ANY THIRD PARTY.</TextBlock>
|
||||
</controls:Card>
|
||||
</StackPanel>
|
||||
<!-- FOOTER -->
|
||||
<StackPanel>
|
||||
<controls:Card Margin="12" Width="300">
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Theme="{StaticResource SubtitleTextBlockStyle}"
|
||||
Margin="0 0 0 8">Resources</TextBlock>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<Button CommandParameter="https://hextechdocs.dev/tag/lcu/" Margin="0 0 8 0">
|
||||
Hextech Docs
|
||||
</Button>
|
||||
<Button CommandParameter="https://hextechdocs.dev/getting-started-with-the-lcu-api/">
|
||||
Getting Started
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
<controls:Card Margin="12" Width="300">
|
||||
<StackPanel>
|
||||
<TextBlock>© 2024 - Blossomi Shymae</TextBlock>
|
||||
<TextBlock>MIT License</TextBlock>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
</StackPanel>
|
||||
<!-- LEGAL -->
|
||||
<controls:Card Margin="12" Width="300">
|
||||
<TextBlock TextWrapping="Wrap">Needlework.Net isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games, and all associated properties are trademarks or registered trademarks of Riot Games, Inc.</TextBlock>
|
||||
</controls:Card>
|
||||
</WrapPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -1,12 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Needlework.Net.Views
|
||||
{
|
||||
public partial class HomeView : UserControl
|
||||
{
|
||||
public HomeView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using FluentAvalonia.UI.Windowing;
|
||||
|
||||
namespace Needlework.Net.Views;
|
||||
|
||||
public partial class MainWindow : AppWindow
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
TitleBar.ExtendsContentIntoTitleBar = true;
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
|
||||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:i="https://github.com/projektanker/icons.avalonia"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels.MainWindow"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Needlework.Net.Views.MainWindow"
|
||||
x:Class="Needlework.Net.Views.MainWindow.MainWindowView"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
Title="Needlework.Net"
|
||||
Icon="/Assets/app.ico"
|
||||
@@ -37,6 +37,8 @@
|
||||
<ui:NavigationView AlwaysShowHeader="False"
|
||||
PaneDisplayMode="Left"
|
||||
IsSettingsVisible="False"
|
||||
IsPaneOpen="False"
|
||||
OpenPaneLength="200"
|
||||
Grid.Row="1"
|
||||
MenuItemsSource="{Binding MenuItems}"
|
||||
SelectedItem="{Binding SelectedMenuItem}">
|
||||
@@ -74,6 +76,7 @@
|
||||
<Grid>
|
||||
<TransitioningContentControl Content="{Binding CurrentPage}"/>
|
||||
<Button Content="{Binding Version}"
|
||||
Background="RoyalBlue"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="16"/>
|
||||
30
Needlework.Net/Views/MainWindow/MainWindowView.axaml.cs
Normal file
30
Needlework.Net/Views/MainWindow/MainWindowView.axaml.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia.Controls;
|
||||
using FluentAvalonia.UI.Windowing;
|
||||
|
||||
namespace Needlework.Net.Views.MainWindow;
|
||||
|
||||
public partial class MainWindowView : AppWindow
|
||||
{
|
||||
public MainWindowView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
TitleBar.ExtendsContentIntoTitleBar = true;
|
||||
TransparencyLevelHint = [WindowTransparencyLevel.Mica, WindowTransparencyLevel.None];
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
if (IsWindows11OrNewer())
|
||||
{
|
||||
Background = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsWindows11OrNewer()
|
||||
{
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build >= 22000;
|
||||
}
|
||||
}
|
||||
64
Needlework.Net/Views/MainWindow/OopsiesDialog.cs
Normal file
64
Needlework.Net/Views/MainWindow/OopsiesDialog.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Needlework.Net.Services;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Needlework.Net.Views.MainWindow;
|
||||
|
||||
public class OopsiesDialog : IDialog, IDisposable
|
||||
{
|
||||
private bool _isDisposing;
|
||||
private string? _text;
|
||||
private ContentDialog _dialog;
|
||||
|
||||
public OopsiesDialog()
|
||||
{
|
||||
_dialog = new ContentDialog
|
||||
{
|
||||
PrimaryButtonText = "Open",
|
||||
CloseButtonText = "Cancel",
|
||||
Title = "Oopsies",
|
||||
Content = "This response is too large to handle for performance reasons.\nIt can be viewed in an external editor or viewer.",
|
||||
IsPrimaryButtonEnabled = true,
|
||||
IsSecondaryButtonEnabled = false,
|
||||
DefaultButton = ContentDialogButton.Primary
|
||||
};
|
||||
_dialog.PrimaryButtonClick += OnPrimaryButtonClick;
|
||||
}
|
||||
|
||||
public async Task<ContentDialogResult> ShowAsync(object data)
|
||||
{
|
||||
_text = (string)data;
|
||||
var result = await _dialog.ShowAsync();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
var temp = Path.GetTempFileName().Replace(".tmp", ".json");
|
||||
File.WriteAllText(temp, _text);
|
||||
Process.Start("explorer", "\"" + temp + "\"");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_isDisposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_text = null;
|
||||
_dialog.PrimaryButtonClick -= OnPrimaryButtonClick;
|
||||
}
|
||||
|
||||
_isDisposing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Needlework.Net.Services;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Needlework.Net.Views
|
||||
{
|
||||
public class OopsiesDialog : IDialog, IDisposable
|
||||
{
|
||||
private bool _isDisposing;
|
||||
private string? _text;
|
||||
private ContentDialog _dialog;
|
||||
|
||||
public OopsiesDialog()
|
||||
{
|
||||
_dialog = new ContentDialog
|
||||
{
|
||||
PrimaryButtonText = "Open",
|
||||
CloseButtonText = "Cancel",
|
||||
Title = "Oopsies",
|
||||
Content = "This response is too large to handle for performance reasons.\nIt can be viewed in an external editor or viewer.",
|
||||
IsPrimaryButtonEnabled = true,
|
||||
IsSecondaryButtonEnabled = false,
|
||||
DefaultButton = ContentDialogButton.Primary
|
||||
};
|
||||
_dialog.PrimaryButtonClick += OnPrimaryButtonClick;
|
||||
}
|
||||
|
||||
public async Task<ContentDialogResult> ShowAsync(object data)
|
||||
{
|
||||
_text = (string)data;
|
||||
var result = await _dialog.ShowAsync();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
var temp = Path.GetTempFileName().Replace(".tmp", ".json");
|
||||
File.WriteAllText(temp, _text);
|
||||
Process.Start("explorer", "\"" + temp + "\"");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_isDisposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_text = null;
|
||||
_dialog.PrimaryButtonClick -= OnPrimaryButtonClick;
|
||||
}
|
||||
|
||||
_isDisposing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Needlework.Net/Views/Pages/AboutView.axaml
Normal file
53
Needlework.Net/Views/Pages/AboutView.axaml
Normal file
@@ -0,0 +1,53 @@
|
||||
<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.Pages"
|
||||
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.Pages.AboutView"
|
||||
x:DataType="vm:AboutViewModel">
|
||||
<ScrollViewer Margin="8">
|
||||
<WrapPanel HorizontalAlignment="Center">
|
||||
<WrapPanel.Styles>
|
||||
<Style Selector="controls|UserCard">
|
||||
<Setter Property="Width" Value="272"/>
|
||||
<Setter Property="MaxHeight" Value="378"/>
|
||||
<Setter Property="Margin" Value="0 16 32 16"/>
|
||||
</Style>
|
||||
</WrapPanel.Styles>
|
||||
<controls:UserCard 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>
|
||||
<controls:UserCard UserImage="/Assets/Users/dysolix.png"
|
||||
UserName="dysolix"
|
||||
UserGithub="dysolix">
|
||||
For providing LCU Schema, the auto-generated OpenAPI document for the LCU.
|
||||
</controls:UserCard>
|
||||
<controls:UserCard UserImage="/Assets/Users/sylv.jpg"
|
||||
UserName="Sylv"
|
||||
UserGithub="AlsoSylv">
|
||||
For providing a fixed up-to-date Game Client schema.
|
||||
</controls:UserCard>
|
||||
<controls:UserCard UserImage="/Assets/Users/ray.png"
|
||||
UserName="Ray"
|
||||
UserGithub="Hi-Ray">
|
||||
For guidance, advice, and providing help via HextechDocs.
|
||||
</controls:UserCard>
|
||||
<controls:UserCard UserImage="/Assets/Users/dubble.png"
|
||||
UserName="dubble"
|
||||
UserGithub="cuppachino">
|
||||
For encouraging me to publish Needlework.Net and other ideas.
|
||||
</controls:UserCard>
|
||||
<controls:UserCard UserImage="/Assets/Users/aoshiw.png"
|
||||
UserName="AoshiW"
|
||||
UserGithub="AoshiW">
|
||||
For PR.
|
||||
</controls:UserCard>
|
||||
</WrapPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -1,6 +1,6 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Needlework.Net.Views;
|
||||
namespace Needlework.Net.Views.Pages;
|
||||
|
||||
public partial class AboutView : UserControl
|
||||
{
|
||||
@@ -5,10 +5,10 @@
|
||||
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels.Pages"
|
||||
xmlns:controls="using:Needlework.Net.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Needlework.Net.Views.ConsoleView"
|
||||
x:Class="Needlework.Net.Views.Pages.ConsoleView"
|
||||
x:DataType="vm:ConsoleViewModel">
|
||||
<controls:BusyArea IsBusy="{Binding IsBusy}"
|
||||
BusyText="Loading...">
|
||||
@@ -16,16 +16,16 @@
|
||||
<Grid Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2">
|
||||
<StackPanel Margin="0 0 0 16">
|
||||
<StackPanel Margin="0 0 0 8">
|
||||
<Grid RowDefinitions="auto" ColumnDefinitions="auto,*,auto">
|
||||
<ComboBox ItemsSource="{Binding RequestMethods}"
|
||||
SelectedItem="{Binding RequestMethodSelected}"
|
||||
SelectedItem="{Binding Request.Method}"
|
||||
Margin="0 0 8 0"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"/>
|
||||
<AutoCompleteBox
|
||||
ItemsSource="{Binding RequestPaths}"
|
||||
Text="{Binding RequestPath}"
|
||||
Text="{Binding Request.RequestPath}"
|
||||
MaxDropDownHeight="400"
|
||||
FilterMode="StartsWith"
|
||||
Grid.Row="0"
|
||||
@@ -49,7 +49,7 @@
|
||||
<TextBox IsReadOnly="True"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Text="{Binding ResponsePath}"/>
|
||||
Text="{Binding Request.ResponsePath}"/>
|
||||
<avaloniaEdit:TextEditor
|
||||
Name="RequestEditor"
|
||||
Text=""
|
||||
@@ -69,7 +69,7 @@
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0">
|
||||
<Button Content="{Binding ResponseStatus}"
|
||||
<Button Content="{Binding Request.ResponseStatus}"
|
||||
FontSize="12"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
@@ -1,17 +1,14 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Styling;
|
||||
using AvaloniaEdit;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Needlework.Net.Extensions;
|
||||
using Needlework.Net.Messages;
|
||||
using Needlework.Net.ViewModels;
|
||||
using Needlework.Net.ViewModels.Pages;
|
||||
using TextMateSharp.Grammars;
|
||||
|
||||
namespace Needlework.Net.Views;
|
||||
namespace Needlework.Net.Views.Pages;
|
||||
|
||||
public partial class ConsoleView : UserControl, IRecipient<ResponseUpdatedMessage>, IRecipient<ContentRequestMessage>
|
||||
public partial class ConsoleView : UserControl
|
||||
{
|
||||
private TextEditor? _responseEditor;
|
||||
private TextEditor? _requestEditor;
|
||||
@@ -21,36 +18,39 @@ public partial class ConsoleView : UserControl, IRecipient<ResponseUpdatedMessag
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void Receive(ResponseUpdatedMessage message)
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_responseEditor!.Text = message.Value;
|
||||
}
|
||||
|
||||
public void Receive(ContentRequestMessage message)
|
||||
{
|
||||
message.Reply(_requestEditor!.Text);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
base.OnAttachedToVisualTree(e);
|
||||
|
||||
_responseEditor = this.FindControl<TextEditor>("ResponseEditor");
|
||||
_requestEditor = this.FindControl<TextEditor>("RequestEditor");
|
||||
_responseEditor?.ApplyJsonEditorSettings();
|
||||
_requestEditor?.ApplyJsonEditorSettings();
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ResponseUpdatedMessage, string>(this, nameof(ConsoleViewModel));
|
||||
WeakReferenceMessenger.Default.Register<ContentRequestMessage, string>(this, "ConsoleRequestEditor");
|
||||
var vm = (ConsoleViewModel)DataContext!;
|
||||
vm.Request.RequestText += LcuRequest_RequestText; ;
|
||||
vm.Request.UpdateText += LcuRequest_UpdateText;
|
||||
|
||||
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
|
||||
}
|
||||
|
||||
private void LcuRequest_RequestText(object? sender, ViewModels.Shared.RequestViewModel e)
|
||||
{
|
||||
e.RequestBody = _requestEditor!.Text;
|
||||
}
|
||||
|
||||
private void LcuRequest_UpdateText(object? sender, string e)
|
||||
{
|
||||
_responseEditor!.Text = e;
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
|
||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||
var vm = (ConsoleViewModel)DataContext!;
|
||||
vm.Request.RequestText -= LcuRequest_RequestText;
|
||||
vm.Request.UpdateText -= LcuRequest_UpdateText;
|
||||
}
|
||||
|
||||
private void OnBaseThemeChanged(ThemeVariant currentTheme)
|
||||
@@ -3,11 +3,11 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
|
||||
xmlns:avalonEdit="https://github.com/avaloniaui/avaloniaedit"
|
||||
xmlns:controls="using:Needlework.Net.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Needlework.Net.Views.EndpointView"
|
||||
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointView"
|
||||
x:DataType="vm:EndpointViewModel">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="DataGrid">
|
||||
@@ -55,23 +55,28 @@
|
||||
<Grid
|
||||
RowDefinitions="*"
|
||||
ColumnDefinitions="auto,*">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
Margin="0 0 8 0"
|
||||
Text="{Binding Method}"
|
||||
Background="{Binding Color}"
|
||||
FontSize="8"
|
||||
Width="50"
|
||||
Padding="10 2 10 2"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"/>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Path}"
|
||||
FontSize="11"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"/>
|
||||
<Grid.ContextFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuItem Header="Copy Swagger URL" Command="{Binding CopyUrlCommand}"/>
|
||||
</MenuFlyout>
|
||||
</Grid.ContextFlyout>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
Margin="0 0 8 0"
|
||||
Text="{Binding Request.Value.Method}"
|
||||
Background="{Binding Request.Value.Color}"
|
||||
FontSize="8"
|
||||
Width="50"
|
||||
Padding="10 2 10 2"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"/>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Path}"
|
||||
FontSize="11"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
@@ -88,14 +93,14 @@
|
||||
ColumnDefinitions="auto,*,auto">
|
||||
<TextBox Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Text="{Binding SelectedPathOperation.Method}"
|
||||
Text="{Binding SelectedPathOperation.Request.Value.Method}"
|
||||
FontSize="12"
|
||||
IsReadOnly="True"
|
||||
Margin="0 0 8 0"/>
|
||||
<TextBox Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
FontSize="12"
|
||||
Text="{Binding SelectedPathOperation.ResponsePath}"
|
||||
Text="{Binding SelectedPathOperation.Request.Value.ResponsePath}"
|
||||
IsReadOnly="True"/>
|
||||
<StackPanel Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
@@ -189,7 +194,7 @@
|
||||
Grid.Column="1"
|
||||
Margin="0 0 0 8"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding SelectedPathOperation.ResponseUsername}" />
|
||||
Text="{Binding SelectedPathOperation.Request.Value.ResponseUsername}" />
|
||||
<TextBlock FontSize="12"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
@@ -201,7 +206,7 @@
|
||||
Grid.Column="1"
|
||||
Margin="0 0 0 8"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding SelectedPathOperation.ResponsePassword}"/>
|
||||
Text="{Binding SelectedPathOperation.Request.Value.ResponsePassword}"/>
|
||||
<TextBlock FontSize="12"
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
@@ -212,7 +217,7 @@
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding SelectedPathOperation.ResponseAuthorization}"/>
|
||||
Text="{Binding SelectedPathOperation.Request.Value.ResponseAuthorization}"/>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="Schemas">
|
||||
@@ -304,22 +309,25 @@
|
||||
FontSize="10"
|
||||
Padding="12 4 12 4"
|
||||
Classes="Flat"
|
||||
Content="{Binding SelectedPathOperation.ResponseStatus}"/>
|
||||
Content="{Binding SelectedPathOperation.Request.Value.ResponseStatus}"/>
|
||||
|
||||
</StackPanel>
|
||||
<Grid Grid.Row="1" Grid.Column="4">
|
||||
<TabControl>
|
||||
<TabItem Header="Preview">
|
||||
<avalonEdit:TextEditor
|
||||
Name="EndpointResponseEditor"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Visible"
|
||||
ShowLineNumbers="True"
|
||||
IsReadOnly="True"
|
||||
Text=""
|
||||
FontSize="12"/>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
<controls:BusyArea BusyText="Loading..."
|
||||
IsBusy="{Binding SelectedPathOperation.IsBusy}">
|
||||
<TabControl>
|
||||
<TabItem Header="Preview">
|
||||
<avalonEdit:TextEditor
|
||||
Name="EndpointResponseEditor"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Visible"
|
||||
ShowLineNumbers="True"
|
||||
IsReadOnly="True"
|
||||
Text=""
|
||||
FontSize="12"/>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</controls:BusyArea>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
94
Needlework.Net/Views/Pages/Endpoints/EndpointView.axaml.cs
Normal file
94
Needlework.Net/Views/Pages/Endpoints/EndpointView.axaml.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Styling;
|
||||
using AvaloniaEdit;
|
||||
using Needlework.Net.Extensions;
|
||||
using Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using TextMateSharp.Grammars;
|
||||
|
||||
namespace Needlework.Net.Views.Pages.Endpoints;
|
||||
|
||||
public partial class EndpointView : UserControl
|
||||
{
|
||||
private TextEditor? _requestEditor;
|
||||
private TextEditor? _responseEditor;
|
||||
private RequestViewModel? _lcuRequestVm;
|
||||
|
||||
public EndpointView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
|
||||
_requestEditor = this.FindControl<TextEditor>("EndpointRequestEditor");
|
||||
_responseEditor = this.FindControl<TextEditor>("EndpointResponseEditor");
|
||||
_requestEditor?.ApplyJsonEditorSettings();
|
||||
_responseEditor?.ApplyJsonEditorSettings();
|
||||
|
||||
var vm = (EndpointViewModel)DataContext!;
|
||||
vm.PathOperationSelected += Vm_PathOperationSelected;
|
||||
|
||||
if (vm.SelectedPathOperation != null)
|
||||
{
|
||||
_lcuRequestVm = vm.SelectedPathOperation.Request.Value;
|
||||
vm.SelectedPathOperation.Request.Value.RequestText += LcuRequest_RequestText;
|
||||
vm.SelectedPathOperation.Request.Value.UpdateText += LcuRequest_UpdateText;
|
||||
}
|
||||
|
||||
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
|
||||
}
|
||||
|
||||
private void Vm_PathOperationSelected(object? sender, string e)
|
||||
{
|
||||
var vm = (EndpointViewModel)DataContext!;
|
||||
if (vm.SelectedPathOperation != null)
|
||||
{
|
||||
_requestEditor!.Text = e;
|
||||
if (_lcuRequestVm != null)
|
||||
{
|
||||
_lcuRequestVm.RequestText -= LcuRequest_RequestText;
|
||||
_lcuRequestVm.UpdateText -= LcuRequest_UpdateText;
|
||||
}
|
||||
vm.SelectedPathOperation.Request.Value.RequestText += LcuRequest_RequestText;
|
||||
vm.SelectedPathOperation.Request.Value.UpdateText += LcuRequest_UpdateText;
|
||||
_lcuRequestVm = vm.SelectedPathOperation.Request.Value;
|
||||
_responseEditor!.Text = vm.SelectedPathOperation.Request.Value.ResponseBody ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
|
||||
var vm = (EndpointViewModel)DataContext!;
|
||||
vm.PathOperationSelected -= Vm_PathOperationSelected;
|
||||
|
||||
if (_lcuRequestVm != null)
|
||||
{
|
||||
_lcuRequestVm.RequestText -= LcuRequest_RequestText;
|
||||
_lcuRequestVm.UpdateText -= LcuRequest_UpdateText;
|
||||
_lcuRequestVm = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBaseThemeChanged(ThemeVariant currentTheme)
|
||||
{
|
||||
var registryOptions = new RegistryOptions(
|
||||
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
|
||||
}
|
||||
|
||||
private void LcuRequest_RequestText(object? sender, RequestViewModel e)
|
||||
{
|
||||
e.RequestBody = _requestEditor!.Text;
|
||||
}
|
||||
|
||||
private void LcuRequest_UpdateText(object? sender, string e)
|
||||
{
|
||||
_responseEditor!.Text = e;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,12 +2,12 @@
|
||||
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:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
|
||||
xmlns:avalonEdit="https://github.com/avaloniaui/avaloniaedit"
|
||||
xmlns:i="https://github.com/projektanker/icons.avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Needlework.Net.Views.EndpointsContainerView"
|
||||
x:DataType="vm:EndpointsContainerViewModel">
|
||||
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsNavigationView"
|
||||
x:DataType="vm:EndpointsNavigationViewModel">
|
||||
<Grid RowDefinitions="auto,*"
|
||||
ColumnDefinitions="*"
|
||||
Margin="16">
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Needlework.Net.Views.Pages.Endpoints;
|
||||
|
||||
public partial class EndpointsNavigationView : UserControl
|
||||
{
|
||||
public EndpointsNavigationView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
75
Needlework.Net/Views/Pages/Endpoints/EndpointsTabView.axaml
Normal file
75
Needlework.Net/Views/Pages/Endpoints/EndpointsTabView.axaml
Normal file
@@ -0,0 +1,75 @@
|
||||
<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:i="https://github.com/projektanker/icons.avalonia"
|
||||
Name="EndpointsTab"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:controls="using:Needlework.Net.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsTabView"
|
||||
x:DataType="vm:EndpointsTabViewModel">
|
||||
<controls:BusyArea IsBusy="{Binding IsBusy}"
|
||||
BusyText="Loading...">
|
||||
<Grid>
|
||||
<ui:TabView TabItems="{Binding Endpoints}"
|
||||
AddTabButtonCommand="{Binding AddEndpointCommand}"
|
||||
AddTabButtonCommandParameter="{x:Static vm:Tab.LCU}"
|
||||
TabCloseRequested="TabView_TabCloseRequested">
|
||||
<!--Need to override Tab header for Mica theme...-->
|
||||
<ui:TabView.Resources>
|
||||
<ResourceDictionary>
|
||||
<SolidColorBrush x:Key="TabViewItemHeaderBackgroundSelected" Color="{DynamicResource ControlFillColorTransparent}"/>
|
||||
</ResourceDictionary>
|
||||
</ui:TabView.Resources>
|
||||
<!--We need to hack this style for Mica theme since there is no way to explicity set style priority in Avalonia...-->
|
||||
<ui:TabView.Styles>
|
||||
<Style Selector="Grid > ContentPresenter#TabContentPresenter">
|
||||
<Style.Animations>
|
||||
<Animation IterationCount="1" Duration="0:0:1" FillMode="Both">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillColorTransparent}"/>
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillColorTransparent}"/>
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
</ui:TabView.Styles>
|
||||
<ui:TabView.TabItemTemplate>
|
||||
<DataTemplate DataType="vm:EndpointItem">
|
||||
<ui:TabViewItem Header="{Binding Header}"
|
||||
IconSource="{Binding IconSource}"
|
||||
IsSelected="{Binding Selected}"
|
||||
Content="{Binding}">
|
||||
<ui:TabViewItem.ContentTemplate>
|
||||
<DataTemplate DataType="vm:EndpointItem">
|
||||
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
|
||||
<Menu Grid.Row="0"
|
||||
Grid.Column="0">
|
||||
<MenuItem Header="_New tab">
|
||||
<MenuItem Header="LCU"
|
||||
Command="{Binding #EndpointsTab.((vm:EndpointsTabViewModel)DataContext).AddEndpointCommand}"
|
||||
CommandParameter="{x:Static vm:Tab.LCU}"/>
|
||||
<MenuItem Header="Game Client"
|
||||
Command="{Binding #EndpointsTab.((vm:EndpointsTabViewModel)DataContext).AddEndpointCommand}"
|
||||
CommandParameter="{x:Static vm:Tab.GameClient}"/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<Separator Grid.Row="1"
|
||||
Grid.Column="0"/>
|
||||
<ContentControl Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Content="{Binding Content}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ui:TabViewItem.ContentTemplate>
|
||||
</ui:TabViewItem>
|
||||
</DataTemplate>
|
||||
</ui:TabView.TabItemTemplate>
|
||||
</ui:TabView>
|
||||
</Grid>
|
||||
</controls:BusyArea>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,24 @@
|
||||
using Avalonia.Controls;
|
||||
using Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
using System.Collections;
|
||||
|
||||
namespace Needlework.Net.Views.Pages.Endpoints;
|
||||
|
||||
public partial class EndpointsTabView : UserControl
|
||||
{
|
||||
public EndpointsTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void TabView_TabCloseRequested(FluentAvalonia.UI.Controls.TabView sender, FluentAvalonia.UI.Controls.TabViewTabCloseRequestedEventArgs args)
|
||||
{
|
||||
if (args.Tab.Content is EndpointItem item && sender.TabItems is IList tabItems)
|
||||
{
|
||||
if (tabItems.Count > 1)
|
||||
{
|
||||
tabItems.Remove(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,13 @@
|
||||
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:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
|
||||
xmlns:controls="using:Needlework.Net.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
Name="EndpointsControl"
|
||||
x:Class="Needlework.Net.Views.EndpointsView"
|
||||
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsView"
|
||||
x:DataType="vm:EndpointsViewModel">
|
||||
<controls:BusyArea IsBusy="{Binding IsBusy}"
|
||||
BusyText="Loading...">
|
||||
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
|
||||
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
|
||||
<TextBox Watermark="Search" Margin="0 4" Text="{Binding Search}" Grid.Row="1" Grid.Column="0"/>
|
||||
<ScrollViewer Grid.Row="2" Grid.Column="0">
|
||||
<ItemsRepeater ItemsSource="{Binding Query}">
|
||||
@@ -28,5 +26,4 @@
|
||||
</ItemsRepeater>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</controls:BusyArea>
|
||||
</UserControl>
|
||||
11
Needlework.Net/Views/Pages/Endpoints/EndpointsView.axaml.cs
Normal file
11
Needlework.Net/Views/Pages/Endpoints/EndpointsView.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Needlework.Net.Views.Pages.Endpoints;
|
||||
|
||||
public partial class EndpointsView : UserControl
|
||||
{
|
||||
public EndpointsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
128
Needlework.Net/Views/Pages/HomeView.axaml
Normal file
128
Needlework.Net/Views/Pages/HomeView.axaml
Normal file
@@ -0,0 +1,128 @@
|
||||
<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.Pages"
|
||||
xmlns:controls="using:Needlework.Net.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
Name="HomeControl"
|
||||
x:Class="Needlework.Net.Views.Pages.HomeView"
|
||||
x:DataType="vm:HomeViewModel">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
|
||||
</Style>
|
||||
<Style Selector="DataGrid">
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ControlElevationBorderBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="DataGridColumnHeader TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="DataGridRow DataGridCell">
|
||||
<Setter Property="FontSize" Value="12"></Setter>
|
||||
</Style>
|
||||
<Style Selector="DataGridRow">
|
||||
<Setter Property="Margin" Value="0 0 0 4"></Setter>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<!-- TOP LEVEL -->
|
||||
<Grid ColumnDefinitions="*,400"
|
||||
RowDefinitions="*">
|
||||
<!-- MAIN AREA -->
|
||||
<ScrollViewer Grid.Column="0"
|
||||
Grid.Row="0">
|
||||
<WrapPanel Margin="8"
|
||||
Orientation="Horizontal">
|
||||
<!-- WELCOME -->
|
||||
<StackPanel>
|
||||
<Border Margin="12">
|
||||
<StackPanel>
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">
|
||||
Welcome to Needlework.Net
|
||||
</TextBlock>
|
||||
<TextBlock>Get started with LCU or Game Client development by clicking on the endpoints tab in the left panel.</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<controls:Card Margin="12">
|
||||
<TextBlock TextWrapping="Wrap">THE PROGRAM IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF MERCHANTABILITY, NONINFRINGMENT, OR OF FITNESS FOR A PARTICULAR PURPOSE. LICENSOR DOES NOT WARRANT THAT THE FUNCTIONS CONTAINED IN THE PROGRAM WILL MEET YOUR REQUIREMENTS OR THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. LICENSOR MAKES NO WARRANTIES RESPECTING ANY HARM THAT MAY BE CAUSED BY MALICIOUS USE OF THIS SOFTWARE. LICENSOR FURTHER EXPRESSLY DISCLAIMS ANY WARRANTY OR REPRESENTATION TO AUTHORIZED USERS OR TO ANY THIRD PARTY.</TextBlock>
|
||||
</controls:Card>
|
||||
</StackPanel>
|
||||
<!-- FOOTER -->
|
||||
<StackPanel>
|
||||
<controls:Card Margin="12" Width="300">
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Theme="{StaticResource SubtitleTextBlockStyle}"
|
||||
Margin="0 0 0 8">Resources</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button CommandParameter="https://hextechdocs.dev/tag/lcu/" Margin="4">
|
||||
Hextech Docs
|
||||
</Button>
|
||||
<Button CommandParameter="https://hextechdocs.dev/getting-started-with-the-lcu-api/" Margin="4">
|
||||
Getting Started
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button CommandParameter="https://discord.com/channels/187652476080488449/516802588805431296" Margin="4">
|
||||
#lcu-api
|
||||
</Button>
|
||||
<Button CommandParameter="https://discord.com/channels/187652476080488449/543112946402721832" Margin="4">
|
||||
#ingame-api
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
<controls:Card Margin="12" Width="300">
|
||||
<StackPanel>
|
||||
<TextBlock>© 2025 - Blossomi Shymae</TextBlock>
|
||||
<TextBlock>MIT License</TextBlock>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
</StackPanel>
|
||||
<!-- LEGAL -->
|
||||
<controls:Card Margin="12" Width="300">
|
||||
<TextBlock TextWrapping="Wrap">Needlework.Net isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games, and all associated properties are trademarks or registered trademarks of Riot Games, Inc.</TextBlock>
|
||||
</controls:Card>
|
||||
</WrapPanel>
|
||||
</ScrollViewer>
|
||||
<!-- LIBRARIES -->
|
||||
<Grid Margin="20"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
ColumnDefinitions="*"
|
||||
RowDefinitions="auto,*">
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0">Libraries</TextBlock>
|
||||
<ScrollViewer Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<ItemsRepeater ItemsSource="{Binding Libraries}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="0 12 0 0">
|
||||
<TextBlock>
|
||||
<Run Text="{Binding Language}"
|
||||
FontWeight="Bold"/>
|
||||
<Bold> - </Bold>
|
||||
<Run Text="{Binding Repo}"
|
||||
FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Description}"
|
||||
IsVisible="{Binding Description, Converter={StaticResource NullBoolConverter}}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="WrapWithOverflow"
|
||||
Width="350"/>
|
||||
<Button Command="{Binding #HomeControl.((vm:HomeViewModel)DataContext).OpenUrlCommand}"
|
||||
CommandParameter="{Binding Link}"
|
||||
Margin="0 4 0 0">
|
||||
<TextBlock Text="{Binding Link}"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Needlework.Net/Views/Pages/HomeView.axaml.cs
Normal file
11
Needlework.Net/Views/Pages/HomeView.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Needlework.Net.Views.Pages;
|
||||
|
||||
public partial class HomeView : UserControl
|
||||
{
|
||||
public HomeView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,29 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Websocket"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Needlework.Net.Views.WebsocketView"
|
||||
x:Class="Needlework.Net.Views.Pages.WebsocketView"
|
||||
x:DataType="vm:WebsocketViewModel">
|
||||
<Grid RowDefinitions="*,auto,*" Margin="16">
|
||||
<Border Grid.Row="0"
|
||||
Padding="0 0 0 8">
|
||||
<Grid RowDefinitions="auto,*" ColumnDefinitions="*">
|
||||
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
|
||||
<Grid Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
RowDefinitions="*">
|
||||
<ComboBox ItemsSource="{Binding EventTypes}"
|
||||
SelectedItem="{Binding EventType}"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"/>
|
||||
</Grid>
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
RowDefinitions="*"
|
||||
ColumnDefinitions="auto,*,auto,auto">
|
||||
ColumnDefinitions="auto,*,auto,auto"
|
||||
Margin="0 8 0 0">
|
||||
<Button Margin="0 0 8 0"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
@@ -37,7 +47,7 @@
|
||||
Content="Tail"
|
||||
IsChecked="{Binding IsTail}"/>
|
||||
</Grid>
|
||||
<ListBox Grid.Row="1"
|
||||
<ListBox Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Name="EventViewer"
|
||||
Margin="0 8 0 0"
|
||||
@@ -6,11 +6,11 @@ using AvaloniaEdit;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Needlework.Net.Extensions;
|
||||
using Needlework.Net.Messages;
|
||||
using Needlework.Net.ViewModels;
|
||||
using Needlework.Net.ViewModels.Pages.Websocket;
|
||||
using System;
|
||||
using TextMateSharp.Grammars;
|
||||
|
||||
namespace Needlework.Net.Views;
|
||||
namespace Needlework.Net.Views.Pages;
|
||||
|
||||
public partial class WebsocketView : UserControl, IRecipient<ResponseUpdatedMessage>
|
||||
{
|
||||
17
README.md
17
README.md
@@ -2,9 +2,16 @@
|
||||
|
||||

|
||||
|
||||
Needlework.Net is an open-source helper tool for the LCU that provides documented endpoints and can send requests without any code setup. Created using .NET! 🌠
|
||||
Needlework.Net is an open-source helper tool for the LCU and Game Client that provides documented endpoints and can send requests without any code setup. Created using .NET! 🌠
|
||||
|
||||
# Features
|
||||
|
||||
- Interactive OpenAPI documentations
|
||||
- REST data transfer console
|
||||
- WebSocket event data viewer
|
||||
|
||||
# Requirements
|
||||
|
||||
- Windows x64
|
||||
- .NET 8 runtime. [It can be downloaded here.](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.7-windows-x64-installer)
|
||||
|
||||
@@ -21,6 +28,7 @@ Needlework.Net is an open-source helper tool for the LCU that provides documente
|
||||
## Credits
|
||||
|
||||
### GrrrLCU
|
||||
|
||||
A simple wrapper for the LCU.
|
||||
- [Repository](https://github.com/BlossomiShymae/GrrrLCU)
|
||||
|
||||
@@ -31,9 +39,14 @@ This project was inspired by LCU Explorer, an application created by the Hextech
|
||||
|
||||
### hasagi-types
|
||||
|
||||
Endpoints and schemas are provided by dysolix's [generated OpenAPI file.](https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json) Thank you!
|
||||
LCU Schema endpoints are provided by dysolix's [generated OpenAPI file.](https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json) Thank you!
|
||||
- [Repository](https://github.com/dysolix/hasagi-types)
|
||||
|
||||
### Irelia
|
||||
|
||||
Game Client endpoints are provided by AlsoSylv's [fixed OpenAPI file.](https://raw.githubusercontent.com/AlsoSylv/Irelia/refs/heads/master/schemas/game_schema.json) Thank you despite not wanting to receive credit. :pout:
|
||||
- [Repository](https://github.com/AlsoSylv/Irelia)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
THE PROGRAM IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF MERCHANTABILITY, NONINFRINGMENT, OR OF FITNESS FOR A PARTICULAR PURPOSE. LICENSOR DOES NOT WARRANT THAT THE FUNCTIONS CONTAINED IN THE PROGRAM WILL MEET YOUR REQUIREMENTS OR THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. LICENSOR MAKES NO WARRANTIES RESPECTING ANY HARM THAT MAY BE CAUSED BY MALICIOUS USE OF THIS SOFTWARE. LICENSOR FURTHER EXPRESSLY DISCLAIMS ANY WARRANTY OR REPRESENTATION TO AUTHORIZED USERS OR TO ANY THIRD PARTY.
|
||||
|
||||
BIN
app-preview.png
BIN
app-preview.png
Binary file not shown.
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 397 KiB |
Reference in New Issue
Block a user