Compare commits
49 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 | ||
|
|
04058f12c1 | ||
|
|
3a7d39971a | ||
|
|
b0b5476c48 | ||
|
|
b3158a81b8 | ||
|
|
83400bceed | ||
|
|
1133f2d785 | ||
|
|
14dde760b0 |
40
.github/workflows/release.yml
vendored
@@ -1,9 +1,12 @@
|
|||||||
# .github/workflows/release.yml
|
# .github/workflows/release.yml
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
release:
|
||||||
branches:
|
types: [created]
|
||||||
- release
|
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: Needlework.Net
|
||||||
|
ASSEMBLY_NAME: NeedleworkDotNet
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -15,30 +18,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
dotnet-version: 8.0.x
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: release
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build Needlework.Net.Desktop -c Release
|
run: dotnet build ${{env.PROJECT_NAME}} -c Release
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: dotnet publish Needlework.Net.Desktop -c Release -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -o publish -r win-x64 --self-contained=false
|
run: dotnet publish ${{env.PROJECT_NAME}} -c Release -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -o publish -r win-x64 --self-contained=false
|
||||||
- name: Get Version
|
- name: Zip files
|
||||||
id: version
|
run: 7z a -tzip ${{env.ASSEMBLY_NAME}}-win-x64.zip ./Publish/* README.md LICENSE
|
||||||
shell: powershell
|
- name: Upload to release
|
||||||
run: |
|
|
||||||
$xml=[xml](Get-Content .\Needlework.Net.Desktop\Needlework.Net.Desktop.csproj)
|
|
||||||
$ver=($xml.Project.PropertyGroup).AssemblyVersion
|
|
||||||
$ver="VERSION=$ver"
|
|
||||||
$ver=$ver -replace '\s',''
|
|
||||||
echo $ver >> $env:GITHUB_OUTPUT
|
|
||||||
- name: Zip Files
|
|
||||||
run: 7z a -tzip NeedleworkDotNet-win-x64.zip ./Publish/* README.md LICENSE
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: ${{startsWith(github.ref, 'refs/tags/') }}
|
||||||
with:
|
with:
|
||||||
name: "Needlework.Net v${{ steps.version.outputs.VERSION }}"
|
files: ${{env.ASSEMBLY_NAME}}-win-x64.zip
|
||||||
prerelease: false
|
|
||||||
tag_name: "${{ steps.version.outputs.VERSION }}"
|
|
||||||
files: |
|
|
||||||
NeedleworkDotNet-win-x64.zip
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using Xunit.Abstractions;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Core.Tests;
|
|
||||||
|
|
||||||
public class LcuSchemaHandlerTest
|
|
||||||
{
|
|
||||||
private readonly ITestOutputHelper _output;
|
|
||||||
|
|
||||||
internal HttpClient HttpClient { get; } = new();
|
|
||||||
|
|
||||||
public LcuSchemaHandlerTest(ITestOutputHelper output)
|
|
||||||
{
|
|
||||||
_output = output;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PluginsTestAsync()
|
|
||||||
{
|
|
||||||
var reader = new LcuSchemaHandler(await Resources.GetOpenApiDocumentAsync(HttpClient));
|
|
||||||
|
|
||||||
var plugins = reader.Plugins.Keys.ToList();
|
|
||||||
foreach (var plugin in plugins)
|
|
||||||
_output.WriteLine($"Plugin: {plugin}");
|
|
||||||
|
|
||||||
Assert.True(plugins.Count > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<IsTestProject>true</IsTestProject>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
|
||||||
<PackageReference Include="xunit" Version="2.5.3" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="Xunit" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Needlework.Net.Core\Needlework.Net.Core.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using Xunit.Abstractions;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Core.Tests;
|
|
||||||
|
|
||||||
public class ResourcesTest
|
|
||||||
{
|
|
||||||
private readonly ITestOutputHelper _output;
|
|
||||||
|
|
||||||
internal HttpClient HttpClient { get; } = new();
|
|
||||||
|
|
||||||
public ResourcesTest(ITestOutputHelper output)
|
|
||||||
{
|
|
||||||
_output = output;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task DocumentTestAsync()
|
|
||||||
{
|
|
||||||
var document = await Resources.GetOpenApiDocumentAsync(HttpClient);
|
|
||||||
|
|
||||||
Assert.True(document.Info.Title == "LCU SCHEMA");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.OpenApi" Version="1.6.16" />
|
|
||||||
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.16" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
|
||||||
{
|
|
||||||
public class ContentRequestMessage : RequestMessage<string>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
using Needlework.Net.Core;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
|
||||||
{
|
|
||||||
public class DataReadyMessage(LcuSchemaHandler handler) : ValueChangedMessage<LcuSchemaHandler>(handler)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
using Needlework.Net.Core;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
|
||||||
{
|
|
||||||
public class DataRequestMessage : RequestMessage<LcuSchemaHandler>
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
|
||||||
{
|
|
||||||
public class EditorUpdateMessage(EditorUpdate editorUpdate) : ValueChangedMessage<EditorUpdate>(editorUpdate)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EditorUpdate
|
|
||||||
{
|
|
||||||
public string Text { get; }
|
|
||||||
public string Key { get; }
|
|
||||||
|
|
||||||
public EditorUpdate(string text, string key)
|
|
||||||
{
|
|
||||||
Text = text;
|
|
||||||
Key = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
|
||||||
{
|
|
||||||
public class OopsiesWindowCanceledMessage(object? data) : ValueChangedMessage<object?>(data)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
|
||||||
{
|
|
||||||
public class OopsiesWindowRequestedMessage(string text) : ValueChangedMessage<string>(text)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using Needlework.Net.Desktop.ViewModels;
|
|
||||||
using Needlework.Net.Desktop.Views;
|
|
||||||
using SukiUI.Controls;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Services
|
|
||||||
{
|
|
||||||
public class WindowService : IRecipient<OopsiesWindowCanceledMessage>
|
|
||||||
{
|
|
||||||
public IServiceProvider ServiceProvider { get; }
|
|
||||||
|
|
||||||
public Dictionary<string, SukiWindow> EndpointWindows { get; } = []; // Workaround memory leak by storing and reusing windows.
|
|
||||||
// Figure out why creating and closing windows leaks memory.
|
|
||||||
|
|
||||||
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", Material.Icons.MaterialIconKind.InfoCircle)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +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 SukiUI.Controls;
|
|
||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
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", Material.Icons.MaterialIconKind.Console, -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(Regex.Replace(requestBody, @"\s+", ""), 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)
|
|
||||||
{
|
|
||||||
await SukiHost.ShowToast("Request Failed", ex.Message, SukiUI.Enums.NotificationType.Error);
|
|
||||||
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,42 +0,0 @@
|
|||||||
using Avalonia.Collections;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using SukiUI.Controls;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.ViewModels
|
|
||||||
{
|
|
||||||
public partial class EndpointViewModel : ObservableObject, ISukiStackPageTitleProvider
|
|
||||||
{
|
|
||||||
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())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using SukiUI.Controls;
|
|
||||||
using System.Net.Http;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.ViewModels
|
|
||||||
{
|
|
||||||
public partial class EndpointsContainerViewModel : PageBase
|
|
||||||
{
|
|
||||||
[ObservableProperty] private ISukiStackPageTitleProvider _activeViewModel;
|
|
||||||
|
|
||||||
public EndpointsContainerViewModel(HttpClient httpClient) : base("Endpoints", Material.Icons.MaterialIconKind.Hub, -500)
|
|
||||||
{
|
|
||||||
_activeViewModel = new EndpointsViewModel(httpClient, OnClicked);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnClicked(ISukiStackPageTitleProvider viewModel)
|
|
||||||
{
|
|
||||||
ActiveViewModel = viewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
using Avalonia.Collections;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using SukiUI.Controls;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.ViewModels
|
|
||||||
{
|
|
||||||
public partial class EndpointsViewModel : ObservableObject, IRecipient<DataReadyMessage>, ISukiStackPageTitleProvider
|
|
||||||
{
|
|
||||||
public HttpClient HttpClient { get; }
|
|
||||||
|
|
||||||
public string Title => "Endpoints";
|
|
||||||
public Action<ISukiStackPageTitleProvider> 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<ISukiStackPageTitleProvider> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnSelectedQueryChanged(string? value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value)) return;
|
|
||||||
|
|
||||||
OnClicked.Invoke(new EndpointViewModel(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
using Avalonia.Media;
|
|
||||||
using BlossomiShymae.GrrrLCU;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.ViewModels
|
|
||||||
{
|
|
||||||
public partial class HomeViewModel : PageBase
|
|
||||||
{
|
|
||||||
[ObservableProperty] private string _statusText = string.Empty;
|
|
||||||
[ObservableProperty] private IBrush? _statusForeground;
|
|
||||||
[ObservableProperty] private string _statusAddress = string.Empty;
|
|
||||||
|
|
||||||
public HomeViewModel() : base("Home", Material.Icons.MaterialIconKind.Home, int.MinValue)
|
|
||||||
{
|
|
||||||
var thread = new Thread(StartProcessing) { IsBackground = true };
|
|
||||||
thread.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartProcessing()
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
void Set(string text, Color color, string address)
|
|
||||||
{
|
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Invoke(() =>
|
|
||||||
{
|
|
||||||
StatusText = text;
|
|
||||||
StatusForeground = new SolidColorBrush(color.ToUInt32());
|
|
||||||
StatusAddress = address;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var processInfo = Connector.GetProcessInfo();
|
|
||||||
Set("Online", Colors.Green, $"https://127.0.0.1:{processInfo.AppPort}/");
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
Set("Offline", Colors.Red, "N/A");
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.Sleep(TimeSpan.FromSeconds(5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void OpenUrl(string url)
|
|
||||||
{
|
|
||||||
var process = new Process()
|
|
||||||
{
|
|
||||||
StartInfo = new ProcessStartInfo(url)
|
|
||||||
{
|
|
||||||
UseShellExecute = true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
process.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
using Avalonia.Collections;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using Needlework.Net.Core;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using Needlework.Net.Desktop.Services;
|
|
||||||
using SukiUI.Controls;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
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>
|
|
||||||
{
|
|
||||||
public IAvaloniaReadOnlyList<PageBase> Pages { get; }
|
|
||||||
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
|
|
||||||
|
|
||||||
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 bool _isUpdateShown = false;
|
|
||||||
|
|
||||||
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, WindowService windowService)
|
|
||||||
{
|
|
||||||
Pages = new AvaloniaList<PageBase>(pages.OrderBy(x => x.Index).ThenBy(x => x.DisplayName));
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(async () =>
|
|
||||||
{
|
|
||||||
await SukiHost.ShowToast("Needlework.Net Update", $"There is a new version available: {release.TagName}.", SukiUI.Enums.NotificationType.Info, TimeSpan.FromSeconds(10), () => OpenUrl("https://github.com/BlossomiShymae/Needlework.Net/releases"));
|
|
||||||
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));
|
|
||||||
await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(async () => await SukiHost.ShowToast("OpenAPI Data Processed", "Some pages can now be used.", SukiUI.Enums.NotificationType.Success, TimeSpan.FromSeconds(5)));
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,153 +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;
|
|
||||||
|
|
||||||
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 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? GetRequestBodyType(OpenApiRequestBody? requestBody)
|
|
||||||
{
|
|
||||||
if (requestBody == null) return null;
|
|
||||||
if (requestBody.Content.TryGetValue("application/json", out var media))
|
|
||||||
{
|
|
||||||
var schema = media.Schema;
|
|
||||||
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);
|
|
||||||
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,13 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using Material.Icons;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.ViewModels
|
|
||||||
{
|
|
||||||
|
|
||||||
public abstract partial class PageBase(string displayName, MaterialIconKind icon, int index = 0) : ObservableValidator
|
|
||||||
{
|
|
||||||
[ObservableProperty] private string _displayName = displayName;
|
|
||||||
[ObservableProperty] private MaterialIconKind _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,137 +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 SukiUI.Controls;
|
|
||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
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(Regex.Replace(requestBody, @"\s+", ""), 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)
|
|
||||||
{
|
|
||||||
await SukiHost.ShowToast("Request Failed", ex.Message, SukiUI.Enums.NotificationType.Error);
|
|
||||||
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,125 +0,0 @@
|
|||||||
using BlossomiShymae.GrrrLCU;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Material.Icons;
|
|
||||||
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", MaterialIconKind.Connection, -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,41 +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:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
|
|
||||||
xmlns:theme="clr-namespace:SukiUI.Theme;assembly=SukiUI"
|
|
||||||
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
|
|
||||||
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
|
|
||||||
theme:WrapPanelExtensions.AnimatedScroll="true"
|
|
||||||
Orientation="Horizontal">
|
|
||||||
<suki:GlassCard Margin="8">
|
|
||||||
<Image Source="/Assets/about.png"
|
|
||||||
RenderOptions.BitmapInterpolationMode="MediumQuality"
|
|
||||||
Width="200"
|
|
||||||
Height="200"/>
|
|
||||||
</suki:GlassCard>
|
|
||||||
<StackPanel>
|
|
||||||
<suki:GlassCard Width="400" Margin="8">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Classes="h3">Blossomi Shymae</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</suki:GlassCard>
|
|
||||||
<suki:GlassCard Width="400" Margin="8">
|
|
||||||
<suki:GroupBox Header="About">
|
|
||||||
<TextBlock TextWrapping="Wrap">
|
|
||||||
Needlework.Net is .NET rewrite of Needlework. Like Needlework, this project is inspired by
|
|
||||||
LCU Explorer. This tool was made to help others with LCU development. Feel free to ask any questions
|
|
||||||
or help contribute to the project! 💜
|
|
||||||
</TextBlock>
|
|
||||||
</suki:GroupBox>
|
|
||||||
</suki:GlassCard>
|
|
||||||
</StackPanel>
|
|
||||||
</WrapPanel>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,86 +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:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
|
|
||||||
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
|
|
||||||
xmlns:theme="clr-namespace:SukiUI.Theme;assembly=SukiUI"
|
|
||||||
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="Needlework.Net.Desktop.Views.ConsoleView"
|
|
||||||
x:DataType="vm:ConsoleViewModel">
|
|
||||||
<suki:BusyArea IsBusy="{Binding IsBusy}" BusyText="Loading...">
|
|
||||||
<Grid Margin="16" RowDefinitions="auto,*" ColumnDefinitions="*,*">
|
|
||||||
<Grid Grid.Row="0"
|
|
||||||
Grid.Column="0"
|
|
||||||
Grid.ColumnSpan="2">
|
|
||||||
<suki:GlassCard Margin="0 0 0 16">
|
|
||||||
<suki:GroupBox Header="Console">
|
|
||||||
<Grid RowDefinitions="auto,auto" ColumnDefinitions="auto,*">
|
|
||||||
<ComboBox ItemsSource="{Binding RequestMethods}" SelectedItem="{Binding RequestMethodSelected}"
|
|
||||||
Grid.Row="0" Grid.Column="0"/>
|
|
||||||
<AutoCompleteBox
|
|
||||||
ItemsSource="{Binding RequestPaths}"
|
|
||||||
Text="{Binding RequestPath}"
|
|
||||||
MaxDropDownHeight="400"
|
|
||||||
FilterMode="StartsWith"
|
|
||||||
Grid.Row="0" Grid.Column="1"/>
|
|
||||||
<avaloniaEdit:TextEditor
|
|
||||||
Name="RequestEditor"
|
|
||||||
Text=""
|
|
||||||
ShowLineNumbers="True"
|
|
||||||
HorizontalScrollBarVisibility="Auto"
|
|
||||||
VerticalScrollBarVisibility="Visible"
|
|
||||||
FontSize="12"
|
|
||||||
Height="100"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
Grid.ColumnSpan="2"/>
|
|
||||||
</Grid>
|
|
||||||
</suki:GroupBox>
|
|
||||||
</suki:GlassCard>
|
|
||||||
<Button Classes="Flat Rounded"
|
|
||||||
Margin="0 0 0 0"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Bottom"
|
|
||||||
FontWeight="DemiBold"
|
|
||||||
Command="{Binding SendRequestCommand}"
|
|
||||||
theme:ButtonExtensions.ShowProgress="{Binding IsRequestBusy}">
|
|
||||||
Send
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
<StackPanel
|
|
||||||
Margin="0 0 8 0"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0">
|
|
||||||
<suki:GlassCard Margin="0 4">
|
|
||||||
<suki:GroupBox Header="Path">
|
|
||||||
<TextBlock Text="{Binding ResponsePath}"/>
|
|
||||||
</suki:GroupBox>
|
|
||||||
</suki:GlassCard>
|
|
||||||
<suki:GlassCard Margin="0 4">
|
|
||||||
<suki:GroupBox Header="Status">
|
|
||||||
<TextBlock Text="{Binding ResponseStatus}"/>
|
|
||||||
</suki:GroupBox>
|
|
||||||
</suki:GlassCard>
|
|
||||||
<suki:GlassCard Margin="0 4">
|
|
||||||
<suki:GroupBox Header="Authorization">
|
|
||||||
<TextBlock Text="{Binding ResponseAuthorization}" />
|
|
||||||
</suki:GroupBox>
|
|
||||||
</suki:GlassCard>
|
|
||||||
</StackPanel>
|
|
||||||
<suki:GlassCard
|
|
||||||
Margin="0 8"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="1">
|
|
||||||
<avaloniaEdit:TextEditor
|
|
||||||
Name="ResponseEditor"
|
|
||||||
HorizontalScrollBarVisibility="Auto"
|
|
||||||
VerticalScrollBarVisibility="Visible"
|
|
||||||
ShowLineNumbers="True"
|
|
||||||
Text=""
|
|
||||||
FontSize="12"/>
|
|
||||||
</suki:GlassCard>
|
|
||||||
</Grid>
|
|
||||||
</suki:BusyArea>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Controls.Primitives;
|
|
||||||
using Avalonia.Styling;
|
|
||||||
using AvaloniaEdit;
|
|
||||||
using AvaloniaEdit.TextMate;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Needlework.Net.Desktop.Extensions;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using Needlework.Net.Desktop.ViewModels;
|
|
||||||
using SukiUI;
|
|
||||||
using TextMateSharp.Grammars;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Views;
|
|
||||||
|
|
||||||
public partial class ConsoleView : UserControl, IRecipient<ResponseUpdatedMessage>, IRecipient<ContentRequestMessage>
|
|
||||||
{
|
|
||||||
private TextEditor? _responseEditor;
|
|
||||||
private TextEditor? _requestEditor;
|
|
||||||
|
|
||||||
public ConsoleView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Receive(ResponseUpdatedMessage message)
|
|
||||||
{
|
|
||||||
_responseEditor!.Text = message.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Receive(ContentRequestMessage message)
|
|
||||||
{
|
|
||||||
message.Reply(_requestEditor!.Text);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnApplyTemplate(e);
|
|
||||||
|
|
||||||
_responseEditor = this.FindControl<TextEditor>("ResponseEditor");
|
|
||||||
_requestEditor = this.FindControl<TextEditor>("RequestEditor");
|
|
||||||
_responseEditor?.ApplyJsonEditorSettings();
|
|
||||||
_requestEditor?.ApplyJsonEditorSettings();
|
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Register<ResponseUpdatedMessage, string>(this, nameof(ConsoleViewModel));
|
|
||||||
WeakReferenceMessenger.Default.Register<ContentRequestMessage, string>(this, "ConsoleRequestEditor");
|
|
||||||
|
|
||||||
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
|
|
||||||
SukiTheme.GetInstance().OnBaseThemeChanged += OnBaseThemeChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnDetachedFromVisualTree(e);
|
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
|
||||||
SukiTheme.GetInstance().OnBaseThemeChanged -= OnBaseThemeChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBaseThemeChanged(ThemeVariant currentTheme)
|
|
||||||
{
|
|
||||||
var registryOptions = new RegistryOptions(
|
|
||||||
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
|
|
||||||
|
|
||||||
var responseTmi = _responseEditor.InstallTextMate(registryOptions);
|
|
||||||
responseTmi.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions
|
|
||||||
.GetLanguageByExtension(".json").Id));
|
|
||||||
var requestTmi = _requestEditor.InstallTextMate(registryOptions);
|
|
||||||
requestTmi.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions
|
|
||||||
.GetLanguageByExtension(".json").Id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Controls.Primitives;
|
|
||||||
using Avalonia.Styling;
|
|
||||||
using AvaloniaEdit;
|
|
||||||
using AvaloniaEdit.TextMate;
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
|
||||||
using Needlework.Net.Desktop.Extensions;
|
|
||||||
using Needlework.Net.Desktop.Messages;
|
|
||||||
using Needlework.Net.Desktop.ViewModels;
|
|
||||||
using SukiUI;
|
|
||||||
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);
|
|
||||||
SukiTheme.GetInstance().OnBaseThemeChanged += OnBaseThemeChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnDetachedFromVisualTree(e);
|
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
|
||||||
SukiTheme.GetInstance().OnBaseThemeChanged -= OnBaseThemeChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBaseThemeChanged(ThemeVariant currentTheme)
|
|
||||||
{
|
|
||||||
var registryOptions = new RegistryOptions(
|
|
||||||
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
|
|
||||||
|
|
||||||
var requestTmi = _requestEditor.InstallTextMate(registryOptions);
|
|
||||||
requestTmi.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions
|
|
||||||
.GetLanguageByExtension(".json").Id));
|
|
||||||
var responseTmi = _requestEditor.InstallTextMate(registryOptions);
|
|
||||||
responseTmi.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions
|
|
||||||
.GetLanguageByExtension(".json").Id));
|
|
||||||
}
|
|
||||||
|
|
||||||
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,14 +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:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
|
|
||||||
xmlns:theme="clr-namespace:SukiUI.Theme;assembly=SukiUI"
|
|
||||||
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
|
|
||||||
xmlns:avalonEdit="https://github.com/avaloniaui/avaloniaedit"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="Needlework.Net.Desktop.Views.EndpointsContainerView"
|
|
||||||
x:DataType="vm:EndpointsContainerViewModel">
|
|
||||||
<suki:SukiStackPage Content="{Binding ActiveViewModel}"
|
|
||||||
Margin="-24 -4 0 0"/>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Views;
|
|
||||||
|
|
||||||
public partial class EndpointsContainerView : UserControl
|
|
||||||
{
|
|
||||||
public EndpointsContainerView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +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:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
|
|
||||||
xmlns:theme="clr-namespace:SukiUI.Theme;assembly=SukiUI"
|
|
||||||
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="Needlework.Net.Desktop.Views.EndpointsView"
|
|
||||||
x:DataType="vm:EndpointsViewModel">
|
|
||||||
<suki:BusyArea IsBusy="{Binding IsBusy}" BusyText="Loading...">
|
|
||||||
<Grid Margin="16" RowDefinitions="auto,auto,*" ColumnDefinitions="*">
|
|
||||||
<TextBox Watermark="Search" Margin="0 4" Text="{Binding Search}" Grid.Row="1" Grid.Column="0"/>
|
|
||||||
<ScrollViewer Grid.Row="2" Grid.Column="0">
|
|
||||||
<ListBox ItemsSource="{Binding Query}" SelectedItem="{Binding SelectedQuery}">
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<TextBlock Text="{Binding}" Foreground="White" />
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
|
||||||
</suki:BusyArea>
|
|
||||||
</UserControl>
|
|
||||||
@@ -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,77 +0,0 @@
|
|||||||
<suki:SukiWindow
|
|
||||||
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:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
|
|
||||||
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"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="Needlework.Net.Desktop.Views.MainWindow"
|
|
||||||
x:DataType="vm:MainWindowViewModel"
|
|
||||||
Title="Needlework.Net"
|
|
||||||
Icon="/Assets/app.ico"
|
|
||||||
Width="1280"
|
|
||||||
Height="720">
|
|
||||||
<suki:SukiWindow.LogoContent>
|
|
||||||
<Image Source="/Assets/app.png"
|
|
||||||
Width="20"
|
|
||||||
Height="20"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
</suki:SukiWindow.LogoContent>
|
|
||||||
<!-- TOP LEVEL -->
|
|
||||||
<suki:SukiSideMenu ItemsSource="{Binding Pages}">
|
|
||||||
<!-- ITEMS -->
|
|
||||||
<suki:SukiSideMenu.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<suki:SukiSideMenuItem Header="{Binding DisplayName}">
|
|
||||||
<suki:SukiSideMenuItem.Icon>
|
|
||||||
<materialIcons:MaterialIcon Kind="{Binding Icon}" />
|
|
||||||
</suki:SukiSideMenuItem.Icon>
|
|
||||||
</suki:SukiSideMenuItem>
|
|
||||||
</DataTemplate>
|
|
||||||
</suki:SukiSideMenu.ItemTemplate>
|
|
||||||
<!-- FOOTER -->
|
|
||||||
<suki:SukiSideMenu.FooterContent>
|
|
||||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
|
||||||
<StackPanel.Styles>
|
|
||||||
<Style Selector="Button.Basic">
|
|
||||||
<Setter Property="Command" Value="{Binding OpenUrlCommand}" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="materialIcons|MaterialIcon">
|
|
||||||
<Setter Property="Width" Value="25" />
|
|
||||||
<Setter Property="Height" Value="25" />
|
|
||||||
</Style>
|
|
||||||
<Style Selector="i|Icon">
|
|
||||||
<Setter Property="FontSize" Value="25" />
|
|
||||||
</Style>
|
|
||||||
</StackPanel.Styles>
|
|
||||||
<Button Classes="Flat"
|
|
||||||
Content="{Binding Version}"
|
|
||||||
FontSize="12"
|
|
||||||
Margin="0 0 4 0"
|
|
||||||
Padding="12 4 12 4"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
<Button Classes="Basic"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
CommandParameter="https://github.com/BlossomiShymae/Needlework.Net"
|
|
||||||
ToolTip.Tip="Open on GitHub."
|
|
||||||
Margin="0 0 4 0">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<materialIcons:MaterialIcon Kind="Github" Margin="0 0 4 0" />
|
|
||||||
<TextBlock FontSize="12"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Foreground="White">Star</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Classes="Basic"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
CommandParameter="https://discord.gg/chEvEX5J4E"
|
|
||||||
ToolTip.Tip="Open Discord server.">
|
|
||||||
<i:Icon Value="fa-brand fa-discord" />
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
</suki:SukiSideMenu.FooterContent>
|
|
||||||
</suki:SukiSideMenu>
|
|
||||||
</suki:SukiWindow>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using SukiUI.Controls;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Views;
|
|
||||||
|
|
||||||
public partial class MainWindow : SukiWindow
|
|
||||||
{
|
|
||||||
public MainWindow()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<suki:SukiWindow
|
|
||||||
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:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
|
|
||||||
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,auto,auto" ColumnDefinitions="auto,auto"
|
|
||||||
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}"
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.Column="0"
|
|
||||||
Margin="0 0 8 0">
|
|
||||||
Open
|
|
||||||
</Button>
|
|
||||||
<Button Command="{Binding CloseDialogCommand}"
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.Column="1"
|
|
||||||
Margin="8 0 0 0">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</suki:SukiWindow>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using SukiUI.Controls;
|
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Views;
|
|
||||||
|
|
||||||
public partial class OopsiesWindow : SukiWindow
|
|
||||||
{
|
|
||||||
public OopsiesWindow()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +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:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
|
|
||||||
xmlns:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
|
|
||||||
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
|
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
|
||||||
x:Class="Needlework.Net.Desktop.Views.WebsocketView"
|
|
||||||
x:DataType="vm:WebsocketViewModel">
|
|
||||||
<Grid RowDefinitions="*,2,*" Margin="16">
|
|
||||||
<Border Grid.Row="0"
|
|
||||||
Padding="0 0 0 8">
|
|
||||||
<suki:GlassCard>
|
|
||||||
<Grid RowDefinitions="auto,*" ColumnDefinitions="*">
|
|
||||||
<Grid
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="0"
|
|
||||||
RowDefinitions="*"
|
|
||||||
ColumnDefinitions="auto,*,auto,auto">
|
|
||||||
<Button Grid.Row="0"
|
|
||||||
Grid.Column="0"
|
|
||||||
Command="{Binding ClearCommand}"
|
|
||||||
Margin="0 0 8 0">Clear</Button>
|
|
||||||
<TextBox Grid.Row="0"
|
|
||||||
Grid.Column="1"
|
|
||||||
Margin="0 0 8 0"
|
|
||||||
Text="{Binding Search, Mode=TwoWay}"/>
|
|
||||||
<StackPanel Orientation="Horizontal"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="2"
|
|
||||||
Margin="0 0 8 0">
|
|
||||||
<ToggleSwitch Margin="0 0 0 8"
|
|
||||||
IsChecked="{Binding IsAttach}"/>
|
|
||||||
<TextBlock Margin="0 6 0 0"
|
|
||||||
FontSize="18">Attach</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="3">
|
|
||||||
<ToggleSwitch Margin="0 0 0 8"
|
|
||||||
IsChecked="{Binding IsTail}"/>
|
|
||||||
<TextBlock Margin="0 6 0 0"
|
|
||||||
FontSize="18">Tail</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
<ListBox Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
Name="EventViewer"
|
|
||||||
ItemsSource="{Binding FilteredEventLog}"
|
|
||||||
SelectedItem="{Binding SelectedEventLog}"/>
|
|
||||||
</Grid>
|
|
||||||
</suki:GlassCard>
|
|
||||||
</Border>
|
|
||||||
<GridSplitter Grid.Row="1" ResizeDirection="Rows" Background="Gray"/>
|
|
||||||
<Border Grid.Row="2"
|
|
||||||
Padding="0 8 0 0">
|
|
||||||
<suki:GlassCard>
|
|
||||||
<avaloniaEdit:TextEditor
|
|
||||||
Name="ResponseEditor"
|
|
||||||
ShowLineNumbers="True"
|
|
||||||
HorizontalScrollBarVisibility="Auto"
|
|
||||||
VerticalScrollBarVisibility="Visible"
|
|
||||||
Text=""
|
|
||||||
FontSize="12"/>
|
|
||||||
</suki:GlassCard>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@@ -3,11 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.0.31903.59
|
VisualStudioVersion = 17.0.31903.59
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Core", "Needlework.Net.Core\Needlework.Net.Core.csproj", "{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net", "Needlework.Net\Needlework.Net.csproj", "{7388B579-2DC0-46D6-957A-6683D0FCF5D3}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Desktop", "Needlework.Net.Desktop\Needlework.Net.Desktop.csproj", "{7388B579-2DC0-46D6-957A-6683D0FCF5D3}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Core.Tests", "Needlework.Net.Core.Tests\Needlework.Net.Core.Tests.csproj", "{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -18,17 +14,9 @@ Global
|
|||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<Application xmlns="https://github.com/avaloniaui"
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
x:Class="Needlework.Net.Desktop.App"
|
x:Class="Needlework.Net.App"
|
||||||
RequestedThemeVariant="Dark"
|
xmlns:local="using:Needlework.Net"
|
||||||
xmlns:local="using:Needlework.Net.Desktop"
|
xmlns:converters="using:Needlework.Net.Converters"
|
||||||
xmlns:converters="using:Needlework.Net.Desktop.Converters"
|
xmlns:sty="using:FluentAvalonia.Styling"
|
||||||
xmlns:sukiUi="clr-namespace:SukiUI;assembly=SukiUI"
|
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia">
|
RequestedThemeVariant="Dark">
|
||||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||||
<Application.DataTemplates>
|
<Application.DataTemplates>
|
||||||
<local:ViewLocator/>
|
<local:ViewLocator/>
|
||||||
</Application.DataTemplates>
|
</Application.DataTemplates>
|
||||||
|
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<FluentTheme></FluentTheme>
|
<sty:FluentAvaloniaTheme PreferSystemTheme="False" PreferUserAccentColor="False" />
|
||||||
<sukiUi:SukiTheme ThemeColor="Blue" />
|
|
||||||
<materialIcons:MaterialIconStyles />
|
<materialIcons:MaterialIconStyles />
|
||||||
|
<StyleInclude Source="Controls/Card.axaml"/>
|
||||||
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
|
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
|
||||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
@@ -3,12 +3,12 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Needlework.Net.Desktop.ViewModels;
|
using Needlework.Net.ViewModels.MainWindow;
|
||||||
using Needlework.Net.Desktop.Views;
|
using Needlework.Net.Views.MainWindow;
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop;
|
namespace Needlework.Net;
|
||||||
|
|
||||||
public partial class App(IServiceProvider serviceProvider) : Application
|
public partial class App(IServiceProvider serviceProvider) : Application
|
||||||
{
|
{
|
||||||
@@ -33,7 +33,7 @@ public partial class App(IServiceProvider serviceProvider) : Application
|
|||||||
{
|
{
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
desktop.MainWindow = new MainWindow()
|
desktop.MainWindow = new MainWindowView()
|
||||||
{
|
{
|
||||||
DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
|
DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
|
||||||
};
|
};
|
||||||
BIN
Needlework.Net/Assets/Icons/home.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Needlework.Net/Assets/Icons/info-circle.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Needlework.Net/Assets/Icons/list-alt.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
Needlework.Net/Assets/Icons/plug.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Needlework.Net/Assets/Icons/terminal.png
Normal file
|
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 |
66
Needlework.Net/Controls/BusyArea.axaml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<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:controls="using:Needlework.Net.Controls"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Needlework.Net.Controls.BusyArea">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="controls|BusyArea">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<Panel>
|
||||||
|
<ContentControl Content="{TemplateBinding Content}"/>
|
||||||
|
<DockPanel Name="LoadingBusyArea"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
LastChildFill="True">
|
||||||
|
<TextBlock Margin="16"
|
||||||
|
DockPanel.Dock="Bottom"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
FontWeight="DemiBold"
|
||||||
|
Text="{TemplateBinding BusyText}"/>
|
||||||
|
<ProgressBar
|
||||||
|
Width="100"
|
||||||
|
IsIndeterminate="True"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</DockPanel>
|
||||||
|
</Panel>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="controls|BusyArea DockPanel#LoadingBusyArea">
|
||||||
|
<Setter Property="Transitions">
|
||||||
|
<Transitions>
|
||||||
|
<DoubleTransition Property="Opacity" Duration="0:0:0.3" />
|
||||||
|
</Transitions>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="controls|BusyArea[IsBusy=True] DockPanel#LoadingBusyArea">
|
||||||
|
<Setter Property="Opacity" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="controls|BusyArea[IsBusy=False] DockPanel#LoadingBusyArea">
|
||||||
|
<Setter Property="Opacity" Value="0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="controls|BusyArea ContentControl">
|
||||||
|
<Setter Property="Transitions">
|
||||||
|
<Transitions>
|
||||||
|
<DoubleTransition Property="Opacity" Duration="0:0:0.3"/>
|
||||||
|
</Transitions>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="controls|BusyArea[IsBusy=True] ContentControl">
|
||||||
|
<Setter Property="Opacity" Value="0.1"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="controls|BusyArea[IsBusy=False] ContentControl">
|
||||||
|
<Setter Property="Opacity" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
</UserControl>
|
||||||
30
Needlework.Net/Controls/BusyArea.axaml.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Controls;
|
||||||
|
|
||||||
|
public partial class BusyArea : UserControl
|
||||||
|
{
|
||||||
|
public BusyArea()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> IsBusyProperty =
|
||||||
|
AvaloniaProperty.Register<BusyArea, bool>(nameof(IsBusy), defaultValue: false);
|
||||||
|
|
||||||
|
public bool IsBusy
|
||||||
|
{
|
||||||
|
get { return GetValue(IsBusyProperty); }
|
||||||
|
set { SetValue(IsBusyProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<string?> BusyTextProperty =
|
||||||
|
AvaloniaProperty.Register<BusyArea, string?>(nameof(BusyText), defaultValue: null);
|
||||||
|
|
||||||
|
public string? BusyText
|
||||||
|
{
|
||||||
|
get => GetValue(BusyTextProperty);
|
||||||
|
set => SetValue(BusyTextProperty, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Needlework.Net/Controls/Card.axaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:controls="using:Needlework.Net.Controls">
|
||||||
|
<Design.PreviewWith>
|
||||||
|
<controls:Card />
|
||||||
|
</Design.PreviewWith>
|
||||||
|
|
||||||
|
<Style Selector="controls|Card">
|
||||||
|
<!-- Set Defaults -->
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<Border Padding="16"
|
||||||
|
CornerRadius="16,16,16,16"
|
||||||
|
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}">
|
||||||
|
<ContentPresenter Content="{TemplateBinding Content}"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
10
Needlework.Net/Controls/Card.axaml.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Controls;
|
||||||
|
|
||||||
|
public class Card : ContentControl
|
||||||
|
{
|
||||||
|
public Card()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Converters
|
namespace Needlework.Net.Converters
|
||||||
{
|
{
|
||||||
public class EnumerableBoolConverter : IValueConverter
|
public class EnumerableBoolConverter : IValueConverter
|
||||||
{
|
{
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Converters
|
namespace Needlework.Net.Converters
|
||||||
{
|
{
|
||||||
public class NullBoolConverter : IValueConverter
|
public class NullBoolConverter : IValueConverter
|
||||||
{
|
{
|
||||||
22
Needlework.Net/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Extensions
|
||||||
|
{
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddSingletonsFromAssemblies<T>(this ServiceCollection services)
|
||||||
|
{
|
||||||
|
var types = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.SelectMany(s => s.GetTypes())
|
||||||
|
.Where(p => !p.IsAbstract && typeof(T).IsAssignableFrom(p));
|
||||||
|
|
||||||
|
foreach (var type in types) services.AddSingleton(typeof(T), type);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ using AvaloniaEdit;
|
|||||||
using AvaloniaEdit.Highlighting;
|
using AvaloniaEdit.Highlighting;
|
||||||
using AvaloniaEdit.Indentation.CSharp;
|
using AvaloniaEdit.Indentation.CSharp;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Extensions
|
namespace Needlework.Net.Extensions
|
||||||
{
|
{
|
||||||
public static class TextEditorExtensions
|
public static class TextEditorExtensions
|
||||||
{
|
{
|
||||||
9
Needlework.Net/Messages/DataReadyMessage.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
|
using Needlework.Net.Models;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Messages
|
||||||
|
{
|
||||||
|
public class DataReadyMessage(OpenApiDocumentWrapper wrapper) : ValueChangedMessage<OpenApiDocumentWrapper>(wrapper)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Needlework.Net/Messages/DataRequestMessage.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
|
using Needlework.Net.Models;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Messages
|
||||||
|
{
|
||||||
|
public class DataRequestMessage : RequestMessage<OpenApiDocumentWrapper>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
namespace Needlework.Net.Messages
|
||||||
{
|
{
|
||||||
public class HostDocumentRequestMessage : RequestMessage<OpenApiDocument>
|
public class HostDocumentRequestMessage : RequestMessage<OpenApiDocument>
|
||||||
{
|
{
|
||||||
9
Needlework.Net/Messages/InfoBarUpdateMessage.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
|
using Needlework.Net.ViewModels.MainWindow;
|
||||||
|
|
||||||
|
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;
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop.Messages
|
namespace Needlework.Net.Messages
|
||||||
{
|
{
|
||||||
public class ResponseUpdatedMessage(string data) : ValueChangedMessage<string>(data)
|
public class ResponseUpdatedMessage(string data) : ValueChangedMessage<string>(data)
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop
|
namespace Needlework.Net.Models
|
||||||
{
|
{
|
||||||
public class GithubRelease
|
public class GithubRelease
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
namespace Needlework.Net.Core;
|
namespace Needlework.Net.Models;
|
||||||
|
|
||||||
public class LcuSchemaHandler
|
public class OpenApiDocumentWrapper
|
||||||
{
|
{
|
||||||
internal OpenApiDocument OpenApiDocument { get; }
|
internal OpenApiDocument OpenApiDocument { get; }
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ public class LcuSchemaHandler
|
|||||||
|
|
||||||
public List<string> Paths => [.. OpenApiDocument.Paths.Keys];
|
public List<string> Paths => [.. OpenApiDocument.Paths.Keys];
|
||||||
|
|
||||||
public LcuSchemaHandler(OpenApiDocument openApiDocument)
|
public OpenApiDocumentWrapper(OpenApiDocument openApiDocument)
|
||||||
{
|
{
|
||||||
OpenApiDocument = openApiDocument;
|
OpenApiDocument = openApiDocument;
|
||||||
var plugins = new SortedDictionary<string, List<PathOperation>>();
|
var plugins = new SortedDictionary<string, List<PathOperation>>();
|
||||||
@@ -68,5 +69,3 @@ public class LcuSchemaHandler
|
|||||||
Plugins = plugins;
|
Plugins = plugins;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record PathOperation(string Method, string Path, OpenApiOperation Operation);
|
|
||||||
5
Needlework.Net/Models/PathOperation.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Models;
|
||||||
|
|
||||||
|
public record PathOperation(string Method, string Path, OpenApiOperation Operation);
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using Microsoft.OpenApi.Models;
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
using Microsoft.OpenApi.Readers;
|
using Microsoft.OpenApi.Readers;
|
||||||
|
|
||||||
namespace Needlework.Net.Core;
|
namespace Needlework.Net.Models;
|
||||||
|
|
||||||
public static class Resources
|
public static class Resources
|
||||||
{
|
{
|
||||||
@@ -11,55 +11,60 @@
|
|||||||
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
|
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
|
||||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||||
<AssemblyName>NeedleworkDotNet</AssemblyName>
|
<AssemblyName>NeedleworkDotNet</AssemblyName>
|
||||||
<AssemblyVersion>0.3.1.0</AssemblyVersion>
|
<AssemblyVersion>0.8.1.0</AssemblyVersion>
|
||||||
<FileVersion>0.3.1.0</FileVersion>
|
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||||
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
|
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" Version="11.1.0-beta2" />
|
<PackageReference Include="Avalonia" Version="11.1.3" />
|
||||||
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
|
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.1.0" />
|
||||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.1.0-beta2" />
|
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.1.3" />
|
||||||
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.0-beta2" />
|
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.3" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.1.0-beta2" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.1.3" />
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.0-beta2" />
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.3" />
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.0-beta2" />
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.0-beta2" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
|
||||||
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.6" />
|
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
|
||||||
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.9.0" />
|
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.14.0" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
|
||||||
|
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0" />
|
||||||
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
|
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.OpenApi" Version="1.6.22" />
|
||||||
|
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.22" />
|
||||||
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.4.0" />
|
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.4.0" />
|
||||||
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
|
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
|
||||||
<PackageReference Include="SukiUI" Version="6.0.0-beta7" />
|
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.64" />
|
||||||
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.60" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Needlework.Net.Core\Needlework.Net.Core.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<UpToDateCheckInput Remove="Views\AboutView.axaml" />
|
<UpToDateCheckInput Remove="Views\AboutView.axaml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Views\EndpointView.axaml.cs">
|
<Compile Update="Controls\BusyArea.axaml.cs">
|
||||||
<DependentUpon>EndpointView.axaml</DependentUpon>
|
<DependentUpon>BusyArea.axaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Views\OopsiesWindow.axaml.cs">
|
<Compile Update="Views\MainWindow\MainWindowView.axaml.cs">
|
||||||
<DependentUpon>OopsiesWindow.axaml</DependentUpon>
|
<DependentUpon>MainWindowView.axaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="Views\Pages\Endpoints\EndpointsNavigationView.axaml.cs">
|
||||||
|
<DependentUpon>EndpointsNavigationView.axaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="Views\Pages\Endpoints\EndpointView.axaml.cs">
|
||||||
|
<DependentUpon>EndpointView.axaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Assets\Users\" />
|
||||||
<Folder Include="Utilities\" />
|
<Folder Include="Utilities\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Needlework.Net.Desktop.Services;
|
using Needlework.Net.Extensions;
|
||||||
using Needlework.Net.Desktop.ViewModels;
|
using Needlework.Net.Services;
|
||||||
|
using Needlework.Net.ViewModels.MainWindow;
|
||||||
|
using Needlework.Net.ViewModels.Pages;
|
||||||
using Projektanker.Icons.Avalonia;
|
using Projektanker.Icons.Avalonia;
|
||||||
using Projektanker.Icons.Avalonia.FontAwesome;
|
using Projektanker.Icons.Avalonia.FontAwesome;
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.IO;
|
||||||
|
|
||||||
namespace Needlework.Net.Desktop;
|
namespace Needlework.Net;
|
||||||
|
|
||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
@@ -15,8 +17,13 @@ class Program
|
|||||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||||
// yet and stuff might break.
|
// yet and stuff might break.
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += Program_UnhandledException;
|
||||||
|
|
||||||
|
BuildAvaloniaApp()
|
||||||
.StartWithClassicDesktopLifetime(args);
|
.StartWithClassicDesktopLifetime(args);
|
||||||
|
}
|
||||||
|
|
||||||
// Avalonia configuration, don't remove; also used by visual designer.
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
@@ -35,17 +42,17 @@ class Program
|
|||||||
var builder = new ServiceCollection();
|
var builder = new ServiceCollection();
|
||||||
|
|
||||||
builder.AddSingleton<MainWindowViewModel>();
|
builder.AddSingleton<MainWindowViewModel>();
|
||||||
builder.AddSingleton<WindowService>();
|
builder.AddSingleton<DialogService>();
|
||||||
// Dynamically add ViewModels
|
builder.AddSingletonsFromAssemblies<PageBase>();
|
||||||
var types = AppDomain.CurrentDomain.GetAssemblies()
|
|
||||||
.SelectMany(s => s.GetTypes())
|
|
||||||
.Where(p => !p.IsAbstract && typeof(PageBase).IsAssignableFrom(p));
|
|
||||||
foreach (var type in types)
|
|
||||||
builder.AddSingleton(typeof(PageBase), type);
|
|
||||||
|
|
||||||
builder.AddHttpClient();
|
builder.AddHttpClient();
|
||||||
|
|
||||||
var services = builder.BuildServiceProvider();
|
var services = builder.BuildServiceProvider();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||||
|
{
|
||||||
|
File.WriteAllText($"errorlog-{DateTime.Now:HHmmssfff}", e.ExceptionObject.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
20
Needlework.Net/Services/DialogService.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Services
|
||||||
|
{
|
||||||
|
public class DialogService
|
||||||
|
{
|
||||||
|
public async Task<ContentDialogResult> ShowAsync<T>(object data)
|
||||||
|
where T : IDialog, IDisposable
|
||||||
|
{
|
||||||
|
T dialog = Activator.CreateInstance<T>();
|
||||||
|
|
||||||
|
var result = await dialog.ShowAsync(data);
|
||||||
|
dialog.Dispose();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Needlework.Net/Services/IDialog.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Needlework.Net.Services
|
||||||
|
{
|
||||||
|
public interface IDialog
|
||||||
|
{
|
||||||
|
public Task<ContentDialogResult> ShowAsync(object data);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
Needlework.Net/ViewLocator.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Needlework.Net
|
||||||
|
{
|
||||||
|
public class ViewLocator : IDataTemplate
|
||||||
|
{
|
||||||
|
private readonly Dictionary<object, Control> _controlCache = [];
|
||||||
|
|
||||||
|
public Control Build(object? data)
|
||||||
|
{
|
||||||
|
var name = data?.GetType().Name;
|
||||||
|
if (name is null)
|
||||||
|
{
|
||||||
|
return new TextBlock { Text = "Data is null or has no name." };
|
||||||
|
}
|
||||||
|
if (!name.Contains("ViewModel"))
|
||||||
|
{
|
||||||
|
return new TextBlock { Text = "Data name must end with ViewModel." };
|
||||||
|
}
|
||||||
|
|
||||||
|
name = name.Replace("ViewModel", "View");
|
||||||
|
var type = Assembly.GetExecutingAssembly()
|
||||||
|
.GetTypes()
|
||||||
|
.Where(t => t.Name == name)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (type is null)
|
||||||
|
{
|
||||||
|
return new TextBlock { Text = $"No view for {name}." };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_controlCache.TryGetValue(data!, out var res))
|
||||||
|
{
|
||||||
|
res ??= (Control)Activator.CreateInstance(type)!;
|
||||||
|
_controlCache[data!] = res;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.DataContext = data;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Match(object? data) => data is INotifyPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Needlework.Net/ViewModels/MainWindow/InfoBarViewModel.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.MainWindow;
|
||||||
|
|
||||||
|
public partial class InfoBarViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private string _title;
|
||||||
|
[ObservableProperty] private bool _isOpen;
|
||||||
|
[ObservableProperty] private string _message;
|
||||||
|
[ObservableProperty] private InfoBarSeverity _severity;
|
||||||
|
[ObservableProperty] private TimeSpan _duration;
|
||||||
|
[ObservableProperty] private Control? _actionButton;
|
||||||
|
|
||||||
|
public InfoBarViewModel(string title, bool isOpen, string message, InfoBarSeverity severity, TimeSpan duration, Control? actionButton = null)
|
||||||
|
{
|
||||||
|
_title = title;
|
||||||
|
_isOpen = isOpen;
|
||||||
|
_message = message;
|
||||||
|
_severity = severity;
|
||||||
|
_duration = duration;
|
||||||
|
_actionButton = actionButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
157
Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Needlework.Net.Messages;
|
||||||
|
using Needlework.Net.Models;
|
||||||
|
using Needlework.Net.Services;
|
||||||
|
using Needlework.Net.ViewModels.Pages;
|
||||||
|
using Needlework.Net.Views.MainWindow;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.MainWindow;
|
||||||
|
|
||||||
|
public partial class MainWindowViewModel
|
||||||
|
: ObservableObject, IRecipient<DataRequestMessage>, IRecipient<HostDocumentRequestMessage>, IRecipient<InfoBarUpdateMessage>, IRecipient<OopsiesDialogRequestedMessage>
|
||||||
|
{
|
||||||
|
public IAvaloniaReadOnlyList<NavigationViewItem> MenuItems { get; }
|
||||||
|
[NotifyPropertyChangedFor(nameof(CurrentPage))]
|
||||||
|
[ObservableProperty] private NavigationViewItem _selectedMenuItem;
|
||||||
|
public PageBase CurrentPage => (PageBase)SelectedMenuItem.Tag!;
|
||||||
|
|
||||||
|
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
|
||||||
|
[ObservableProperty] private bool _isUpdateShown = false;
|
||||||
|
|
||||||
|
public HttpClient HttpClient { get; }
|
||||||
|
public DialogService DialogService { get; }
|
||||||
|
public OpenApiDocumentWrapper? OpenApiDocumentWrapper { get; set; }
|
||||||
|
public OpenApiDocument? HostDocument { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isBusy = true;
|
||||||
|
|
||||||
|
[ObservableProperty] private ObservableCollection<InfoBarViewModel> _infoBarItems = [];
|
||||||
|
|
||||||
|
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, DialogService dialogService)
|
||||||
|
{
|
||||||
|
MenuItems = new AvaloniaList<NavigationViewItem>(pages
|
||||||
|
.OrderBy(p => p.Index)
|
||||||
|
.ThenBy(p => p.DisplayName)
|
||||||
|
.Select(p => new NavigationViewItem()
|
||||||
|
{
|
||||||
|
Content = p.DisplayName,
|
||||||
|
Tag = p,
|
||||||
|
IconSource = new BitmapIconSource() { UriSource = new Uri($"avares://NeedleworkDotNet/Assets/Icons/{p.Icon}.png") }
|
||||||
|
}));
|
||||||
|
SelectedMenuItem = MenuItems[0];
|
||||||
|
|
||||||
|
HttpClient = httpClient;
|
||||||
|
DialogService = dialogService;
|
||||||
|
|
||||||
|
WeakReferenceMessenger.Default.RegisterAll(this);
|
||||||
|
|
||||||
|
Task.Run(FetchDataAsync);
|
||||||
|
new Thread(ProcessEvents) { IsBackground = true }.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessEvents(object? obj)
|
||||||
|
{
|
||||||
|
while (!IsUpdateShown)
|
||||||
|
{
|
||||||
|
Task.Run(CheckLatestVersionAsync);
|
||||||
|
|
||||||
|
Thread.Sleep(TimeSpan.FromMinutes(10)); // Avoid tripping unauthenticated rate limits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckLatestVersionAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/BlossomiShymae/Needlework.Net/releases/latest");
|
||||||
|
request.Headers.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("Needlework.Net", Version));
|
||||||
|
|
||||||
|
var response = await HttpClient.SendAsync(request);
|
||||||
|
var release = await response.Content.ReadFromJsonAsync<GithubRelease>();
|
||||||
|
if (release == null) return;
|
||||||
|
|
||||||
|
var currentVersion = int.Parse(Version.Replace(".", ""));
|
||||||
|
|
||||||
|
if (release.IsLatest(currentVersion))
|
||||||
|
{
|
||||||
|
Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
|
||||||
|
{
|
||||||
|
await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(10), new Avalonia.Controls.Button()
|
||||||
|
{
|
||||||
|
Command = OpenUrlCommand,
|
||||||
|
CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases",
|
||||||
|
Content = "Download"
|
||||||
|
}));
|
||||||
|
IsUpdateShown = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FetchDataAsync()
|
||||||
|
{
|
||||||
|
var document = await Resources.GetOpenApiDocumentAsync(HttpClient);
|
||||||
|
HostDocument = document;
|
||||||
|
var handler = new OpenApiDocumentWrapper(document);
|
||||||
|
OpenApiDocumentWrapper = handler;
|
||||||
|
|
||||||
|
WeakReferenceMessenger.Default.Send(new DataReadyMessage(handler));
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(DataRequestMessage message)
|
||||||
|
{
|
||||||
|
message.Reply(OpenApiDocumentWrapper!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(HostDocumentRequestMessage message)
|
||||||
|
{
|
||||||
|
message.Reply(HostDocument!);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenUrl(string url)
|
||||||
|
{
|
||||||
|
var process = new Process()
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo(url)
|
||||||
|
{
|
||||||
|
UseShellExecute = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(InfoBarUpdateMessage message)
|
||||||
|
{
|
||||||
|
Avalonia.Threading.Dispatcher.UIThread.Post(async () => await ShowInfoBarAsync(message.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ShowInfoBarAsync(InfoBarViewModel vm)
|
||||||
|
{
|
||||||
|
InfoBarItems.Add(vm);
|
||||||
|
await Task.Delay(vm.Duration);
|
||||||
|
InfoBarItems.Remove(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(OopsiesDialogRequestedMessage message)
|
||||||
|
{
|
||||||
|
Avalonia.Threading.Dispatcher.UIThread.Invoke(async () => await DialogService.ShowAsync<OopsiesDialog>(message.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Needlework.Net/ViewModels/Pages/AboutViewModel.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages;
|
||||||
|
|
||||||
|
public partial class AboutViewModel : PageBase
|
||||||
|
{
|
||||||
|
public HttpClient HttpClient { get; }
|
||||||
|
|
||||||
|
public AboutViewModel(HttpClient httpClient) : base("About", "info-circle")
|
||||||
|
{
|
||||||
|
HttpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenUrl(string url)
|
||||||
|
{
|
||||||
|
var process = new Process()
|
||||||
|
{
|
||||||
|
StartInfo = new ProcessStartInfo(url) { UseShellExecute = true }
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Needlework.Net/ViewModels/Pages/ConsoleViewModel.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Needlework.Net.Messages;
|
||||||
|
using Needlework.Net.ViewModels.Shared;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages;
|
||||||
|
|
||||||
|
public partial class ConsoleViewModel : PageBase, IRecipient<DataReadyMessage>
|
||||||
|
{
|
||||||
|
public IAvaloniaReadOnlyList<string> RequestMethods { get; } = new AvaloniaList<string>(["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE"]);
|
||||||
|
public IAvaloniaList<string> RequestPaths { get; } = new AvaloniaList<string>();
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isBusy = true;
|
||||||
|
[ObservableProperty] private LcuRequestViewModel _lcuRequest = new();
|
||||||
|
|
||||||
|
public ConsoleViewModel() : base("Console", "terminal", -200)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Register<DataReadyMessage>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task SendRequest()
|
||||||
|
{
|
||||||
|
await LcuRequest.ExecuteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(DataReadyMessage message)
|
||||||
|
{
|
||||||
|
Avalonia.Threading.Dispatcher.UIThread.Invoke(() =>
|
||||||
|
{
|
||||||
|
RequestPaths.Clear();
|
||||||
|
RequestPaths.AddRange(message.Value.Paths);
|
||||||
|
IsBusy = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Needlework.Net.Messages;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public partial class EndpointViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public string Endpoint { get; }
|
||||||
|
public string Title => Endpoint;
|
||||||
|
|
||||||
|
|
||||||
|
public IAvaloniaReadOnlyList<PathOperationViewModel> PathOperations { get; }
|
||||||
|
[ObservableProperty] private PathOperationViewModel? _selectedPathOperation;
|
||||||
|
|
||||||
|
[ObservableProperty] private string? _search;
|
||||||
|
public IAvaloniaList<PathOperationViewModel> FilteredPathOperations { get; }
|
||||||
|
|
||||||
|
public event EventHandler<string>? PathOperationSelected;
|
||||||
|
|
||||||
|
public EndpointViewModel(string endpoint)
|
||||||
|
{
|
||||||
|
Endpoint = endpoint;
|
||||||
|
|
||||||
|
var handler = WeakReferenceMessenger.Default.Send<DataRequestMessage>().Response;
|
||||||
|
PathOperations = new AvaloniaList<PathOperationViewModel>(handler.Plugins[endpoint].Select(x => new PathOperationViewModel(x)));
|
||||||
|
FilteredPathOperations = new AvaloniaList<PathOperationViewModel>(PathOperations);
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSearchChanged(string? value)
|
||||||
|
{
|
||||||
|
FilteredPathOperations.Clear();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
FilteredPathOperations.AddRange(PathOperations);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FilteredPathOperations.AddRange(PathOperations.Where(o => o.Path.Contains(value, StringComparison.InvariantCultureIgnoreCase)));
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedPathOperationChanged(PathOperationViewModel? value)
|
||||||
|
{
|
||||||
|
if (value == null) return;
|
||||||
|
PathOperationSelected?.Invoke(this, value.Operation.RequestTemplate ?? string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public partial class EndpointsNavigationViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public Guid Guid { get; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
[ObservableProperty] private ObservableObject _activeViewModel;
|
||||||
|
[ObservableProperty] private ObservableObject _endpointsViewModel;
|
||||||
|
[ObservableProperty] private string _title = string.Empty;
|
||||||
|
|
||||||
|
private readonly Action<string?, Guid> _onEndpointNavigation;
|
||||||
|
|
||||||
|
public EndpointsNavigationViewModel(IAvaloniaList<string> plugins, Action<string?, Guid> onEndpointNavigation)
|
||||||
|
{
|
||||||
|
_activeViewModel = _endpointsViewModel = new EndpointsViewModel(plugins, OnClicked);
|
||||||
|
_onEndpointNavigation = onEndpointNavigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClicked(ObservableObject viewModel)
|
||||||
|
{
|
||||||
|
ActiveViewModel = viewModel;
|
||||||
|
if (viewModel is EndpointViewModel endpoint)
|
||||||
|
{
|
||||||
|
Title = endpoint.Title;
|
||||||
|
_onEndpointNavigation.Invoke(endpoint.Title, Guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void GoBack()
|
||||||
|
{
|
||||||
|
ActiveViewModel = EndpointsViewModel;
|
||||||
|
Title = string.Empty;
|
||||||
|
_onEndpointNavigation.Invoke(null, Guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using Needlework.Net.Messages;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public partial class EndpointsTabViewModel : PageBase, IRecipient<DataReadyMessage>
|
||||||
|
{
|
||||||
|
public IAvaloniaList<string> Plugins { get; } = new AvaloniaList<string>();
|
||||||
|
public IAvaloniaList<EndpointItem> Endpoints { get; } = new AvaloniaList<EndpointItem>();
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isBusy = true;
|
||||||
|
|
||||||
|
public EndpointsTabViewModel() : base("Endpoints", "list-alt", -500)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.RegisterAll(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(DataReadyMessage message)
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
Plugins.Clear();
|
||||||
|
Plugins.AddRange(message.Value.Plugins.Keys);
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(AddEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddEndpoint()
|
||||||
|
{
|
||||||
|
Endpoints.Add(new()
|
||||||
|
{
|
||||||
|
Content = new EndpointsNavigationViewModel(Plugins, OnEndpointNavigation),
|
||||||
|
Selected = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEndpointNavigation(string? title, Guid guid)
|
||||||
|
{
|
||||||
|
foreach (var endpoint in Endpoints)
|
||||||
|
{
|
||||||
|
if (endpoint.Content.Guid.Equals(guid))
|
||||||
|
{
|
||||||
|
endpoint.Header = title ?? "Endpoints";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class EndpointItem : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private string _header = "Endpoints";
|
||||||
|
public IconSource IconSource { get; set; } = new SymbolIconSource() { Symbol = Symbol.Document, FontSize = 20.0, Foreground = Avalonia.Media.Brushes.White };
|
||||||
|
public bool Selected { get; set; } = false;
|
||||||
|
public required EndpointsNavigationViewModel Content { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public partial class EndpointsViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public IAvaloniaList<string> Plugins { get; }
|
||||||
|
public IAvaloniaList<string> Query { get; }
|
||||||
|
|
||||||
|
[ObservableProperty] private string _search = string.Empty;
|
||||||
|
[ObservableProperty] private string? _selectedQuery = string.Empty;
|
||||||
|
|
||||||
|
public Action<ObservableObject> OnClicked { get; }
|
||||||
|
|
||||||
|
public EndpointsViewModel(IAvaloniaList<string> plugins, Action<ObservableObject> onClicked)
|
||||||
|
{
|
||||||
|
Plugins = new AvaloniaList<string>(plugins);
|
||||||
|
Query = new AvaloniaList<string>(plugins);
|
||||||
|
OnClicked = onClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSearchChanged(string value)
|
||||||
|
{
|
||||||
|
Query.Clear();
|
||||||
|
if (!string.IsNullOrEmpty(Search))
|
||||||
|
Query.AddRange(Plugins.Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase)));
|
||||||
|
else
|
||||||
|
Query.AddRange(Plugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenEndpoint(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value)) return;
|
||||||
|
|
||||||
|
OnClicked.Invoke(new EndpointViewModel(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
232
Needlework.Net/ViewModels/Pages/Endpoints/OperationViewModel.cs
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Needlework.Net.Messages;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public partial class OperationViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public string Summary { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
public string ReturnType { get; }
|
||||||
|
public bool IsRequestBody { get; }
|
||||||
|
public string? RequestBodyType { get; }
|
||||||
|
public IAvaloniaReadOnlyList<PropertyClassViewModel> RequestClasses { get; }
|
||||||
|
public IAvaloniaReadOnlyList<PropertyClassViewModel> ResponseClasses { get; }
|
||||||
|
public IAvaloniaReadOnlyList<ParameterViewModel> PathParameters { get; }
|
||||||
|
public IAvaloniaReadOnlyList<ParameterViewModel> QueryParameters { get; }
|
||||||
|
public string? RequestTemplate { get; }
|
||||||
|
|
||||||
|
public OperationViewModel(OpenApiOperation operation)
|
||||||
|
{
|
||||||
|
Summary = operation.Summary ?? string.Empty;
|
||||||
|
Description = operation.Description ?? string.Empty;
|
||||||
|
IsRequestBody = operation.RequestBody != null;
|
||||||
|
ReturnType = GetReturnType(operation.Responses);
|
||||||
|
RequestClasses = GetRequestClasses(operation.RequestBody);
|
||||||
|
ResponseClasses = GetResponseClasses(operation.Responses);
|
||||||
|
PathParameters = GetParameters(operation.Parameters, ParameterLocation.Path);
|
||||||
|
QueryParameters = GetParameters(operation.Parameters, ParameterLocation.Query);
|
||||||
|
RequestBodyType = GetRequestBodyType(operation.RequestBody);
|
||||||
|
RequestTemplate = GetRequestTemplate(operation.RequestBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetRequestTemplate(OpenApiRequestBody? requestBody)
|
||||||
|
{
|
||||||
|
var requestClasses = GetRequestClasses(requestBody);
|
||||||
|
if (requestClasses.Count == 0)
|
||||||
|
{
|
||||||
|
var type = GetRequestBodyType(requestBody);
|
||||||
|
if (type == null) return null;
|
||||||
|
return GetRequestDefaultValue(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
var template = CreateTemplate(requestClasses);
|
||||||
|
return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(string.Join(string.Empty, template)), App.JsonSerializerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> CreateTemplate(AvaloniaList<PropertyClassViewModel> requestClasses)
|
||||||
|
{
|
||||||
|
if (requestClasses.Count == 0) return [];
|
||||||
|
List<string> template = [];
|
||||||
|
template.Add("{");
|
||||||
|
|
||||||
|
var rootClass = requestClasses.First();
|
||||||
|
if (rootClass.PropertyEnums.Any()) return [rootClass.PropertyEnums.First().Values];
|
||||||
|
var propertyFields = rootClass.PropertyFields;
|
||||||
|
for (int i = 0; i < propertyFields.Count; i++)
|
||||||
|
{
|
||||||
|
template.Add($"\"{propertyFields[i].Name}\"");
|
||||||
|
template.Add(":");
|
||||||
|
template.Add($"#{propertyFields[i].Type}");
|
||||||
|
|
||||||
|
if (i == propertyFields.Count - 1) template.Add("}");
|
||||||
|
else template.Add(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < template.Count; i++)
|
||||||
|
{
|
||||||
|
var type = template[i];
|
||||||
|
if (!type.Contains("#")) continue;
|
||||||
|
|
||||||
|
var foundClass = requestClasses.Where(c => c.Id == type.Replace("#", string.Empty));
|
||||||
|
if (foundClass.Any())
|
||||||
|
{
|
||||||
|
if (foundClass.First().PropertyEnums.Any())
|
||||||
|
{
|
||||||
|
template[i] = string.Join(string.Empty, CreateTemplate([.. foundClass]));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AvaloniaList<PropertyClassViewModel> classes = [.. requestClasses];
|
||||||
|
classes.Remove(rootClass);
|
||||||
|
template[i] = string.Join(string.Empty, CreateTemplate(classes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
template[i] = GetRequestDefaultValue(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetRequestDefaultValue(string type)
|
||||||
|
{
|
||||||
|
var defaultValue = string.Empty;
|
||||||
|
if (type.Contains("[]")) defaultValue = "[]";
|
||||||
|
else if (type.Contains("string")) defaultValue = "\"\"";
|
||||||
|
else if (type.Contains("boolean")) defaultValue = "false";
|
||||||
|
else if (type.Contains("integer")) defaultValue = "0";
|
||||||
|
else if (type.Contains("double") || type.Contains("float")) defaultValue = "0.0";
|
||||||
|
else if (type.Contains("object")) defaultValue = "{}";
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetRequestBodyType(OpenApiRequestBody? requestBody)
|
||||||
|
{
|
||||||
|
if (requestBody == null) return null;
|
||||||
|
if (requestBody.Content.TryGetValue("application/json", out var media))
|
||||||
|
{
|
||||||
|
var schema = media.Schema;
|
||||||
|
if (schema == null) return null; // Because "PostLolAccountVerificationV1SendDeactivationPin" exists where the media body is empty...
|
||||||
|
return GetSchemaType(schema);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AvaloniaList<ParameterViewModel> GetParameters(IList<OpenApiParameter> parameters, ParameterLocation location)
|
||||||
|
{
|
||||||
|
var pathParameters = new AvaloniaList<ParameterViewModel>();
|
||||||
|
foreach (var parameter in parameters)
|
||||||
|
{
|
||||||
|
if (parameter.In != location) continue;
|
||||||
|
pathParameters.Add(new ParameterViewModel(parameter.Name, GetSchemaType(parameter.Schema), parameter.Required));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AvaloniaList<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses)
|
||||||
|
{
|
||||||
|
if (responses.TryGetValue("2XX", out var response)
|
||||||
|
&& response.Content.TryGetValue("application/json", out var media))
|
||||||
|
{
|
||||||
|
var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response;
|
||||||
|
var schema = media.Schema;
|
||||||
|
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
|
||||||
|
WalkSchema(schema, propertyClasses, document);
|
||||||
|
return propertyClasses;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WalkSchema(OpenApiSchema schema, AvaloniaList<PropertyClassViewModel> propertyClasses, OpenApiDocument document)
|
||||||
|
{
|
||||||
|
var type = GetSchemaType(schema);
|
||||||
|
if (IsComponent(type))
|
||||||
|
{
|
||||||
|
string componentId = GetComponentId(schema);
|
||||||
|
var componentSchema = document.Components.Schemas[componentId];
|
||||||
|
var responseClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum);
|
||||||
|
|
||||||
|
if (propertyClasses.Where(c => c.Id == componentId).Any()) return; // Avoid adding duplicate schemas in classes
|
||||||
|
propertyClasses.Add(responseClass);
|
||||||
|
|
||||||
|
foreach ((var _, var property) in componentSchema.Properties)
|
||||||
|
// Check for self-references like "LolLootLootOddsResponse"
|
||||||
|
// I blame dubble
|
||||||
|
if (IsComponent(GetSchemaType(property)) && componentId != GetComponentId(property))
|
||||||
|
WalkSchema(property, propertyClasses, document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetComponentId(OpenApiSchema schema)
|
||||||
|
{
|
||||||
|
string componentId;
|
||||||
|
if (schema.Reference != null) componentId = schema.Reference.Id;
|
||||||
|
else if (schema.Items != null) componentId = schema.Items.Reference.Id;
|
||||||
|
else componentId = schema.AdditionalProperties.Reference.Id;
|
||||||
|
return componentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsComponent(string type)
|
||||||
|
{
|
||||||
|
return !(type.Contains("object")
|
||||||
|
|| type.Contains("array")
|
||||||
|
|| type.Contains("bool")
|
||||||
|
|| type.Contains("string")
|
||||||
|
|| type.Contains("integer")
|
||||||
|
|| type.Contains("number"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AvaloniaList<PropertyClassViewModel> GetRequestClasses(OpenApiRequestBody? requestBody)
|
||||||
|
{
|
||||||
|
if (requestBody == null) return [];
|
||||||
|
if (requestBody.Content.TryGetValue("application/json", out var media))
|
||||||
|
{
|
||||||
|
var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response;
|
||||||
|
var schema = media.Schema;
|
||||||
|
if (schema == null) return [];
|
||||||
|
|
||||||
|
var type = GetSchemaType(media.Schema);
|
||||||
|
if (IsComponent(type))
|
||||||
|
{
|
||||||
|
var componentId = GetComponentId(schema);
|
||||||
|
var componentSchema = document.Components.Schemas[componentId];
|
||||||
|
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
|
||||||
|
WalkSchema(componentSchema, propertyClasses, document);
|
||||||
|
return propertyClasses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetReturnType(OpenApiResponses responses)
|
||||||
|
{
|
||||||
|
if (responses.TryGetValue("2XX", out var response)
|
||||||
|
&& response.Content.TryGetValue("application/json", out var media))
|
||||||
|
{
|
||||||
|
var schema = media.Schema;
|
||||||
|
return GetSchemaType(schema);
|
||||||
|
}
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetSchemaType(OpenApiSchema schema)
|
||||||
|
{
|
||||||
|
if (schema.Reference != null) return schema.Reference.Id;
|
||||||
|
if (schema.Type == "object" && schema.AdditionalProperties?.Reference != null) return schema.AdditionalProperties.Reference.Id;
|
||||||
|
if (schema.Type == "integer" || schema.Type == "number") return $"{schema.Type}:{schema.Format}";
|
||||||
|
if (schema.Type == "array" && schema.Items.Reference != null) return $"{schema.Items.Reference.Id}[]";
|
||||||
|
if (schema.Type == "array" && (schema.Items.Type == "integer" || schema.Items.Type == "number")) return $"{schema.Items.Type}:{schema.Items.Format}[]";
|
||||||
|
if (schema.Type == "array") return $"{schema.Items.Type}[]";
|
||||||
|
return schema.Type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public partial class ParameterViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public string Type { get; }
|
||||||
|
public bool IsRequired { get; }
|
||||||
|
[ObservableProperty] private string? _value = null;
|
||||||
|
|
||||||
|
public ParameterViewModel(string name, string type, bool isRequired, string? value = null)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Type = type;
|
||||||
|
IsRequired = isRequired;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Needlework.Net.Models;
|
||||||
|
using Needlework.Net.ViewModels.Shared;
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public partial class PathOperationViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public string Path { get; }
|
||||||
|
public OperationViewModel Operation { get; }
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isBusy;
|
||||||
|
[ObservableProperty] private Lazy<LcuRequestViewModel> _lcuRequest;
|
||||||
|
|
||||||
|
public PathOperationViewModel(PathOperation pathOperation)
|
||||||
|
{
|
||||||
|
Path = pathOperation.Path;
|
||||||
|
Operation = new OperationViewModel(pathOperation.Operation);
|
||||||
|
LcuRequest = new(() => new LcuRequestViewModel()
|
||||||
|
{
|
||||||
|
Method = pathOperation.Method.ToUpper()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task SendRequest()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(Path);
|
||||||
|
foreach (var pathParameter in Operation.PathParameters)
|
||||||
|
{
|
||||||
|
sb.Replace($"{{{pathParameter.Name}}}", pathParameter.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstQueryAdded = false;
|
||||||
|
foreach (var queryParameter in Operation.QueryParameters)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(queryParameter.Value))
|
||||||
|
{
|
||||||
|
sb.Append(firstQueryAdded ? '&' : '?');
|
||||||
|
firstQueryAdded = true;
|
||||||
|
sb.Append($"{queryParameter.Name}={Uri.EscapeDataString(queryParameter.Value)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LcuRequest.Value.RequestPath = sb.ToString();
|
||||||
|
await LcuRequest.Value.ExecuteAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Avalonia.Collections;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Microsoft.OpenApi.Any;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public class PropertyClassViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
public IAvaloniaReadOnlyList<PropertyFieldViewModel> PropertyFields { get; } = new AvaloniaList<PropertyFieldViewModel>();
|
||||||
|
public IAvaloniaReadOnlyList<PropertyEnumViewModel> PropertyEnums { get; } = new AvaloniaList<PropertyEnumViewModel>();
|
||||||
|
|
||||||
|
public PropertyClassViewModel(string id, IDictionary<string, OpenApiSchema> properties, IList<IOpenApiAny> enumValue)
|
||||||
|
{
|
||||||
|
AvaloniaList<PropertyFieldViewModel> propertyFields = [];
|
||||||
|
AvaloniaList<PropertyEnumViewModel> propertyEnums = [];
|
||||||
|
foreach ((var propertyName, var propertySchema) in properties)
|
||||||
|
{
|
||||||
|
var type = OperationViewModel.GetSchemaType(propertySchema);
|
||||||
|
var field = new PropertyFieldViewModel(propertyName, type);
|
||||||
|
propertyFields.Add(field);
|
||||||
|
}
|
||||||
|
if (enumValue != null && enumValue.Any())
|
||||||
|
{
|
||||||
|
var propertyEnum = new PropertyEnumViewModel(enumValue);
|
||||||
|
propertyEnums.Add(propertyEnum);
|
||||||
|
}
|
||||||
|
PropertyFields = propertyFields;
|
||||||
|
PropertyEnums = propertyEnums;
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Microsoft.OpenApi.Any;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public class PropertyEnumViewModel
|
||||||
|
{
|
||||||
|
public string Type { get; } = "Enum";
|
||||||
|
public string Values { get; }
|
||||||
|
|
||||||
|
public PropertyEnumViewModel(IList<IOpenApiAny> enumValue)
|
||||||
|
{
|
||||||
|
Values = $"[{string.Join(", ", enumValue.Select(x => $"\"{((OpenApiString)x).Value}\"").ToList())}]";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public class PropertyFieldViewModel
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public string Type { get; }
|
||||||
|
|
||||||
|
public PropertyFieldViewModel(string name, string type)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using BlossomiShymae.GrrrLCU;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||||
|
|
||||||
|
public partial class ResponseViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private string? _path;
|
||||||
|
[ObservableProperty] private string? _status;
|
||||||
|
[ObservableProperty] private string? _authentication;
|
||||||
|
[ObservableProperty] private string? _username;
|
||||||
|
[ObservableProperty] private string? _password;
|
||||||
|
[ObservableProperty] private string? _authorization;
|
||||||
|
|
||||||
|
public ResponseViewModel(string path)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
var processInfo = GetProcessInfo();
|
||||||
|
if (processInfo != null)
|
||||||
|
{
|
||||||
|
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
|
||||||
|
Path = $"https://127.0.0.1:{processInfo.AppPort}{path}";
|
||||||
|
Username = riotAuthentication.Username;
|
||||||
|
Password = riotAuthentication.Password;
|
||||||
|
Authorization = $"Basic {riotAuthentication.RawValue}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProcessInfo? GetProcessInfo()
|
||||||
|
{
|
||||||
|
if (ProcessFinder.IsActive()) return ProcessFinder.GetProcessInfo();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||