52 Commits

Author SHA1 Message Date
BlossomiShymae
2e4637f533 Bump version 2024-12-06 22:18:26 -06:00
BlossomiShymae
7aaa79956c Add event type selection for event viewer 2024-12-06 22:17:48 -06:00
BlossomiShymae
e9d4615ecf Add libraries list 2024-12-06 18:59:43 -06:00
BlossomiShymae
fb63adc1b7 Bump version 2024-12-06 16:25:33 -06:00
BlossomiShymae
b41be19cd9 Fix bug where endpoints search breaks, resolves #6 2024-12-06 16:19:38 -06:00
BlossomiShymae
38e1ea2301 Fix workflows, lol 2024-12-06 01:46:13 -06:00
BlossomiShymae
30451b8c8c Bump version 2024-12-06 01:25:25 -06:00
BlossomiShymae
05927030eb Slow down ProcessEvents 2024-12-06 01:23:36 -06:00
BlossomiShymae
e9f99a9e28 Update README.md 2024-12-05 23:06:06 -06:00
BlossomiShymae
1e838abdbf Merge branch 'main' of github.com:BlossomiShymae/Needlework.Net 2024-12-05 19:21:04 -06:00
BlossomiShymae
dede2e909c Add tabs view to endpoints 2024-12-05 12:31:52 -06:00
BlossomiShymae
58556283f0 Update dependencies 2024-12-04 17:47:34 -06:00
Blossomi Shymae
16781a4df4 Update README.md 2024-11-25 19:37:58 -06:00
BlossomiShymae
569f49d484 Collapse pane by default and reduce width 2024-10-28 22:24:29 -05:00
BlossomiShymae
8eabd64911 Store response body instead of updating once 2024-10-28 20:10:35 -05:00
BlossomiShymae
02e739e1a3 Refactor endpoints views 2024-10-28 19:54:54 -05:00
BlossomiShymae
c253d00ff1 Refactor request viewmodels 2024-10-28 19:50:19 -05:00
BlossomiShymae
4edd71a04a Update workflows 2024-10-28 15:44:54 -05:00
BlossomiShymae
a4fe10157f Refactor folder structure 2024-10-28 15:33:21 -05:00
BlossomiShymae
bc4ed78767 Add gears icon 2024-10-28 13:00:45 -05:00
BlossomiShymae
375285067d Bump version 2024-08-23 22:53:05 -05:00
BlossomiShymae
3ec277bdd3 Update GrrrLCU 2024-08-23 22:50:58 -05:00
BlossomiShymae
c097890588 Fix issues where messages failed to register 2024-08-23 21:21:01 -05:00
BlossomiShymae
3352740733 Add busy area for sending request in endpoint 2024-08-23 20:26:08 -05:00
BlossomiShymae
48751efc28 Fix endpoints not retaining state 2024-08-23 20:03:18 -05:00
BlossomiShymae
b6f713c675 Update GrrrLCU 2024-08-23 19:39:49 -05:00
BlossomiShymae
59619764c2 Improve endpoint loading times by using lazy loading 2024-08-22 20:15:13 -05:00
BlossomiShymae
de6f9f64dd Fix use of GrrrLCU 2024-08-22 19:39:27 -05:00
BlossomiShymae
4eae0bd913 Update GrrrLCU 2024-08-22 19:26:11 -05:00
BlossomiShymae
7288c471a4 Bump version, update GrrrLCU 2024-08-20 06:09:19 -05:00
BlossomiShymae
7faedcf039 Update app preview 2024-08-19 06:45:37 -05:00
BlossomiShymae
641d230647 Bump version 2024-08-19 05:15:40 -05:00
BlossomiShymae
d53c24c57f Update event viewer to have colored text 2024-08-19 05:15:14 -05:00
BlossomiShymae
a24a72b3b2 Update about page 2024-08-19 04:40:36 -05:00
BlossomiShymae
2c88ae44a2 Add unhandled exception logging 2024-08-19 02:58:36 -05:00
BlossomiShymae
f0294b3042 Use dialog instead of window for oopsies 2024-08-18 22:10:04 -05:00
BlossomiShymae
d26180dce5 Refactor folder stucture 2024-08-18 20:08:25 -05:00
BlossomiShymae
baf189e6a9 Refactor workspace name 2024-08-18 19:03:47 -05:00
BlossomiShymae
88149d1458 Bump version 2024-08-17 16:26:52 -05:00
BlossomiShymae
79fd79c01d Fix bug where event viewer may crash from a race condition, resolves #4 2024-08-17 16:25:43 -05:00
BlossomiShymae
7550102406 Fix bug where body template can be incorrect, resolves #5 2024-08-17 16:15:06 -05:00
BlossomiShymae
98996609a3 Bump version, fix error, complete TODOs 2024-08-16 15:04:42 -05:00
Blossomi Shymae
65464d22e3 Merge pull request #2 from AoshiW/perf
bug fix in WebsocketView(Model), optimization and others
2024-08-16 14:49:34 -05:00
AoshiW
0ca7f7869d add missing changes/feedback 2024-08-16 21:14:57 +02:00
AoshiW
af47e7c763 Merge branch 'main' into perf 2024-08-16 08:49:47 +02:00
BlossomiShymae
04058f12c1 Bump version, fix bug where whitespace in request body is removed 2024-08-16 01:30:39 -05:00
AoshiW
3a7d39971a bug fix in WebsocketView, optimization and others 2024-08-16 08:17:43 +02:00
BlossomiShymae
b0b5476c48 Bump version, fix social links not working 2024-08-15 17:32:35 -05:00
BlossomiShymae
b3158a81b8 Update app preview 2024-08-15 17:17:11 -05:00
BlossomiShymae
83400bceed Bump version, migrate to FluentAvalonia with bug fixes 2024-08-15 17:06:17 -05:00
BlossomiShymae
1133f2d785 Update dependencies 2024-08-14 04:43:58 -05:00
BlossomiShymae
14dde760b0 Fix bug where /lol-account-verification failed to open 2024-08-14 03:46:11 -05:00
127 changed files with 2696 additions and 2096 deletions

View File

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

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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");
}
}

View File

@@ -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>

View File

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

View File

@@ -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)
{
}
}

View File

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

View File

@@ -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;
}
}
}

View File

@@ -1,8 +0,0 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Desktop.Messages
{
public class OopsiesWindowCanceledMessage(object? data) : ValueChangedMessage<object?>(data)
{
}
}

View File

@@ -1,8 +0,0 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Desktop.Messages
{
public class OopsiesWindowRequestedMessage(string text) : ValueChangedMessage<string>(text)
{
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,9 +0,0 @@
namespace Needlework.Net.Desktop.ViewModels
{
public class AboutViewModel : PageBase
{
public AboutViewModel() : base("About", Material.Icons.MaterialIconKind.InfoCircle)
{
}
}
}

View File

@@ -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;
});
}
}
}

View File

@@ -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())));
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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.")
};
}
}

View File

@@ -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;
}
}
}

View File

@@ -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())}]";
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
});
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace Needlework.Net.Desktop.Views;
public partial class EndpointsContainerView : UserControl
{
public EndpointsContainerView()
{
InitializeComponent();
}
}

View File

@@ -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>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
namespace Needlework.Net.Desktop.Views
{
public partial class EndpointsView : UserControl
{
public EndpointsView()
{
InitializeComponent();
}
}
}

View File

@@ -1,75 +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.HomeView"
x:DataType="vm:HomeViewModel">
<!-- TOP LEVEL -->
<ScrollViewer>
<WrapPanel Margin="8"
theme:WrapPanelExtensions.AnimatedScroll="true"
Orientation="Horizontal">
<!-- WELCOME -->
<StackPanel>
<suki:GlassCard Margin="8">
<StackPanel>
<TextBlock Classes="h3">Welcome to Needlework.Net</TextBlock>
<TextBlock>Get started with LCU development by clicking on the endpoints tab in the left panel.</TextBlock>
</StackPanel>
</suki:GlassCard>
<suki:GlassCard Margin="8" Classes="Accent">
<TextBlock TextWrapping="Wrap">THE PROGRAM IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF MERCHANTABILITY, NONINFRINGMENT, OR OF FITNESS FOR A PARTICULAR PURPOSE. LICENSOR DOES NOT WARRANT THAT THE FUNCTIONS CONTAINED IN THE PROGRAM WILL MEET YOUR REQUIREMENTS OR THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. LICENSOR MAKES NO WARRANTIES RESPECTING ANY HARM THAT MAY BE CAUSED BY MALICIOUS USE OF THIS SOFTWARE. LICENSOR FURTHER EXPRESSLY DISCLAIMS ANY WARRANTY OR REPRESENTATION TO AUTHORIZED USERS OR TO ANY THIRD PARTY.</TextBlock>
</suki:GlassCard>
</StackPanel>
<!-- STATUS -->
<StackPanel>
<suki:GlassCard Margin="8" Width="250">
<suki:GroupBox Header="Status">
<TextBlock FontSize="24" FontWeight="Bold" Margin="0 4" Foreground="{Binding StatusForeground}" Text="{Binding StatusText}" />
</suki:GroupBox>
</suki:GlassCard>
<suki:GlassCard Margin="8" Width="250">
<suki:GroupBox Header="Address">
<TextBlock Text="{Binding StatusAddress}"/>
</suki:GroupBox>
</suki:GlassCard>
</StackPanel>
<!-- LEGAL -->
<suki:GlassCard Margin="8" Width="300">
<suki:GroupBox Header="Disclaimer">
<TextBlock TextWrapping="Wrap">Needlework.Net isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games, and all associated properties are trademarks or registered trademarks of Riot Games, Inc.</TextBlock>
</suki:GroupBox>
</suki:GlassCard>
<!-- FOOTER -->
<StackPanel>
<suki:GlassCard Margin="8" Width="400">
<StackPanel>
<TextBlock>© 2024 - Blossomi Shymae</TextBlock>
<TextBlock>MIT License</TextBlock>
</StackPanel>
</suki:GlassCard>
<suki:GlassCard Margin="8" Width="400">
<suki:GroupBox Header="Resources">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
</Style>
</StackPanel.Styles>
<Button CommandParameter="https://hextechdocs.dev/tag/lcu/" Margin="0 0 16 0">
Hextech Docs
</Button>
<Button CommandParameter="https://hextechdocs.dev/getting-started-with-the-lcu-api/">
Getting Started
</Button>
</StackPanel>
</suki:GroupBox>
</suki:GlassCard>
</StackPanel>
</WrapPanel>
</ScrollViewer>
</UserControl>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
namespace Needlework.Net.Desktop.Views
{
public partial class HomeView : UserControl
{
public HomeView()
{
InitializeComponent();
}
}
}

View File

@@ -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>

View File

@@ -1,11 +0,0 @@
using SukiUI.Controls;
namespace Needlework.Net.Desktop.Views;
public partial class MainWindow : SukiWindow
{
public MainWindow()
{
InitializeComponent();
}
}

View File

@@ -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>

View File

@@ -1,11 +0,0 @@
using SukiUI.Controls;
namespace Needlework.Net.Desktop.Views;
public partial class OopsiesWindow : SukiWindow
{
public OopsiesWindow()
{
InitializeComponent();
}
}

View File

@@ -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>

View File

@@ -3,11 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Core", "Needlework.Net.Core\Needlework.Net.Core.csproj", "{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Desktop", "Needlework.Net.Desktop\Needlework.Net.Desktop.csproj", "{7388B579-2DC0-46D6-957A-6683D0FCF5D3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Core.Tests", "Needlework.Net.Core.Tests\Needlework.Net.Core.Tests.csproj", "{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net", "Needlework.Net\Needlework.Net.csproj", "{7388B579-2DC0-46D6-957A-6683D0FCF5D3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -18,17 +14,9 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Release|Any CPU.Build.0 = Release|Any CPU
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.Build.0 = Release|Any CPU
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -1,20 +1,20 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Needlework.Net.Desktop.App"
RequestedThemeVariant="Dark"
xmlns:local="using:Needlework.Net.Desktop"
xmlns:converters="using:Needlework.Net.Desktop.Converters"
xmlns:sukiUi="clr-namespace:SukiUI;assembly=SukiUI"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia">
x:Class="Needlework.Net.App"
xmlns:local="using:Needlework.Net"
xmlns:converters="using:Needlework.Net.Converters"
xmlns:sty="using:FluentAvalonia.Styling"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme></FluentTheme>
<sukiUi:SukiTheme ThemeColor="Blue" />
<sty:FluentAvaloniaTheme PreferSystemTheme="False" PreferUserAccentColor="False" />
<materialIcons:MaterialIconStyles />
<StyleInclude Source="Controls/Card.axaml"/>
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
</Application.Styles>

View File

@@ -3,12 +3,12 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Needlework.Net.Desktop.ViewModels;
using Needlework.Net.Desktop.Views;
using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.Views.MainWindow;
using System;
using System.Text.Json;
namespace Needlework.Net.Desktop;
namespace Needlework.Net;
public partial class App(IServiceProvider serviceProvider) : Application
{
@@ -33,7 +33,7 @@ public partial class App(IServiceProvider serviceProvider) : Application
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow()
desktop.MainWindow = new MainWindowView()
{
DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

View File

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 221 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -0,0 +1 @@
[{"Repo":"GrrrLCU","Description":"A simple wrapper for the LCU. Grrr. x3","Language":"C#","Link":"https://github.com/BlossomiShymae/GrrrLCU"},{"Repo":"Kunc.RiotGames","Description":null,"Language":"C#","Link":"https://github.com/AoshiW/Kunc.RiotGames"},{"Repo":"rito","Description":"Rito is a simple, crossplatform (Windows and Linux) C++20 library interfacing with Riot services (i.e. Riot REST API and League of Legends client).","Language":"cpp","Link":"https://github.com/bartekprtc/rito"},{"Repo":"R4J","Description":"A Java library containing the API for every Riot game","Language":"Java","Link":"https://github.com/stelar7/R4J"},{"Repo":"hasagi-core","Description":"LCU library with auto-generated types for request parameters and responses","Language":"JavaScript","Link":"https://github.com/dysolix/hasagi-core"},{"Repo":"lcu-driver","Description":"Python3 helper for the League of Legends LCU API.","Language":"Python","Link":"https://github.com/sousa-andre/lcu-driver"},{"Repo":"willump","Description":"Python3 helper for the League of Legends LCU API.","Language":"Python","Link":"https://github.com/elliejs/Willump"},{"Repo":"Irelia","Description":"LoL LCU Wrapper for Rust, built on top of hyper!","Language":"Rust","Link":"https://github.com/AlsoSylv/Irelia"},{"Repo":"Shaco","Description":"League of Legends LCU wrapper for rust","Language":"Rust","Link":"https://github.com/Leastrio/Shaco"},{"Repo":"hasagi-core","Description":"LCU library with auto-generated types for request parameters and responses","Language":"TypeScript","Link":"https://github.com/dysolix/hasagi-core"},{"Repo":"hexgate","Description":"LCU API wrapper for League of Legends","Language":"TypeScript","Link":"https://github.com/cuppachino/hexgate"}]

View 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>

View 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);
}
}

View 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>

View File

@@ -0,0 +1,10 @@
using Avalonia.Controls;
namespace Needlework.Net.Controls;
public class Card : ContentControl
{
public Card()
{
}
}

View File

@@ -4,7 +4,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Needlework.Net.Desktop.Converters
namespace Needlework.Net.Converters
{
public class EnumerableBoolConverter : IValueConverter
{

View File

@@ -2,7 +2,7 @@
using System;
using System.Globalization;
namespace Needlework.Net.Desktop.Converters
namespace Needlework.Net.Converters
{
public class NullBoolConverter : IValueConverter
{

View File

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

View File

@@ -3,7 +3,7 @@ using AvaloniaEdit;
using AvaloniaEdit.Highlighting;
using AvaloniaEdit.Indentation.CSharp;
namespace Needlework.Net.Desktop.Extensions
namespace Needlework.Net.Extensions
{
public static class TextEditorExtensions
{

View File

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

View File

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

View File

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

View 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)
{
}
}

View File

@@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Messages
{
public class OopsiesDialogRequestedMessage(string text) : ValueChangedMessage<string>(text)
{
}
}

View File

@@ -1,6 +1,6 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Desktop.Messages
namespace Needlework.Net.Messages
{
public class ResponseUpdatedMessage(string data) : ValueChangedMessage<string>(data)
{

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Needlework.Net.Desktop
namespace Needlework.Net.Models
{
public class GithubRelease
{

View File

@@ -0,0 +1,9 @@
namespace Needlework.Net.Models;
public class Library
{
public required string Repo { get; init; }
public string? Description { get; init; }
public required string Language { get; init; }
public required string Link { get; init; }
}

View File

@@ -1,8 +1,9 @@
using System.Collections.Generic;
using Microsoft.OpenApi.Models;
namespace Needlework.Net.Core;
namespace Needlework.Net.Models;
public class LcuSchemaHandler
public class OpenApiDocumentWrapper
{
internal OpenApiDocument OpenApiDocument { get; }
@@ -12,7 +13,7 @@ public class LcuSchemaHandler
public List<string> Paths => [.. OpenApiDocument.Paths.Keys];
public LcuSchemaHandler(OpenApiDocument openApiDocument)
public OpenApiDocumentWrapper(OpenApiDocument openApiDocument)
{
OpenApiDocument = openApiDocument;
var plugins = new SortedDictionary<string, List<PathOperation>>();
@@ -68,5 +69,3 @@ public class LcuSchemaHandler
Plugins = plugins;
}
}
public record PathOperation(string Method, string Path, OpenApiOperation Operation);

View File

@@ -0,0 +1,5 @@
using Microsoft.OpenApi.Models;
namespace Needlework.Net.Models;
public record PathOperation(string Method, string Path, OpenApiOperation Operation);

View File

@@ -1,7 +1,9 @@
using Microsoft.OpenApi.Models;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
namespace Needlework.Net.Core;
namespace Needlework.Net.Models;
public static class Resources
{

View File

@@ -11,39 +11,37 @@
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>NeedleworkDotNet</AssemblyName>
<AssemblyVersion>0.3.1.0</AssemblyVersion>
<FileVersion>0.3.1.0</FileVersion>
<AssemblyVersion>0.9.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.Desktop" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia" Version="11.1.3" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.1.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.1.3" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.1.3" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.3" />
<!--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 Include="Avalonia.Themes.Fluent" Version="11.1.0-beta2" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.6" />
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.9.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.14.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.22" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.22" />
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.4.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
<PackageReference Include="SukiUI" Version="6.0.0-beta7" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.60" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.64" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Needlework.Net.Core\Needlework.Net.Core.csproj" />
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
@@ -51,15 +49,22 @@
</ItemGroup>
<ItemGroup>
<Compile Update="Views\EndpointView.axaml.cs">
<DependentUpon>EndpointView.axaml</DependentUpon>
<Compile Update="Controls\BusyArea.axaml.cs">
<DependentUpon>BusyArea.axaml</DependentUpon>
</Compile>
<Compile Update="Views\OopsiesWindow.axaml.cs">
<DependentUpon>OopsiesWindow.axaml</DependentUpon>
<Compile Update="Views\MainWindow\MainWindowView.axaml.cs">
<DependentUpon>MainWindowView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\EndpointsNavigationView.axaml.cs">
<DependentUpon>EndpointsNavigationView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\EndpointView.axaml.cs">
<DependentUpon>EndpointView.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\Users\" />
<Folder Include="Utilities\" />
</ItemGroup>
</Project>

View File

@@ -1,13 +1,15 @@
using Avalonia;
using Microsoft.Extensions.DependencyInjection;
using Needlework.Net.Desktop.Services;
using Needlework.Net.Desktop.ViewModels;
using Needlework.Net.Extensions;
using Needlework.Net.Services;
using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages;
using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome;
using System;
using System.Linq;
using System.IO;
namespace Needlework.Net.Desktop;
namespace Needlework.Net;
class Program
{
@@ -15,8 +17,13 @@ class Program
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
public static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += Program_UnhandledException;
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
@@ -35,17 +42,17 @@ class Program
var builder = new ServiceCollection();
builder.AddSingleton<MainWindowViewModel>();
builder.AddSingleton<WindowService>();
// Dynamically add ViewModels
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => !p.IsAbstract && typeof(PageBase).IsAssignableFrom(p));
foreach (var type in types)
builder.AddSingleton(typeof(PageBase), type);
builder.AddSingleton<DialogService>();
builder.AddSingletonsFromAssemblies<PageBase>();
builder.AddHttpClient();
var services = builder.BuildServiceProvider();
return services;
}
private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
File.WriteAllText($"errorlog-{DateTime.Now:HHmmssfff}", e.ExceptionObject.ToString());
}
}

View 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;
}
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,157 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using FluentAvalonia.UI.Controls;
using Microsoft.OpenApi.Models;
using Needlework.Net.Messages;
using Needlework.Net.Models;
using Needlework.Net.Services;
using Needlework.Net.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));
}
}

View 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();
}
}

View 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;
});
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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; }
}

View File

@@ -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));
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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())}]";
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,26 @@
using Avalonia.Platform;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
namespace Needlework.Net.ViewModels.Pages;
public partial class HomeViewModel : PageBase
{
public List<Library> Libraries { get; } = JsonSerializer.Deserialize<List<Library>>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json")))!;
public HomeViewModel() : base("Home", "home", int.MinValue) { }
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo(url) { UseShellExecute = true }
};
process.Start();
}
}

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