Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
98996609a3 | ||
|
|
65464d22e3 | ||
|
|
0ca7f7869d | ||
|
|
af47e7c763 | ||
|
|
3a7d39971a |
40
.github/workflows/release.yml
vendored
@@ -1,9 +1,12 @@
|
||||
# .github/workflows/release.yml
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
env:
|
||||
PROJECT_NAME: Needlework.Net
|
||||
ASSEMBLY_NAME: NeedleworkDotNet
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -15,30 +18,15 @@ jobs:
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: release
|
||||
uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: dotnet build Needlework.Net.Desktop -c Release
|
||||
run: dotnet build ${{env.PROJECT_NAME}} -c Release
|
||||
- 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
|
||||
- name: Get Version
|
||||
id: version
|
||||
shell: powershell
|
||||
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
|
||||
run: dotnet publish ${{env.PROJECT_NAME}} -c Release -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -o publish -r win-x64 --self-contained=false
|
||||
- name: Zip files
|
||||
run: 7z a -tzip ${{env.ASSEMBLY_NAME}}-win-x64.zip ./Publish/* README.md LICENSE
|
||||
- name: Upload to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{startsWith(github.ref, 'refs/tags/') }}
|
||||
with:
|
||||
name: "Needlework.Net v${{ steps.version.outputs.VERSION }}"
|
||||
prerelease: false
|
||||
tag_name: "${{ steps.version.outputs.VERSION }}"
|
||||
files: |
|
||||
NeedleworkDotNet-win-x64.zip
|
||||
files: ${{env.ASSEMBLY_NAME}}-win-x64.zip
|
||||
@@ -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,101 +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"]);
|
||||
|
||||
[ObservableProperty] private bool _isBusy = true;
|
||||
[ObservableProperty] private bool _isRequestBusy = false;
|
||||
[ObservableProperty] private IAvaloniaReadOnlyList<string> _requestPaths = new AvaloniaList<string>();
|
||||
[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) ?? throw new Exception("Response is null.");
|
||||
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
|
||||
body = !string.IsNullOrEmpty(body) ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(body), 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 = new AvaloniaList<string>([.. message.Value.Paths]);
|
||||
IsBusy = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Needlework.Net.Desktop.Messages;
|
||||
using System.Linq;
|
||||
|
||||
namespace Needlework.Net.Desktop.ViewModels
|
||||
{
|
||||
public partial class EndpointViewModel : ObservableObject
|
||||
{
|
||||
public string Endpoint { get; }
|
||||
public string Title => Endpoint;
|
||||
|
||||
|
||||
[ObservableProperty] private IAvaloniaReadOnlyList<PathOperationViewModel> _pathOperations;
|
||||
[ObservableProperty] private PathOperationViewModel? _selectedPathOperation;
|
||||
|
||||
[ObservableProperty] private string? _search;
|
||||
[ObservableProperty] private IAvaloniaReadOnlyList<PathOperationViewModel> _filteredPathOperations;
|
||||
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
FilteredPathOperations = new AvaloniaList<PathOperationViewModel>(PathOperations);
|
||||
return;
|
||||
}
|
||||
|
||||
FilteredPathOperations = new AvaloniaList<PathOperationViewModel>(PathOperations.Where(o => o.Path.ToLower().Contains(value.ToLower())));
|
||||
}
|
||||
|
||||
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,56 +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;
|
||||
|
||||
[ObservableProperty] private IAvaloniaReadOnlyList<string> _plugins = new AvaloniaList<string>();
|
||||
[ObservableProperty] private bool _isBusy = true;
|
||||
[ObservableProperty] private string _search = string.Empty;
|
||||
[ObservableProperty] private IAvaloniaReadOnlyList<string> _query = new AvaloniaList<string>();
|
||||
[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 = new AvaloniaList<string>([.. message.Value.Plugins.Keys]);
|
||||
Query = new AvaloniaList<string>([.. Plugins]);
|
||||
}
|
||||
|
||||
partial void OnSearchChanged(string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Search))
|
||||
Query = new AvaloniaList<string>(Plugins.Where(x => x.Contains(value)));
|
||||
else
|
||||
Query = 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 (true)
|
||||
{
|
||||
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) && !IsUpdateShown)
|
||||
{
|
||||
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,135 +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.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(pathOperation.Method.ToUpper()));
|
||||
Path = pathOperation.Path;
|
||||
Operation = new OperationViewModel(pathOperation.Operation);
|
||||
ProcessInfo = GetProcessInfo();
|
||||
ResponsePath = ProcessInfo != null ? $"https://127.0.0.1:{ProcessInfo.AppPort}{Path}" : null;
|
||||
ResponseUsername = ProcessInfo != null ? new RiotAuthentication(ProcessInfo.RemotingAuthToken).Username : null;
|
||||
ResponsePassword = ProcessInfo != null ? new RiotAuthentication(ProcessInfo.RemotingAuthToken).Password : null;
|
||||
ResponseAuthorization = ProcessInfo != null ? $"Basic {new RiotAuthentication(ProcessInfo.RemotingAuthToken).Value}" : null;
|
||||
}
|
||||
|
||||
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.ToUpper() 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 path = Path;
|
||||
foreach (var pathParameter in Operation.PathParameters)
|
||||
{
|
||||
path = path.Replace($"{{{pathParameter.Name}}}", pathParameter.Value);
|
||||
}
|
||||
|
||||
var query = "";
|
||||
foreach (var queryParameter in Operation.QueryParameters)
|
||||
{
|
||||
if (query.Length != 0 && !string.IsNullOrWhiteSpace(queryParameter.Value))
|
||||
query += $"&{queryParameter.Name}={Uri.EscapeDataString(queryParameter.Value)}";
|
||||
else if (query.Length == 0 && !string.IsNullOrWhiteSpace(queryParameter.Value))
|
||||
query += $"?{queryParameter.Name}={Uri.EscapeDataString(queryParameter.Value)}";
|
||||
}
|
||||
var uri = $"{path}{query}";
|
||||
|
||||
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) ?? throw new Exception("Response is null.");
|
||||
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
responseBody = !string.IsNullOrEmpty(responseBody) ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), 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,124 +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
|
||||
{
|
||||
[NotifyPropertyChangedFor(nameof(FilteredEventLog))]
|
||||
[ObservableProperty] private ObservableCollection<string> _eventLog = [];
|
||||
[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 List<string> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? [.. EventLog] : [.. EventLog.Where(x => x.ToLower().Contains(Search.ToLower()))];
|
||||
|
||||
public WebsocketViewModel(WindowService windowService) : base("Event Viewer", "plug", -100)
|
||||
{
|
||||
WindowService = windowService;
|
||||
|
||||
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()
|
||||
{
|
||||
EventLog = [];
|
||||
}
|
||||
|
||||
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}";
|
||||
var log = EventLog.ToList();
|
||||
Trace.WriteLine($"Message: {line}");
|
||||
if (log.Count < 1000)
|
||||
{
|
||||
log.Add(line);
|
||||
_events[line] = message;
|
||||
}
|
||||
else
|
||||
{
|
||||
var key = $"{log[0]}";
|
||||
log.RemoveAt(0);
|
||||
_events.Remove(key);
|
||||
|
||||
log.Add(line);
|
||||
_events[line] = message;
|
||||
}
|
||||
|
||||
EventLog = []; // This is a hack needed to update for ListBox
|
||||
EventLog = new ObservableCollection<string>(log);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
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}"
|
||||
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}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net", "Needlework.Net\Needlework.Net.csproj", "{7388B579-2DC0-46D6-957A-6683D0FCF5D3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -18,17 +14,9 @@ Global
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
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.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.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
|
||||
EndGlobal
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Needlework.Net.Desktop.App"
|
||||
xmlns:local="using:Needlework.Net.Desktop"
|
||||
xmlns:converters="using:Needlework.Net.Desktop.Converters"
|
||||
x:Class="Needlework.Net.App"
|
||||
xmlns:local="using:Needlework.Net"
|
||||
xmlns:converters="using:Needlework.Net.Converters"
|
||||
xmlns:sty="using:FluentAvalonia.Styling"
|
||||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
RequestedThemeVariant="Dark">
|
||||
@@ -3,12 +3,12 @@ using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Needlework.Net.Desktop.ViewModels;
|
||||
using Needlework.Net.Desktop.Views;
|
||||
using Needlework.Net.ViewModels.MainWindow;
|
||||
using Needlework.Net.Views.MainWindow;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Needlework.Net.Desktop;
|
||||
namespace Needlework.Net;
|
||||
|
||||
public partial class App(IServiceProvider serviceProvider) : Application
|
||||
{
|
||||
@@ -33,7 +33,7 @@ public partial class App(IServiceProvider serviceProvider) : Application
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow()
|
||||
desktop.MainWindow = new MainWindowView()
|
||||
{
|
||||
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 |
@@ -2,9 +2,9 @@
|
||||
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:controls="using:Needlework.Net.Desktop.Controls"
|
||||
xmlns:controls="using:Needlework.Net.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Needlework.Net.Desktop.Controls.BusyArea">
|
||||
x:Class="Needlework.Net.Controls.BusyArea">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="controls|BusyArea">
|
||||
<Setter Property="Template">
|
||||
@@ -1,7 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Needlework.Net.Desktop.Controls;
|
||||
namespace Needlework.Net.Controls;
|
||||
|
||||
public partial class BusyArea : UserControl
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Needlework.Net.Desktop.Controls">
|
||||
xmlns:controls="using:Needlework.Net.Controls">
|
||||
<Design.PreviewWith>
|
||||
<controls:Card />
|
||||
</Design.PreviewWith>
|
||||
@@ -1,6 +1,6 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Needlework.Net.Desktop.Controls;
|
||||
namespace Needlework.Net.Controls;
|
||||
|
||||
public class Card : ContentControl
|
||||
{
|
||||
@@ -4,7 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Needlework.Net.Desktop.Converters
|
||||
namespace Needlework.Net.Converters
|
||||
{
|
||||
public class EnumerableBoolConverter : IValueConverter
|
||||
{
|
||||
@@ -2,7 +2,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Needlework.Net.Desktop.Converters
|
||||
namespace Needlework.Net.Converters
|
||||
{
|
||||
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.Indentation.CSharp;
|
||||
|
||||
namespace Needlework.Net.Desktop.Extensions
|
||||
namespace Needlework.Net.Extensions
|
||||
{
|
||||
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 Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Needlework.Net.Desktop.Messages
|
||||
namespace Needlework.Net.Messages
|
||||
{
|
||||
public class HostDocumentRequestMessage : RequestMessage<OpenApiDocument>
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
||||
namespace Needlework.Net.Desktop.Messages
|
||||
namespace Needlework.Net.Messages
|
||||
{
|
||||
public class ResponseUpdatedMessage(string data) : ValueChangedMessage<string>(data)
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Needlework.Net.Desktop
|
||||
namespace Needlework.Net.Models
|
||||
{
|
||||
public class GithubRelease
|
||||
{
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Needlework.Net.Core;
|
||||
namespace Needlework.Net.Models;
|
||||
|
||||
public class LcuSchemaHandler
|
||||
public class OpenApiDocumentWrapper
|
||||
{
|
||||
internal OpenApiDocument OpenApiDocument { get; }
|
||||
|
||||
@@ -12,7 +13,7 @@ public class LcuSchemaHandler
|
||||
|
||||
public List<string> Paths => [.. OpenApiDocument.Paths.Keys];
|
||||
|
||||
public LcuSchemaHandler(OpenApiDocument openApiDocument)
|
||||
public OpenApiDocumentWrapper(OpenApiDocument openApiDocument)
|
||||
{
|
||||
OpenApiDocument = openApiDocument;
|
||||
var plugins = new SortedDictionary<string, List<PathOperation>>();
|
||||
@@ -68,5 +69,3 @@ public class LcuSchemaHandler
|
||||
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;
|
||||
|
||||
namespace Needlework.Net.Core;
|
||||
namespace Needlework.Net.Models;
|
||||
|
||||
public static class Resources
|
||||
{
|
||||
@@ -11,8 +11,8 @@
|
||||
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
|
||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||
<AssemblyName>NeedleworkDotNet</AssemblyName>
|
||||
<AssemblyVersion>0.4.2.0</AssemblyVersion>
|
||||
<FileVersion>0.4.2.0</FileVersion>
|
||||
<AssemblyVersion>0.8.1.0</AssemblyVersion>
|
||||
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -27,25 +27,23 @@
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
|
||||
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
|
||||
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.9.0" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.14.0" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0" />
|
||||
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="1.6.22" />
|
||||
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.22" />
|
||||
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.4.0" />
|
||||
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
|
||||
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.62" />
|
||||
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.64" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Needlework.Net.Core\Needlework.Net.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<UpToDateCheckInput Remove="Views\AboutView.axaml" />
|
||||
</ItemGroup>
|
||||
@@ -54,15 +52,19 @@
|
||||
<Compile Update="Controls\BusyArea.axaml.cs">
|
||||
<DependentUpon>BusyArea.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\EndpointView.axaml.cs">
|
||||
<DependentUpon>EndpointView.axaml</DependentUpon>
|
||||
<Compile Update="Views\MainWindow\MainWindowView.axaml.cs">
|
||||
<DependentUpon>MainWindowView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\OopsiesWindow.axaml.cs">
|
||||
<DependentUpon>OopsiesWindow.axaml</DependentUpon>
|
||||
<Compile Update="Views\Pages\Endpoints\EndpointsNavigationView.axaml.cs">
|
||||
<DependentUpon>EndpointsNavigationView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\Pages\Endpoints\EndpointView.axaml.cs">
|
||||
<DependentUpon>EndpointView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Assets\Users\" />
|
||||
<Folder Include="Utilities\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,13 +1,15 @@
|
||||
using Avalonia;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Needlework.Net.Desktop.Services;
|
||||
using Needlework.Net.Desktop.ViewModels;
|
||||
using Needlework.Net.Extensions;
|
||||
using Needlework.Net.Services;
|
||||
using Needlework.Net.ViewModels.MainWindow;
|
||||
using Needlework.Net.ViewModels.Pages;
|
||||
using Projektanker.Icons.Avalonia;
|
||||
using Projektanker.Icons.Avalonia.FontAwesome;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
|
||||
namespace Needlework.Net.Desktop;
|
||||
namespace Needlework.Net;
|
||||
|
||||
class Program
|
||||
{
|
||||
@@ -15,8 +17,13 @@ class Program
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
[STAThread]
|
||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += Program_UnhandledException;
|
||||
|
||||
BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
@@ -35,17 +42,17 @@ class Program
|
||||
var builder = new ServiceCollection();
|
||||
|
||||
builder.AddSingleton<MainWindowViewModel>();
|
||||
builder.AddSingleton<WindowService>();
|
||||
// Dynamically add ViewModels
|
||||
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.AddSingleton<DialogService>();
|
||||
builder.AddSingletonsFromAssemblies<PageBase>();
|
||||
|
||||
builder.AddHttpClient();
|
||||
|
||||
var services = builder.BuildServiceProvider();
|
||||
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;
|
||||
}
|
||||
}
|
||||
19
Needlework.Net/ViewModels/Pages/HomeViewModel.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
125
Needlework.Net/ViewModels/Pages/Websocket/WebsocketViewModel.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using BlossomiShymae.GrrrLCU;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Needlework.Net.Messages;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Websocket.Client;
|
||||
|
||||
namespace Needlework.Net.ViewModels.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;
|
||||
|
||||
private Dictionary<string, EventMessage> _events = [];
|
||||
|
||||
public WebsocketClient? Client { get; set; }
|
||||
|
||||
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
|
||||
|
||||
public WebsocketViewModel() : base("Event Viewer", "plug", -100)
|
||||
{
|
||||
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
|
||||
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private void InitializeWebsocket()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = Connector.CreateLcuWebsocketClient();
|
||||
client.EventReceived.Subscribe(OnMessage);
|
||||
client.DisconnectionHappened.Subscribe(OnDisconnection);
|
||||
client.ReconnectionHappened.Subscribe(OnReconnection);
|
||||
|
||||
client.Start();
|
||||
client.Send(new EventMessage(EventRequestType.Subscribe, EventKinds.OnJsonApiEvent));
|
||||
Client = client;
|
||||
return;
|
||||
}
|
||||
catch (Exception) { }
|
||||
Thread.Sleep(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedEventLogChanged(EventViewModel? value)
|
||||
{
|
||||
if (value == null) return;
|
||||
if (_events.TryGetValue(value.Key, out var message))
|
||||
{
|
||||
var text = JsonSerializer.Serialize(message, App.JsonSerializerOptions);
|
||||
if (text.Length >= App.MaxCharacters) WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(text));
|
||||
else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebsocketViewModel));
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Clear()
|
||||
{
|
||||
_events.Clear();
|
||||
EventLog.Clear();
|
||||
}
|
||||
|
||||
private void OnReconnection(ReconnectionInfo info)
|
||||
{
|
||||
Trace.WriteLine($"-- Reconnection --\nType{info.Type}");
|
||||
}
|
||||
|
||||
private void OnDisconnection(DisconnectionInfo info)
|
||||
{
|
||||
Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}");
|
||||
Client?.Dispose();
|
||||
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private void OnMessage(EventMessage message)
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Invoke(async () =>
|
||||
{
|
||||
if (!IsAttach) return;
|
||||
|
||||
var line = new EventViewModel(message.Data!);
|
||||
|
||||
await EventLogLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (EventLog.Count < 1000)
|
||||
{
|
||||
EventLog.Add(line);
|
||||
_events[line.Key] = message;
|
||||
}
|
||||
else
|
||||
{
|
||||
var _event = EventLog[0];
|
||||
EventLog.RemoveAt(0);
|
||||
_events.Remove(_event.Key);
|
||||
|
||||
EventLog.Add(line);
|
||||
_events[line.Key] = message;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EventLogLock.Release();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
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,14 +7,14 @@
|
||||
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
|
||||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:i="https://github.com/projektanker/icons.avalonia"
|
||||
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels.MainWindow"
|
||||
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"
|
||||
Title="Needlework.Net"
|
||||
Icon="/Assets/app.ico"
|
||||
Width="1280"
|
||||
Height="720">
|
||||
Height="720">
|
||||
<Grid RowDefinitions="auto,*">
|
||||
<Grid ColumnDefinitions="auto,auto,*,auto"
|
||||
Background="Transparent"
|
||||
@@ -37,6 +37,8 @@
|
||||
<ui:NavigationView AlwaysShowHeader="False"
|
||||
PaneDisplayMode="Left"
|
||||
IsSettingsVisible="False"
|
||||
IsPaneOpen="False"
|
||||
OpenPaneLength="200"
|
||||
Grid.Row="1"
|
||||
MenuItemsSource="{Binding MenuItems}"
|
||||
SelectedItem="{Binding SelectedMenuItem}">
|
||||
@@ -83,6 +85,7 @@
|
||||
<DataTemplate>
|
||||
<Border Margin="4">
|
||||
<ui:InfoBar
|
||||
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
|
||||
Title="{Binding Title}"
|
||||
IsOpen="{Binding IsOpen}"
|
||||
Severity="{Binding Severity}"
|
||||
@@ -1,10 +1,10 @@
|
||||
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();
|
||||
|
||||
64
Needlework.Net/Views/MainWindow/OopsiesDialog.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Needlework.Net.Services;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Needlework.Net.Views.MainWindow;
|
||||
|
||||
public class OopsiesDialog : IDialog, IDisposable
|
||||
{
|
||||
private bool _isDisposing;
|
||||
private string? _text;
|
||||
private ContentDialog _dialog;
|
||||
|
||||
public OopsiesDialog()
|
||||
{
|
||||
_dialog = new ContentDialog
|
||||
{
|
||||
PrimaryButtonText = "Open",
|
||||
CloseButtonText = "Cancel",
|
||||
Title = "Oopsies",
|
||||
Content = "This response is too large to handle for performance reasons.\nIt can be viewed in an external editor or viewer.",
|
||||
IsPrimaryButtonEnabled = true,
|
||||
IsSecondaryButtonEnabled = false,
|
||||
DefaultButton = ContentDialogButton.Primary
|
||||
};
|
||||
_dialog.PrimaryButtonClick += OnPrimaryButtonClick;
|
||||
}
|
||||
|
||||
public async Task<ContentDialogResult> ShowAsync(object data)
|
||||
{
|
||||
_text = (string)data;
|
||||
var result = await _dialog.ShowAsync();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
var temp = Path.GetTempFileName().Replace(".tmp", ".json");
|
||||
File.WriteAllText(temp, _text);
|
||||
Process.Start("explorer", "\"" + temp + "\"");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_isDisposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_text = null;
|
||||
_dialog.PrimaryButtonClick -= OnPrimaryButtonClick;
|
||||
}
|
||||
|
||||
_isDisposing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
167
Needlework.Net/Views/Pages/AboutView.axaml
Normal file
@@ -0,0 +1,167 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:Needlework.Net.ViewModels.Pages"
|
||||
xmlns:controls="using:Needlework.Net.Controls"
|
||||
xmlns:i="https://github.com/projektanker/icons.avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Needlework.Net.Views.Pages.AboutView"
|
||||
x:DataType="vm:AboutViewModel">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Theme" Value="{StaticResource TransparentButton}"/>
|
||||
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
|
||||
</Style>
|
||||
<Style Selector="i|Icon">
|
||||
<Setter Property="FontSize" Value="20" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<Grid Margin="8"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center">
|
||||
<StackPanel Spacing="8">
|
||||
<Grid HorizontalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<controls:Card Margin="8">
|
||||
<Image Source="/Assets/Users/blossomishymae.png"
|
||||
RenderOptions.BitmapInterpolationMode="MediumQuality"
|
||||
Width="200"
|
||||
Height="200"/>
|
||||
</controls:Card>
|
||||
<StackPanel Margin="8 0 0 0">
|
||||
<controls:Card Width="400" Margin="8">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
|
||||
Margin="0 0 8 0">Blossomi Shymae</TextBlock>
|
||||
<Button CommandParameter="https://github.com/BlossomiShymae">
|
||||
<i:Icon Value="fa-github"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
<controls:Card Width="400" Margin="8">
|
||||
<StackPanel >
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">About</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
Needlework.Net is the .NET rewrite of Needlework. This tool was made to help others with LCU development. Feel free to ask any questions
|
||||
or help contribute to the project! Made with love. 💜
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border Width="800">
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Thanks to the friends and people who made this tool possible...</TextBlock>
|
||||
</Border>
|
||||
<WrapPanel Orientation="Horizontal">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Margin="8">
|
||||
<controls:Card>
|
||||
<Image Source="/Assets/Users/dysolix.png"
|
||||
RenderOptions.BitmapInterpolationMode="MediumQuality"
|
||||
Width="100"
|
||||
Height="100"/>
|
||||
</controls:Card>
|
||||
<StackPanel Margin="2 0 0 0">
|
||||
<controls:Card Width="250" Margin="2">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
|
||||
Margin="0 0 8 0">dysolix</TextBlock>
|
||||
<Button CommandParameter="https://github.com/dysolix">
|
||||
<i:Icon Value="fa-github"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
<controls:Card Width="250" Margin="2">
|
||||
<StackPanel >
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
For providing and hosting an auto-generated OpenAPI document of the LCU.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Margin="8">
|
||||
<controls:Card>
|
||||
<Image Source="/Assets/Users/ray.png"
|
||||
RenderOptions.BitmapInterpolationMode="MediumQuality"
|
||||
Width="100"
|
||||
Height="100"/>
|
||||
</controls:Card>
|
||||
<StackPanel Margin="2 0 0 0">
|
||||
<controls:Card Width="250" Margin="2">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
|
||||
Margin="0 0 8 0">Ray</TextBlock>
|
||||
<Button CommandParameter="https://github.com/Hi-Ray">
|
||||
<i:Icon Value="fa-github"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
<controls:Card Width="250" Margin="2">
|
||||
<StackPanel >
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
For guidance, advice, or providing help via HextechDocs.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Margin="8">
|
||||
<controls:Card>
|
||||
<Image Source="/Assets/Users/dubble.png"
|
||||
RenderOptions.BitmapInterpolationMode="MediumQuality"
|
||||
Width="100"
|
||||
Height="100"/>
|
||||
</controls:Card>
|
||||
<StackPanel Margin="4 0 0 0">
|
||||
<controls:Card Width="250" Margin="2">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
|
||||
Margin="0 0 8 0">dubble</TextBlock>
|
||||
<Button CommandParameter="https://github.com/cuppachino">
|
||||
<i:Icon Value="fa-github"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
<controls:Card Width="250" Margin="2">
|
||||
<StackPanel >
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
For encouraging me to publish Needlework. This project may never have seen the light of day without him.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Margin="8">
|
||||
<controls:Card>
|
||||
<Image Source="/Assets/Users/community.png"
|
||||
RenderOptions.BitmapInterpolationMode="MediumQuality"
|
||||
Width="100"
|
||||
Height="100"/>
|
||||
</controls:Card>
|
||||
<StackPanel Margin="4 0 0 0">
|
||||
<controls:Card Width="250" Margin="2">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
|
||||
Width="250"
|
||||
TextWrapping="Wrap">Third Party Developer Community</TextBlock>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
<controls:Card Width="250" Margin="2">
|
||||
<StackPanel >
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
For providing numerous documentation on the LCU.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,6 +1,6 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Needlework.Net.Desktop.Views;
|
||||
namespace Needlework.Net.Views.Pages;
|
||||
|
||||
public partial class AboutView : UserControl
|
||||
{
|
||||