22 Commits

Author SHA1 Message Date
estrogen elf
adc8b0c0f1 feat: update README.md 2025-05-30 13:46:22 -05:00
estrogen elf
be7d575b48 fix: insecure SSL for game client api 2025-05-30 13:10:57 -05:00
estrogen elf
f9dd654b6a fix: lcu schema update 2025-05-30 12:43:55 -05:00
estrogen elf
57d3eb4172 feat: add game client channel link 2025-05-30 12:15:46 -05:00
estrogen elf
ce2336ab4d fix: adjust margin for console 2025-05-30 12:13:50 -05:00
estrogen elf
9a76e1af4a feat: add AoshiW to about page 2025-05-30 12:08:39 -05:00
estrogen elf
6f0126863b feat: increment version 2025-05-29 23:10:40 -05:00
estrogen elf
826134888e feat: update about page 2025-05-29 23:07:39 -05:00
estrogen elf
ef16642c04 fix: prevent closing only tab 2025-05-29 15:32:24 -05:00
estrogen elf
a5f49c48b8 refactor: remove unused code 2025-05-29 15:13:50 -05:00
estrogen elf
1364cdc38c feat: Add Game Client to Endpoints 2025-05-29 15:10:00 -05:00
estrogen elf
c51f20a324 refactor: use data source, remove data loading in ctor of view models 2025-05-25 12:56:40 -05:00
estrogen elf
6d1acee8df refactor: logging 2025-05-24 13:26:38 -05:00
estrogen elf
375d5a2ff8 Update app preview 2025-05-05 03:21:52 -05:00
estrogen elf
2aa77f3e02 Update picture 2025-05-05 03:05:32 -05:00
estrogen elf
576863bd72 Update year 2025-05-05 03:02:02 -05:00
estrogen elf
68e5abd1d1 Add copy Swagger URL context flyout 2025-05-05 02:55:36 -05:00
estrogen elf
b18f425257 Match sorting with that of https://swagger.dysolix.dev 2025-05-05 00:19:29 -05:00
estrogen elf
5ebed22ae3 Add LCU Schema build check 2025-05-04 23:56:13 -05:00
estrogen elf
dc44cf72df Change plugin filtering 2025-05-04 20:52:28 -05:00
estrogen elf
01cb8886c6 Increment version 2025-05-03 20:48:44 -05:00
estrogen elf
38e4a64bb8 Add Mica theme for Windows 11 and newer 2025-05-03 19:19:59 -05:00
42 changed files with 832 additions and 423 deletions

View File

@@ -15,6 +15,7 @@
<sty:FluentAvaloniaTheme PreferSystemTheme="False" PreferUserAccentColor="False" />
<materialIcons:MaterialIconStyles />
<StyleInclude Source="Controls/Card.axaml"/>
<StyleInclude Source="Controls/UserCard.axaml"/>
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
</Application.Styles>

View File

@@ -40,8 +40,6 @@ public partial class App(IServiceProvider serviceProvider) : Application
MainWindow = desktop.MainWindow;
}
base.OnFrameworkInitializationCompleted();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,71 @@
<Styles 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:controls="using:Needlework.Net.Controls">
<Design.PreviewWith>
<WrapPanel>
<controls:UserCard
Width="300"
Height="400"
UserImage="/Assets/Users/blossomishymae.png"
UserName="estrogen elf"
UserGithub="BlossomiShymae">
Needlework.Net is the .NET rewrite of Needlework. This tool was made to help others with LCU and Game Client development. Feel free to ask any questions
or help contribute to the project! Made with love. 💜
</controls:UserCard>
</WrapPanel>
</Design.PreviewWith>
<Style Selector="controls|UserCard">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<Border CornerRadius="16,16,16,16"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
Margin="0 50 0 0"
Padding="16 66 16 16">
<Grid RowDefinitions="auto,auto,auto"
ColumnDefinitions="*">
<TextBlock Grid.Row="0"
Grid.Column="0"
Theme="{StaticResource SubtitleTextBlockStyle}"
Margin="0 0 0 4"
Text="{TemplateBinding UserName}"/>
<Grid Grid.Row="1"
Grid.Column="0"
RowDefinitions="*"
ColumnDefinitions="auto,auto"
Margin="0 0 0 16">
<Button Grid.Row="0"
Grid.Column="0"
Theme="{StaticResource TransparentButton}"
FontSize="20"
Name="PART_GithubButton">
<i:Icon Value="fa-github"/>
</Button>
<TextBlock Grid.Row="0"
Grid.Column="1"
Margin="8 0 0 0"
VerticalAlignment="Center"
Text="{TemplateBinding UserGithub}"/>
</Grid>
<TextBlock Grid.Row="2"
Grid.Column="0"
TextWrapping="WrapWithOverflow"
Text="{TemplateBinding Content}"/>
</Grid>
</Border>
<Border CornerRadius="100"
Width="100"
Height="100"
Margin="{TemplateBinding UserImageMargin}"
ClipToBounds="True">
<Image Source="{TemplateBinding UserImage}"
RenderOptions.BitmapInterpolationMode="HighQuality"/>
</Border>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

View File

@@ -0,0 +1,91 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Media;
using System.Diagnostics;
namespace Needlework.Net.Controls;
[TemplatePart("PART_GithubButton", typeof(Button))]
public partial class UserCard : ContentControl
{
private Button? _githubButton;
public UserCard()
{
UserImageMargin = new(0, !double.IsNaN(Height) ? 100 - Height : 0, 0, 0);
}
public static readonly StyledProperty<IImage?> UserImageProperty =
AvaloniaProperty.Register<UserCard, IImage?>(nameof(UserImage), defaultValue: null);
public IImage? UserImage
{
get { return GetValue(UserImageProperty); }
set { SetValue(UserImageProperty, value); }
}
public static readonly StyledProperty<string?> UserNameProperty =
AvaloniaProperty.Register<UserCard, string?>(nameof(UserName), defaultValue: null);
public string? UserName
{
get { return GetValue(UserNameProperty); }
set { SetValue(UserNameProperty, value); }
}
public static readonly StyledProperty<string?> UserGithubProperty =
AvaloniaProperty.Register<UserCard, string?>(nameof(UserGithub), defaultValue: null);
public string? UserGithub
{
get { return GetValue(UserGithubProperty); }
set { SetValue(UserGithubProperty, value); }
}
public static readonly DirectProperty<UserCard, Thickness> UserImageMarginProperty =
AvaloniaProperty.RegisterDirect<UserCard, Thickness>(nameof(UserImageMargin), o => o.UserImageMargin);
private Thickness _userImageMargin = new(0, 0, 0, 0);
public Thickness UserImageMargin
{
get { return _userImageMargin; }
private set { SetAndRaise(UserImageMarginProperty, ref _userImageMargin, value); }
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
SizeChanged += UserCard_SizeChanged;
if (_githubButton != null)
{
_githubButton.Click -= GithubButton_Click;
}
_githubButton = e.NameScope.Find("PART_GithubButton") as Button;
if (_githubButton != null)
{
_githubButton.Click += GithubButton_Click;
}
}
private void UserCard_SizeChanged(object? sender, SizeChangedEventArgs e)
{
UserImageMargin = new(0, !double.IsNaN(e.NewSize.Height) ? 100 - e.NewSize.Height : 0, 0, 0);
}
private void GithubButton_Click(object? sender, RoutedEventArgs e)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo($"https://github.com/{UserGithub}") { UseShellExecute = true }
};
process.Start();
}
}

View File

@@ -0,0 +1,60 @@
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Readers;
using Needlework.Net.Models;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace Needlework.Net
{
public class DataSource
{
private readonly ILogger<DataSource> _logger;
private readonly HttpClient _httpClient;
private Document? _lcuSchemaDocument;
private Document? _lolClientDocument;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new();
public DataSource(HttpClient httpClient, ILogger<DataSource> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<Document> GetLcuSchemaDocumentAsync()
{
await _taskCompletionSource.Task;
return _lcuSchemaDocument ?? throw new InvalidOperationException();
}
public async Task<Document> GetLolClientDocumentAsync()
{
await _taskCompletionSource.Task;
return _lolClientDocument ?? throw new InvalidOperationException();
}
public async Task InitializeAsync()
{
try
{
var reader = new OpenApiStreamReader();
var lcuSchemaStream = await _httpClient.GetStreamAsync("https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json");
var lcuSchemaRaw = reader.Read(lcuSchemaStream, out var _);
_lcuSchemaDocument = new Document(lcuSchemaRaw);
var lolClientStream = await _httpClient.GetStreamAsync("https://raw.githubusercontent.com/AlsoSylv/Irelia/refs/heads/master/schemas/game_schema.json");
var lolClientRaw = reader.Read(lolClientStream, out var _);
_lolClientDocument = new Document(lolClientRaw);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize DataSource");
}
finally
{
_taskCompletionSource.SetResult(true);
}
}
}
}

27
Needlework.Net/Logger.cs Normal file
View File

@@ -0,0 +1,27 @@
using Microsoft.Extensions.Logging;
using Serilog;
using System;
using System.IO;
using System.Reflection;
namespace Needlework.Net
{
public static class Logger
{
public static void Setup(ILoggingBuilder builder)
{
var logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File("Logs/debug-", rollingInterval: RollingInterval.Day, shared: true)
.CreateLogger();
logger.Debug("NeedleworkDotNet version: {Version}", Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0");
logger.Debug("OS description: {Description}", System.Runtime.InteropServices.RuntimeInformation.OSDescription);
builder.AddSerilog(logger);
}
public static void LogFatal(UnhandledExceptionEventArgs e)
{
File.WriteAllText($"Logs/fatal-{DateTime.Now:HHmmssfff}", e.ExceptionObject.ToString());
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Models;
namespace Needlework.Net.Models;
public class OpenApiDocumentWrapper
public class Document
{
internal OpenApiDocument OpenApiDocument { get; }
@@ -13,7 +14,7 @@ public class OpenApiDocumentWrapper
public List<string> Paths => [.. OpenApiDocument.Paths.Keys];
public OpenApiDocumentWrapper(OpenApiDocument openApiDocument)
public Document(OpenApiDocument openApiDocument)
{
OpenApiDocument = openApiDocument;
var plugins = new SortedDictionary<string, List<PathOperation>>();
@@ -35,10 +36,10 @@ public class OpenApiDocumentWrapper
{
pluginsKey = "default";
if (plugins.TryGetValue(pluginsKey, out var p))
p.Add(new(method.ToString(), path, operation));
p.Add(new(method.ToString(), path, pluginsKey, operation));
else
{
operations.Add(new(method.ToString(), path, operation));
operations.Add(new(method.ToString(), path, pluginsKey, operation));
plugins[pluginsKey] = operations;
}
}
@@ -46,19 +47,16 @@ public class OpenApiDocumentWrapper
{
foreach (var tag in operation.Tags)
{
var lowercaseTag = tag.Name.ToLower();
if (lowercaseTag == "plugins")
if (tag.Name == "plugins")
continue;
else if (lowercaseTag.Contains("plugin "))
pluginsKey = lowercaseTag.Replace("plugin ", "");
else
pluginsKey = lowercaseTag;
pluginsKey = tag.Name;
if (plugins.TryGetValue(pluginsKey, out var p))
p.Add(new(method.ToString(), path, operation));
p.Add(new(method.ToString(), path, tag.Name, operation));
else
{
operations.Add(new(method.ToString(), path, operation));
operations.Add(new(method.ToString(), path, tag.Name, operation));
plugins[pluginsKey] = operations;
}
}
@@ -66,6 +64,10 @@ public class OpenApiDocumentWrapper
}
}
plugins = new(plugins.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.OrderBy(x => x.Path).ToList()));
Plugins = plugins;
}
}

View File

@@ -2,4 +2,4 @@ using Microsoft.OpenApi.Models;
namespace Needlework.Net.Models;
public record PathOperation(string Method, string Path, OpenApiOperation Operation);
public record PathOperation(string Method, string Path, string Tag, OpenApiOperation Operation);

View File

@@ -1,23 +0,0 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
namespace Needlework.Net.Models;
public static class Resources
{
/// <summary>
/// Get the OpenApi document of the LCU schema. Provided by dysolix.
/// </summary>
/// <param name="httpClient"></param>
/// <returns></returns>
public static async Task<OpenApiDocument> GetOpenApiDocumentAsync(HttpClient httpClient)
{
var stream = await httpClient.GetStreamAsync("https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json");
var document = new OpenApiStreamReader().Read(stream, out var _);
return document;
}
}

View File

@@ -0,0 +1,10 @@
namespace Needlework.Net.Models
{
public class SystemBuild
{
public string Branch { get; set; } = string.Empty;
public string Patchline { get; set; } = string.Empty;
public string PatchlineVisibleName { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
}
}

View File

@@ -11,7 +11,7 @@
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>NeedleworkDotNet</AssemblyName>
<AssemblyVersion>0.10.0.0</AssemblyVersion>
<AssemblyVersion>0.12.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
</PropertyGroup>
@@ -48,6 +48,13 @@
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Remove="Utilities\**" />
<Compile Remove="Utilities\**" />
<EmbeddedResource Remove="Utilities\**" />
<None Remove="Utilities\**" />
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Views\AboutView.axaml" />
</ItemGroup>
@@ -69,6 +76,5 @@
<ItemGroup>
<Folder Include="Assets\Users\" />
<Folder Include="Utilities\" />
</ItemGroup>
</Project>

View File

@@ -4,12 +4,12 @@ using Needlework.Net.Extensions;
using Needlework.Net.Services;
using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages;
using Needlework.Net.ViewModels.Pages.Endpoints;
using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome;
using Serilog;
using System;
using System.IO;
using System.Reflection;
using System.Net.Http;
using System.Threading.Tasks;
namespace Needlework.Net;
@@ -32,11 +32,23 @@ class Program
{
IconProvider.Current
.Register<FontAwesomeIconProvider>();
var services = BuildServices();
Task.Run(async () => await InitializeDataSourceAsync(services));
return AppBuilder.Configure(() => new App(BuildServices()))
return AppBuilder.Configure(() => new App(services))
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
.LogToTrace()
.With(new Win32PlatformOptions
{
CompositionMode = [Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition]
});
}
private static async Task InitializeDataSourceAsync(IServiceProvider services)
{
var dataSource = services.GetRequiredService<DataSource>();
await dataSource.InitializeAsync();
}
private static IServiceProvider BuildServices()
@@ -45,16 +57,18 @@ class Program
builder.AddSingleton<MainWindowViewModel>();
builder.AddSingleton<DialogService>();
builder.AddSingleton<DataSource>();
builder.AddSingletonsFromAssemblies<PageBase>();
builder.AddHttpClient();
var logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File("Logs/NeedleworkDotNet.log", rollingInterval: RollingInterval.Day, shared: true)
.CreateLogger();
logger.Debug("NeedleworkDotNet version: {Version}", Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0");
logger.Debug("OS description: {Description}", System.Runtime.InteropServices.RuntimeInformation.OSDescription);
builder.AddLogging(builder => builder.AddSerilog(logger));
builder.AddHttpClient(nameof(EndpointsTabViewModel)).ConfigurePrimaryHttpMessageHandler(() => // Insecure SSL for Game Client API
{
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
return handler;
});
builder.AddLogging(Logger.Setup);
var services = builder.BuildServiceProvider();
return services;
@@ -62,6 +76,6 @@ class Program
private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
File.WriteAllText($"errorlog-{DateTime.Now:HHmmssfff}", e.ExceptionObject.ToString());
Logger.LogFatal(e);
}
}

View File

@@ -1,10 +1,10 @@
using Avalonia.Collections;
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Needlework.Net.Messages;
using Needlework.Net.Models;
using Needlework.Net.Services;
@@ -18,26 +18,28 @@ using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
namespace Needlework.Net.ViewModels.MainWindow;
public partial class MainWindowViewModel
: ObservableObject, IRecipient<DataRequestMessage>, IRecipient<HostDocumentRequestMessage>, IRecipient<InfoBarUpdateMessage>, IRecipient<OopsiesDialogRequestedMessage>
: ObservableObject, IRecipient<InfoBarUpdateMessage>, IRecipient<OopsiesDialogRequestedMessage>
{
public IAvaloniaReadOnlyList<NavigationViewItem> MenuItems { get; }
[NotifyPropertyChangedFor(nameof(CurrentPage))]
[ObservableProperty] private NavigationViewItem _selectedMenuItem;
public PageBase CurrentPage => (PageBase)SelectedMenuItem.Tag!;
[ObservableProperty] private PageBase _currentPage;
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
[ObservableProperty] private bool _isUpdateShown = false;
[ObservableProperty] private string _schemaVersion = "N/A";
[ObservableProperty] private string _schemaVersionLatest = "N/A";
public HttpClient HttpClient { get; }
public DialogService DialogService { get; }
public OpenApiDocumentWrapper? OpenApiDocumentWrapper { get; set; }
public OpenApiDocument? HostDocument { get; set; }
private readonly DataSource _dataSource;
[ObservableProperty] private bool _isBusy = true;
@@ -45,9 +47,23 @@ public partial class MainWindowViewModel
private readonly ILogger<MainWindowViewModel> _logger;
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, DialogService dialogService, ILogger<MainWindowViewModel> logger)
private readonly System.Timers.Timer _latestUpdateTimer = new()
{
Interval = TimeSpan.FromMinutes(10).TotalMilliseconds,
Enabled = true
};
private readonly System.Timers.Timer _schemaVersionTimer = new()
{
Interval = TimeSpan.FromSeconds(5).TotalMilliseconds,
Enabled = true
};
private bool _isSchemaVersionChecked = false;
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, DialogService dialogService, ILogger<MainWindowViewModel> logger, DataSource dataSource)
{
_logger = logger;
_dataSource = dataSource;
MenuItems = new AvaloniaList<NavigationViewItem>(pages
.OrderBy(p => p.Index)
@@ -59,27 +75,78 @@ public partial class MainWindowViewModel
IconSource = new BitmapIconSource() { UriSource = new Uri($"avares://NeedleworkDotNet/Assets/Icons/{p.Icon}.png") }
}));
SelectedMenuItem = MenuItems[0];
CurrentPage = (PageBase)MenuItems[0].Tag!;
HttpClient = httpClient;
DialogService = dialogService;
WeakReferenceMessenger.Default.RegisterAll(this);
Task.Run(FetchDataAsync);
new Thread(ProcessEvents) { IsBackground = true }.Start();
_latestUpdateTimer.Elapsed += OnLatestUpdateTimerElapsed;
_schemaVersionTimer.Elapsed += OnSchemaVersionTimerElapsed;
_latestUpdateTimer.Start();
_schemaVersionTimer.Start();
OnLatestUpdateTimerElapsed(null, null);
OnSchemaVersionTimerElapsed(null, null);
}
private void ProcessEvents(object? obj)
partial void OnSelectedMenuItemChanged(NavigationViewItem value)
{
while (!IsUpdateShown)
if (value.Tag is PageBase page)
{
Task.Run(CheckLatestVersionAsync);
Thread.Sleep(TimeSpan.FromMinutes(10)); // Avoid tripping unauthenticated rate limits
CurrentPage = page;
if (!page.IsInitialized)
{
Task.Run(page.InitializeAsync);
}
}
}
private async Task CheckLatestVersionAsync()
private async void OnSchemaVersionTimerElapsed(object? sender, ElapsedEventArgs? e)
{
if (!ProcessFinder.IsPortOpen()) return;
var lcuSchemaDocument = await _dataSource.GetLcuSchemaDocumentAsync();
try
{
var client = Connector.GetLcuHttpClientInstance();
var currentSemVer = lcuSchemaDocument.Info.Version.Split('.');
var systemBuild = await client.GetFromJsonAsync<SystemBuild>("/system/v1/builds") ?? throw new NullReferenceException();
var latestSemVer = systemBuild.Version.Split('.');
if (!_isSchemaVersionChecked)
{
_logger.LogInformation("LCU Schema (current): {Version}", lcuSchemaDocument.Info.Version);
_logger.LogInformation("LCU Schema (latest): {Version}", systemBuild.Version);
_isSchemaVersionChecked = true;
}
bool isVersionMatching = currentSemVer[0] == latestSemVer[0] && currentSemVer[1] == latestSemVer[1]; // Compare major and minor versions
if (!isVersionMatching)
{
Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
{
await ShowInfoBarAsync(new("Newer System Build", true, $"LCU Schema is possibly outdated compared to latest system build. Consider submitting a pull request on dysolix/hasagi-types.\nCurrent: {string.Join(".", currentSemVer)}\nLatest: {string.Join(".", latestSemVer)}", InfoBarSeverity.Warning, TimeSpan.FromSeconds(60), new Avalonia.Controls.Button()
{
Command = OpenUrlCommand,
CommandParameter = "https://github.com/dysolix/hasagi-types#updating-the-types",
Content = "Submit PR"
}));
});
_schemaVersionTimer.Elapsed -= OnSchemaVersionTimerElapsed;
_schemaVersionTimer.Stop();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Schema version check failed");
}
}
private async void OnLatestUpdateTimerElapsed(object? sender, ElapsedEventArgs? e)
{
try
{
@@ -100,14 +167,16 @@ public partial class MainWindowViewModel
{
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()
await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(30), new Avalonia.Controls.Button()
{
Command = OpenUrlCommand,
CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases",
Content = "Download"
}));
IsUpdateShown = true;
});
_latestUpdateTimer.Elapsed -= OnLatestUpdateTimerElapsed;
_latestUpdateTimer.Stop();
}
}
catch (Exception ex)
@@ -116,34 +185,6 @@ public partial class MainWindowViewModel
}
}
private async Task FetchDataAsync()
{
try
{
var document = await Resources.GetOpenApiDocumentAsync(HttpClient);
HostDocument = document;
var handler = new OpenApiDocumentWrapper(document);
OpenApiDocumentWrapper = handler;
WeakReferenceMessenger.Default.Send(new DataReadyMessage(handler));
IsBusy = false;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch OpenAPI data");
}
}
public void Receive(DataRequestMessage message)
{
message.Reply(OpenApiDocumentWrapper!);
}
public void Receive(HostDocumentRequestMessage message)
{
message.Reply(HostDocument!);
}
[RelayCommand]
private void OpenUrl(string url)
{

View File

@@ -1,6 +1,7 @@
using CommunityToolkit.Mvvm.Input;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages;
@@ -13,6 +14,12 @@ public partial class AboutViewModel : PageBase
HttpClient = httpClient;
}
public override Task InitializeAsync()
{
IsInitialized = true;
return Task.CompletedTask;
}
[RelayCommand]
private void OpenUrl(string url)
{

View File

@@ -1,41 +1,45 @@
using Avalonia.Collections;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using Needlework.Net.Messages;
using Needlework.Net.ViewModels.Shared;
using System.Net.Http;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages;
public partial class ConsoleViewModel : PageBase, IRecipient<DataReadyMessage>
public partial class ConsoleViewModel : PageBase
{
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;
[ObservableProperty] private RequestViewModel _request;
public ConsoleViewModel(ILogger<LcuRequestViewModel> lcuRequestViewModelLogger) : base("Console", "terminal", -200)
private readonly DataSource _dataSource;
public ConsoleViewModel(ILogger<RequestViewModel> requestViewModelLogger, DataSource dataSource, HttpClient httpClient) : base("Console", "terminal", -200)
{
_lcuRequest = new(lcuRequestViewModelLogger);
WeakReferenceMessenger.Default.Register<DataReadyMessage>(this);
_request = new(requestViewModelLogger, Endpoints.Tab.LCU, httpClient);
_dataSource = dataSource;
}
public override async Task InitializeAsync()
{
var document = await _dataSource.GetLcuSchemaDocumentAsync();
Dispatcher.UIThread.Invoke(() =>
{
RequestPaths.Clear();
RequestPaths.AddRange(document.Paths);
});
IsBusy = false;
IsInitialized = true;
}
[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;
});
await Request.ExecuteAsync();
}
}

View File

@@ -1,8 +1,6 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using Needlework.Net.Messages;
using Needlework.Net.ViewModels.Shared;
using System;
using System.Linq;
@@ -23,12 +21,10 @@ public partial class EndpointViewModel : ObservableObject
public event EventHandler<string>? PathOperationSelected;
public EndpointViewModel(string endpoint, ILogger<LcuRequestViewModel> lcuRequestViewModelLogger)
public EndpointViewModel(string endpoint, ILogger<RequestViewModel> requestViewModelLogger, Models.Document document, Tab tab, System.Net.Http.HttpClient httpClient)
{
Endpoint = endpoint;
var handler = WeakReferenceMessenger.Default.Send<DataRequestMessage>().Response;
PathOperations = new AvaloniaList<PathOperationViewModel>(handler.Plugins[endpoint].Select(x => new PathOperationViewModel(x, lcuRequestViewModelLogger)));
PathOperations = new AvaloniaList<PathOperationViewModel>(document.Plugins[endpoint].Select(x => new PathOperationViewModel(x, requestViewModelLogger, document, tab, httpClient)));
FilteredPathOperations = new AvaloniaList<PathOperationViewModel>(PathOperations);
}

View File

@@ -13,14 +13,27 @@ public partial class EndpointsNavigationViewModel : ObservableObject
[ObservableProperty] private ObservableObject _activeViewModel;
[ObservableProperty] private ObservableObject _endpointsViewModel;
[ObservableProperty] private string _title = string.Empty;
[ObservableProperty] private string _title;
private readonly Action<string?, Guid> _onEndpointNavigation;
private readonly Tab _tab;
public EndpointsNavigationViewModel(IAvaloniaList<string> plugins, Action<string?, Guid> onEndpointNavigation, ILogger<LcuRequestViewModel> lcuRequestViewModelLogger)
public EndpointsNavigationViewModel(IAvaloniaList<string> plugins, Action<string?, Guid> onEndpointNavigation, ILogger<RequestViewModel> requestViewModelLogger, Models.Document document, Tab tab, System.Net.Http.HttpClient httpClient)
{
_activeViewModel = _endpointsViewModel = new EndpointsViewModel(plugins, OnClicked, lcuRequestViewModelLogger);
_activeViewModel = _endpointsViewModel = new EndpointsViewModel(plugins, OnClicked, requestViewModelLogger, document, tab, httpClient);
_onEndpointNavigation = onEndpointNavigation;
_tab = tab;
_title = GetTitle(tab);
}
private string GetTitle(Tab tab)
{
return tab switch
{
Tab.LCU => "LCU",
Tab.GameClient => "Game Client",
_ => string.Empty,
};
}
private void OnClicked(ObservableObject viewModel)
@@ -28,7 +41,7 @@ public partial class EndpointsNavigationViewModel : ObservableObject
ActiveViewModel = viewModel;
if (viewModel is EndpointViewModel endpoint)
{
Title = endpoint.Title;
Title = $"{GetTitle(_tab)} - {endpoint.Title}";
_onEndpointNavigation.Invoke(endpoint.Title, Guid);
}
}
@@ -37,7 +50,7 @@ public partial class EndpointsNavigationViewModel : ObservableObject
private void GoBack()
{
ActiveViewModel = EndpointsViewModel;
Title = string.Empty;
Title = GetTitle(_tab);
_onEndpointNavigation.Invoke(null, Guid);
}
}

View File

@@ -2,45 +2,64 @@
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using Needlework.Net.Messages;
using Needlework.Net.Models;
using Needlework.Net.ViewModels.Shared;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class EndpointsTabViewModel : PageBase, IRecipient<DataReadyMessage>
public enum Tab
{
LCU,
GameClient
}
public partial class EndpointsTabViewModel : PageBase
{
public IAvaloniaList<string> Plugins { get; } = new AvaloniaList<string>();
public IAvaloniaList<EndpointItem> Endpoints { get; } = new AvaloniaList<EndpointItem>();
[ObservableProperty] private bool _isBusy = true;
private readonly ILogger<LcuRequestViewModel> _lcuRequestViewModelLogger;
private readonly ILogger<RequestViewModel> _requestViewModelLogger;
private readonly DataSource _dataSource;
private readonly HttpClient _httpClient;
public EndpointsTabViewModel(ILogger<LcuRequestViewModel> lcuRequestViewModelLogger) : base("Endpoints", "list-alt", -500)
public EndpointsTabViewModel(ILogger<RequestViewModel> requestViewModelLogger, DataSource dataSource, IHttpClientFactory httpClientFactory) : base("Endpoints", "list-alt", -500)
{
_lcuRequestViewModelLogger = lcuRequestViewModelLogger;
WeakReferenceMessenger.Default.RegisterAll(this);
_requestViewModelLogger = requestViewModelLogger;
_dataSource = dataSource;
_httpClient = httpClientFactory.CreateClient(nameof(EndpointsTabViewModel));
}
public void Receive(DataReadyMessage message)
public override async Task InitializeAsync()
{
await Dispatcher.UIThread.Invoke(async () => await AddEndpoint(Tab.LCU));
IsBusy = false;
Plugins.Clear();
Plugins.AddRange(message.Value.Plugins.Keys);
Dispatcher.UIThread.Post(AddEndpoint);
IsInitialized = true;
}
[RelayCommand]
private void AddEndpoint()
private async Task AddEndpoint(Tab tab)
{
Document document = tab switch
{
Tab.LCU => await _dataSource.GetLcuSchemaDocumentAsync(),
Tab.GameClient => await _dataSource.GetLolClientDocumentAsync(),
_ => throw new NotImplementedException(),
};
Plugins.Clear();
Plugins.AddRange(document.Plugins.Keys);
var vm = new EndpointsNavigationViewModel(Plugins, OnEndpointNavigation, _requestViewModelLogger, document, tab, _httpClient);
Endpoints.Add(new()
{
Content = new EndpointsNavigationViewModel(Plugins, OnEndpointNavigation, _lcuRequestViewModelLogger),
Content = vm,
Header = vm.Title,
Selected = true
});
}
@@ -51,7 +70,7 @@ public partial class EndpointsTabViewModel : PageBase, IRecipient<DataReadyMessa
{
if (endpoint.Content.Guid.Equals(guid))
{
endpoint.Header = title ?? "Endpoints";
endpoint.Header = endpoint.Content.Title;
break;
}
}
@@ -60,7 +79,7 @@ public partial class EndpointsTabViewModel : PageBase, IRecipient<DataReadyMessa
public partial class EndpointItem : ObservableObject
{
[ObservableProperty] private string _header = "Endpoints";
[ObservableProperty] private string _header = string.Empty;
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

@@ -2,9 +2,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Needlework.Net.Models;
using Needlework.Net.ViewModels.Shared;
using System;
using System.Linq;
using System.Net.Http;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
@@ -18,14 +20,20 @@ public partial class EndpointsViewModel : ObservableObject
public Action<ObservableObject> OnClicked { get; }
private readonly ILogger<LcuRequestViewModel> _lcuRequestViewModelLogger;
private readonly ILogger<RequestViewModel> _requestViewModelLogger;
private readonly Document _document;
private readonly Tab _tab;
private readonly HttpClient _httpClient;
public EndpointsViewModel(IAvaloniaList<string> plugins, Action<ObservableObject> onClicked, ILogger<LcuRequestViewModel> lcuRequestViewModelLogger)
public EndpointsViewModel(IAvaloniaList<string> plugins, Action<ObservableObject> onClicked, ILogger<RequestViewModel> requestViewModelLogger, Models.Document document, Tab tab, System.Net.Http.HttpClient httpClient)
{
Plugins = new AvaloniaList<string>(plugins);
Query = new AvaloniaList<string>(plugins);
OnClicked = onClicked;
_lcuRequestViewModelLogger = lcuRequestViewModelLogger;
_requestViewModelLogger = requestViewModelLogger;
_document = document;
_tab = tab;
_httpClient = httpClient;
}
partial void OnSearchChanged(string value)
@@ -42,6 +50,6 @@ public partial class EndpointsViewModel : ObservableObject
{
if (string.IsNullOrEmpty(value)) return;
OnClicked.Invoke(new EndpointViewModel(value, _lcuRequestViewModelLogger));
OnClicked.Invoke(new EndpointViewModel(value, _requestViewModelLogger, _document, _tab, _httpClient));
}
}

View File

@@ -1,9 +1,9 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.OpenApi.Models;
using Needlework.Net.Messages;
using Needlework.Net.Models;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
@@ -22,23 +22,23 @@ public partial class OperationViewModel : ObservableObject
public IAvaloniaReadOnlyList<ParameterViewModel> QueryParameters { get; }
public string? RequestTemplate { get; }
public OperationViewModel(OpenApiOperation operation)
public OperationViewModel(OpenApiOperation operation, Models.Document document)
{
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);
RequestClasses = GetRequestClasses(operation.RequestBody, document);
ResponseClasses = GetResponseClasses(operation.Responses, document);
PathParameters = GetParameters(operation.Parameters, ParameterLocation.Path);
QueryParameters = GetParameters(operation.Parameters, ParameterLocation.Query);
RequestBodyType = GetRequestBodyType(operation.RequestBody);
RequestTemplate = GetRequestTemplate(operation.RequestBody);
RequestTemplate = GetRequestTemplate(operation.RequestBody, document);
}
private string? GetRequestTemplate(OpenApiRequestBody? requestBody)
private string? GetRequestTemplate(OpenApiRequestBody? requestBody, Document document)
{
var requestClasses = GetRequestClasses(requestBody);
var requestClasses = GetRequestClasses(requestBody, document);
if (requestClasses.Count == 0)
{
var type = GetRequestBodyType(requestBody);
@@ -133,17 +133,40 @@ public partial class OperationViewModel : ObservableObject
return pathParameters;
}
private AvaloniaList<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses)
private bool TryGetResponse(OpenApiResponses responses, [NotNullWhen(true)] out OpenApiResponse? response)
{
if (responses.TryGetValue("2XX", out var response)
&& response.Content.TryGetValue("application/json", out var media))
response = null;
var flag = false;
if (responses.TryGetValue("2XX", out var x))
{
var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response;
response = x;
flag = true;
}
else if (responses.TryGetValue("200", out var y))
{
response = y;
flag = true;
}
return flag;
}
private AvaloniaList<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses, Document document)
{
if (!TryGetResponse(responses, out var response))
return [];
if (response.Content.TryGetValue("application/json", out var media))
{
var rawDocument = document.OpenApiDocument;
var schema = media.Schema;
if (schema == null) return [];
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
WalkSchema(schema, propertyClasses, document);
WalkSchema(schema, propertyClasses, rawDocument);
return propertyClasses;
}
return [];
}
@@ -186,12 +209,12 @@ public partial class OperationViewModel : ObservableObject
|| type.Contains("number"));
}
private AvaloniaList<PropertyClassViewModel> GetRequestClasses(OpenApiRequestBody? requestBody)
private AvaloniaList<PropertyClassViewModel> GetRequestClasses(OpenApiRequestBody? requestBody, Document document)
{
if (requestBody == null) return [];
if (requestBody.Content.TryGetValue("application/json", out var media))
{
var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response;
var rawDocument = document.OpenApiDocument;
var schema = media.Schema;
if (schema == null) return [];
@@ -199,9 +222,9 @@ public partial class OperationViewModel : ObservableObject
if (IsComponent(type))
{
var componentId = GetComponentId(schema);
var componentSchema = document.Components.Schemas[componentId];
var componentSchema = rawDocument.Components.Schemas[componentId];
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
WalkSchema(componentSchema, propertyClasses, document);
WalkSchema(componentSchema, propertyClasses, rawDocument);
return propertyClasses;
}
}
@@ -210,12 +233,15 @@ public partial class OperationViewModel : ObservableObject
private string GetReturnType(OpenApiResponses responses)
{
if (responses.TryGetValue("2XX", out var response)
&& response.Content.TryGetValue("application/json", out var media))
if (!TryGetResponse(responses, out var response))
return "none";
if (response.Content.TryGetValue("application/json", out var media))
{
var schema = media.Schema;
return GetSchemaType(schema);
}
return "none";
}
@@ -224,6 +250,7 @@ public partial class OperationViewModel : ObservableObject
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.AdditionalProperties?.Reference != null) return schema.AdditionalProperties.Reference.Id;
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}[]";

View File

@@ -14,17 +14,20 @@ public partial class PathOperationViewModel : ObservableObject
public string Path { get; }
public OperationViewModel Operation { get; }
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private Lazy<LcuRequestViewModel> _lcuRequest;
public string Url { get; }
public PathOperationViewModel(PathOperation pathOperation, ILogger<LcuRequestViewModel> lcuRequestViewModelLogger)
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private Lazy<RequestViewModel> _request;
public PathOperationViewModel(PathOperation pathOperation, ILogger<RequestViewModel> requestViewModelLogger, Document document, Tab tab, System.Net.Http.HttpClient httpClient)
{
Path = pathOperation.Path;
Operation = new OperationViewModel(pathOperation.Operation);
LcuRequest = new(() => new LcuRequestViewModel(lcuRequestViewModelLogger)
Operation = new OperationViewModel(pathOperation.Operation, document);
Request = new(() => new RequestViewModel(requestViewModelLogger, tab, httpClient)
{
Method = pathOperation.Method.ToUpper()
});
Url = $"https://swagger.dysolix.dev/lcu/#/{pathOperation.Tag}/{pathOperation.Operation.OperationId}";
}
[RelayCommand]
@@ -47,7 +50,13 @@ public partial class PathOperationViewModel : ObservableObject
}
}
LcuRequest.Value.RequestPath = sb.ToString();
await LcuRequest.Value.ExecuteAsync();
Request.Value.RequestPath = sb.ToString();
await Request.Value.ExecuteAsync();
}
[RelayCommand]
private void CopyUrl()
{
App.MainWindow?.Clipboard?.SetTextAsync(Url);
}
}

View File

@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages;
@@ -14,6 +15,12 @@ public partial class HomeViewModel : PageBase
public HomeViewModel() : base("Home", "home", int.MinValue) { }
public override Task InitializeAsync()
{
IsInitialized = true;
return Task.CompletedTask;
}
[RelayCommand]
private void OpenUrl(string url)
{
@@ -23,4 +30,5 @@ public partial class HomeViewModel : PageBase
};
process.Start();
}
}

View File

@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages;
@@ -8,4 +9,7 @@ public abstract partial class PageBase(string displayName, string icon, int inde
[ObservableProperty] private string _displayName = displayName;
[ObservableProperty] private string _icon = icon;
[ObservableProperty] private int _index = index;
[ObservableProperty] private bool _isInitialized;
public abstract Task InitializeAsync();
}

View File

@@ -59,6 +59,12 @@ public partial class WebsocketViewModel : PageBase
});
}
public override Task InitializeAsync()
{
IsInitialized = true;
return Task.CompletedTask;
}
private async Task InitializeEventTypes()
{
try

View File

@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using Needlework.Net.Messages;
using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System;
using System.Net.Http;
using System.Text.Json;
@@ -12,7 +13,7 @@ using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Shared;
public partial class LcuRequestViewModel : ObservableObject
public partial class RequestViewModel : ObservableObject
{
[ObservableProperty] private string? _method = "GET";
[ObservableProperty] private SolidColorBrush _color = new(GetColor("GET"));
@@ -29,14 +30,18 @@ public partial class LcuRequestViewModel : ObservableObject
[ObservableProperty] private string? _responseAuthorization = null;
[ObservableProperty] private string? _responseBody = null;
public event EventHandler<LcuRequestViewModel>? RequestText;
public event EventHandler<RequestViewModel>? RequestText;
public event EventHandler<string>? UpdateText;
private readonly ILogger<LcuRequestViewModel> _logger;
private readonly ILogger<RequestViewModel> _logger;
private readonly Tab _tab;
private readonly HttpClient _httpClient;
public LcuRequestViewModel(ILogger<LcuRequestViewModel> logger)
public RequestViewModel(ILogger<RequestViewModel> logger, Pages.Endpoints.Tab tab, HttpClient httpClient)
{
_logger = logger;
_tab = tab;
_httpClient = httpClient;
}
partial void OnMethodChanged(string? oldValue, string? newValue)
@@ -47,25 +52,80 @@ public partial class LcuRequestViewModel : ObservableObject
}
public async Task ExecuteAsync()
{
switch (_tab)
{
case Tab.LCU:
await ExecuteLcuAsync();
break;
case Tab.GameClient:
await ExecuteGameClientAsync();
break;
default:
break;
}
}
private async Task ExecuteGameClientAsync()
{
try
{
IsRequestBusy = true;
if (string.IsNullOrEmpty(RequestPath))
throw new Exception("Path is empty.");
var method = GetMethod();
var method = Method switch
_logger.LogDebug("Sending request: {Tuple}", (Method, RequestPath));
RequestText?.Invoke(this, this);
var content = new StringContent(RequestBody ?? string.Empty, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var responsePath = $"https://127.0.0.1:2999{RequestPath}";
var response = await _httpClient.SendAsync(new HttpRequestMessage(method, responsePath) { Content = content });
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)
{
"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."),
};
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 = responsePath;
}
catch (Exception ex)
{
_logger.LogError(ex, "Request failed: {Tuple}", (Method, RequestPath));
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 async Task ExecuteLcuAsync()
{
try
{
IsRequestBusy = true;
if (string.IsNullOrEmpty(RequestPath))
throw new Exception("Path is empty.");
var method = GetMethod();
_logger.LogDebug("Sending request: {Tuple}", (Method, RequestPath));
@@ -116,6 +176,22 @@ public partial class LcuRequestViewModel : ObservableObject
}
}
private HttpMethod GetMethod()
{
return 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."),
};
}
private static Color GetColor(string method) => method switch
{
"GET" => Avalonia.Media.Color.FromRgb(95, 99, 186),

View File

@@ -1,3 +1,6 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using FluentAvalonia.UI.Windowing;
namespace Needlework.Net.Views.MainWindow;
@@ -9,5 +12,19 @@ public partial class MainWindowView : AppWindow
InitializeComponent();
TitleBar.ExtendsContentIntoTitleBar = true;
TransparencyLevelHint = [WindowTransparencyLevel.Mica, WindowTransparencyLevel.None];
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (IsWindows11OrNewer())
{
Background = null;
}
}
}
private static bool IsWindows11OrNewer()
{
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build >= 22000;
}
}

View File

@@ -8,160 +8,46 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.AboutView"
x:DataType="vm:AboutViewModel">
<UserControl.Styles>
<Style Selector="Button">
<Setter Property="Theme" Value="{StaticResource TransparentButton}"/>
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
<ScrollViewer Margin="8">
<WrapPanel HorizontalAlignment="Center">
<WrapPanel.Styles>
<Style Selector="controls|UserCard">
<Setter Property="Width" Value="272"/>
<Setter Property="MaxHeight" Value="378"/>
<Setter Property="Margin" Value="0 16 32 16"/>
</Style>
<Style Selector="i|Icon">
<Setter Property="FontSize" Value="20" />
</Style>
</UserControl.Styles>
<Grid Margin="8"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<StackPanel Spacing="8">
<Grid HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal">
<controls:Card Margin="8">
<Image Source="/Assets/Users/blossomishymae.png"
RenderOptions.BitmapInterpolationMode="MediumQuality"
Width="200"
Height="200"/>
</controls:Card>
<StackPanel Margin="8 0 0 0">
<controls:Card Width="400" Margin="8">
<StackPanel Orientation="Horizontal">
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
Margin="0 0 8 0">Blossomi Shymae</TextBlock>
<Button CommandParameter="https://github.com/BlossomiShymae">
<i:Icon Value="fa-github"/>
</Button>
</StackPanel>
</controls:Card>
<controls:Card Width="400" Margin="8">
<StackPanel >
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">About</TextBlock>
<TextBlock TextWrapping="Wrap">
Needlework.Net is the .NET rewrite of Needlework. This tool was made to help others with LCU development. Feel free to ask any questions
</WrapPanel.Styles>
<controls:UserCard UserImage="/Assets/Users/blossomishymae.png"
UserName="estrogen elf"
UserGithub="BlossomiShymae">
Needlework.Net is the .NET rewrite of Needlework. This tool was made to help others with LCU and Game Client development. Feel free to ask any questions
or help contribute to the project! Made with love. 💜
</TextBlock>
</StackPanel>
</controls:Card>
</StackPanel>
</StackPanel>
</Grid>
<Border Width="800">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Thanks to the friends and people who made this tool possible...</TextBlock>
</Border>
<WrapPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal"
Margin="8">
<controls:Card>
<Image Source="/Assets/Users/dysolix.png"
RenderOptions.BitmapInterpolationMode="MediumQuality"
Width="100"
Height="100"/>
</controls:Card>
<StackPanel Margin="2 0 0 0">
<controls:Card Width="250" Margin="2">
<StackPanel Orientation="Horizontal">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
Margin="0 0 8 0">dysolix</TextBlock>
<Button CommandParameter="https://github.com/dysolix">
<i:Icon Value="fa-github"/>
</Button>
</StackPanel>
</controls:Card>
<controls:Card Width="250" Margin="2">
<StackPanel >
<TextBlock TextWrapping="Wrap">
For providing and hosting an auto-generated OpenAPI document of the LCU.
</TextBlock>
</StackPanel>
</controls:Card>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal"
Margin="8">
<controls:Card>
<Image Source="/Assets/Users/ray.png"
RenderOptions.BitmapInterpolationMode="MediumQuality"
Width="100"
Height="100"/>
</controls:Card>
<StackPanel Margin="2 0 0 0">
<controls:Card Width="250" Margin="2">
<StackPanel Orientation="Horizontal">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
Margin="0 0 8 0">Ray</TextBlock>
<Button CommandParameter="https://github.com/Hi-Ray">
<i:Icon Value="fa-github"/>
</Button>
</StackPanel>
</controls:Card>
<controls:Card Width="250" Margin="2">
<StackPanel >
<TextBlock TextWrapping="Wrap">
For guidance, advice, or providing help via HextechDocs.
</TextBlock>
</StackPanel>
</controls:Card>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal"
Margin="8">
<controls:Card>
<Image Source="/Assets/Users/dubble.png"
RenderOptions.BitmapInterpolationMode="MediumQuality"
Width="100"
Height="100"/>
</controls:Card>
<StackPanel Margin="4 0 0 0">
<controls:Card Width="250" Margin="2">
<StackPanel Orientation="Horizontal">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
Margin="0 0 8 0">dubble</TextBlock>
<Button CommandParameter="https://github.com/cuppachino">
<i:Icon Value="fa-github"/>
</Button>
</StackPanel>
</controls:Card>
<controls:Card Width="250" Margin="2">
<StackPanel >
<TextBlock TextWrapping="Wrap">
For encouraging me to publish Needlework. This project may never have seen the light of day without him.
</TextBlock>
</StackPanel>
</controls:Card>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal"
Margin="8">
<controls:Card>
<Image Source="/Assets/Users/community.png"
RenderOptions.BitmapInterpolationMode="MediumQuality"
Width="100"
Height="100"/>
</controls:Card>
<StackPanel Margin="4 0 0 0">
<controls:Card Width="250" Margin="2">
<StackPanel Orientation="Horizontal">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
Width="250"
TextWrapping="Wrap">Third Party Developer Community</TextBlock>
</StackPanel>
</controls:Card>
<controls:Card Width="250" Margin="2">
<StackPanel >
<TextBlock TextWrapping="Wrap">
For providing numerous documentation on the LCU.
</TextBlock>
</StackPanel>
</controls:Card>
</StackPanel>
</StackPanel>
</controls:UserCard>
<controls:UserCard UserImage="/Assets/Users/dysolix.png"
UserName="dysolix"
UserGithub="dysolix">
For providing LCU Schema, the auto-generated OpenAPI document for the LCU.
</controls:UserCard>
<controls:UserCard UserImage="/Assets/Users/sylv.jpg"
UserName="Sylv"
UserGithub="AlsoSylv">
For providing a fixed up-to-date Game Client schema.
</controls:UserCard>
<controls:UserCard UserImage="/Assets/Users/ray.png"
UserName="Ray"
UserGithub="Hi-Ray">
For guidance, advice, and providing help via HextechDocs.
</controls:UserCard>
<controls:UserCard UserImage="/Assets/Users/dubble.png"
UserName="dubble"
UserGithub="cuppachino">
For encouraging me to publish Needlework.Net and other ideas.
</controls:UserCard>
<controls:UserCard UserImage="/Assets/Users/aoshiw.png"
UserName="AoshiW"
UserGithub="AoshiW">
For PR.
</controls:UserCard>
</WrapPanel>
</StackPanel>
</Grid>
</ScrollViewer>
</UserControl>

View File

@@ -16,16 +16,16 @@
<Grid Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2">
<StackPanel Margin="0 0 0 16">
<StackPanel Margin="0 0 0 8">
<Grid RowDefinitions="auto" ColumnDefinitions="auto,*,auto">
<ComboBox ItemsSource="{Binding RequestMethods}"
SelectedItem="{Binding LcuRequest.Method}"
SelectedItem="{Binding Request.Method}"
Margin="0 0 8 0"
Grid.Row="0"
Grid.Column="0"/>
<AutoCompleteBox
ItemsSource="{Binding RequestPaths}"
Text="{Binding LcuRequest.RequestPath}"
Text="{Binding Request.RequestPath}"
MaxDropDownHeight="400"
FilterMode="StartsWith"
Grid.Row="0"
@@ -49,7 +49,7 @@
<TextBox IsReadOnly="True"
Grid.Row="0"
Grid.Column="0"
Text="{Binding LcuRequest.ResponsePath}"/>
Text="{Binding Request.ResponsePath}"/>
<avaloniaEdit:TextEditor
Name="RequestEditor"
Text=""
@@ -69,7 +69,7 @@
<StackPanel Orientation="Horizontal"
Grid.Row="0"
Grid.Column="0">
<Button Content="{Binding LcuRequest.ResponseStatus}"
<Button Content="{Binding Request.ResponseStatus}"
FontSize="12"
VerticalAlignment="Center"/>
</StackPanel>

View File

@@ -28,13 +28,13 @@ public partial class ConsoleView : UserControl
_requestEditor?.ApplyJsonEditorSettings();
var vm = (ConsoleViewModel)DataContext!;
vm.LcuRequest.RequestText += LcuRequest_RequestText; ;
vm.LcuRequest.UpdateText += LcuRequest_UpdateText;
vm.Request.RequestText += LcuRequest_RequestText; ;
vm.Request.UpdateText += LcuRequest_UpdateText;
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
}
private void LcuRequest_RequestText(object? sender, ViewModels.Shared.LcuRequestViewModel e)
private void LcuRequest_RequestText(object? sender, ViewModels.Shared.RequestViewModel e)
{
e.RequestBody = _requestEditor!.Text;
}
@@ -49,8 +49,8 @@ public partial class ConsoleView : UserControl
base.OnDetachedFromVisualTree(e);
var vm = (ConsoleViewModel)DataContext!;
vm.LcuRequest.RequestText -= LcuRequest_RequestText;
vm.LcuRequest.UpdateText -= LcuRequest_UpdateText;
vm.Request.RequestText -= LcuRequest_RequestText;
vm.Request.UpdateText -= LcuRequest_UpdateText;
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)

View File

@@ -55,12 +55,17 @@
<Grid
RowDefinitions="*"
ColumnDefinitions="auto,*">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Copy Swagger URL" Command="{Binding CopyUrlCommand}"/>
</MenuFlyout>
</Grid.ContextFlyout>
<TextBlock
VerticalAlignment="Center"
TextAlignment="Center"
Margin="0 0 8 0"
Text="{Binding LcuRequest.Value.Method}"
Background="{Binding LcuRequest.Value.Color}"
Text="{Binding Request.Value.Method}"
Background="{Binding Request.Value.Color}"
FontSize="8"
Width="50"
Padding="10 2 10 2"
@@ -88,14 +93,14 @@
ColumnDefinitions="auto,*,auto">
<TextBox Grid.Row="0"
Grid.Column="0"
Text="{Binding SelectedPathOperation.LcuRequest.Value.Method}"
Text="{Binding SelectedPathOperation.Request.Value.Method}"
FontSize="12"
IsReadOnly="True"
Margin="0 0 8 0"/>
<TextBox Grid.Row="0"
Grid.Column="1"
FontSize="12"
Text="{Binding SelectedPathOperation.LcuRequest.Value.ResponsePath}"
Text="{Binding SelectedPathOperation.Request.Value.ResponsePath}"
IsReadOnly="True"/>
<StackPanel Grid.Row="0"
Grid.Column="2"
@@ -189,7 +194,7 @@
Grid.Column="1"
Margin="0 0 0 8"
IsReadOnly="True"
Text="{Binding SelectedPathOperation.LcuRequest.Value.ResponseUsername}" />
Text="{Binding SelectedPathOperation.Request.Value.ResponseUsername}" />
<TextBlock FontSize="12"
Grid.Row="1"
Grid.Column="0"
@@ -201,7 +206,7 @@
Grid.Column="1"
Margin="0 0 0 8"
IsReadOnly="True"
Text="{Binding SelectedPathOperation.LcuRequest.Value.ResponsePassword}"/>
Text="{Binding SelectedPathOperation.Request.Value.ResponsePassword}"/>
<TextBlock FontSize="12"
Grid.Row="2"
Grid.Column="0"
@@ -212,7 +217,7 @@
Grid.Row="2"
Grid.Column="1"
IsReadOnly="True"
Text="{Binding SelectedPathOperation.LcuRequest.Value.ResponseAuthorization}"/>
Text="{Binding SelectedPathOperation.Request.Value.ResponseAuthorization}"/>
</Grid>
</TabItem>
<TabItem Header="Schemas">
@@ -304,7 +309,7 @@
FontSize="10"
Padding="12 4 12 4"
Classes="Flat"
Content="{Binding SelectedPathOperation.LcuRequest.Value.ResponseStatus}"/>
Content="{Binding SelectedPathOperation.Request.Value.ResponseStatus}"/>
</StackPanel>
<Grid Grid.Row="1" Grid.Column="4">

View File

@@ -13,7 +13,7 @@ public partial class EndpointView : UserControl
{
private TextEditor? _requestEditor;
private TextEditor? _responseEditor;
private LcuRequestViewModel? _lcuRequestVm;
private RequestViewModel? _lcuRequestVm;
public EndpointView()
{
@@ -34,9 +34,9 @@ public partial class EndpointView : UserControl
if (vm.SelectedPathOperation != null)
{
_lcuRequestVm = vm.SelectedPathOperation.LcuRequest.Value;
vm.SelectedPathOperation.LcuRequest.Value.RequestText += LcuRequest_RequestText;
vm.SelectedPathOperation.LcuRequest.Value.UpdateText += LcuRequest_UpdateText;
_lcuRequestVm = vm.SelectedPathOperation.Request.Value;
vm.SelectedPathOperation.Request.Value.RequestText += LcuRequest_RequestText;
vm.SelectedPathOperation.Request.Value.UpdateText += LcuRequest_UpdateText;
}
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
@@ -53,10 +53,10 @@ public partial class EndpointView : UserControl
_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;
vm.SelectedPathOperation.Request.Value.RequestText += LcuRequest_RequestText;
vm.SelectedPathOperation.Request.Value.UpdateText += LcuRequest_UpdateText;
_lcuRequestVm = vm.SelectedPathOperation.Request.Value;
_responseEditor!.Text = vm.SelectedPathOperation.Request.Value.ResponseBody ?? string.Empty;
}
}
@@ -81,7 +81,7 @@ public partial class EndpointView : UserControl
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
}
private void LcuRequest_RequestText(object? sender, LcuRequestViewModel e)
private void LcuRequest_RequestText(object? sender, RequestViewModel e)
{
e.RequestBody = _requestEditor!.Text;
}

View File

@@ -3,6 +3,7 @@
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"
Name="EndpointsTab"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:controls="using:Needlework.Net.Controls"
@@ -14,7 +15,29 @@
<Grid>
<ui:TabView TabItems="{Binding Endpoints}"
AddTabButtonCommand="{Binding AddEndpointCommand}"
AddTabButtonCommandParameter="{x:Static vm:Tab.LCU}"
TabCloseRequested="TabView_TabCloseRequested">
<!--Need to override Tab header for Mica theme...-->
<ui:TabView.Resources>
<ResourceDictionary>
<SolidColorBrush x:Key="TabViewItemHeaderBackgroundSelected" Color="{DynamicResource ControlFillColorTransparent}"/>
</ResourceDictionary>
</ui:TabView.Resources>
<!--We need to hack this style for Mica theme since there is no way to explicity set style priority in Avalonia...-->
<ui:TabView.Styles>
<Style Selector="Grid > ContentPresenter#TabContentPresenter">
<Style.Animations>
<Animation IterationCount="1" Duration="0:0:1" FillMode="Both">
<KeyFrame Cue="0%">
<Setter Property="Background" Value="{DynamicResource ControlFillColorTransparent}"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Background" Value="{DynamicResource ControlFillColorTransparent}"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</ui:TabView.Styles>
<ui:TabView.TabItemTemplate>
<DataTemplate DataType="vm:EndpointItem">
<ui:TabViewItem Header="{Binding Header}"
@@ -23,7 +46,24 @@
Content="{Binding}">
<ui:TabViewItem.ContentTemplate>
<DataTemplate DataType="vm:EndpointItem">
<ContentControl Content="{Binding Content}"/>
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
<Menu Grid.Row="0"
Grid.Column="0">
<MenuItem Header="_New tab">
<MenuItem Header="LCU"
Command="{Binding #EndpointsTab.((vm:EndpointsTabViewModel)DataContext).AddEndpointCommand}"
CommandParameter="{x:Static vm:Tab.LCU}"/>
<MenuItem Header="Game Client"
Command="{Binding #EndpointsTab.((vm:EndpointsTabViewModel)DataContext).AddEndpointCommand}"
CommandParameter="{x:Static vm:Tab.GameClient}"/>
</MenuItem>
</Menu>
<Separator Grid.Row="1"
Grid.Column="0"/>
<ContentControl Grid.Row="2"
Grid.Column="0"
Content="{Binding Content}"/>
</Grid>
</DataTemplate>
</ui:TabViewItem.ContentTemplate>
</ui:TabViewItem>

View File

@@ -13,7 +13,12 @@ public partial class EndpointsTabView : UserControl
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);
if (args.Tab.Content is EndpointItem item && sender.TabItems is IList tabItems)
{
if (tabItems.Count > 1)
{
tabItems.Remove(item);
}
}
}
}

View File

@@ -40,7 +40,7 @@
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">
Welcome to Needlework.Net
</TextBlock>
<TextBlock>Get started with LCU development by clicking on the endpoints tab in the left panel.</TextBlock>
<TextBlock>Get started with LCU or Game Client development by clicking on the endpoints tab in the left panel.</TextBlock>
</StackPanel>
</Border>
<controls:Card Margin="12">
@@ -66,12 +66,15 @@
<Button CommandParameter="https://discord.com/channels/187652476080488449/516802588805431296" Margin="4">
#lcu-api
</Button>
<Button CommandParameter="https://discord.com/channels/187652476080488449/543112946402721832" Margin="4">
#ingame-api
</Button>
</StackPanel>
</StackPanel>
</controls:Card>
<controls:Card Margin="12" Width="300">
<StackPanel>
<TextBlock>© 2024 - Blossomi Shymae</TextBlock>
<TextBlock>© 2025 - Blossomi Shymae</TextBlock>
<TextBlock>MIT License</TextBlock>
</StackPanel>
</controls:Card>

View File

@@ -2,11 +2,11 @@
![App preview](app-preview.png)
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! 🌠
Needlework.Net is an open-source helper tool for the LCU and Game Client that provides documented endpoints and can send requests without any code setup. Created using .NET! 🌠
# Features
- Interactive OpenAPI documentation
- Interactive OpenAPI documentations
- REST data transfer console
- WebSocket event data viewer
@@ -39,9 +39,14 @@ This project was inspired by LCU Explorer, an application created by the Hextech
### hasagi-types
Endpoints and schemas are provided by dysolix's [generated OpenAPI file.](https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json) Thank you!
LCU Schema endpoints are provided by dysolix's [generated OpenAPI file.](https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json) Thank you!
- [Repository](https://github.com/dysolix/hasagi-types)
### Irelia
Game Client endpoints are provided by AlsoSylv's [fixed OpenAPI file.](https://raw.githubusercontent.com/AlsoSylv/Irelia/refs/heads/master/schemas/game_schema.json) Thank you despite not wanting to receive credit. :pout:
- [Repository](https://github.com/AlsoSylv/Irelia)
## Disclaimer
THE PROGRAM IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF MERCHANTABILITY, NONINFRINGMENT, OR OF FITNESS FOR A PARTICULAR PURPOSE. LICENSOR DOES NOT WARRANT THAT THE FUNCTIONS CONTAINED IN THE PROGRAM WILL MEET YOUR REQUIREMENTS OR THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. LICENSOR MAKES NO WARRANTIES RESPECTING ANY HARM THAT MAY BE CAUSED BY MALICIOUS USE OF THIS SOFTWARE. LICENSOR FURTHER EXPRESSLY DISCLAIMS ANY WARRANTY OR REPRESENTATION TO AUTHORIZED USERS OR TO ANY THIRD PARTY.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

After

Width:  |  Height:  |  Size: 397 KiB