26 Commits

Author SHA1 Message Date
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
70 changed files with 1531 additions and 1410 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 -c Release
run: dotnet build ${{env.PROJECT_NAME}} -c Release
- name: Publish
run: dotnet publish Needlework.Net -c Release -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -o publish -r win-x64 --self-contained=false
- name: Get Version
id: version
shell: powershell
run: |
$xml=[xml](Get-Content .\Needlework.Net\Needlework.Net.csproj)
$ver=($xml.Project.PropertyGroup).AssemblyVersion
$ver="VERSION=$ver"
$ver=$ver -replace '\s',''
echo $ver >> $env:GITHUB_OUTPUT
- name: Zip Files
run: 7z a -tzip NeedleworkDotNet-win-x64.zip ./Publish/* README.md LICENSE
- name: Release
run: dotnet publish ${{env.PROJECT_NAME}} -c Release -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -o publish -r win-x64 --self-contained=false
- name: Zip files
run: 7z a -tzip ${{env.ASSEMBLY_NAME}}-win-x64.zip ./Publish/* README.md LICENSE
- name: Upload to release
uses: softprops/action-gh-release@v1
if: ${{startsWith(github.ref, 'refs/tags/') }}
with:
name: "Needlework.Net v${{ steps.version.outputs.VERSION }}"
prerelease: false
tag_name: "${{ steps.version.outputs.VERSION }}"
files: |
NeedleworkDotNet-win-x64.zip
files: ${{env.ASSEMBLY_NAME}}-win-x64.zip

View File

@@ -3,8 +3,8 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Needlework.Net.ViewModels;
using Needlework.Net.Views;
using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.Views.MainWindow;
using System;
using System.Text.Json;
@@ -33,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>()
};

View File

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

View File

@@ -1,20 +0,0 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Messages
{
public class EditorUpdateMessage(EditorUpdate editorUpdate) : ValueChangedMessage<EditorUpdate>(editorUpdate)
{
}
public class EditorUpdate
{
public string Text { get; }
public string Key { get; }
public EditorUpdate(string text, string key)
{
Text = text;
Key = key;
}
}
}

View File

@@ -1,5 +1,5 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using Needlework.Net.ViewModels;
using Needlework.Net.ViewModels.MainWindow;
namespace Needlework.Net.Messages
{

View File

@@ -11,7 +11,7 @@
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>NeedleworkDotNet</AssemblyName>
<AssemblyVersion>0.6.1.0</AssemblyVersion>
<AssemblyVersion>0.8.1.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
</PropertyGroup>
@@ -27,17 +27,17 @@
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" />
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.11.1" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.14.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.17" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.17" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.22" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.22" />
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.4.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.62" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.64" />
</ItemGroup>
<ItemGroup>
@@ -52,7 +52,13 @@
<Compile Update="Controls\BusyArea.axaml.cs">
<DependentUpon>BusyArea.axaml</DependentUpon>
</Compile>
<Compile Update="Views\EndpointView.axaml.cs">
<Compile Update="Views\MainWindow\MainWindowView.axaml.cs">
<DependentUpon>MainWindowView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\EndpointsNavigationView.axaml.cs">
<DependentUpon>EndpointsNavigationView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\EndpointView.axaml.cs">
<DependentUpon>EndpointView.axaml</DependentUpon>
</Compile>
</ItemGroup>

View File

@@ -2,7 +2,8 @@
using Microsoft.Extensions.DependencyInjection;
using Needlework.Net.Extensions;
using Needlework.Net.Services;
using Needlework.Net.ViewModels;
using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages;
using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome;
using System;

View File

@@ -1,28 +1,50 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
namespace Needlework.Net
{
public class ViewLocator : IDataTemplate
{
public Control? Build(object? param)
private readonly Dictionary<object, Control> _controlCache = [];
public Control Build(object? data)
{
if (param is null) return new TextBlock { Text = "data was null" };
var name = data?.GetType().Name;
if (name is null)
{
return new TextBlock { Text = "Data is null or has no name." };
}
if (!name.Contains("ViewModel"))
{
return new TextBlock { Text = "Data name must end with ViewModel." };
}
var name = param.GetType().FullName!
.Replace("ViewModels", "Views")
.Replace("ViewModel", "View");
var type = Type.GetType(name);
name = name.Replace("ViewModel", "View");
var type = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.Name == name)
.FirstOrDefault();
if (type != null) return (Control)Activator.CreateInstance(type)!;
else return new TextBlock { Text = "Not Found: " + name };
if (type is null)
{
return new TextBlock { Text = $"No view for {name}." };
}
if (!_controlCache.TryGetValue(data!, out var res))
{
res ??= (Control)Activator.CreateInstance(type)!;
_controlCache[data!] = res;
}
res.DataContext = data;
return res;
}
public bool Match(object? data)
{
return data is INotifyPropertyChanged;
}
public bool Match(object? data) => data is INotifyPropertyChanged;
}
}

View File

@@ -1,26 +0,0 @@
using CommunityToolkit.Mvvm.Input;
using System.Diagnostics;
using System.Net.Http;
namespace Needlework.Net.ViewModels
{
public partial class AboutViewModel : PageBase
{
public HttpClient HttpClient { get; }
public AboutViewModel(HttpClient httpClient) : base("About", "info-circle")
{
HttpClient = httpClient;
}
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo(url) { UseShellExecute = true }
};
process.Start();
}
}
}

View File

@@ -1,97 +0,0 @@
using Avalonia.Collections;
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels
{
public partial class ConsoleViewModel : PageBase, IRecipient<DataReadyMessage>
{
public IAvaloniaReadOnlyList<string> RequestMethods { get; } = new AvaloniaList<string>(["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE"]);
public IAvaloniaList<string> RequestPaths { get; } = new AvaloniaList<string>();
[ObservableProperty] private bool _isBusy = true;
[ObservableProperty] private bool _isRequestBusy = false;
[ObservableProperty] private string? _requestMethodSelected = "GET";
[ObservableProperty] private string? _requestPath = null;
[ObservableProperty] private string? _requestBody = null;
[ObservableProperty] private string? _responsePath = null;
[ObservableProperty] private string? _responseStatus = null;
[ObservableProperty] private string? _responseAuthorization = null;
public ConsoleViewModel() : base("Console", "terminal", -200)
{
WeakReferenceMessenger.Default.Register<DataReadyMessage>(this);
}
[RelayCommand]
private async Task SendRequest()
{
try
{
IsRequestBusy = true;
if (string.IsNullOrEmpty(RequestPath)) throw new Exception("Path is empty.");
var method = RequestMethodSelected switch
{
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
"DELETE" => HttpMethod.Delete,
"HEAD" => HttpMethod.Head,
"PATCH" => HttpMethod.Patch,
"OPTIONS" => HttpMethod.Options,
"TRACE" => HttpMethod.Trace,
_ => throw new Exception("Method is not selected."),
};
var processInfo = Connector.GetProcessInfo();
var requestBody = WeakReferenceMessenger.Default.Send(new ContentRequestMessage(), "ConsoleRequestEditor").Response;
var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var response = await Connector.SendAsync(method, RequestPath, content);
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
var responseBody = await response.Content.ReadAsByteArrayAsync();
var body = responseBody.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), App.JsonSerializerOptions) : string.Empty;
if (body.Length >= App.MaxCharacters)
{
WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(body));
WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(string.Empty), nameof(ConsoleViewModel));
}
else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(body), nameof(ConsoleViewModel));
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}";
ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{RequestPath}";
ResponseAuthorization = $"Basic {riotAuthentication.Value}";
}
catch (Exception ex)
{
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5))));
ResponseStatus = null;
ResponsePath = null;
ResponseAuthorization = null;
WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(string.Empty), nameof(ConsoleViewModel));
}
finally
{
IsRequestBusy = false;
}
}
public void Receive(DataReadyMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(() =>
{
RequestPaths.Clear();
RequestPaths.AddRange(message.Value.Paths);
IsBusy = false;
});
}
}
}

View File

@@ -1,49 +0,0 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using System;
using System.Linq;
namespace Needlework.Net.ViewModels
{
public partial class EndpointViewModel : ObservableObject
{
public string Endpoint { get; }
public string Title => Endpoint;
public IAvaloniaReadOnlyList<PathOperationViewModel> PathOperations { get; }
[ObservableProperty] private PathOperationViewModel? _selectedPathOperation;
[ObservableProperty] private string? _search;
public IAvaloniaList<PathOperationViewModel> FilteredPathOperations { get; }
public EndpointViewModel(string endpoint)
{
Endpoint = endpoint;
var handler = WeakReferenceMessenger.Default.Send<DataRequestMessage>().Response;
PathOperations = new AvaloniaList<PathOperationViewModel>(handler.Plugins[endpoint].Select(x => new PathOperationViewModel(x)));
FilteredPathOperations = new AvaloniaList<PathOperationViewModel>(PathOperations);
}
partial void OnSearchChanged(string? value)
{
FilteredPathOperations.Clear();
if (string.IsNullOrWhiteSpace(value))
{
FilteredPathOperations.AddRange(PathOperations);
return;
}
FilteredPathOperations.AddRange(PathOperations.Where(o => o.Path.Contains(value, StringComparison.InvariantCultureIgnoreCase)));
}
partial void OnSelectedPathOperationChanged(PathOperationViewModel? value)
{
if (value == null) return;
WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(value.Operation.RequestTemplate ?? string.Empty, "EndpointRequestEditor")));
}
}
}

View File

@@ -1,31 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Net.Http;
namespace Needlework.Net.ViewModels
{
public partial class EndpointsContainerViewModel : PageBase
{
[ObservableProperty] private ObservableObject _activeViewModel;
[ObservableProperty] private ObservableObject _endpointsViewModel;
[ObservableProperty] private string _title = string.Empty;
public EndpointsContainerViewModel(HttpClient httpClient) : base("Endpoints", "list-alt", -500)
{
_activeViewModel = _endpointsViewModel = new EndpointsViewModel(httpClient, OnClicked);
}
private void OnClicked(ObservableObject viewModel)
{
ActiveViewModel = viewModel;
if (viewModel is EndpointViewModel endpoint) Title = endpoint.Title;
}
[RelayCommand]
private void GoBack()
{
ActiveViewModel = EndpointsViewModel;
Title = string.Empty;
}
}
}

View File

@@ -1,59 +0,0 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using System;
using System.Linq;
using System.Net.Http;
namespace Needlework.Net.ViewModels
{
public partial class EndpointsViewModel : ObservableObject, IRecipient<DataReadyMessage>
{
public HttpClient HttpClient { get; }
public string Title => "Endpoints";
public Action<ObservableObject> OnClicked;
public IAvaloniaList<string> Plugins { get; } = new AvaloniaList<string>();
public IAvaloniaList<string> Query { get; } = new AvaloniaList<string>();
[ObservableProperty] private bool _isBusy = true;
[ObservableProperty] private string _search = string.Empty;
[ObservableProperty] private string? _selectedQuery = string.Empty;
public EndpointsViewModel(HttpClient httpClient, Action<ObservableObject> onClicked)
{
HttpClient = httpClient;
OnClicked = onClicked;
WeakReferenceMessenger.Default.Register(this);
}
public void Receive(DataReadyMessage message)
{
IsBusy = false;
Plugins.Clear();
Plugins.AddRange(message.Value.Plugins.Keys);
Query.Clear();
Query.AddRange(Plugins);
}
partial void OnSearchChanged(string value)
{
Query.Clear();
if (!string.IsNullOrEmpty(Search))
Query.AddRange(Plugins.Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase)));
else
Query.AddRange(Plugins);
}
[RelayCommand]
private void OpenEndpoint(string? value)
{
if (string.IsNullOrEmpty(value)) return;
OnClicked.Invoke(new EndpointViewModel(value));
}
}
}

View File

@@ -1,22 +0,0 @@
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
namespace Needlework.Net.ViewModels
{
public class EventViewModel : ObservableObject
{
public string Time { get; }
public string Type { get; }
public string Uri { get; }
public string Key => $"{Time} {Type} {Uri}";
public EventViewModel(EventData eventData)
{
Time = $"{DateTime.Now:HH:mm:ss.fff}";
Type = eventData?.EventType.ToUpper() ?? string.Empty;
Uri = eventData?.Uri ?? string.Empty;
}
}
}

View File

@@ -1,20 +0,0 @@
using CommunityToolkit.Mvvm.Input;
using System.Diagnostics;
namespace Needlework.Net.ViewModels
{
public partial class HomeViewModel : PageBase
{
public HomeViewModel() : base("Home", "home", int.MinValue) { }
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo(url) { UseShellExecute = true }
};
process.Start();
}
}
}

View File

@@ -1,27 +0,0 @@
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using System;
namespace Needlework.Net.ViewModels
{
public partial class InfoBarViewModel : ObservableObject
{
[ObservableProperty] private string _title;
[ObservableProperty] private bool _isOpen;
[ObservableProperty] private string _message;
[ObservableProperty] private InfoBarSeverity _severity;
[ObservableProperty] private TimeSpan _duration;
[ObservableProperty] private Control? _actionButton;
public InfoBarViewModel(string title, bool isOpen, string message, InfoBarSeverity severity, TimeSpan duration, Control? actionButton = null)
{
_title = title;
_isOpen = isOpen;
_message = message;
_severity = severity;
_duration = duration;
_actionButton = actionButton;
}
}
}

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

@@ -1,157 +0,0 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using FluentAvalonia.UI.Controls;
using Microsoft.OpenApi.Models;
using Needlework.Net.Messages;
using Needlework.Net.Models;
using Needlework.Net.Services;
using Needlework.Net.Views;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels
{
public partial class MainWindowViewModel
: ObservableObject, IRecipient<DataRequestMessage>, IRecipient<HostDocumentRequestMessage>, IRecipient<InfoBarUpdateMessage>, IRecipient<OopsiesDialogRequestedMessage>
{
public IAvaloniaReadOnlyList<NavigationViewItem> MenuItems { get; }
[NotifyPropertyChangedFor(nameof(CurrentPage))]
[ObservableProperty] private NavigationViewItem _selectedMenuItem;
public PageBase CurrentPage => (PageBase)SelectedMenuItem.Tag!;
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
[ObservableProperty] private bool _isUpdateShown = false;
public HttpClient HttpClient { get; }
public DialogService DialogService { get; }
public OpenApiDocumentWrapper? OpenApiDocumentWrapper { get; set; }
public OpenApiDocument? HostDocument { get; set; }
[ObservableProperty] private bool _isBusy = true;
[ObservableProperty] private ObservableCollection<InfoBarViewModel> _infoBarItems = [];
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, DialogService dialogService)
{
MenuItems = new AvaloniaList<NavigationViewItem>(pages
.OrderBy(p => p.Index)
.ThenBy(p => p.DisplayName)
.Select(p => new NavigationViewItem()
{
Content = p.DisplayName,
Tag = p,
IconSource = new BitmapIconSource() { UriSource = new Uri($"avares://NeedleworkDotNet/Assets/Icons/{p.Icon}.png") }
}));
SelectedMenuItem = MenuItems[0];
HttpClient = httpClient;
DialogService = dialogService;
WeakReferenceMessenger.Default.RegisterAll(this);
Task.Run(FetchDataAsync);
new Thread(ProcessEvents) { IsBackground = true }.Start();
}
private void ProcessEvents(object? obj)
{
while (!IsUpdateShown)
{
Task.Run(CheckLatestVersionAsync);
Thread.Sleep(TimeSpan.FromSeconds(60));
}
}
private async Task CheckLatestVersionAsync()
{
try
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/BlossomiShymae/Needlework.Net/releases/latest");
request.Headers.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("Needlework.Net", Version));
var response = await HttpClient.SendAsync(request);
var release = await response.Content.ReadFromJsonAsync<GithubRelease>();
if (release == null) return;
var currentVersion = int.Parse(Version.Replace(".", ""));
if (release.IsLatest(currentVersion))
{
Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
{
await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(10), new Avalonia.Controls.Button()
{
Command = OpenUrlCommand,
CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases",
Content = "Download"
}));
IsUpdateShown = true;
});
}
}
catch (Exception) { }
}
private async Task FetchDataAsync()
{
var document = await Resources.GetOpenApiDocumentAsync(HttpClient);
HostDocument = document;
var handler = new OpenApiDocumentWrapper(document);
OpenApiDocumentWrapper = handler;
WeakReferenceMessenger.Default.Send(new DataReadyMessage(handler));
IsBusy = false;
}
public void Receive(DataRequestMessage message)
{
message.Reply(OpenApiDocumentWrapper!);
}
public void Receive(HostDocumentRequestMessage message)
{
message.Reply(HostDocument!);
}
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo(url)
{
UseShellExecute = true
}
};
process.Start();
}
public void Receive(InfoBarUpdateMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Post(async () => await ShowInfoBarAsync(message.Value));
}
private async Task ShowInfoBarAsync(InfoBarViewModel vm)
{
InfoBarItems.Add(vm);
await Task.Delay(vm.Duration);
InfoBarItems.Remove(vm);
}
public void Receive(OopsiesDialogRequestedMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(async () => await DialogService.ShowAsync<OopsiesDialog>(message.Value));
}
}
}

View File

@@ -1,233 +0,0 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.OpenApi.Models;
using Needlework.Net.Messages;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
namespace Needlework.Net.ViewModels
{
public partial class OperationViewModel : ObservableObject
{
public string Summary { get; }
public string Description { get; }
public string ReturnType { get; }
public bool IsRequestBody { get; }
public string? RequestBodyType { get; }
public IAvaloniaReadOnlyList<PropertyClassViewModel> RequestClasses { get; }
public IAvaloniaReadOnlyList<PropertyClassViewModel> ResponseClasses { get; }
public IAvaloniaReadOnlyList<ParameterViewModel> PathParameters { get; }
public IAvaloniaReadOnlyList<ParameterViewModel> QueryParameters { get; }
public string? RequestTemplate { get; }
public OperationViewModel(OpenApiOperation operation)
{
Summary = operation.Summary ?? string.Empty;
Description = operation.Description ?? string.Empty;
IsRequestBody = operation.RequestBody != null;
ReturnType = GetReturnType(operation.Responses);
RequestClasses = GetRequestClasses(operation.RequestBody);
ResponseClasses = GetResponseClasses(operation.Responses);
PathParameters = GetParameters(operation.Parameters, ParameterLocation.Path);
QueryParameters = GetParameters(operation.Parameters, ParameterLocation.Query);
RequestBodyType = GetRequestBodyType(operation.RequestBody);
RequestTemplate = GetRequestTemplate(operation.RequestBody);
}
private string? GetRequestTemplate(OpenApiRequestBody? requestBody)
{
var requestClasses = GetRequestClasses(requestBody);
if (requestClasses.Count == 0)
{
var type = GetRequestBodyType(requestBody);
if (type == null) return null;
return GetRequestDefaultValue(type);
}
var template = CreateTemplate(requestClasses);
return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(string.Join(string.Empty, template)), App.JsonSerializerOptions);
}
private List<string> CreateTemplate(AvaloniaList<PropertyClassViewModel> requestClasses)
{
if (requestClasses.Count == 0) return [];
List<string> template = [];
template.Add("{");
var rootClass = requestClasses.First();
if (rootClass.PropertyEnums.Any()) return [rootClass.PropertyEnums.First().Values];
var propertyFields = rootClass.PropertyFields;
for (int i = 0; i < propertyFields.Count; i++)
{
template.Add($"\"{propertyFields[i].Name}\"");
template.Add(":");
template.Add($"#{propertyFields[i].Type}");
if (i == propertyFields.Count - 1) template.Add("}");
else template.Add(",");
}
for (int i = 0; i < template.Count; i++)
{
var type = template[i];
if (!type.Contains("#")) continue;
var foundClass = requestClasses.Where(c => c.Id == type.Replace("#", string.Empty));
if (foundClass.Any())
{
if (foundClass.First().PropertyEnums.Any())
{
template[i] = string.Join(string.Empty, CreateTemplate([.. foundClass]));
}
else
{
AvaloniaList<PropertyClassViewModel> classes = [.. requestClasses];
classes.Remove(rootClass);
template[i] = string.Join(string.Empty, CreateTemplate(classes));
}
}
else
{
template[i] = GetRequestDefaultValue(type);
}
}
return template;
}
private static string GetRequestDefaultValue(string type)
{
var defaultValue = string.Empty;
if (type.Contains("[]")) defaultValue = "[]";
else if (type.Contains("string")) defaultValue = "\"\"";
else if (type.Contains("boolean")) defaultValue = "false";
else if (type.Contains("integer")) defaultValue = "0";
else if (type.Contains("double") || type.Contains("float")) defaultValue = "0.0";
else if (type.Contains("object")) defaultValue = "{}";
return defaultValue;
}
private string? GetRequestBodyType(OpenApiRequestBody? requestBody)
{
if (requestBody == null) return null;
if (requestBody.Content.TryGetValue("application/json", out var media))
{
var schema = media.Schema;
if (schema == null) return null; // Because "PostLolAccountVerificationV1SendDeactivationPin" exists where the media body is empty...
return GetSchemaType(schema);
}
return null;
}
private AvaloniaList<ParameterViewModel> GetParameters(IList<OpenApiParameter> parameters, ParameterLocation location)
{
var pathParameters = new AvaloniaList<ParameterViewModel>();
foreach (var parameter in parameters)
{
if (parameter.In != location) continue;
pathParameters.Add(new ParameterViewModel(parameter.Name, GetSchemaType(parameter.Schema), parameter.Required));
}
return pathParameters;
}
private AvaloniaList<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses)
{
if (responses.TryGetValue("2XX", out var response)
&& response.Content.TryGetValue("application/json", out var media))
{
var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response;
var schema = media.Schema;
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
WalkSchema(schema, propertyClasses, document);
return propertyClasses;
}
return [];
}
private void WalkSchema(OpenApiSchema schema, AvaloniaList<PropertyClassViewModel> propertyClasses, OpenApiDocument document)
{
var type = GetSchemaType(schema);
if (IsComponent(type))
{
string componentId = GetComponentId(schema);
var componentSchema = document.Components.Schemas[componentId];
var responseClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum);
if (propertyClasses.Where(c => c.Id == componentId).Any()) return; // Avoid adding duplicate schemas in classes
propertyClasses.Add(responseClass);
foreach ((var _, var property) in componentSchema.Properties)
// Check for self-references like "LolLootLootOddsResponse"
// I blame dubble
if (IsComponent(GetSchemaType(property)) && componentId != GetComponentId(property))
WalkSchema(property, propertyClasses, document);
}
}
private static string GetComponentId(OpenApiSchema schema)
{
string componentId;
if (schema.Reference != null) componentId = schema.Reference.Id;
else if (schema.Items != null) componentId = schema.Items.Reference.Id;
else componentId = schema.AdditionalProperties.Reference.Id;
return componentId;
}
private static bool IsComponent(string type)
{
return !(type.Contains("object")
|| type.Contains("array")
|| type.Contains("bool")
|| type.Contains("string")
|| type.Contains("integer")
|| type.Contains("number"));
}
private AvaloniaList<PropertyClassViewModel> GetRequestClasses(OpenApiRequestBody? requestBody)
{
if (requestBody == null) return [];
if (requestBody.Content.TryGetValue("application/json", out var media))
{
var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response;
var schema = media.Schema;
if (schema == null) return [];
var type = GetSchemaType(media.Schema);
if (IsComponent(type))
{
var componentId = GetComponentId(schema);
var componentSchema = document.Components.Schemas[componentId];
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
WalkSchema(componentSchema, propertyClasses, document);
return propertyClasses;
}
}
return [];
}
private string GetReturnType(OpenApiResponses responses)
{
if (responses.TryGetValue("2XX", out var response)
&& response.Content.TryGetValue("application/json", out var media))
{
var schema = media.Schema;
return GetSchemaType(schema);
}
return "none";
}
public static string GetSchemaType(OpenApiSchema schema)
{
if (schema.Reference != null) return schema.Reference.Id;
if (schema.Type == "object" && schema.AdditionalProperties?.Reference != null) return schema.AdditionalProperties.Reference.Id;
if (schema.Type == "integer" || schema.Type == "number") return $"{schema.Type}:{schema.Format}";
if (schema.Type == "array" && schema.Items.Reference != null) return $"{schema.Items.Reference.Id}[]";
if (schema.Type == "array" && (schema.Items.Type == "integer" || schema.Items.Type == "number")) return $"{schema.Items.Type}:{schema.Items.Format}[]";
if (schema.Type == "array") return $"{schema.Items.Type}[]";
return schema.Type;
}
}
}

View File

@@ -1,12 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.ViewModels
{
public abstract partial class PageBase(string displayName, string icon, int index = 0) : ObservableValidator
{
[ObservableProperty] private string _displayName = displayName;
[ObservableProperty] private string _icon = icon;
[ObservableProperty] private int _index = index;
}
}

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,19 @@
using CommunityToolkit.Mvvm.Input;
using System.Diagnostics;
namespace Needlework.Net.ViewModels.Pages;
public partial class HomeViewModel : PageBase
{
public HomeViewModel() : base("Home", "home", int.MinValue) { }
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo(url) { UseShellExecute = true }
};
process.Start();
}
}

View File

@@ -0,0 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.ViewModels.Pages;
public abstract partial class PageBase(string displayName, string icon, int index = 0) : ObservableValidator
{
[ObservableProperty] private string _displayName = displayName;
[ObservableProperty] private string _icon = icon;
[ObservableProperty] private int _index = index;
}

View File

@@ -0,0 +1,21 @@
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
namespace Needlework.Net.ViewModels.Pages.Websocket;
public class EventViewModel : ObservableObject
{
public string Time { get; }
public string Type { get; }
public string Uri { get; }
public string Key => $"{Time} {Type} {Uri}";
public EventViewModel(EventData eventData)
{
Time = $"{DateTime.Now:HH:mm:ss.fff}";
Type = eventData?.EventType.ToUpper() ?? string.Empty;
Uri = eventData?.Uri ?? string.Empty;
}
}

View File

@@ -0,0 +1,125 @@
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading;
using Websocket.Client;
namespace Needlework.Net.ViewModels.Pages.Websocket;
public partial class WebsocketViewModel : PageBase
{
public ObservableCollection<EventViewModel> EventLog { get; } = [];
public SemaphoreSlim EventLogLock { get; } = new(1, 1);
[NotifyPropertyChangedFor(nameof(FilteredEventLog))]
[ObservableProperty] private string _search = string.Empty;
[ObservableProperty] private bool _isAttach = true;
[ObservableProperty] private bool _isTail = false;
[ObservableProperty] private EventViewModel? _selectedEventLog = null;
private Dictionary<string, EventMessage> _events = [];
public WebsocketClient? Client { get; set; }
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
public WebsocketViewModel() : base("Event Viewer", "plug", -100)
{
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
thread.Start();
}
private void InitializeWebsocket()
{
while (true)
{
try
{
var client = Connector.CreateLcuWebsocketClient();
client.EventReceived.Subscribe(OnMessage);
client.DisconnectionHappened.Subscribe(OnDisconnection);
client.ReconnectionHappened.Subscribe(OnReconnection);
client.Start();
client.Send(new EventMessage(EventRequestType.Subscribe, EventKinds.OnJsonApiEvent));
Client = client;
return;
}
catch (Exception) { }
Thread.Sleep(TimeSpan.FromSeconds(5));
}
}
partial void OnSelectedEventLogChanged(EventViewModel? value)
{
if (value == null) return;
if (_events.TryGetValue(value.Key, out var message))
{
var text = JsonSerializer.Serialize(message, App.JsonSerializerOptions);
if (text.Length >= App.MaxCharacters) WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(text));
else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebsocketViewModel));
}
}
[RelayCommand]
private void Clear()
{
_events.Clear();
EventLog.Clear();
}
private void OnReconnection(ReconnectionInfo info)
{
Trace.WriteLine($"-- Reconnection --\nType{info.Type}");
}
private void OnDisconnection(DisconnectionInfo info)
{
Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}");
Client?.Dispose();
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
thread.Start();
}
private void OnMessage(EventMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(async () =>
{
if (!IsAttach) return;
var line = new EventViewModel(message.Data!);
await EventLogLock.WaitAsync();
try
{
if (EventLog.Count < 1000)
{
EventLog.Add(line);
_events[line.Key] = message;
}
else
{
var _event = EventLog[0];
EventLog.RemoveAt(0);
_events.Remove(_event.Key);
EventLog.Add(line);
_events[line.Key] = message;
}
}
finally
{
EventLogLock.Release();
}
});
}
}

View File

@@ -1,20 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.ViewModels
{
public partial class ParameterViewModel : ObservableObject
{
public string Name { get; }
public string Type { get; }
public bool IsRequired { get; }
[ObservableProperty] private string? _value = null;
public ParameterViewModel(string name, string type, bool isRequired, string? value = null)
{
Name = name;
Type = type;
IsRequired = isRequired;
Value = value;
}
}
}

View File

@@ -1,142 +0,0 @@
using Avalonia.Media;
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using Needlework.Net.Models;
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels
{
public partial class PathOperationViewModel : ObservableObject
{
public string Method { get; }
public SolidColorBrush Color { get; }
public string Path { get; }
public OperationViewModel Operation { get; }
public ProcessInfo? ProcessInfo { get; }
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private string? _responsePath;
[ObservableProperty] private string? _responseStatus;
[ObservableProperty] private string? _responseAuthentication;
[ObservableProperty] private string? _responseUsername;
[ObservableProperty] private string? _responsePassword;
[ObservableProperty] private string? _responseAuthorization;
public PathOperationViewModel(PathOperation pathOperation)
{
Method = pathOperation.Method.ToUpper();
Color = new SolidColorBrush(GetColor(Method));
Path = pathOperation.Path;
Operation = new OperationViewModel(pathOperation.Operation);
ProcessInfo = GetProcessInfo();
if (ProcessInfo != null)
{
ResponsePath = $"https://127.0.0.1:{ProcessInfo.AppPort}{Path}";
var riotAuth = new RiotAuthentication(ProcessInfo.RemotingAuthToken);
ResponseUsername = riotAuth.Username;
ResponsePassword = riotAuth.Password;
ResponseAuthorization = $"Basic {riotAuth.Value}";
}
}
private ProcessInfo? GetProcessInfo()
{
try
{
var processInfo = Connector.GetProcessInfo();
return processInfo;
}
catch (Exception) { }
return null;
}
[RelayCommand]
public async Task SendRequest()
{
try
{
IsBusy = true;
var method = Method switch
{
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
"DELETE" => HttpMethod.Delete,
"HEAD" => HttpMethod.Head,
"PATCH" => HttpMethod.Patch,
"OPTIONS" => HttpMethod.Options,
"TRACE" => HttpMethod.Trace,
_ => throw new Exception("Method is missing.")
};
var processInfo = Connector.GetProcessInfo();
var sb = new StringBuilder(Path);
foreach (var pathParameter in Operation.PathParameters)
{
sb.Replace($"{{{pathParameter.Name}}}", pathParameter.Value);
}
var firstQueryAdded = false;
foreach (var queryParameter in Operation.QueryParameters)
{
if (!string.IsNullOrWhiteSpace(queryParameter.Value))
{
sb.Append(firstQueryAdded ? '&' : '?');
firstQueryAdded = true;
sb.Append($"{queryParameter.Name}={Uri.EscapeDataString(queryParameter.Value)}");
}
}
var uri = sb.ToString();
var requestBody = WeakReferenceMessenger.Default.Send(new ContentRequestMessage(), "EndpointRequestEditor").Response;
var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var response = await Connector.SendAsync(method, uri, content);
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
var responseBytes = await response.Content.ReadAsByteArrayAsync();
var responseBody = responseBytes.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBytes), App.JsonSerializerOptions) : string.Empty;
if (responseBody.Length >= App.MaxCharacters)
{
WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(responseBody));
WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(string.Empty, "EndpointResponseEditor")));
}
else WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(responseBody, "EndpointResponseEditor")));
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode}";
ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{uri}";
ResponseAuthentication = $"Basic {riotAuthentication.Value}";
ResponseUsername = riotAuthentication.Username;
ResponsePassword = riotAuthentication.Password;
}
catch (Exception ex)
{
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5))));
WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(string.Empty, "EndpointResponseEditor")));
}
finally
{
IsBusy = false;
}
}
public static Color GetColor(string method) => method switch
{
"GET" => Avalonia.Media.Color.FromRgb(95, 99, 186),
"POST" => Avalonia.Media.Color.FromRgb(103, 186, 95),
"PUT" => Avalonia.Media.Color.FromRgb(186, 139, 95),
"DELETE" => Avalonia.Media.Color.FromRgb(186, 95, 95),
"HEAD" => Avalonia.Media.Color.FromRgb(136, 95, 186),
"PATCH" => Avalonia.Media.Color.FromRgb(95, 186, 139),
_ => throw new InvalidOperationException("Method does not have assigned color.")
};
}
}

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.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.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.ViewModels
{
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,118 @@
using Avalonia.Media;
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using Needlework.Net.ViewModels.MainWindow;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Shared;
public partial class LcuRequestViewModel : ObservableObject
{
[ObservableProperty] private string? _method = "GET";
[ObservableProperty] private SolidColorBrush _color = new(GetColor("GET"));
[ObservableProperty] private bool _isRequestBusy = false;
[ObservableProperty] private string? _requestPath = null;
[ObservableProperty] private string? _requestBody = null;
[ObservableProperty] private string? _responsePath = null;
[ObservableProperty] private string? _responseStatus = null;
[ObservableProperty] private string? _responseAuthentication = null;
[ObservableProperty] private string? _responseUsername = null;
[ObservableProperty] private string? _responsePassword = null;
[ObservableProperty] private string? _responseAuthorization = null;
[ObservableProperty] private string? _responseBody = null;
public event EventHandler<LcuRequestViewModel>? RequestText;
public event EventHandler<string>? UpdateText;
partial void OnMethodChanged(string? oldValue, string? newValue)
{
if (newValue == null) return;
Color = new(GetColor(newValue));
}
public async Task ExecuteAsync()
{
try
{
IsRequestBusy = true;
if (string.IsNullOrEmpty(RequestPath))
throw new Exception("Path is empty.");
var method = Method switch
{
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
"DELETE" => HttpMethod.Delete,
"HEAD" => HttpMethod.Head,
"PATCH" => HttpMethod.Patch,
"OPTIONS" => HttpMethod.Options,
"TRACE" => HttpMethod.Trace,
_ => throw new Exception("Method is not selected or missing."),
};
var processInfo = ProcessFinder.GetProcessInfo();
RequestText?.Invoke(this, this);
var content = new StringContent(RequestBody ?? string.Empty, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var client = Connector.GetLcuHttpClientInstance();
var response = await client.SendAsync(new(method, RequestPath) { Content = content });
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
var responseBody = await response.Content.ReadAsByteArrayAsync();
var body = responseBody.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), App.JsonSerializerOptions) : string.Empty;
if (body.Length >= App.MaxCharacters)
{
WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(body));
UpdateText?.Invoke(this, string.Empty);
}
else
{
ResponseBody = body;
UpdateText?.Invoke(this, body);
}
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}";
ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{RequestPath}";
ResponseAuthentication = riotAuthentication.Value;
ResponseAuthorization = $"Basic {riotAuthentication.Value}";
ResponseUsername = riotAuthentication.Username;
ResponsePassword = riotAuthentication.Password;
}
catch (Exception ex)
{
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5))));
UpdateText?.Invoke(this, string.Empty);
ResponseStatus = null;
ResponsePath = null;
ResponseAuthentication = null;
ResponseAuthorization = null;
ResponseUsername = null;
ResponsePassword = null;
ResponseBody = null;
}
finally
{
IsRequestBusy = false;
}
}
private static Color GetColor(string method) => method switch
{
"GET" => Avalonia.Media.Color.FromRgb(95, 99, 186),
"POST" => Avalonia.Media.Color.FromRgb(103, 186, 95),
"PUT" => Avalonia.Media.Color.FromRgb(186, 139, 95),
"DELETE" => Avalonia.Media.Color.FromRgb(186, 95, 95),
"HEAD" => Avalonia.Media.Color.FromRgb(136, 95, 186),
"PATCH" => Avalonia.Media.Color.FromRgb(95, 186, 139),
_ => throw new InvalidOperationException("Method does not have assigned color.")
};
}

View File

@@ -1,126 +0,0 @@
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Messages;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading;
using Websocket.Client;
namespace Needlework.Net.ViewModels
{
public partial class WebsocketViewModel : PageBase
{
public ObservableCollection<EventViewModel> EventLog { get; } = [];
public SemaphoreSlim EventLogLock { get; } = new(1, 1);
[NotifyPropertyChangedFor(nameof(FilteredEventLog))]
[ObservableProperty] private string _search = string.Empty;
[ObservableProperty] private bool _isAttach = true;
[ObservableProperty] private bool _isTail = false;
[ObservableProperty] private EventViewModel? _selectedEventLog = null;
private Dictionary<string, EventMessage> _events = [];
public WebsocketClient? Client { get; set; }
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
public WebsocketViewModel() : base("Event Viewer", "plug", -100)
{
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
thread.Start();
}
private void InitializeWebsocket()
{
while (true)
{
try
{
var client = Connector.CreateLcuWebsocketClient();
client.EventReceived.Subscribe(OnMessage);
client.DisconnectionHappened.Subscribe(OnDisconnection);
client.ReconnectionHappened.Subscribe(OnReconnection);
client.Start();
client.Send(new EventMessage(RequestType.Subscribe, EventMessage.Kinds.OnJsonApiEvent));
Client = client;
return;
}
catch (Exception) { }
Thread.Sleep(TimeSpan.FromSeconds(5));
}
}
partial void OnSelectedEventLogChanged(EventViewModel? value)
{
if (value == null) return;
if (_events.TryGetValue(value.Key, out var message))
{
var text = JsonSerializer.Serialize(message, App.JsonSerializerOptions);
if (text.Length >= App.MaxCharacters) WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(text));
else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebsocketViewModel));
}
}
[RelayCommand]
private void Clear()
{
_events.Clear();
EventLog.Clear();
}
private void OnReconnection(ReconnectionInfo info)
{
Trace.WriteLine($"-- Reconnection --\nType{info.Type}");
}
private void OnDisconnection(DisconnectionInfo info)
{
Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}");
Client?.Dispose();
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
thread.Start();
}
private void OnMessage(EventMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(async () =>
{
if (!IsAttach) return;
var line = new EventViewModel(message.Data!);
await EventLogLock.WaitAsync();
try
{
if (EventLog.Count < 1000)
{
EventLog.Add(line);
_events[line.Key] = message;
}
else
{
var _event = EventLog[0];
EventLog.RemoveAt(0);
_events.Remove(_event.Key);
EventLog.Add(line);
_events[line.Key] = message;
}
}
finally
{
EventLogLock.Release();
}
});
}
}
}

View File

@@ -1,72 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using AvaloniaEdit;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Extensions;
using Needlework.Net.Messages;
using Needlework.Net.ViewModels;
using TextMateSharp.Grammars;
namespace Needlework.Net.Views;
public partial class EndpointView : UserControl, IRecipient<EditorUpdateMessage>, IRecipient<ContentRequestMessage>
{
private TextEditor? _requestEditor;
private TextEditor? _responseEditor;
public EndpointView()
{
InitializeComponent();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var vm = (EndpointViewModel)DataContext!;
_requestEditor = this.FindControl<TextEditor>("EndpointRequestEditor");
_responseEditor = this.FindControl<TextEditor>("EndpointResponseEditor");
_requestEditor?.ApplyJsonEditorSettings();
_responseEditor?.ApplyJsonEditorSettings();
WeakReferenceMessenger.Default.Register<EditorUpdateMessage>(this);
WeakReferenceMessenger.Default.Register<ContentRequestMessage, string>(this, "EndpointRequestEditor");
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
WeakReferenceMessenger.Default.UnregisterAll(this);
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)
{
var registryOptions = new RegistryOptions(
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
}
public void Receive(EditorUpdateMessage message)
{
switch (message.Value.Key)
{
case "EndpointRequestEditor":
_requestEditor!.Text = message.Value.Text;
break;
case "EndpointResponseEditor":
_responseEditor!.Text = message.Value.Text;
break;
default:
break;
}
}
public void Receive(ContentRequestMessage message)
{
message.Reply(_requestEditor!.Text);
}
}

View File

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

View File

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

View File

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

View File

@@ -7,9 +7,9 @@
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:i="https://github.com/projektanker/icons.avalonia"
xmlns:vm="using:Needlework.Net.ViewModels"
xmlns:vm="using:Needlework.Net.ViewModels.MainWindow"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.MainWindow"
x:Class="Needlework.Net.Views.MainWindow.MainWindowView"
x:DataType="vm:MainWindowViewModel"
Title="Needlework.Net"
Icon="/Assets/app.ico"
@@ -37,6 +37,8 @@
<ui:NavigationView AlwaysShowHeader="False"
PaneDisplayMode="Left"
IsSettingsVisible="False"
IsPaneOpen="False"
OpenPaneLength="200"
Grid.Row="1"
MenuItemsSource="{Binding MenuItems}"
SelectedItem="{Binding SelectedMenuItem}">

View File

@@ -1,10 +1,10 @@
using FluentAvalonia.UI.Windowing;
namespace Needlework.Net.Views;
namespace Needlework.Net.Views.MainWindow;
public partial class MainWindow : AppWindow
public partial class MainWindowView : AppWindow
{
public MainWindow()
public MainWindowView()
{
InitializeComponent();

View File

@@ -0,0 +1,64 @@
using FluentAvalonia.UI.Controls;
using Needlework.Net.Services;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace Needlework.Net.Views.MainWindow;
public class OopsiesDialog : IDialog, IDisposable
{
private bool _isDisposing;
private string? _text;
private ContentDialog _dialog;
public OopsiesDialog()
{
_dialog = new ContentDialog
{
PrimaryButtonText = "Open",
CloseButtonText = "Cancel",
Title = "Oopsies",
Content = "This response is too large to handle for performance reasons.\nIt can be viewed in an external editor or viewer.",
IsPrimaryButtonEnabled = true,
IsSecondaryButtonEnabled = false,
DefaultButton = ContentDialogButton.Primary
};
_dialog.PrimaryButtonClick += OnPrimaryButtonClick;
}
public async Task<ContentDialogResult> ShowAsync(object data)
{
_text = (string)data;
var result = await _dialog.ShowAsync();
return result;
}
private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
var temp = Path.GetTempFileName().Replace(".tmp", ".json");
File.WriteAllText(temp, _text);
Process.Start("explorer", "\"" + temp + "\"");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_isDisposing)
{
if (disposing)
{
_text = null;
_dialog.PrimaryButtonClick -= OnPrimaryButtonClick;
}
_isDisposing = true;
}
}
}

View File

@@ -1,65 +0,0 @@
using FluentAvalonia.UI.Controls;
using Needlework.Net.Services;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace Needlework.Net.Views
{
public class OopsiesDialog : IDialog, IDisposable
{
private bool _isDisposing;
private string? _text;
private ContentDialog _dialog;
public OopsiesDialog()
{
_dialog = new ContentDialog
{
PrimaryButtonText = "Open",
CloseButtonText = "Cancel",
Title = "Oopsies",
Content = "This response is too large to handle for performance reasons.\nIt can be viewed in an external editor or viewer.",
IsPrimaryButtonEnabled = true,
IsSecondaryButtonEnabled = false,
DefaultButton = ContentDialogButton.Primary
};
_dialog.PrimaryButtonClick += OnPrimaryButtonClick;
}
public async Task<ContentDialogResult> ShowAsync(object data)
{
_text = (string)data;
var result = await _dialog.ShowAsync();
return result;
}
private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
var temp = Path.GetTempFileName().Replace(".tmp", ".json");
File.WriteAllText(temp, _text);
Process.Start("explorer", "\"" + temp + "\"");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_isDisposing)
{
if (disposing)
{
_text = null;
_dialog.PrimaryButtonClick -= OnPrimaryButtonClick;
}
_isDisposing = true;
}
}
}
}

View File

@@ -2,11 +2,11 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels"
xmlns:vm="using:Needlework.Net.ViewModels.Pages"
xmlns:controls="using:Needlework.Net.Controls"
xmlns:i="https://github.com/projektanker/icons.avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.AboutView"
x:Class="Needlework.Net.Views.Pages.AboutView"
x:DataType="vm:AboutViewModel">
<UserControl.Styles>
<Style Selector="Button">

View File

@@ -1,6 +1,6 @@
using Avalonia.Controls;
namespace Needlework.Net.Views;
namespace Needlework.Net.Views.Pages;
public partial class AboutView : UserControl
{

View File

@@ -5,10 +5,10 @@
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
xmlns:vm="using:Needlework.Net.ViewModels"
xmlns:vm="using:Needlework.Net.ViewModels.Pages"
xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.ConsoleView"
x:Class="Needlework.Net.Views.Pages.ConsoleView"
x:DataType="vm:ConsoleViewModel">
<controls:BusyArea IsBusy="{Binding IsBusy}"
BusyText="Loading...">
@@ -19,13 +19,13 @@
<StackPanel Margin="0 0 0 16">
<Grid RowDefinitions="auto" ColumnDefinitions="auto,*,auto">
<ComboBox ItemsSource="{Binding RequestMethods}"
SelectedItem="{Binding RequestMethodSelected}"
SelectedItem="{Binding LcuRequest.Method}"
Margin="0 0 8 0"
Grid.Row="0"
Grid.Column="0"/>
<AutoCompleteBox
ItemsSource="{Binding RequestPaths}"
Text="{Binding RequestPath}"
Text="{Binding LcuRequest.RequestPath}"
MaxDropDownHeight="400"
FilterMode="StartsWith"
Grid.Row="0"
@@ -49,7 +49,7 @@
<TextBox IsReadOnly="True"
Grid.Row="0"
Grid.Column="0"
Text="{Binding ResponsePath}"/>
Text="{Binding LcuRequest.ResponsePath}"/>
<avaloniaEdit:TextEditor
Name="RequestEditor"
Text=""
@@ -69,7 +69,7 @@
<StackPanel Orientation="Horizontal"
Grid.Row="0"
Grid.Column="0">
<Button Content="{Binding ResponseStatus}"
<Button Content="{Binding LcuRequest.ResponseStatus}"
FontSize="12"
VerticalAlignment="Center"/>
</StackPanel>

View File

@@ -1,17 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using AvaloniaEdit;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Extensions;
using Needlework.Net.Messages;
using Needlework.Net.ViewModels;
using Needlework.Net.ViewModels.Pages;
using TextMateSharp.Grammars;
namespace Needlework.Net.Views;
namespace Needlework.Net.Views.Pages;
public partial class ConsoleView : UserControl, IRecipient<ResponseUpdatedMessage>, IRecipient<ContentRequestMessage>
public partial class ConsoleView : UserControl
{
private TextEditor? _responseEditor;
private TextEditor? _requestEditor;
@@ -21,36 +18,39 @@ public partial class ConsoleView : UserControl, IRecipient<ResponseUpdatedMessag
InitializeComponent();
}
public void Receive(ResponseUpdatedMessage message)
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
_responseEditor!.Text = message.Value;
}
public void Receive(ContentRequestMessage message)
{
message.Reply(_requestEditor!.Text);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
base.OnAttachedToVisualTree(e);
_responseEditor = this.FindControl<TextEditor>("ResponseEditor");
_requestEditor = this.FindControl<TextEditor>("RequestEditor");
_responseEditor?.ApplyJsonEditorSettings();
_requestEditor?.ApplyJsonEditorSettings();
WeakReferenceMessenger.Default.Register<ResponseUpdatedMessage, string>(this, nameof(ConsoleViewModel));
WeakReferenceMessenger.Default.Register<ContentRequestMessage, string>(this, "ConsoleRequestEditor");
var vm = (ConsoleViewModel)DataContext!;
vm.LcuRequest.RequestText += LcuRequest_RequestText; ;
vm.LcuRequest.UpdateText += LcuRequest_UpdateText;
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
}
private void LcuRequest_RequestText(object? sender, ViewModels.Shared.LcuRequestViewModel e)
{
e.RequestBody = _requestEditor!.Text;
}
private void LcuRequest_UpdateText(object? sender, string e)
{
_responseEditor!.Text = e;
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
WeakReferenceMessenger.Default.UnregisterAll(this);
var vm = (ConsoleViewModel)DataContext!;
vm.LcuRequest.RequestText -= LcuRequest_RequestText;
vm.LcuRequest.UpdateText -= LcuRequest_UpdateText;
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)

View File

@@ -3,11 +3,11 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:vm="using:Needlework.Net.ViewModels"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:avalonEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.EndpointView"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointView"
x:DataType="vm:EndpointViewModel">
<UserControl.Styles>
<Style Selector="DataGrid">
@@ -59,8 +59,8 @@
VerticalAlignment="Center"
TextAlignment="Center"
Margin="0 0 8 0"
Text="{Binding Method}"
Background="{Binding Color}"
Text="{Binding LcuRequest.Value.Method}"
Background="{Binding LcuRequest.Value.Color}"
FontSize="8"
Width="50"
Padding="10 2 10 2"
@@ -88,14 +88,14 @@
ColumnDefinitions="auto,*,auto">
<TextBox Grid.Row="0"
Grid.Column="0"
Text="{Binding SelectedPathOperation.Method}"
Text="{Binding SelectedPathOperation.LcuRequest.Value.Method}"
FontSize="12"
IsReadOnly="True"
Margin="0 0 8 0"/>
<TextBox Grid.Row="0"
Grid.Column="1"
FontSize="12"
Text="{Binding SelectedPathOperation.ResponsePath}"
Text="{Binding SelectedPathOperation.LcuRequest.Value.ResponsePath}"
IsReadOnly="True"/>
<StackPanel Grid.Row="0"
Grid.Column="2"
@@ -189,7 +189,7 @@
Grid.Column="1"
Margin="0 0 0 8"
IsReadOnly="True"
Text="{Binding SelectedPathOperation.ResponseUsername}" />
Text="{Binding SelectedPathOperation.LcuRequest.Value.ResponseUsername}" />
<TextBlock FontSize="12"
Grid.Row="1"
Grid.Column="0"
@@ -201,7 +201,7 @@
Grid.Column="1"
Margin="0 0 0 8"
IsReadOnly="True"
Text="{Binding SelectedPathOperation.ResponsePassword}"/>
Text="{Binding SelectedPathOperation.LcuRequest.Value.ResponsePassword}"/>
<TextBlock FontSize="12"
Grid.Row="2"
Grid.Column="0"
@@ -212,7 +212,7 @@
Grid.Row="2"
Grid.Column="1"
IsReadOnly="True"
Text="{Binding SelectedPathOperation.ResponseAuthorization}"/>
Text="{Binding SelectedPathOperation.LcuRequest.Value.ResponseAuthorization}"/>
</Grid>
</TabItem>
<TabItem Header="Schemas">
@@ -304,22 +304,25 @@
FontSize="10"
Padding="12 4 12 4"
Classes="Flat"
Content="{Binding SelectedPathOperation.ResponseStatus}"/>
Content="{Binding SelectedPathOperation.LcuRequest.Value.ResponseStatus}"/>
</StackPanel>
<Grid Grid.Row="1" Grid.Column="4">
<TabControl>
<TabItem Header="Preview">
<avalonEdit:TextEditor
Name="EndpointResponseEditor"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible"
ShowLineNumbers="True"
IsReadOnly="True"
Text=""
FontSize="12"/>
</TabItem>
</TabControl>
<controls:BusyArea BusyText="Loading..."
IsBusy="{Binding SelectedPathOperation.IsBusy}">
<TabControl>
<TabItem Header="Preview">
<avalonEdit:TextEditor
Name="EndpointResponseEditor"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible"
ShowLineNumbers="True"
IsReadOnly="True"
Text=""
FontSize="12"/>
</TabItem>
</TabControl>
</controls:BusyArea>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,94 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Styling;
using AvaloniaEdit;
using Needlework.Net.Extensions;
using Needlework.Net.ViewModels.Pages.Endpoints;
using Needlework.Net.ViewModels.Shared;
using TextMateSharp.Grammars;
namespace Needlework.Net.Views.Pages.Endpoints;
public partial class EndpointView : UserControl
{
private TextEditor? _requestEditor;
private TextEditor? _responseEditor;
private LcuRequestViewModel? _lcuRequestVm;
public EndpointView()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_requestEditor = this.FindControl<TextEditor>("EndpointRequestEditor");
_responseEditor = this.FindControl<TextEditor>("EndpointResponseEditor");
_requestEditor?.ApplyJsonEditorSettings();
_responseEditor?.ApplyJsonEditorSettings();
var vm = (EndpointViewModel)DataContext!;
vm.PathOperationSelected += Vm_PathOperationSelected;
if (vm.SelectedPathOperation != null)
{
_lcuRequestVm = vm.SelectedPathOperation.LcuRequest.Value;
vm.SelectedPathOperation.LcuRequest.Value.RequestText += LcuRequest_RequestText;
vm.SelectedPathOperation.LcuRequest.Value.UpdateText += LcuRequest_UpdateText;
}
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
}
private void Vm_PathOperationSelected(object? sender, string e)
{
var vm = (EndpointViewModel)DataContext!;
if (vm.SelectedPathOperation != null)
{
_requestEditor!.Text = e;
if (_lcuRequestVm != null)
{
_lcuRequestVm.RequestText -= LcuRequest_RequestText;
_lcuRequestVm.UpdateText -= LcuRequest_UpdateText;
}
vm.SelectedPathOperation.LcuRequest.Value.RequestText += LcuRequest_RequestText;
vm.SelectedPathOperation.LcuRequest.Value.UpdateText += LcuRequest_UpdateText;
_lcuRequestVm = vm.SelectedPathOperation.LcuRequest.Value;
_responseEditor!.Text = vm.SelectedPathOperation.LcuRequest.Value.ResponseBody ?? string.Empty;
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
var vm = (EndpointViewModel)DataContext!;
vm.PathOperationSelected -= Vm_PathOperationSelected;
if (_lcuRequestVm != null)
{
_lcuRequestVm.RequestText -= LcuRequest_RequestText;
_lcuRequestVm.UpdateText -= LcuRequest_UpdateText;
_lcuRequestVm = null;
}
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)
{
var registryOptions = new RegistryOptions(
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
}
private void LcuRequest_RequestText(object? sender, LcuRequestViewModel e)
{
e.RequestBody = _requestEditor!.Text;
}
private void LcuRequest_UpdateText(object? sender, string e)
{
_responseEditor!.Text = e;
}
}

View File

@@ -2,12 +2,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:avalonEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:i="https://github.com/projektanker/icons.avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.EndpointsContainerView"
x:DataType="vm:EndpointsContainerViewModel">
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsNavigationView"
x:DataType="vm:EndpointsNavigationViewModel">
<Grid RowDefinitions="auto,*"
ColumnDefinitions="*"
Margin="16">

View File

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

View File

@@ -0,0 +1,35 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="https://github.com/projektanker/icons.avalonia"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsTabView"
x:DataType="vm:EndpointsTabViewModel">
<controls:BusyArea IsBusy="{Binding IsBusy}"
BusyText="Loading...">
<Grid>
<ui:TabView TabItems="{Binding Endpoints}"
AddTabButtonCommand="{Binding AddEndpointCommand}"
TabCloseRequested="TabView_TabCloseRequested">
<ui:TabView.TabItemTemplate>
<DataTemplate DataType="vm:EndpointItem">
<ui:TabViewItem Header="{Binding Header}"
IconSource="{Binding IconSource}"
IsSelected="{Binding Selected}"
Content="{Binding}">
<ui:TabViewItem.ContentTemplate>
<DataTemplate DataType="vm:EndpointItem">
<ContentControl Content="{Binding Content}"/>
</DataTemplate>
</ui:TabViewItem.ContentTemplate>
</ui:TabViewItem>
</DataTemplate>
</ui:TabView.TabItemTemplate>
</ui:TabView>
</Grid>
</controls:BusyArea>
</UserControl>

View File

@@ -0,0 +1,19 @@
using Avalonia.Controls;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System.Collections;
namespace Needlework.Net.Views.Pages.Endpoints;
public partial class EndpointsTabView : UserControl
{
public EndpointsTabView()
{
InitializeComponent();
}
private void TabView_TabCloseRequested(FluentAvalonia.UI.Controls.TabView sender, FluentAvalonia.UI.Controls.TabViewTabCloseRequestedEventArgs args)
{
if (args.Tab.Content is EndpointItem item)
((IList)sender.TabItems).Remove(item);
}
}

View File

@@ -2,15 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Name="EndpointsControl"
x:Class="Needlework.Net.Views.EndpointsView"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsView"
x:DataType="vm:EndpointsViewModel">
<controls:BusyArea IsBusy="{Binding IsBusy}"
BusyText="Loading...">
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
<TextBox Watermark="Search" Margin="0 4" Text="{Binding Search}" Grid.Row="1" Grid.Column="0"/>
<ScrollViewer Grid.Row="2" Grid.Column="0">
<ItemsRepeater ItemsSource="{Binding Query}">
@@ -28,5 +26,4 @@
</ItemsRepeater>
</ScrollViewer>
</Grid>
</controls:BusyArea>
</UserControl>

View File

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

View File

@@ -2,10 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels"
xmlns:vm="using:Needlework.Net.ViewModels.Pages"
xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.HomeView"
x:Class="Needlework.Net.Views.Pages.HomeView"
x:DataType="vm:HomeViewModel">
<!-- TOP LEVEL -->
<ScrollViewer>

View File

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

View File

@@ -3,9 +3,9 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:vm="using:Needlework.Net.ViewModels"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Websocket"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.WebsocketView"
x:Class="Needlework.Net.Views.Pages.WebsocketView"
x:DataType="vm:WebsocketViewModel">
<Grid RowDefinitions="*,auto,*" Margin="16">
<Border Grid.Row="0"

View File

@@ -6,11 +6,11 @@ using AvaloniaEdit;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Extensions;
using Needlework.Net.Messages;
using Needlework.Net.ViewModels;
using Needlework.Net.ViewModels.Pages.Websocket;
using System;
using TextMateSharp.Grammars;
namespace Needlework.Net.Views;
namespace Needlework.Net.Views.Pages;
public partial class WebsocketView : UserControl, IRecipient<ResponseUpdatedMessage>
{

View File

@@ -4,7 +4,14 @@
Needlework.Net is an open-source helper tool for the LCU that provides documented endpoints and can send requests without any code setup. Created using .NET! 🌠
# Features
- Interactive OpenAPI documentation
- REST data transfer console
- WebSocket event data viewer
# Requirements
- Windows x64
- .NET 8 runtime. [It can be downloaded here.](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.7-windows-x64-installer)
@@ -21,6 +28,7 @@ Needlework.Net is an open-source helper tool for the LCU that provides documente
## Credits
### GrrrLCU
A simple wrapper for the LCU.
- [Repository](https://github.com/BlossomiShymae/GrrrLCU)