13 Commits

Author SHA1 Message Date
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
BlossomiShymae
b63713f054 Bump version 2024-12-18 01:07:25 -06:00
BlossomiShymae
6a776dfd5f Add #lcu-api channel link 2024-12-17 23:49:13 -06:00
BlossomiShymae
9270c6d1f1 Add logging 2024-12-17 23:36:28 -06:00
BlossomiShymae
f65c6f1b09 Update and add packages 2024-12-17 21:09:40 -06:00
20 changed files with 272 additions and 71 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
namespace Needlework.Net.Models; namespace Needlework.Net.Models;
@@ -35,10 +36,10 @@ public class OpenApiDocumentWrapper
{ {
pluginsKey = "default"; pluginsKey = "default";
if (plugins.TryGetValue(pluginsKey, out var p)) if (plugins.TryGetValue(pluginsKey, out var p))
p.Add(new(method.ToString(), path, operation)); p.Add(new(method.ToString(), path, pluginsKey, operation));
else else
{ {
operations.Add(new(method.ToString(), path, operation)); operations.Add(new(method.ToString(), path, pluginsKey, operation));
plugins[pluginsKey] = operations; plugins[pluginsKey] = operations;
} }
} }
@@ -46,19 +47,16 @@ public class OpenApiDocumentWrapper
{ {
foreach (var tag in operation.Tags) foreach (var tag in operation.Tags)
{ {
var lowercaseTag = tag.Name.ToLower(); if (tag.Name == "plugins")
if (lowercaseTag == "plugins")
continue; continue;
else if (lowercaseTag.Contains("plugin "))
pluginsKey = lowercaseTag.Replace("plugin ", "");
else else
pluginsKey = lowercaseTag; pluginsKey = tag.Name;
if (plugins.TryGetValue(pluginsKey, out var p)) if (plugins.TryGetValue(pluginsKey, out var p))
p.Add(new(method.ToString(), path, operation)); p.Add(new(method.ToString(), path, tag.Name, operation));
else else
{ {
operations.Add(new(method.ToString(), path, operation)); operations.Add(new(method.ToString(), path, tag.Name, operation));
plugins[pluginsKey] = operations; 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; Plugins = plugins;
} }
} }

View File

@@ -2,4 +2,4 @@ using Microsoft.OpenApi.Models;
namespace Needlework.Net.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

@@ -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> <AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
<ApplicationIcon>app.ico</ApplicationIcon> <ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>NeedleworkDotNet</AssemblyName> <AssemblyName>NeedleworkDotNet</AssemblyName>
<AssemblyVersion>0.9.1.0</AssemblyVersion> <AssemblyVersion>0.11.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion> <FileVersion>$(AssemblyVersion)</FileVersion>
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions> <AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
</PropertyGroup> </PropertyGroup>
@@ -31,13 +31,17 @@
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0" /> <PackageReference Include="FluentAvaloniaUI" Version="2.1.0" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" /> <PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.22" /> <PackageReference Include="Microsoft.OpenApi" Version="1.6.22" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.22" /> <PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.22" />
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.4.0" /> <PackageReference Include="Projektanker.Icons.Avalonia" Version="9.4.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" /> <PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.64" /> <PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.65" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -6,8 +6,10 @@ using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages; using Needlework.Net.ViewModels.Pages;
using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome; using Projektanker.Icons.Avalonia.FontAwesome;
using Serilog;
using System; using System;
using System.IO; using System.IO;
using System.Reflection;
namespace Needlework.Net; namespace Needlework.Net;
@@ -34,7 +36,11 @@ class Program
return AppBuilder.Configure(() => new App(BuildServices())) return AppBuilder.Configure(() => new App(BuildServices()))
.UsePlatformDetect() .UsePlatformDetect()
.WithInterFont() .WithInterFont()
.LogToTrace(); .LogToTrace()
.With(new Win32PlatformOptions
{
CompositionMode = [ Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition ]
});
} }
private static IServiceProvider BuildServices() private static IServiceProvider BuildServices()
@@ -44,9 +50,16 @@ class Program
builder.AddSingleton<MainWindowViewModel>(); builder.AddSingleton<MainWindowViewModel>();
builder.AddSingleton<DialogService>(); builder.AddSingleton<DialogService>();
builder.AddSingletonsFromAssemblies<PageBase>(); builder.AddSingletonsFromAssemblies<PageBase>();
builder.AddHttpClient(); 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));
var services = builder.BuildServiceProvider(); var services = builder.BuildServiceProvider();
return services; return services;
} }

View File

@@ -1,8 +1,10 @@
using Avalonia.Collections; using Avalonia.Collections;
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Needlework.Net.Messages; using Needlework.Net.Messages;
using Needlework.Net.Models; using Needlework.Net.Models;
@@ -17,8 +19,10 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Reflection; using System.Reflection;
using System.Text.Json.Nodes;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers;
namespace Needlework.Net.ViewModels.MainWindow; namespace Needlework.Net.ViewModels.MainWindow;
@@ -33,6 +37,9 @@ public partial class MainWindowViewModel
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0"; public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
[ObservableProperty] private bool _isUpdateShown = false; [ObservableProperty] private bool _isUpdateShown = false;
[ObservableProperty] private string _schemaVersion = "N/A";
[ObservableProperty] private string _schemaVersionLatest = "N/A";
public HttpClient HttpClient { get; } public HttpClient HttpClient { get; }
public DialogService DialogService { get; } public DialogService DialogService { get; }
public OpenApiDocumentWrapper? OpenApiDocumentWrapper { get; set; } public OpenApiDocumentWrapper? OpenApiDocumentWrapper { get; set; }
@@ -42,8 +49,25 @@ public partial class MainWindowViewModel
[ObservableProperty] private ObservableCollection<InfoBarViewModel> _infoBarItems = []; [ObservableProperty] private ObservableCollection<InfoBarViewModel> _infoBarItems = [];
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, DialogService dialogService) private readonly 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)
{
_logger = logger;
MenuItems = new AvaloniaList<NavigationViewItem>(pages MenuItems = new AvaloniaList<NavigationViewItem>(pages
.OrderBy(p => p.Index) .OrderBy(p => p.Index)
.ThenBy(p => p.DisplayName) .ThenBy(p => p.DisplayName)
@@ -61,20 +85,60 @@ public partial class MainWindowViewModel
WeakReferenceMessenger.Default.RegisterAll(this); WeakReferenceMessenger.Default.RegisterAll(this);
Task.Run(FetchDataAsync); 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) private async void OnSchemaVersionTimerElapsed(object? sender, ElapsedEventArgs? e)
{ {
while (!IsUpdateShown) if (OpenApiDocumentWrapper == null) return;
if (!ProcessFinder.IsPortOpen()) return;
try
{ {
Task.Run(CheckLatestVersionAsync); var client = Connector.GetLcuHttpClientInstance();
Thread.Sleep(TimeSpan.FromMinutes(10)); // Avoid tripping unauthenticated rate limits var currentSemVer = OpenApiDocumentWrapper.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}", OpenApiDocumentWrapper.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 Task CheckLatestVersionAsync() private async void OnLatestUpdateTimerElapsed(object? sender, ElapsedEventArgs? e)
{ {
try try
{ {
@@ -83,7 +147,11 @@ public partial class MainWindowViewModel
var response = await HttpClient.SendAsync(request); var response = await HttpClient.SendAsync(request);
var release = await response.Content.ReadFromJsonAsync<GithubRelease>(); var release = await response.Content.ReadFromJsonAsync<GithubRelease>();
if (release == null) return; if (release == null)
{
_logger.LogWarning("Release response is null");
return;
}
var currentVersion = int.Parse(Version.Replace(".", "")); var currentVersion = int.Parse(Version.Replace(".", ""));
@@ -91,20 +159,27 @@ public partial class MainWindowViewModel
{ {
Avalonia.Threading.Dispatcher.UIThread.Post(async () => 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, Command = OpenUrlCommand,
CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases", CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases",
Content = "Download" Content = "Download"
})); }));
IsUpdateShown = true;
}); });
_latestUpdateTimer.Elapsed -= OnLatestUpdateTimerElapsed;
_latestUpdateTimer.Stop();
} }
} }
catch (Exception) { } catch (Exception ex)
{
_logger.LogError(ex, "Failed to check for latest version");
}
} }
private async Task FetchDataAsync() private async Task FetchDataAsync()
{
try
{ {
var document = await Resources.GetOpenApiDocumentAsync(HttpClient); var document = await Resources.GetOpenApiDocumentAsync(HttpClient);
HostDocument = document; HostDocument = document;
@@ -114,6 +189,11 @@ public partial class MainWindowViewModel
WeakReferenceMessenger.Default.Send(new DataReadyMessage(handler)); WeakReferenceMessenger.Default.Send(new DataReadyMessage(handler));
IsBusy = false; IsBusy = false;
} }
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch OpenAPI data");
}
}
public void Receive(DataRequestMessage message) public void Receive(DataRequestMessage message)
{ {

View File

@@ -2,6 +2,7 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using Needlework.Net.Messages; using Needlework.Net.Messages;
using Needlework.Net.ViewModels.Shared; using Needlework.Net.ViewModels.Shared;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -14,10 +15,11 @@ public partial class ConsoleViewModel : PageBase, IRecipient<DataReadyMessage>
public IAvaloniaList<string> RequestPaths { get; } = new AvaloniaList<string>(); public IAvaloniaList<string> RequestPaths { get; } = new AvaloniaList<string>();
[ObservableProperty] private bool _isBusy = true; [ObservableProperty] private bool _isBusy = true;
[ObservableProperty] private LcuRequestViewModel _lcuRequest = new(); [ObservableProperty] private LcuRequestViewModel _lcuRequest;
public ConsoleViewModel() : base("Console", "terminal", -200) public ConsoleViewModel(ILogger<LcuRequestViewModel> lcuRequestViewModelLogger) : base("Console", "terminal", -200)
{ {
_lcuRequest = new(lcuRequestViewModelLogger);
WeakReferenceMessenger.Default.Register<DataReadyMessage>(this); WeakReferenceMessenger.Default.Register<DataReadyMessage>(this);
} }

View File

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

View File

@@ -1,6 +1,8 @@
using Avalonia.Collections; using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Needlework.Net.ViewModels.Shared;
using System; using System;
namespace Needlework.Net.ViewModels.Pages.Endpoints; namespace Needlework.Net.ViewModels.Pages.Endpoints;
@@ -15,9 +17,9 @@ public partial class EndpointsNavigationViewModel : ObservableObject
private readonly Action<string?, Guid> _onEndpointNavigation; private readonly Action<string?, Guid> _onEndpointNavigation;
public EndpointsNavigationViewModel(IAvaloniaList<string> plugins, Action<string?, Guid> onEndpointNavigation) public EndpointsNavigationViewModel(IAvaloniaList<string> plugins, Action<string?, Guid> onEndpointNavigation, ILogger<LcuRequestViewModel> lcuRequestViewModelLogger)
{ {
_activeViewModel = _endpointsViewModel = new EndpointsViewModel(plugins, OnClicked); _activeViewModel = _endpointsViewModel = new EndpointsViewModel(plugins, OnClicked, lcuRequestViewModelLogger);
_onEndpointNavigation = onEndpointNavigation; _onEndpointNavigation = onEndpointNavigation;
} }

View File

@@ -4,7 +4,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using Needlework.Net.Messages; using Needlework.Net.Messages;
using Needlework.Net.ViewModels.Shared;
using System; using System;
namespace Needlework.Net.ViewModels.Pages.Endpoints; namespace Needlework.Net.ViewModels.Pages.Endpoints;
@@ -16,8 +18,11 @@ public partial class EndpointsTabViewModel : PageBase, IRecipient<DataReadyMessa
[ObservableProperty] private bool _isBusy = true; [ObservableProperty] private bool _isBusy = true;
public EndpointsTabViewModel() : base("Endpoints", "list-alt", -500) private readonly ILogger<LcuRequestViewModel> _lcuRequestViewModelLogger;
public EndpointsTabViewModel(ILogger<LcuRequestViewModel> lcuRequestViewModelLogger) : base("Endpoints", "list-alt", -500)
{ {
_lcuRequestViewModelLogger = lcuRequestViewModelLogger;
WeakReferenceMessenger.Default.RegisterAll(this); WeakReferenceMessenger.Default.RegisterAll(this);
} }
@@ -35,7 +40,7 @@ public partial class EndpointsTabViewModel : PageBase, IRecipient<DataReadyMessa
{ {
Endpoints.Add(new() Endpoints.Add(new()
{ {
Content = new EndpointsNavigationViewModel(Plugins, OnEndpointNavigation), Content = new EndpointsNavigationViewModel(Plugins, OnEndpointNavigation, _lcuRequestViewModelLogger),
Selected = true Selected = true
}); });
} }

View File

@@ -1,6 +1,8 @@
using Avalonia.Collections; using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Needlework.Net.ViewModels.Shared;
using System; using System;
using System.Linq; using System.Linq;
@@ -16,11 +18,14 @@ public partial class EndpointsViewModel : ObservableObject
public Action<ObservableObject> OnClicked { get; } public Action<ObservableObject> OnClicked { get; }
public EndpointsViewModel(IAvaloniaList<string> plugins, Action<ObservableObject> onClicked) private readonly ILogger<LcuRequestViewModel> _lcuRequestViewModelLogger;
public EndpointsViewModel(IAvaloniaList<string> plugins, Action<ObservableObject> onClicked, ILogger<LcuRequestViewModel> lcuRequestViewModelLogger)
{ {
Plugins = new AvaloniaList<string>(plugins); Plugins = new AvaloniaList<string>(plugins);
Query = new AvaloniaList<string>(plugins); Query = new AvaloniaList<string>(plugins);
OnClicked = onClicked; OnClicked = onClicked;
_lcuRequestViewModelLogger = lcuRequestViewModelLogger;
} }
partial void OnSearchChanged(string value) partial void OnSearchChanged(string value)
@@ -37,6 +42,6 @@ public partial class EndpointsViewModel : ObservableObject
{ {
if (string.IsNullOrEmpty(value)) return; if (string.IsNullOrEmpty(value)) return;
OnClicked.Invoke(new EndpointViewModel(value)); OnClicked.Invoke(new EndpointViewModel(value, _lcuRequestViewModelLogger));
} }
} }

View File

@@ -1,5 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel; using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Needlework.Net.Models; using Needlework.Net.Models;
using Needlework.Net.ViewModels.Shared; using Needlework.Net.ViewModels.Shared;
using System; using System;
@@ -13,17 +15,20 @@ public partial class PathOperationViewModel : ObservableObject
public string Path { get; } public string Path { get; }
public OperationViewModel Operation { get; } public OperationViewModel Operation { get; }
public string Url { get; }
[ObservableProperty] private bool _isBusy; [ObservableProperty] private bool _isBusy;
[ObservableProperty] private Lazy<LcuRequestViewModel> _lcuRequest; [ObservableProperty] private Lazy<LcuRequestViewModel> _lcuRequest;
public PathOperationViewModel(PathOperation pathOperation) public PathOperationViewModel(PathOperation pathOperation, ILogger<LcuRequestViewModel> lcuRequestViewModelLogger)
{ {
Path = pathOperation.Path; Path = pathOperation.Path;
Operation = new OperationViewModel(pathOperation.Operation); Operation = new OperationViewModel(pathOperation.Operation);
LcuRequest = new(() => new LcuRequestViewModel() LcuRequest = new(() => new LcuRequestViewModel(lcuRequestViewModelLogger)
{ {
Method = pathOperation.Method.ToUpper() Method = pathOperation.Method.ToUpper()
}); });
Url = $"https://swagger.dysolix.dev/lcu/#/{pathOperation.Tag}/{pathOperation.Operation.OperationId}";
} }
[RelayCommand] [RelayCommand]
@@ -49,4 +54,10 @@ public partial class PathOperationViewModel : ObservableObject
LcuRequest.Value.RequestPath = sb.ToString(); LcuRequest.Value.RequestPath = sb.ToString();
await LcuRequest.Value.ExecuteAsync(); await LcuRequest.Value.ExecuteAsync();
} }
[RelayCommand]
private void CopyUrl()
{
App.MainWindow?.Clipboard?.SetTextAsync(Url);
}
} }

View File

@@ -3,11 +3,11 @@ using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using Needlework.Net.Messages; using Needlework.Net.Messages;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
@@ -45,8 +45,11 @@ public partial class WebsocketViewModel : PageBase
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))]; public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
public WebsocketViewModel(HttpClient httpClient) : base("Event Viewer", "plug", -100) private readonly ILogger<WebsocketViewModel> _logger;
public WebsocketViewModel(HttpClient httpClient, ILogger<WebsocketViewModel> logger) : base("Event Viewer", "plug", -100)
{ {
_logger = logger;
HttpClient = httpClient; HttpClient = httpClient;
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog)); EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
Task.Run(async () => Task.Run(async () =>
@@ -66,6 +69,7 @@ public partial class WebsocketViewModel : PageBase
} }
catch (HttpRequestException ex) catch (HttpRequestException ex)
{ {
_logger.LogError(ex, "Failed to get event types");
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new("Failed to get event types", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(10)))); WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new("Failed to get event types", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(10))));
} }
} }
@@ -76,6 +80,7 @@ public partial class WebsocketViewModel : PageBase
{ {
if (Client != null) if (Client != null)
{ {
_logger.LogDebug("Disposing old connection");
foreach (var disposable in ClientDisposables) foreach (var disposable in ClientDisposables)
disposable.Dispose(); disposable.Dispose();
ClientDisposables.Clear(); ClientDisposables.Clear();
@@ -105,6 +110,7 @@ public partial class WebsocketViewModel : PageBase
}) })
{ IsBackground = true }; { IsBackground = true };
thread.Start(); thread.Start();
_logger.LogDebug("Initialized new connection: {EventType}", EventType);
TokenSource = tokenSource; TokenSource = tokenSource;
} }
} }
@@ -129,12 +135,12 @@ public partial class WebsocketViewModel : PageBase
private void OnReconnection(ReconnectionInfo info) private void OnReconnection(ReconnectionInfo info)
{ {
Trace.WriteLine($"-- Reconnection --\nType{info.Type}"); _logger.LogTrace("Reconnected: {Type}", info.Type);
} }
private void OnDisconnection(DisconnectionInfo info) private void OnDisconnection(DisconnectionInfo info)
{ {
Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProtocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}"); _logger.LogTrace("Disconnected: {Type}", info.Type);
InitializeWebsocket(); InitializeWebsocket();
} }

View File

@@ -2,6 +2,7 @@
using BlossomiShymae.GrrrLCU; using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using Needlework.Net.Messages; using Needlework.Net.Messages;
using Needlework.Net.ViewModels.MainWindow; using Needlework.Net.ViewModels.MainWindow;
using System; using System;
@@ -31,6 +32,13 @@ public partial class LcuRequestViewModel : ObservableObject
public event EventHandler<LcuRequestViewModel>? RequestText; public event EventHandler<LcuRequestViewModel>? RequestText;
public event EventHandler<string>? UpdateText; public event EventHandler<string>? UpdateText;
private readonly ILogger<LcuRequestViewModel> _logger;
public LcuRequestViewModel(ILogger<LcuRequestViewModel> logger)
{
_logger = logger;
}
partial void OnMethodChanged(string? oldValue, string? newValue) partial void OnMethodChanged(string? oldValue, string? newValue)
{ {
if (newValue == null) return; if (newValue == null) return;
@@ -59,6 +67,8 @@ public partial class LcuRequestViewModel : ObservableObject
_ => throw new Exception("Method is not selected or missing."), _ => throw new Exception("Method is not selected or missing."),
}; };
_logger.LogDebug("Sending request: {Tuple}", (Method, RequestPath));
var processInfo = ProcessFinder.GetProcessInfo(); var processInfo = ProcessFinder.GetProcessInfo();
RequestText?.Invoke(this, this); RequestText?.Invoke(this, this);
var content = new StringContent(RequestBody ?? string.Empty, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")); var content = new StringContent(RequestBody ?? string.Empty, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
@@ -88,6 +98,7 @@ public partial class LcuRequestViewModel : ObservableObject
} }
catch (Exception ex) 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)))); 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); UpdateText?.Invoke(this, string.Empty);

View File

@@ -1,3 +1,6 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using FluentAvalonia.UI.Windowing; using FluentAvalonia.UI.Windowing;
namespace Needlework.Net.Views.MainWindow; namespace Needlework.Net.Views.MainWindow;
@@ -9,5 +12,19 @@ public partial class MainWindowView : AppWindow
InitializeComponent(); InitializeComponent();
TitleBar.ExtendsContentIntoTitleBar = true; 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

@@ -55,6 +55,11 @@
<Grid <Grid
RowDefinitions="*" RowDefinitions="*"
ColumnDefinitions="auto,*"> ColumnDefinitions="auto,*">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Copy Swagger URL" Command="{Binding CopyUrlCommand}"/>
</MenuFlyout>
</Grid.ContextFlyout>
<TextBlock <TextBlock
VerticalAlignment="Center" VerticalAlignment="Center"
TextAlignment="Center" TextAlignment="Center"

View File

@@ -15,6 +15,27 @@
<ui:TabView TabItems="{Binding Endpoints}" <ui:TabView TabItems="{Binding Endpoints}"
AddTabButtonCommand="{Binding AddEndpointCommand}" AddTabButtonCommand="{Binding AddEndpointCommand}"
TabCloseRequested="TabView_TabCloseRequested"> 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> <ui:TabView.TabItemTemplate>
<DataTemplate DataType="vm:EndpointItem"> <DataTemplate DataType="vm:EndpointItem">
<ui:TabViewItem Header="{Binding Header}" <ui:TabViewItem Header="{Binding Header}"

View File

@@ -55,18 +55,23 @@
Theme="{StaticResource SubtitleTextBlockStyle}" Theme="{StaticResource SubtitleTextBlockStyle}"
Margin="0 0 0 8">Resources</TextBlock> Margin="0 0 0 8">Resources</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button CommandParameter="https://hextechdocs.dev/tag/lcu/" Margin="0 0 8 0"> <Button CommandParameter="https://hextechdocs.dev/tag/lcu/" Margin="4">
Hextech Docs Hextech Docs
</Button> </Button>
<Button CommandParameter="https://hextechdocs.dev/getting-started-with-the-lcu-api/"> <Button CommandParameter="https://hextechdocs.dev/getting-started-with-the-lcu-api/" Margin="4">
Getting Started Getting Started
</Button> </Button>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button CommandParameter="https://discord.com/channels/187652476080488449/516802588805431296" Margin="4">
#lcu-api
</Button>
</StackPanel>
</StackPanel> </StackPanel>
</controls:Card> </controls:Card>
<controls:Card Margin="12" Width="300"> <controls:Card Margin="12" Width="300">
<StackPanel> <StackPanel>
<TextBlock>© 2024 - Blossomi Shymae</TextBlock> <TextBlock>© 2025 - Blossomi Shymae</TextBlock>
<TextBlock>MIT License</TextBlock> <TextBlock>MIT License</TextBlock>
</StackPanel> </StackPanel>
</controls:Card> </controls:Card>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

After

Width:  |  Height:  |  Size: 397 KiB