Compare commits
43 Commits
0.5.0.0
...
bd6589c310
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
7288c471a4 | ||
|
|
7faedcf039 | ||
|
|
641d230647 | ||
|
|
d53c24c57f | ||
|
|
a24a72b3b2 | ||
|
|
2c88ae44a2 | ||
|
|
f0294b3042 | ||
|
|
d26180dce5 | ||
|
|
baf189e6a9 | ||
|
|
88149d1458 | ||
|
|
79fd79c01d | ||
|
|
7550102406 |
40
.github/workflows/release.yml
vendored
@@ -1,9 +1,12 @@
|
|||||||
# .github/workflows/release.yml
|
# .github/workflows/release.yml
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
release:
|
||||||
branches:
|
types: [created]
|
||||||
- release
|
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: Needlework.Net
|
||||||
|
ASSEMBLY_NAME: NeedleworkDotNet
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -15,30 +18,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
dotnet-version: 8.0.x
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: release
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build Needlework.Net.Desktop -c Release
|
run: dotnet build ${{env.PROJECT_NAME}} -c Release
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: dotnet publish Needlework.Net.Desktop -c Release -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -o publish -r win-x64 --self-contained=false
|
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: Get Version
|
- name: Zip files
|
||||||
id: version
|
run: 7z a -tzip ${{env.ASSEMBLY_NAME}}-win-x64.zip ./Publish/* README.md LICENSE
|
||||||
shell: powershell
|
- name: Upload to release
|
||||||
run: |
|
|
||||||
$xml=[xml](Get-Content .\Needlework.Net.Desktop\Needlework.Net.Desktop.csproj)
|
|
||||||
$ver=($xml.Project.PropertyGroup).AssemblyVersion
|
|
||||||
$ver="VERSION=$ver"
|
|
||||||
$ver=$ver -replace '\s',''
|
|
||||||
echo $ver >> $env:GITHUB_OUTPUT
|
|
||||||
- name: Zip Files
|
|
||||||
run: 7z a -tzip NeedleworkDotNet-win-x64.zip ./Publish/* README.md LICENSE
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: ${{startsWith(github.ref, 'refs/tags/') }}
|
||||||
with:
|
with:
|
||||||
name: "Needlework.Net v${{ steps.version.outputs.VERSION }}"
|
files: ${{env.ASSEMBLY_NAME}}-win-x64.zip
|
||||||
prerelease: false
|
|
||||||
tag_name: "${{ steps.version.outputs.VERSION }}"
|
|
||||||
files: |
|
|
||||||
NeedleworkDotNet-win-x64.zip
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using Xunit.Abstractions;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Core.Tests;
|
|
||||||
|
|
||||||
public class LcuSchemaHandlerTest
|
|
||||||
{
|
|
||||||
private readonly ITestOutputHelper _output;
|
|
||||||
|
|
||||||
internal HttpClient HttpClient { get; } = new();
|
|
||||||
|
|
||||||
public LcuSchemaHandlerTest(ITestOutputHelper output)
|
|
||||||
{
|
|
||||||
_output = output;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PluginsTestAsync()
|
|
||||||
{
|
|
||||||
var reader = new LcuSchemaHandler(await Resources.GetOpenApiDocumentAsync(HttpClient));
|
|
||||||
|
|
||||||
var plugins = reader.Plugins.Keys.ToList();
|
|
||||||
foreach (var plugin in plugins)
|
|
||||||
_output.WriteLine($"Plugin: {plugin}");
|
|
||||||
|
|
||||||
Assert.True(plugins.Count > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<IsTestProject>true</IsTestProject>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
|
||||||
<PackageReference Include="xunit" Version="2.5.3" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="Xunit" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Needlework.Net.Core\Needlework.Net.Core.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using Xunit.Abstractions;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Core.Tests;
|
|
||||||
|
|
||||||
public class ResourcesTest
|
|
||||||
{
|
|
||||||
private readonly ITestOutputHelper _output;
|
|
||||||
|
|
||||||
internal HttpClient HttpClient { get; } = new();
|
|
||||||
|
|
||||||
public ResourcesTest(ITestOutputHelper output)
|
|
||||||
{
|
|
||||||
_output = output;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task DocumentTestAsync()
|
|
||||||
{
|
|
||||||
var document = await Resources.GetOpenApiDocumentAsync(HttpClient);
|
|
||||||
|
|
||||||
Assert.True(document.Info.Title == "LCU SCHEMA");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.OpenApi" Version="1.6.16" />
|
|
||||||
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.16" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
|
||||||
{
|
|
||||||
public class ContentRequestMessage : RequestMessage<string>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
using Needlework.Net.Core;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
|
||||||
{
|
|
||||||
public class DataReadyMessage(LcuSchemaHandler handler) : ValueChangedMessage<LcuSchemaHandler>(handler)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
using Needlework.Net.Core;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
|
||||||
{
|
|
||||||
public class DataRequestMessage : RequestMessage<LcuSchemaHandler>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.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,8 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
|
||||||
{
|
|
||||||
public class OopsiesWindowCanceledMessage(object? data) : ValueChangedMessage<object?>(data)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
|
||||||
{
|
|
||||||
public class OopsiesWindowRequestedMessage(string text) : ValueChangedMessage<string>(text)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using Needlework.Net.Desktop.ViewModels;
|
|
||||||
using Needlework.Net.Desktop.Views;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Services
|
|
||||||
{
|
|
||||||
public class WindowService : IRecipient<OopsiesWindowCanceledMessage>
|
|
||||||
{
|
|
||||||
public IServiceProvider ServiceProvider { get; }
|
|
||||||
|
|
||||||
public OopsiesWindow? OopsiesWindow { get; set; }
|
|
||||||
|
|
||||||
public WindowService(IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
ServiceProvider = serviceProvider;
|
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Register<OopsiesWindowCanceledMessage>(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowOopsiesWindow(string text)
|
|
||||||
{
|
|
||||||
if (OopsiesWindow != null) OopsiesWindow!.Close();
|
|
||||||
|
|
||||||
var window = new OopsiesWindow();
|
|
||||||
window.DataContext = new OopsiesWindowViewModel(text);
|
|
||||||
window.Show(App.MainWindow!);
|
|
||||||
window.Closed += OnOopsiesWindowClosed;
|
|
||||||
OopsiesWindow = window;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnOopsiesWindowClosed(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (sender == null) return;
|
|
||||||
|
|
||||||
var window = (OopsiesWindow)sender;
|
|
||||||
window.DataContext = null;
|
|
||||||
window.Closed -= OnOopsiesWindowClosed;
|
|
||||||
OopsiesWindow = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Receive(OopsiesWindowCanceledMessage message)
|
|
||||||
{
|
|
||||||
if (OopsiesWindow is OopsiesWindow window) window.Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Controls.Templates;
|
|
||||||
using System;
|
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop
|
|
||||||
{
|
|
||||||
public class ViewLocator : IDataTemplate
|
|
||||||
{
|
|
||||||
public Control? Build(object? param)
|
|
||||||
{
|
|
||||||
if (param is null) return new TextBlock { Text = "data was null" };
|
|
||||||
|
|
||||||
var name = param.GetType().FullName!
|
|
||||||
.Replace("ViewModels", "Views")
|
|
||||||
.Replace("ViewModel", "View");
|
|
||||||
var type = Type.GetType(name);
|
|
||||||
|
|
||||||
if (type != null) return (Control)Activator.CreateInstance(type)!;
|
|
||||||
else return new TextBlock { Text = "Not Found: " + name };
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Match(object? data)
|
|
||||||
{
|
|
||||||
return data is INotifyPropertyChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Needlework.Net.Desktop.ViewModels
|
|
||||||
{
|
|
||||||
public class AboutViewModel : PageBase
|
|
||||||
{
|
|
||||||
public AboutViewModel() : base("About", "info-circle")
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
using Avalonia.Collections;
|
|
||||||
using BlossomiShymae.GrrrLCU;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using Needlework.Net.Desktop.Services;
|
|
||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.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 WindowService WindowService { get; }
|
|
||||||
|
|
||||||
public ConsoleViewModel(WindowService windowService) : base("Console", "terminal", -200)
|
|
||||||
{
|
|
||||||
WindowService = windowService;
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
WindowService.ShowOopsiesWindow(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,50 +0,0 @@
|
|||||||
using Avalonia.Collections;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.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.Desktop.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.Desktop.Messages;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.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,20 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.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.Desktop.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +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.Core;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using Needlework.Net.Desktop.Services;
|
|
||||||
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.Desktop.ViewModels
|
|
||||||
{
|
|
||||||
public partial class MainWindowViewModel : ObservableObject, IRecipient<DataRequestMessage>, IRecipient<HostDocumentRequestMessage>, IRecipient<OopsiesWindowRequestedMessage>, IRecipient<InfoBarUpdateMessage>
|
|
||||||
{
|
|
||||||
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 WindowService WindowService { get; }
|
|
||||||
public LcuSchemaHandler? LcuSchemaHandler { get; set; }
|
|
||||||
public OpenApiDocument? HostDocument { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty] private bool _isBusy = true;
|
|
||||||
|
|
||||||
[ObservableProperty] private ObservableCollection<InfoBarViewModel> _infoBarItems = [];
|
|
||||||
|
|
||||||
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, WindowService windowService)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
WindowService = windowService;
|
|
||||||
|
|
||||||
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 LcuSchemaHandler(document);
|
|
||||||
LcuSchemaHandler = handler;
|
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send(new DataReadyMessage(handler));
|
|
||||||
IsBusy = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Receive(DataRequestMessage message)
|
|
||||||
{
|
|
||||||
message.Reply(LcuSchemaHandler!);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(OopsiesWindowRequestedMessage message)
|
|
||||||
{
|
|
||||||
WindowService.ShowOopsiesWindow(message.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.ViewModels
|
|
||||||
{
|
|
||||||
public partial class OopsiesWindowViewModel(string text) : ObservableObject
|
|
||||||
{
|
|
||||||
public string Text { get; } = text;
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void OpenDefaultEditor()
|
|
||||||
{
|
|
||||||
var temp = Path.GetTempFileName().Replace(".tmp", ".json");
|
|
||||||
File.WriteAllText(temp, Text);
|
|
||||||
Process.Start("explorer", "\"" + temp + "\"");
|
|
||||||
CloseDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void CloseDialog()
|
|
||||||
{
|
|
||||||
WeakReferenceMessenger.Default.Send(new OopsiesWindowCanceledMessage(null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
using Avalonia.Collections;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.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;
|
|
||||||
if (requestClasses.Where(c => c.Id == type.Replace("#", string.Empty)).Any())
|
|
||||||
{
|
|
||||||
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.Desktop.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.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.Core;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.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 OopsiesWindowRequestedMessage(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.Desktop.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.Desktop.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.Desktop.ViewModels
|
|
||||||
{
|
|
||||||
public class PropertyFieldViewModel
|
|
||||||
{
|
|
||||||
public string Name { get; }
|
|
||||||
public string Type { get; }
|
|
||||||
|
|
||||||
public PropertyFieldViewModel(string name, string type)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
Type = type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
using BlossomiShymae.GrrrLCU;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using Needlework.Net.Desktop.Services;
|
|
||||||
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.Desktop.ViewModels
|
|
||||||
{
|
|
||||||
public partial class WebsocketViewModel : PageBase
|
|
||||||
{
|
|
||||||
public ObservableCollection<string> EventLog { get; } = [];
|
|
||||||
[NotifyPropertyChangedFor(nameof(FilteredEventLog))]
|
|
||||||
[ObservableProperty] private string _search = string.Empty;
|
|
||||||
[ObservableProperty] private bool _isAttach = true;
|
|
||||||
[ObservableProperty] private bool _isTail = false;
|
|
||||||
[ObservableProperty] private string? _selectedEventLog = null;
|
|
||||||
|
|
||||||
private Dictionary<string, EventMessage> _events = [];
|
|
||||||
|
|
||||||
public WebsocketClient? Client { get; set; }
|
|
||||||
|
|
||||||
public WindowService WindowService { get; }
|
|
||||||
|
|
||||||
public IReadOnlyList<string> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
|
|
||||||
|
|
||||||
public WebsocketViewModel(WindowService windowService) : base("Event Viewer", "plug", -100)
|
|
||||||
{
|
|
||||||
WindowService = windowService;
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void Clear()
|
|
||||||
{
|
|
||||||
_events.Clear();
|
|
||||||
EventLog.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnSelectedEventLogChanged(string? value)
|
|
||||||
{
|
|
||||||
if (value == null) return;
|
|
||||||
if (_events.TryGetValue(value, out var message))
|
|
||||||
{
|
|
||||||
var text = JsonSerializer.Serialize(message, App.JsonSerializerOptions);
|
|
||||||
if (text.Length >= App.MaxCharacters) WindowService.ShowOopsiesWindow(text);
|
|
||||||
else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebsocketViewModel));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(() =>
|
|
||||||
{
|
|
||||||
if (!IsAttach) return;
|
|
||||||
|
|
||||||
var line = $"{DateTime.Now:HH:mm:ss.fff} {message.Data?.EventType.ToUpper()} {message.Data?.Uri}";
|
|
||||||
Trace.WriteLine($"Message: {line}");
|
|
||||||
if (EventLog.Count < 1000)
|
|
||||||
{
|
|
||||||
EventLog.Add(line);
|
|
||||||
_events[line] = message;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var key = EventLog[0];
|
|
||||||
EventLog.RemoveAt(0);
|
|
||||||
_events.Remove(key);
|
|
||||||
|
|
||||||
EventLog.Add(line);
|
|
||||||
_events[line] = message;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +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.Desktop.ViewModels"
|
|
||||||
xmlns:controls="using:Needlework.Net.Desktop.Controls"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="Needlework.Net.Desktop.Views.AboutView"
|
|
||||||
x:DataType="vm:AboutViewModel">
|
|
||||||
<Grid Margin="8"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
HorizontalAlignment="Center">
|
|
||||||
<WrapPanel Orientation="Horizontal">
|
|
||||||
<controls:Card Margin="8">
|
|
||||||
<Image Source="/Assets/about.png"
|
|
||||||
RenderOptions.BitmapInterpolationMode="MediumQuality"
|
|
||||||
Width="200"
|
|
||||||
Height="200"/>
|
|
||||||
</controls:Card>
|
|
||||||
<StackPanel Margin="8 0 0 0">
|
|
||||||
<controls:Card Width="400" Margin="8">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Blossomi Shymae</TextBlock>
|
|
||||||
</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>
|
|
||||||
</WrapPanel>
|
|
||||||
</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.Desktop.Extensions;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using Needlework.Net.Desktop.ViewModels;
|
|
||||||
using TextMateSharp.Grammars;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.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.Desktop.Views;
|
|
||||||
|
|
||||||
public partial class EndpointsContainerView : UserControl
|
|
||||||
{
|
|
||||||
public EndpointsContainerView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.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.Desktop.ViewModels"
|
|
||||||
xmlns:controls="using:Needlework.Net.Desktop.Controls"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="Needlework.Net.Desktop.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.Desktop.Views
|
|
||||||
{
|
|
||||||
public partial class HomeView : UserControl
|
|
||||||
{
|
|
||||||
public HomeView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<Window
|
|
||||||
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.Desktop.ViewModels"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="Needlework.Net.Desktop.Views.OopsiesWindow"
|
|
||||||
x:DataType="vm:OopsiesWindowViewModel"
|
|
||||||
Title="Needlework.Net - Oopsies"
|
|
||||||
WindowStartupLocation="CenterOwner"
|
|
||||||
Width="560"
|
|
||||||
Height="200">
|
|
||||||
<Grid RowDefinitions="auto,*">
|
|
||||||
<Grid ColumnDefinitions="auto,auto,*,auto"
|
|
||||||
Background="Transparent"
|
|
||||||
Height="40"
|
|
||||||
Grid.Row="0">
|
|
||||||
<Image Margin="12 4"
|
|
||||||
IsHitTestVisible="False"
|
|
||||||
Source="/Assets/app.png"
|
|
||||||
Width="18"
|
|
||||||
Height="18"
|
|
||||||
DockPanel.Dock="Left"
|
|
||||||
Grid.Column="0"/>
|
|
||||||
<TextBlock FontSize="12"
|
|
||||||
IsHitTestVisible="False"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Grid.Column="1">
|
|
||||||
Needlework.Net - Oopsies
|
|
||||||
</TextBlock>
|
|
||||||
</Grid>
|
|
||||||
<Grid RowDefinitions="auto,auto,auto"
|
|
||||||
ColumnDefinitions="auto,auto"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
Margin="8"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
HorizontalAlignment="Center">
|
|
||||||
<TextBlock
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="0"
|
|
||||||
Grid.ColumnSpan="2">
|
|
||||||
This response is too large for Needlework.Net to handle for performance reasons.
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
Grid.ColumnSpan="2"
|
|
||||||
Margin="0 0 0 12">
|
|
||||||
It can be viewed in an external editor or viewer.
|
|
||||||
</TextBlock>
|
|
||||||
<Button Command="{Binding OpenDefaultEditorCommand}"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.Column="0"
|
|
||||||
Margin="0 0 8 0">
|
|
||||||
Open
|
|
||||||
</Button>
|
|
||||||
<Button Command="{Binding CloseDialogCommand}"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.Column="1"
|
|
||||||
Margin="8 0 0 0">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Window>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using FluentAvalonia.UI.Windowing;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Views;
|
|
||||||
|
|
||||||
public partial class OopsiesWindow : AppWindow
|
|
||||||
{
|
|
||||||
public OopsiesWindow()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
TitleBar.ExtendsContentIntoTitleBar = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.0.31903.59
|
VisualStudioVersion = 17.0.31903.59
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Core", "Needlework.Net.Core\Needlework.Net.Core.csproj", "{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net", "Needlework.Net\Needlework.Net.csproj", "{7388B579-2DC0-46D6-957A-6683D0FCF5D3}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Desktop", "Needlework.Net.Desktop\Needlework.Net.Desktop.csproj", "{7388B579-2DC0-46D6-957A-6683D0FCF5D3}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Core.Tests", "Needlework.Net.Core.Tests\Needlework.Net.Core.Tests.csproj", "{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -18,17 +14,9 @@ Global
|
|||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<Application xmlns="https://github.com/avaloniaui"
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
x:Class="Needlework.Net.Desktop.App"
|
x:Class="Needlework.Net.App"
|
||||||
xmlns:local="using:Needlework.Net.Desktop"
|
xmlns:local="using:Needlework.Net"
|
||||||
xmlns:converters="using:Needlework.Net.Desktop.Converters"
|
xmlns:converters="using:Needlework.Net.Converters"
|
||||||
xmlns:sty="using:FluentAvalonia.Styling"
|
xmlns:sty="using:FluentAvalonia.Styling"
|
||||||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
RequestedThemeVariant="Dark">
|
RequestedThemeVariant="Dark">
|
||||||
@@ -3,12 +3,12 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Needlework.Net.Desktop.ViewModels;
|
using Needlework.Net.ViewModels.MainWindow;
|
||||||
using Needlework.Net.Desktop.Views;
|
using Needlework.Net.Views.MainWindow;
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop;
|
namespace Needlework.Net;
|
||||||
|
|
||||||
public partial class App(IServiceProvider serviceProvider) : Application
|
public partial class App(IServiceProvider serviceProvider) : Application
|
||||||
{
|
{
|
||||||
@@ -33,7 +33,7 @@ public partial class App(IServiceProvider serviceProvider) : Application
|
|||||||
{
|
{
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
desktop.MainWindow = new MainWindow()
|
desktop.MainWindow = new MainWindowView()
|
||||||
{
|
{
|
||||||
DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
|
DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
|
||||||
};
|
};
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 938 B After Width: | Height: | Size: 938 B |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
BIN
Needlework.Net/Assets/Users/community.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
Needlework.Net/Assets/Users/dubble.png
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
Needlework.Net/Assets/Users/dysolix.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
Needlework.Net/Assets/Users/ray.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
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"}]
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:controls="using:Needlework.Net.Desktop.Controls"
|
xmlns:controls="using:Needlework.Net.Controls"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Needlework.Net.Desktop.Controls.BusyArea">
|
x:Class="Needlework.Net.Controls.BusyArea">
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Style Selector="controls|BusyArea">
|
<Style Selector="controls|BusyArea">
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Controls;
|
namespace Needlework.Net.Controls;
|
||||||
|
|
||||||
public partial class BusyArea : UserControl
|
public partial class BusyArea : UserControl
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<Styles xmlns="https://github.com/avaloniaui"
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:controls="using:Needlework.Net.Desktop.Controls">
|
xmlns:controls="using:Needlework.Net.Controls">
|
||||||
<Design.PreviewWith>
|
<Design.PreviewWith>
|
||||||
<controls:Card />
|
<controls:Card />
|
||||||
</Design.PreviewWith>
|
</Design.PreviewWith>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Controls;
|
namespace Needlework.Net.Controls;
|
||||||
|
|
||||||
public class Card : ContentControl
|
public class Card : ContentControl
|
||||||
{
|
{
|
||||||
@@ -4,7 +4,7 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Converters
|
namespace Needlework.Net.Converters
|
||||||
{
|
{
|
||||||
public class EnumerableBoolConverter : IValueConverter
|
public class EnumerableBoolConverter : IValueConverter
|
||||||
{
|
{
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Converters
|
namespace Needlework.Net.Converters
|
||||||
{
|
{
|
||||||
public class NullBoolConverter : IValueConverter
|
public class NullBoolConverter : IValueConverter
|
||||||
{
|
{
|
||||||
22
Needlework.Net/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Extensions
|
||||||
|
{
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddSingletonsFromAssemblies<T>(this ServiceCollection services)
|
||||||
|
{
|
||||||
|
var types = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.SelectMany(s => s.GetTypes())
|
||||||
|
.Where(p => !p.IsAbstract && typeof(T).IsAssignableFrom(p));
|
||||||
|
|
||||||
|
foreach (var type in types) services.AddSingleton(typeof(T), type);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ using AvaloniaEdit;
|
|||||||
using AvaloniaEdit.Highlighting;
|
using AvaloniaEdit.Highlighting;
|
||||||
using AvaloniaEdit.Indentation.CSharp;
|
using AvaloniaEdit.Indentation.CSharp;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Extensions
|
namespace Needlework.Net.Extensions
|
||||||
{
|
{
|
||||||
public static class TextEditorExtensions
|
public static class TextEditorExtensions
|
||||||
{
|
{
|
||||||
9
Needlework.Net/Messages/DataReadyMessage.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
|
using Needlework.Net.Models;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Messages
|
||||||
|
{
|
||||||
|
public class DataReadyMessage(OpenApiDocumentWrapper wrapper) : ValueChangedMessage<OpenApiDocumentWrapper>(wrapper)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Needlework.Net/Messages/DataRequestMessage.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
|
using Needlework.Net.Models;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Messages
|
||||||
|
{
|
||||||
|
public class DataRequestMessage : RequestMessage<OpenApiDocumentWrapper>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
namespace Needlework.Net.Messages
|
||||||
{
|
{
|
||||||
public class HostDocumentRequestMessage : RequestMessage<OpenApiDocument>
|
public class HostDocumentRequestMessage : RequestMessage<OpenApiDocument>
|
||||||
{
|
{
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
using Needlework.Net.Desktop.ViewModels;
|
using Needlework.Net.ViewModels.MainWindow;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
namespace Needlework.Net.Messages
|
||||||
{
|
{
|
||||||
public class InfoBarUpdateMessage(InfoBarViewModel vm) : ValueChangedMessage<InfoBarViewModel>(vm)
|
public class InfoBarUpdateMessage(InfoBarViewModel vm) : ValueChangedMessage<InfoBarViewModel>(vm)
|
||||||
{
|
{
|
||||||
8
Needlework.Net/Messages/OopsiesDialogRequestedMessage.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Messages
|
||||||
|
{
|
||||||
|
public class OopsiesDialogRequestedMessage(string text) : ValueChangedMessage<string>(text)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
namespace Needlework.Net.Messages
|
||||||
{
|
{
|
||||||
public class ResponseUpdatedMessage(string data) : ValueChangedMessage<string>(data)
|
public class ResponseUpdatedMessage(string data) : ValueChangedMessage<string>(data)
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop
|
namespace Needlework.Net.Models
|
||||||
{
|
{
|
||||||
public class GithubRelease
|
public class GithubRelease
|
||||||
{
|
{
|
||||||
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; }
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
namespace Needlework.Net.Core;
|
namespace Needlework.Net.Models;
|
||||||
|
|
||||||
public class LcuSchemaHandler
|
public class OpenApiDocumentWrapper
|
||||||
{
|
{
|
||||||
internal OpenApiDocument OpenApiDocument { get; }
|
internal OpenApiDocument OpenApiDocument { get; }
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ public class LcuSchemaHandler
|
|||||||
|
|
||||||
public List<string> Paths => [.. OpenApiDocument.Paths.Keys];
|
public List<string> Paths => [.. OpenApiDocument.Paths.Keys];
|
||||||
|
|
||||||
public LcuSchemaHandler(OpenApiDocument openApiDocument)
|
public OpenApiDocumentWrapper(OpenApiDocument openApiDocument)
|
||||||
{
|
{
|
||||||
OpenApiDocument = openApiDocument;
|
OpenApiDocument = openApiDocument;
|
||||||
var plugins = new SortedDictionary<string, List<PathOperation>>();
|
var plugins = new SortedDictionary<string, List<PathOperation>>();
|
||||||
@@ -68,5 +69,3 @@ public class LcuSchemaHandler
|
|||||||
Plugins = plugins;
|
Plugins = plugins;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record PathOperation(string Method, string Path, OpenApiOperation Operation);
|
|
||||||
5
Needlework.Net/Models/PathOperation.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Models;
|
||||||
|
|
||||||
|
public record PathOperation(string Method, string Path, OpenApiOperation Operation);
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using Microsoft.OpenApi.Models;
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
using Microsoft.OpenApi.Readers;
|
using Microsoft.OpenApi.Readers;
|
||||||
|
|
||||||
namespace Needlework.Net.Core;
|
namespace Needlework.Net.Models;
|
||||||
|
|
||||||
public static class Resources
|
public static class Resources
|
||||||
{
|
{
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
|
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
|
||||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||||
<AssemblyName>NeedleworkDotNet</AssemblyName>
|
<AssemblyName>NeedleworkDotNet</AssemblyName>
|
||||||
<AssemblyVersion>0.5.0.0</AssemblyVersion>
|
<AssemblyVersion>0.9.1.0</AssemblyVersion>
|
||||||
<FileVersion>$(AssemblyVersion)</FileVersion>
|
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||||
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
|
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -27,25 +27,23 @@
|
|||||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
|
||||||
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
|
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
|
||||||
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.9.0" />
|
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.14.0" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
|
||||||
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0" />
|
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0" />
|
||||||
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
|
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.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" Version="9.4.0" />
|
||||||
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
|
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
|
||||||
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.62" />
|
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.64" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Needlework.Net.Core\Needlework.Net.Core.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<UpToDateCheckInput Remove="Views\AboutView.axaml" />
|
<UpToDateCheckInput Remove="Views\AboutView.axaml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -54,15 +52,19 @@
|
|||||||
<Compile Update="Controls\BusyArea.axaml.cs">
|
<Compile Update="Controls\BusyArea.axaml.cs">
|
||||||
<DependentUpon>BusyArea.axaml</DependentUpon>
|
<DependentUpon>BusyArea.axaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Views\EndpointView.axaml.cs">
|
<Compile Update="Views\MainWindow\MainWindowView.axaml.cs">
|
||||||
<DependentUpon>EndpointView.axaml</DependentUpon>
|
<DependentUpon>MainWindowView.axaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Views\OopsiesWindow.axaml.cs">
|
<Compile Update="Views\Pages\Endpoints\EndpointsNavigationView.axaml.cs">
|
||||||
<DependentUpon>OopsiesWindow.axaml</DependentUpon>
|
<DependentUpon>EndpointsNavigationView.axaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="Views\Pages\Endpoints\EndpointView.axaml.cs">
|
||||||
|
<DependentUpon>EndpointView.axaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Assets\Users\" />
|
||||||
<Folder Include="Utilities\" />
|
<Folder Include="Utilities\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Needlework.Net.Desktop.Services;
|
using Needlework.Net.Extensions;
|
||||||
using Needlework.Net.Desktop.ViewModels;
|
using Needlework.Net.Services;
|
||||||
|
using Needlework.Net.ViewModels.MainWindow;
|
||||||
|
using Needlework.Net.ViewModels.Pages;
|
||||||
using Projektanker.Icons.Avalonia;
|
using Projektanker.Icons.Avalonia;
|
||||||
using Projektanker.Icons.Avalonia.FontAwesome;
|
using Projektanker.Icons.Avalonia.FontAwesome;
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.IO;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop;
|
namespace Needlework.Net;
|
||||||
|
|
||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
@@ -15,8 +17,13 @@ class Program
|
|||||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||||
// yet and stuff might break.
|
// yet and stuff might break.
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += Program_UnhandledException;
|
||||||
|
|
||||||
|
BuildAvaloniaApp()
|
||||||
.StartWithClassicDesktopLifetime(args);
|
.StartWithClassicDesktopLifetime(args);
|
||||||
|
}
|
||||||
|
|
||||||
// Avalonia configuration, don't remove; also used by visual designer.
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
@@ -35,17 +42,17 @@ class Program
|
|||||||
var builder = new ServiceCollection();
|
var builder = new ServiceCollection();
|
||||||
|
|
||||||
builder.AddSingleton<MainWindowViewModel>();
|
builder.AddSingleton<MainWindowViewModel>();
|
||||||
builder.AddSingleton<WindowService>();
|
builder.AddSingleton<DialogService>();
|
||||||
// Dynamically add ViewModels
|
builder.AddSingletonsFromAssemblies<PageBase>();
|
||||||
var types = AppDomain.CurrentDomain.GetAssemblies()
|
|
||||||
.SelectMany(s => s.GetTypes())
|
|
||||||
.Where(p => !p.IsAbstract && typeof(PageBase).IsAssignableFrom(p));
|
|
||||||
foreach (var type in types)
|
|
||||||
builder.AddSingleton(typeof(PageBase), type);
|
|
||||||
|
|
||||||
builder.AddHttpClient();
|
builder.AddHttpClient();
|
||||||
|
|
||||||
var services = builder.BuildServiceProvider();
|
var services = builder.BuildServiceProvider();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||||
|
{
|
||||||
|
File.WriteAllText($"errorlog-{DateTime.Now:HHmmssfff}", e.ExceptionObject.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
20
Needlework.Net/Services/DialogService.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Services
|
||||||
|
{
|
||||||
|
public class DialogService
|
||||||
|
{
|
||||||
|
public async Task<ContentDialogResult> ShowAsync<T>(object data)
|
||||||
|
where T : IDialog, IDisposable
|
||||||
|
{
|
||||||
|
T dialog = Activator.CreateInstance<T>();
|
||||||
|
|
||||||
|
var result = await dialog.ShowAsync(data);
|
||||||
|
dialog.Dispose();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Needlework.Net/Services/IDialog.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Services
|
||||||
|
{
|
||||||
|
public interface IDialog
|
||||||
|
{
|
||||||
|
public Task<ContentDialogResult> ShowAsync(object data);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
Needlework.Net/ViewLocator.cs
Normal file
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
private readonly Dictionary<object, Control> _controlCache = [];
|
||||||
|
|
||||||
|
public Control Build(object? data)
|
||||||
|
{
|
||||||
|
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." };
|
||||||
|
}
|
||||||
|
|
||||||
|
name = name.Replace("ViewModel", "View");
|
||||||
|
var type = Assembly.GetExecutingAssembly()
|
||||||
|
.GetTypes()
|
||||||
|
.Where(t => t.Name == name)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
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) => data is INotifyPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
157
Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Needlework.Net.Messages;
|
||||||
|
using Needlework.Net.Models;
|
||||||
|
using Needlework.Net.Services;
|
||||||
|
using Needlework.Net.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;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.MainWindow;
|
||||||
|
|
||||||
|
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.FromMinutes(10)); // Avoid tripping unauthenticated rate limits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Needlework.Net/ViewModels/Pages/AboutViewModel.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Needlework.Net/ViewModels/Pages/ConsoleViewModel.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Needlework.Net.Messages;
|
||||||
|
using Needlework.Net.ViewModels.Shared;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages;
|
||||||
|
|
||||||
|
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 LcuRequestViewModel _lcuRequest = new();
|
||||||
|
|
||||||
|
public ConsoleViewModel() : base("Console", "terminal", -200)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Register<DataReadyMessage>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task SendRequest()
|
||||||
|
{
|
||||||
|
await LcuRequest.ExecuteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(DataReadyMessage message)
|
||||||
|
{
|
||||||
|
Avalonia.Threading.Dispatcher.UIThread.Invoke(() =>
|
||||||
|
{
|
||||||
|
RequestPaths.Clear();
|
||||||
|
RequestPaths.AddRange(message.Value.Paths);
|
||||||
|
IsBusy = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Needlework.Net.Messages;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
PathOperationSelected?.Invoke(this, value.Operation.RequestTemplate ?? string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
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 = string.Empty;
|
||||||
|
|
||||||
|
private readonly Action<string?, Guid> _onEndpointNavigation;
|
||||||
|
|
||||||
|
public EndpointsNavigationViewModel(IAvaloniaList<string> plugins, Action<string?, Guid> onEndpointNavigation)
|
||||||
|
{
|
||||||
|
_activeViewModel = _endpointsViewModel = new EndpointsViewModel(plugins, OnClicked);
|
||||||
|
_onEndpointNavigation = onEndpointNavigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClicked(ObservableObject viewModel)
|
||||||
|
{
|
||||||
|
ActiveViewModel = viewModel;
|
||||||
|
if (viewModel is EndpointViewModel endpoint)
|
||||||
|
{
|
||||||
|
Title = endpoint.Title;
|
||||||
|
_onEndpointNavigation.Invoke(endpoint.Title, Guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void GoBack()
|
||||||
|
{
|
||||||
|
ActiveViewModel = EndpointsViewModel;
|
||||||
|
Title = string.Empty;
|
||||||
|
_onEndpointNavigation.Invoke(null, Guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using Needlework.Net.Messages;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public partial class EndpointsTabViewModel : PageBase, IRecipient<DataReadyMessage>
|
||||||
|
{
|
||||||
|
public IAvaloniaList<string> Plugins { get; } = new AvaloniaList<string>();
|
||||||
|
public IAvaloniaList<EndpointItem> Endpoints { get; } = new AvaloniaList<EndpointItem>();
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isBusy = true;
|
||||||
|
|
||||||
|
public EndpointsTabViewModel() : base("Endpoints", "list-alt", -500)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.RegisterAll(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(DataReadyMessage message)
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
Plugins.Clear();
|
||||||
|
Plugins.AddRange(message.Value.Plugins.Keys);
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(AddEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddEndpoint()
|
||||||
|
{
|
||||||
|
Endpoints.Add(new()
|
||||||
|
{
|
||||||
|
Content = new EndpointsNavigationViewModel(Plugins, OnEndpointNavigation),
|
||||||
|
Selected = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEndpointNavigation(string? title, Guid guid)
|
||||||
|
{
|
||||||
|
foreach (var endpoint in Endpoints)
|
||||||
|
{
|
||||||
|
if (endpoint.Content.Guid.Equals(guid))
|
||||||
|
{
|
||||||
|
endpoint.Header = title ?? "Endpoints";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class EndpointItem : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private string _header = "Endpoints";
|
||||||
|
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,42 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
public EndpointsViewModel(IAvaloniaList<string> plugins, Action<ObservableObject> onClicked)
|
||||||
|
{
|
||||||
|
Plugins = new AvaloniaList<string>(plugins);
|
||||||
|
Query = new AvaloniaList<string>(plugins);
|
||||||
|
OnClicked = onClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
232
Needlework.Net/ViewModels/Pages/Endpoints/OperationViewModel.cs
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
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.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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,52 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Needlework.Net.Models;
|
||||||
|
using Needlework.Net.ViewModels.Shared;
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public partial class PathOperationViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public string Path { get; }
|
||||||
|
public OperationViewModel Operation { get; }
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isBusy;
|
||||||
|
[ObservableProperty] private Lazy<LcuRequestViewModel> _lcuRequest;
|
||||||
|
|
||||||
|
public PathOperationViewModel(PathOperation pathOperation)
|
||||||
|
{
|
||||||
|
Path = pathOperation.Path;
|
||||||
|
Operation = new OperationViewModel(pathOperation.Operation);
|
||||||
|
LcuRequest = new(() => new LcuRequestViewModel()
|
||||||
|
{
|
||||||
|
Method = pathOperation.Method.ToUpper()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[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)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LcuRequest.Value.RequestPath = sb.ToString();
|
||||||
|
await LcuRequest.Value.ExecuteAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Needlework.Net/ViewModels/Pages/HomeViewModel.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Avalonia.Platform;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Needlework.Net.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
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) { }
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenUrl(string url)
|
||||||
|
{
|
||||||
|
var process = new Process()
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo(url) { UseShellExecute = true }
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Needlework.Net/ViewModels/Pages/PageBase.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
181
Needlework.Net/ViewModels/Pages/Websocket/WebsocketViewModel.cs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
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.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
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))];
|
||||||
|
|
||||||
|
public WebsocketViewModel(HttpClient httpClient) : base("Event Viewer", "plug", -100)
|
||||||
|
{
|
||||||
|
HttpClient = httpClient;
|
||||||
|
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await InitializeEventTypes();
|
||||||
|
InitializeWebsocket();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"-- Reconnection --\nType{info.Type}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisconnection(DisconnectionInfo info)
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProtocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}");
|
||||||
|
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();
|
||||||
|
}
|
||||||
118
Needlework.Net/ViewModels/Shared/LcuRequestViewModel.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using Avalonia.Media;
|
||||||
|
using BlossomiShymae.GrrrLCU;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Needlework.Net.Messages;
|
||||||
|
using Needlework.Net.ViewModels.MainWindow;
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Shared;
|
||||||
|
|
||||||
|
public partial class LcuRequestViewModel : 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<LcuRequestViewModel>? RequestText;
|
||||||
|
public event EventHandler<string>? UpdateText;
|
||||||
|
|
||||||
|
partial void OnMethodChanged(string? oldValue, string? newValue)
|
||||||
|
{
|
||||||
|
if (newValue == null) return;
|
||||||
|
|
||||||
|
Color = new(GetColor(newValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsRequestBusy = true;
|
||||||
|
if (string.IsNullOrEmpty(RequestPath))
|
||||||
|
throw new Exception("Path is empty.");
|
||||||
|
|
||||||
|
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 not selected or missing."),
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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 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.")
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
|
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
|
||||||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
xmlns:i="https://github.com/projektanker/icons.avalonia"
|
xmlns:i="https://github.com/projektanker/icons.avalonia"
|
||||||
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
|
xmlns:vm="using:Needlework.Net.ViewModels.MainWindow"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Needlework.Net.Desktop.Views.MainWindow"
|
x:Class="Needlework.Net.Views.MainWindow.MainWindowView"
|
||||||
x:DataType="vm:MainWindowViewModel"
|
x:DataType="vm:MainWindowViewModel"
|
||||||
Title="Needlework.Net"
|
Title="Needlework.Net"
|
||||||
Icon="/Assets/app.ico"
|
Icon="/Assets/app.ico"
|
||||||
@@ -37,6 +37,8 @@
|
|||||||
<ui:NavigationView AlwaysShowHeader="False"
|
<ui:NavigationView AlwaysShowHeader="False"
|
||||||
PaneDisplayMode="Left"
|
PaneDisplayMode="Left"
|
||||||
IsSettingsVisible="False"
|
IsSettingsVisible="False"
|
||||||
|
IsPaneOpen="False"
|
||||||
|
OpenPaneLength="200"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
MenuItemsSource="{Binding MenuItems}"
|
MenuItemsSource="{Binding MenuItems}"
|
||||||
SelectedItem="{Binding SelectedMenuItem}">
|
SelectedItem="{Binding SelectedMenuItem}">
|
||||||
@@ -74,6 +76,7 @@
|
|||||||
<Grid>
|
<Grid>
|
||||||
<TransitioningContentControl Content="{Binding CurrentPage}"/>
|
<TransitioningContentControl Content="{Binding CurrentPage}"/>
|
||||||
<Button Content="{Binding Version}"
|
<Button Content="{Binding Version}"
|
||||||
|
Background="RoyalBlue"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
Margin="16"/>
|
Margin="16"/>
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
using FluentAvalonia.UI.Windowing;
|
using FluentAvalonia.UI.Windowing;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Views;
|
namespace Needlework.Net.Views.MainWindow;
|
||||||
|
|
||||||
public partial class MainWindow : AppWindow
|
public partial class MainWindowView : AppWindow
|
||||||
{
|
{
|
||||||
public MainWindow()
|
public MainWindowView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||