9 Commits

Author SHA1 Message Date
estrogen elf
7b831b6c1f feat: fubar 2025-06-12 19:26:31 -05:00
estrogen elf
9515377df9 feat: change time format and ext for logs 2025-06-08 02:51:16 -05:00
estrogen elf
8821119c18 build: add reactive dependencies 2025-06-08 02:48:30 -05:00
estrogen elf
d0a48e3490 build: update dependencies 2025-06-07 17:54:37 -05:00
estrogen elf
e95aa987a1 feat: add MacOS platform options 2025-06-01 16:40:33 -05:00
estrogen elf
7997cf222c feat: update Briar 2025-06-01 16:34:23 -05:00
estrogen elf
a321d84757 feat: increment version 2025-05-31 21:28:36 -05:00
estrogen elf
4bef9a20dd refactor: change dependency from GrrrLCU to Briar 2025-05-31 21:28:11 -05:00
estrogen elf
fb5fbe1fea fix: encode Swagger URL, add Markdown copy 2025-05-31 17:24:43 -05:00
88 changed files with 2097 additions and 1732 deletions

View File

@@ -7,12 +7,8 @@
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
RequestedThemeVariant="Dark"> RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles> <Application.Styles>
<sty:FluentAvaloniaTheme PreferSystemTheme="False" PreferUserAccentColor="False" /> <sty:FluentAvaloniaTheme />
<materialIcons:MaterialIconStyles /> <materialIcons:MaterialIconStyles />
<StyleInclude Source="Controls/Card.axaml"/> <StyleInclude Source="Controls/Card.axaml"/>
<StyleInclude Source="Controls/UserCard.axaml"/> <StyleInclude Source="Controls/UserCard.axaml"/>

View File

@@ -2,18 +2,31 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection; using Flurl.Http.Configuration;
using Needlework.Net.Converters;
using Needlework.Net.Services;
using Needlework.Net.ViewModels.MainWindow; using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages;
using Needlework.Net.ViewModels.Pages.About;
using Needlework.Net.ViewModels.Pages.Console;
using Needlework.Net.ViewModels.Pages.Endpoints;
using Needlework.Net.ViewModels.Pages.Home;
using Needlework.Net.ViewModels.Pages.WebSocket;
using Needlework.Net.Views.MainWindow; using Needlework.Net.Views.MainWindow;
using System; using Needlework.Net.Views.Pages.About;
using Needlework.Net.Views.Pages.Console;
using Needlework.Net.Views.Pages.Endpoints;
using Needlework.Net.Views.Pages.Home;
using Needlework.Net.Views.Pages.WebSocket;
using ReactiveUI;
using Splat;
using Splat.Serilog;
using System.Text.Json; using System.Text.Json;
namespace Needlework.Net; namespace Needlework.Net;
public partial class App(IServiceProvider serviceProvider) : Application public partial class App : Application
{ {
private readonly IServiceProvider _serviceProvider = serviceProvider;
public static JsonSerializerOptions JsonSerializerOptions { get; } = new() public static JsonSerializerOptions JsonSerializerOptions { get; } = new()
{ {
WriteIndented = true, WriteIndented = true,
@@ -31,15 +44,62 @@ public partial class App(IServiceProvider serviceProvider) : Application
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
RegisterValueConverters();
RegisterAppServices();
RegisterViews();
RegisterViewModels();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow = new MainWindowView() desktop.MainWindow = new MainWindow() { DataContext = Locator.Current.GetService<IScreen>() };
{
DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
};
MainWindow = desktop.MainWindow; MainWindow = desktop.MainWindow;
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
private void RegisterValueConverters()
{
Locator.CurrentMutable.RegisterConstant<IBindingTypeConverter>(new NullableToVisibilityConverter());
Locator.CurrentMutable.RegisterConstant<IBindingTypeConverter>(new EnumerableToVisibilityConverter());
}
private static void RegisterViewModels()
{
Locator.CurrentMutable.RegisterConstant<PageBase>(new HomeViewModel());
Locator.CurrentMutable.RegisterConstant<PageBase>(new EndpointsViewModel());
Locator.CurrentMutable.RegisterConstant<PageBase>(new ConsoleViewModel());
Locator.CurrentMutable.RegisterConstant<PageBase>(new WebSocketViewModel());
Locator.CurrentMutable.RegisterConstant<PageBase>(new AboutViewModel());
Locator.CurrentMutable.RegisterConstant<IScreen>(new MainWindowViewModel());
}
private static void RegisterViews()
{
Locator.CurrentMutable.Register<IViewFor<LibraryViewModel>>(() => new LibraryView());
Locator.CurrentMutable.Register<IViewFor<NotificationViewModel>>(() => new NotificationView());
Locator.CurrentMutable.Register<IViewFor<EventViewModel>>(() => new EventView());
Locator.CurrentMutable.Register<IViewFor<EndpointTabListViewModel>>(() => new EndpointTabListView());
Locator.CurrentMutable.Register<IViewFor<EndpointTabItemContentViewModel>>(() => new EndpointTabItemContentView());
Locator.CurrentMutable.Register<IViewFor<EndpointSearchDetailsViewModel>>(() => new EndpointSearchDetailsView());
Locator.CurrentMutable.Register<IViewFor<PluginViewModel>>(() => new PluginView());
Locator.CurrentMutable.Register<IViewFor<PropertyClassViewModel>>(() => new PropertyClassView());
Locator.CurrentMutable.Register<IViewFor<PathOperationViewModel>>(() => new PathOperationView());
Locator.CurrentMutable.RegisterConstant<IViewFor<HomeViewModel>>(new HomePage());
Locator.CurrentMutable.RegisterConstant<IViewFor<EndpointsViewModel>>(new EndpointsPage());
Locator.CurrentMutable.RegisterConstant<IViewFor<ConsoleViewModel>>(new ConsolePage());
Locator.CurrentMutable.RegisterConstant<IViewFor<WebSocketViewModel>>(new WebSocketPage());
Locator.CurrentMutable.RegisterConstant<IViewFor<AboutViewModel>>(new AboutPage());
}
private static void RegisterAppServices()
{
Locator.CurrentMutable.UseSerilogFullLogger(Logger.Setup());
Locator.CurrentMutable.RegisterConstant<IFlurlClientCache>(new FlurlClientCache()
.Add("GithubClient", "https://api.github.com")
.Add("GithubUserContentClient", "https://raw.githubusercontent.com"));
Locator.CurrentMutable.RegisterConstant<NotificationService>(new NotificationService());
Locator.CurrentMutable.RegisterConstant<DataSource>(new DataSource());
}
} }

View File

@@ -0,0 +1,33 @@
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Needlework.Net.Converters
{
public class EnumerableToVisibilityConverter : IBindingTypeConverter
{
public int GetAffinityForObjects(Type fromType, Type toType)
{
if (typeof(IEnumerable<object>).IsAssignableFrom(fromType) && toType == typeof(bool))
{
return 100;
}
return 0;
}
public bool TryConvert(object? from, Type toType, object? conversionHint, out object? result)
{
try
{
result = from is IEnumerable<object> values && values.Any();
return true;
}
catch (Exception)
{
result = null;
return false;
}
}
}
}

View File

@@ -0,0 +1,23 @@
using ReactiveUI;
using System;
namespace Needlework.Net.Converters
{
public class NullableToVisibilityConverter : IBindingTypeConverter
{
public int GetAffinityForObjects(Type fromType, Type toType)
{
if (typeof(object).IsAssignableFrom(fromType) && toType == typeof(bool))
{
return 100;
}
return 0;
}
public bool TryConvert(object? from, Type toType, object? conversionHint, out object? result)
{
result = from != null;
return true;
}
}
}

View File

@@ -1,60 +1,53 @@
using Microsoft.Extensions.Logging; using FastCache;
using Flurl.Http;
using Flurl.Http.Configuration;
using Microsoft.OpenApi.Readers; using Microsoft.OpenApi.Readers;
using Needlework.Net.Models; using Needlework.Net.Models;
using Splat;
using System; using System;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Needlework.Net namespace Needlework.Net
{ {
public class DataSource public class DataSource : IEnableLogger
{ {
private readonly ILogger<DataSource> _logger; private readonly OpenApiStreamReader _reader = new();
private readonly HttpClient _httpClient;
private Document? _lcuSchemaDocument;
private Document? _lolClientDocument;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new();
private readonly IFlurlClient _githubUserContentClient;
public DataSource(HttpClient httpClient, ILogger<DataSource> logger) public DataSource(IFlurlClientCache? clients = null)
{ {
_httpClient = httpClient; _githubUserContentClient = clients?.Get("GithubUserContentClient") ?? Locator.Current.GetService<IFlurlClientCache>()!.Get("GithubUserContentClient")!;
_logger = logger;
} }
public async Task<Document> GetLcuSchemaDocumentAsync() public async Task<Document> GetLcuSchemaDocumentAsync()
{ {
await _taskCompletionSource.Task; if (Cached<Document>.TryGet(nameof(GetLcuSchemaDocumentAsync), out var cached))
return _lcuSchemaDocument ?? throw new InvalidOperationException(); {
return cached;
}
var lcuSchemaStream = await _githubUserContentClient.Request("/dysolix/hasagi-types/main/swagger.json")
.GetStreamAsync();
var lcuSchemaRaw = _reader.Read(lcuSchemaStream, out var _);
var document = new Document(lcuSchemaRaw);
return cached.Save(document, TimeSpan.FromMinutes(60));
} }
public async Task<Document> GetLolClientDocumentAsync() public async Task<Document> GetLolClientDocumentAsync()
{ {
await _taskCompletionSource.Task; if (Cached<Document>.TryGet(nameof(GetLolClientDocumentAsync), out var cached))
return _lolClientDocument ?? throw new InvalidOperationException(); {
} return cached;
}
public async Task InitializeAsync() var lolClientStream = await _githubUserContentClient.Request("/AlsoSylv/Irelia/refs/heads/master/schemas/game_schema.json")
{ .GetStreamAsync();
try var lolClientRaw = _reader.Read(lolClientStream, out var _);
{ var document = new Document(lolClientRaw);
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"); return cached.Save(document, TimeSpan.FromMinutes(60));
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);
}
} }
} }
} }

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using Needlework.Net.ViewModels.MainWindow;
namespace Needlework.Net.Messages
{
public class InfoBarUpdateMessage(InfoBarViewModel vm) : ValueChangedMessage<InfoBarViewModel>(vm)
{
}
}

View File

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

View File

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

View File

@@ -7,6 +7,6 @@ namespace Needlework.Net.Models
[JsonPropertyName("tag_name")] [JsonPropertyName("tag_name")]
public string TagName { get; set; } = string.Empty; public string TagName { get; set; } = string.Empty;
public bool IsLatest(int version) => int.Parse(TagName.Replace(".", "")) > version; public bool IsLatest(string assemblyVersion) => int.Parse(TagName.Replace(".", string.Empty)) > int.Parse(assemblyVersion.Replace(".", string.Empty));
} }
} }

View File

@@ -0,0 +1,9 @@
using FluentAvalonia.UI.Controls;
using System;
namespace Needlework.Net.Models
{
public record Notification(string Title, string Message, InfoBarSeverity InfoBarSeverity, TimeSpan? Duration = null, string? Url = null)
{
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
@@ -11,37 +11,46 @@
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch> <AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
<ApplicationIcon>app.ico</ApplicationIcon> <ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>NeedleworkDotNet</AssemblyName> <AssemblyName>NeedleworkDotNet</AssemblyName>
<AssemblyVersion>0.12.0.0</AssemblyVersion> <AssemblyVersion>0.13.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion> <FileVersion>$(AssemblyVersion)</FileVersion>
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions> <AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.3" /> <PackageReference Include="Avalonia" Version="11.3.1" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.1.0" /> <PackageReference Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.1.3" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.1" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.3" /> <PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.1.3" /> <PackageReference Include="Avalonia.Desktop" Version="11.3.1" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.3" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.1" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.3.1" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.1.0" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.1" />
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.14.0" /> <PackageReference Include="AvaloniaEdit.TextMate" Version="11.3.0" />
<PackageReference Include="BlossomiShymae.Briar" Version="0.2.0" />
<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="FastCache.Cached" Version="1.8.2" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" /> <PackageReference Include="FluentAvaloniaUI" Version="2.3.0" />
<PackageReference Include="Flurl" Version="4.0.0" />
<PackageReference Include="Flurl.Http" Version="4.0.2" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.4.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" 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.24" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.22" /> <PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.24" />
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.4.0" /> <PackageReference Include="Projektanker.Icons.Avalonia" Version="9.6.2" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" /> <PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.6.2" />
<PackageReference Include="Serilog" Version="4.2.0" /> <PackageReference Include="ReactiveUI.SourceGenerators" Version="2.2.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.65" /> <PackageReference Include="Splat.Serilog" Version="15.3.1" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.69" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -63,14 +72,32 @@
<Compile Update="Controls\BusyArea.axaml.cs"> <Compile Update="Controls\BusyArea.axaml.cs">
<DependentUpon>BusyArea.axaml</DependentUpon> <DependentUpon>BusyArea.axaml</DependentUpon>
</Compile> </Compile>
<Compile Update="Views\MainWindow\MainWindowView.axaml.cs"> <Compile Update="Views\MainWindow\MainWindow.axaml.cs">
<DependentUpon>MainWindowView.axaml</DependentUpon> <DependentUpon>MainWindow.axaml</DependentUpon>
</Compile> </Compile>
<Compile Update="Views\Pages\Endpoints\EndpointsNavigationView.axaml.cs"> <Compile Update="Views\Pages\About\AboutPage.axaml.cs">
<DependentUpon>EndpointsNavigationView.axaml</DependentUpon> <DependentUpon>AboutPage.axaml</DependentUpon>
</Compile> </Compile>
<Compile Update="Views\Pages\Endpoints\EndpointView.axaml.cs"> <Compile Update="Views\Pages\Console\ConsolePage.axaml.cs">
<DependentUpon>EndpointView.axaml</DependentUpon> <DependentUpon>ConsolePage.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\EndpointTabListView.axaml.cs">
<DependentUpon>EndpointTabListView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\EndpointTabItemContentView.axaml.cs">
<DependentUpon>EndpointTabItemContentView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\EndpointsPage.axaml.cs">
<DependentUpon>EndpointsPage.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\PluginView.axaml.cs">
<DependentUpon>PluginView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Home\HomePage.axaml.cs">
<DependentUpon>HomePage.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\WebSocket\WebSocketPage.axaml.cs">
<DependentUpon>WebSocketPage.axaml</DependentUpon>
</Compile> </Compile>
</ItemGroup> </ItemGroup>

View File

@@ -1,15 +1,8 @@
using Avalonia; using Avalonia;
using Microsoft.Extensions.DependencyInjection; using Avalonia.ReactiveUI;
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;
using Projektanker.Icons.Avalonia.FontAwesome; using Projektanker.Icons.Avalonia.FontAwesome;
using System; using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace Needlework.Net; namespace Needlework.Net;
@@ -32,46 +25,14 @@ class Program
{ {
IconProvider.Current IconProvider.Current
.Register<FontAwesomeIconProvider>(); .Register<FontAwesomeIconProvider>();
var services = BuildServices();
Task.Run(async () => await InitializeDataSourceAsync(services));
return AppBuilder.Configure(() => new App(services)) return AppBuilder.Configure<App>()
.UsePlatformDetect() .UsePlatformDetect()
.WithInterFont() .WithInterFont()
.With(new Win32PlatformOptions { CompositionMode = [Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition] })
.With(new MacOSPlatformOptions { ShowInDock = true, })
.LogToTrace() .LogToTrace()
.With(new Win32PlatformOptions .UseReactiveUI();
{
CompositionMode = [Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition]
});
}
private static async Task InitializeDataSourceAsync(IServiceProvider services)
{
var dataSource = services.GetRequiredService<DataSource>();
await dataSource.InitializeAsync();
}
private static IServiceProvider BuildServices()
{
var builder = new ServiceCollection();
builder.AddSingleton<MainWindowViewModel>();
builder.AddSingleton<DialogService>();
builder.AddSingleton<DataSource>();
builder.AddSingletonsFromAssemblies<PageBase>();
builder.AddHttpClient();
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;
} }
private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e) private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e)

View File

@@ -0,0 +1,19 @@
using FluentAvalonia.UI.Controls;
using Needlework.Net.Models;
using System;
using System.Reactive.Subjects;
namespace Needlework.Net.Services
{
public class NotificationService
{
private readonly Subject<Notification> _notificationSubject = new();
public IObservable<Notification> Notifications { get { return _notificationSubject; } }
public void Notify(string title, string message, InfoBarSeverity severity, TimeSpan? duration = null, string? url = null)
{
_notificationSubject.OnNext(new Notification(title, message, severity, duration, url));
}
}
}

View File

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

View File

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

View File

@@ -1,217 +1,162 @@
using Avalonia.Collections; using BlossomiShymae.Briar;
using BlossomiShymae.GrrrLCU; using BlossomiShymae.Briar.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Flurl.Http;
using Flurl.Http.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Needlework.Net.Messages;
using Needlework.Net.Models; using Needlework.Net.Models;
using Needlework.Net.Services; using Needlework.Net.Services;
using Needlework.Net.ViewModels.Pages; using Needlework.Net.ViewModels.Pages;
using Needlework.Net.Views.MainWindow; using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Splat;
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.Json; using System.Net.Http.Json;
using System.Reactive;
using System.Reactive.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers;
namespace Needlework.Net.ViewModels.MainWindow; namespace Needlework.Net.ViewModels.MainWindow;
public partial class MainWindowViewModel public partial class MainWindowViewModel
: ObservableObject, IRecipient<InfoBarUpdateMessage>, IRecipient<OopsiesDialogRequestedMessage> : ReactiveObject, IScreen, IEnableLogger
{ {
public IAvaloniaReadOnlyList<NavigationViewItem> MenuItems { get; } private readonly IEnumerable<PageBase> _pages;
[ObservableProperty] private NavigationViewItem _selectedMenuItem;
[ObservableProperty] private PageBase _currentPage;
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0"; private readonly IFlurlClient _githubClient;
[ObservableProperty] private bool _isUpdateShown = false;
[ObservableProperty] private string _schemaVersion = "N/A"; private readonly NotificationService _notificationService;
[ObservableProperty] private string _schemaVersionLatest = "N/A";
public HttpClient HttpClient { get; }
public DialogService DialogService { get; }
private readonly DataSource _dataSource; private readonly DataSource _dataSource;
[ObservableProperty] private bool _isBusy = true; private readonly IDisposable _checkForUpdatesDisposable;
[ObservableProperty] private ObservableCollection<InfoBarViewModel> _infoBarItems = []; private readonly IDisposable _checkForSchemaVersionDisposable;
private readonly ILogger<MainWindowViewModel> _logger; public MainWindowViewModel(IEnumerable<PageBase>? pages = null, IFlurlClientCache? clients = null, NotificationService? notificationService = null, DataSource? dataSource = null)
private readonly System.Timers.Timer _latestUpdateTimer = new()
{ {
Interval = TimeSpan.FromMinutes(10).TotalMilliseconds, _pages = pages ?? Locator.Current.GetServices<PageBase>();
Enabled = true _githubClient = clients?.Get("GithubClient") ?? Locator.Current.GetService<IFlurlClientCache>()!.Get("GithubClient");
}; _notificationService = notificationService ?? Locator.Current.GetService<NotificationService>()!;
_dataSource = dataSource ?? Locator.Current.GetService<DataSource>()!;
private readonly System.Timers.Timer _schemaVersionTimer = new() PageItems = _pages
{
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) .OrderBy(p => p.Index)
.ThenBy(p => p.DisplayName) .ThenBy(p => p.DisplayName)
.Select(p => new NavigationViewItem() .Select(ToNavigationViewItem)
{ .ToList();
Content = p.DisplayName,
Tag = p,
IconSource = new BitmapIconSource() { UriSource = new Uri($"avares://NeedleworkDotNet/Assets/Icons/{p.Icon}.png") }
}));
SelectedMenuItem = MenuItems[0];
CurrentPage = (PageBase)MenuItems[0].Tag!;
HttpClient = httpClient; SelectedPageItem = PageItems.First();
DialogService = dialogService;
WeakReferenceMessenger.Default.RegisterAll(this); this.WhenAnyValue(x => x.SelectedPageItem)
.Subscribe(x => Router.Navigate.Execute((IRoutableViewModel)x.Tag!));
_latestUpdateTimer.Elapsed += OnLatestUpdateTimerElapsed; _notificationService.Notifications.Subscribe(async notification =>
_schemaVersionTimer.Elapsed += OnSchemaVersionTimerElapsed; {
_latestUpdateTimer.Start(); var vm = new NotificationViewModel(notification);
_schemaVersionTimer.Start(); Notifications.Add(vm);
OnLatestUpdateTimerElapsed(null, null); await Task.Delay(notification.Duration ?? TimeSpan.FromSeconds(10));
OnSchemaVersionTimerElapsed(null, null); Notifications.Remove(vm);
});
CheckForUpdatesCommand.ThrownExceptions.Subscribe(ex =>
{
var message = "Failed to check for updates. Please check your internet connection or try again later.";
this.Log()
.Error(ex, message);
_notificationService.Notify("Needlework.Net", message, InfoBarSeverity.Error);
_checkForUpdatesDisposable?.Dispose();
});
_checkForUpdatesDisposable = Observable.Timer(TimeSpan.Zero, TimeSpan.FromMinutes(10))
.Select(time => Unit.Default)
.InvokeCommand(this, x => x.CheckForUpdatesCommand);
CheckForSchemaVersionCommand.ThrownExceptions.Subscribe(ex =>
{
var message = "Failed to check for schema version. Please check your internet connection or try again later.";
this.Log()
.Error(ex, message);
_notificationService.Notify("Needlework.Net", message, InfoBarSeverity.Error);
_checkForSchemaVersionDisposable?.Dispose();
});
_checkForSchemaVersionDisposable = Observable.Timer(TimeSpan.Zero, TimeSpan.FromMinutes(10))
.Select(time => Unit.Default)
.InvokeCommand(this, x => x.CheckForSchemaVersionCommand);
} }
partial void OnSelectedMenuItemChanged(NavigationViewItem value) [Reactive]
private RoutingState _router = new();
[Reactive]
private ObservableCollection<NotificationViewModel> _notifications = [];
[Reactive]
private NavigationViewItem _selectedPageItem;
public List<NavigationViewItem> PageItems = [];
public bool IsSchemaVersionChecked { get; private set; } = false;
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
private NavigationViewItem ToNavigationViewItem(PageBase page) => new()
{ {
if (value.Tag is PageBase page) Content = page.DisplayName,
Tag = page,
IconSource = new BitmapIconSource() { UriSource = new Uri($"avares://NeedleworkDotNet/Assets/Icons/{page.Icon}.png") }
};
[ReactiveCommand]
private async Task CheckForUpdatesAsync()
{
var release = await _githubClient
.Request("/repos/BlossomiShymae/Needlework.Net/releases/latest")
.WithHeader("User-Agent", $"Needlework.Net/{Version}")
.GetJsonAsync<GithubRelease>();
if (release.IsLatest(Version))
{ {
CurrentPage = page; this.Log()
if (!page.IsInitialized) .Info("New version available: {TagName}", release.TagName);
{ _notificationService.Notify("Needlework.Net", $"New version available: {release.TagName}", InfoBarSeverity.Informational, null, "https://github.com/BlossomiShymae/Needlework.Net/releases/latest");
Task.Run(page.InitializeAsync); _checkForUpdatesDisposable?.Dispose();
}
} }
} }
private async void OnSchemaVersionTimerElapsed(object? sender, ElapsedEventArgs? e)
[ReactiveCommand]
private async Task CheckForSchemaVersionAsync()
{ {
if (!ProcessFinder.IsPortOpen()) return; if (!ProcessFinder.IsPortOpen()) return;
var lcuSchemaDocument = await _dataSource.GetLcuSchemaDocumentAsync(); var lcuSchemaDocument = await _dataSource.GetLcuSchemaDocumentAsync();
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('.');
try if (!IsSchemaVersionChecked)
{ {
var client = Connector.GetLcuHttpClientInstance(); this.Log()
.Info("LCU Schema (current): {Version}", lcuSchemaDocument.Info.Version);
var currentSemVer = lcuSchemaDocument.Info.Version.Split('.'); this.Log()
var systemBuild = await client.GetFromJsonAsync<SystemBuild>("/system/v1/builds") ?? throw new NullReferenceException(); .Info("LCU Schema (latest): {Version}", systemBuild.Version);
var latestSemVer = systemBuild.Version.Split('.'); IsSchemaVersionChecked = true;
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)
bool isVersionMatching = currentSemVer[0] == latestSemVer[0] && currentSemVer[1] == latestSemVer[1]; // Compare major and minor versions
if (!isVersionMatching)
{ {
_logger.LogError(ex, "Schema version check failed"); this.Log()
.Warn("LCU Schema version mismatch: Current {CurrentVersion}, Latest {LatestVersion}", lcuSchemaDocument.Info.Version, systemBuild.Version);
_notificationService.Notify("Needlework.Net", $"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, null, "https://github.com/dysolix/hasagi-types#updating-the-types");
_checkForSchemaVersionDisposable?.Dispose();
} }
} }
private async void OnLatestUpdateTimerElapsed(object? sender, ElapsedEventArgs? e)
{
try
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/BlossomiShymae/Needlework.Net/releases/latest");
request.Headers.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("Needlework.Net", Version));
var response = await HttpClient.SendAsync(request);
var release = await response.Content.ReadFromJsonAsync<GithubRelease>();
if (release == null)
{
_logger.LogWarning("Release response is null");
return;
}
var currentVersion = int.Parse(Version.Replace(".", ""));
if (release.IsLatest(currentVersion))
{
Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
{
await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(30), new Avalonia.Controls.Button()
{
Command = OpenUrlCommand,
CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases",
Content = "Download"
}));
});
_latestUpdateTimer.Elapsed -= OnLatestUpdateTimerElapsed;
_latestUpdateTimer.Stop();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to check for latest version");
}
}
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo(url)
{
UseShellExecute = true
}
};
process.Start();
}
public void Receive(InfoBarUpdateMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Post(async () => await ShowInfoBarAsync(message.Value));
}
private async Task ShowInfoBarAsync(InfoBarViewModel vm)
{
InfoBarItems.Add(vm);
await Task.Delay(vm.Duration);
InfoBarItems.Remove(vm);
}
public void Receive(OopsiesDialogRequestedMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(async () => await DialogService.ShowAsync<OopsiesDialog>(message.Value));
}
} }

View File

@@ -0,0 +1,36 @@
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
namespace Needlework.Net.ViewModels.MainWindow
{
public partial class NotificationViewModel : ReactiveObject
{
private IObservable<bool> _canExecute;
public NotificationViewModel(Needlework.Net.Models.Notification notification)
{
Notification = notification;
_canExecute = this.WhenAnyValue(x => x.Notification.Url)
.Select(url => !string.IsNullOrEmpty(url));
_isButtonVisibleHelper = _canExecute.ToProperty(this, x => x.IsButtonVisible);
}
[ObservableAsProperty]
private bool _isButtonVisible = false;
public Needlework.Net.Models.Notification Notification { get; }
[ReactiveCommand(CanExecute = nameof(_canExecute))]
public void OpenUrl()
{
var process = new Process() { StartInfo = new() { UseShellExecute = true } };
process.Start();
}
}
}

View File

@@ -0,0 +1,16 @@
using ReactiveUI;
using Splat;
namespace Needlework.Net.ViewModels.Pages.About;
public partial class AboutViewModel : PageBase
{
public AboutViewModel(IScreen? screen = null) : base("About", "info-circle")
{
HostScreen = screen ?? Locator.Current.GetService<IScreen>()!;
}
public override string? UrlPathSegment => "about";
public override IScreen HostScreen { get; }
}

View File

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

View File

@@ -0,0 +1,66 @@
using DynamicData;
using Needlework.Net.ViewModels.Shared;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Splat;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Console;
public partial class ConsoleViewModel : PageBase
{
private readonly DataSource _dataSource;
public ConsoleViewModel(IScreen? screen = null, DataSource? dataSource = null) : base("Console", "terminal", -200)
{
_dataSource = dataSource ?? Locator.Current.GetService<DataSource>()!;
_request = new(Endpoints.Tab.LCU);
HostScreen = screen ?? Locator.Current.GetService<IScreen>()!;
GetRequestPathsCommand.Subscribe(paths =>
{
RequestPaths.Clear();
RequestPaths.AddRange(paths);
IsBusy = false;
});
GetRequestPathsCommand.ThrownExceptions.Subscribe(ex =>
{
this.Log()
.Error(ex, "Failed to load request paths from LCU Schema document.");
IsBusy = false;
});
}
public ObservableCollection<string> RequestMethods { get; } = ["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE"];
public override string? UrlPathSegment => "console";
public override ReactiveUI.IScreen HostScreen { get; }
[Reactive]
private ObservableCollection<string> _requestPaths = [];
[Reactive]
private bool _isBusy = true;
[Reactive]
private RequestViewModel _request;
[ReactiveCommand]
public async Task<List<string>> GetRequestPathsAsync()
{
var document = await _dataSource.GetLcuSchemaDocumentAsync();
return document.Paths;
}
[ReactiveCommand]
private async Task SendRequestAsync()
{
await Request.ExecuteAsync();
}
}

View File

@@ -1,45 +0,0 @@
using Avalonia.Collections;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Needlework.Net.ViewModels.Shared;
using System.Net.Http;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages;
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 RequestViewModel _request;
private readonly DataSource _dataSource;
public ConsoleViewModel(ILogger<RequestViewModel> requestViewModelLogger, DataSource dataSource, HttpClient httpClient) : base("Console", "terminal", -200)
{
_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 Request.ExecuteAsync();
}
}

View File

@@ -0,0 +1,35 @@
using Needlework.Net.Models;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System;
namespace Needlework.Net.ViewModels.Pages.Endpoints
{
public partial class EndpointSearchDetailsViewModel : ReactiveObject
{
private readonly Document _document;
private readonly Tab _tab;
private readonly Action<ReactiveObject> _onClicked;
public EndpointSearchDetailsViewModel(Document document, Tab tab, Action<ReactiveObject> onClicked, string? plugin)
{
_document = document;
_tab = tab;
_onClicked = onClicked;
_plugin = plugin;
}
[Reactive]
private string? _plugin;
[ReactiveCommand]
private void OpenEndpoint()
{
if (string.IsNullOrEmpty(_plugin)) return;
_onClicked.Invoke(new PluginViewModel(_plugin, _document, _tab));
}
}
}

View File

@@ -0,0 +1,61 @@
using CommunityToolkit.Mvvm.Input;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System;
using System.Collections.ObjectModel;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class EndpointTabItemContentViewModel : ReactiveObject
{
private readonly Action<string?, Guid> _onEndpointNavigation;
private readonly Tab _tab;
public EndpointTabItemContentViewModel(ObservableCollection<string> plugins, Action<string?, Guid> onEndpointNavigation, Models.Document document, Tab tab)
{
_activeViewModel = _endpointsViewModel = new EndpointTabListViewModel(plugins, OnClicked, document, tab);
_onEndpointNavigation = onEndpointNavigation;
_tab = tab;
_title = GetTitle(tab);
}
public Guid Guid { get; } = Guid.NewGuid();
[Reactive]
private ReactiveObject _activeViewModel;
[Reactive]
private ReactiveObject _endpointsViewModel;
[Reactive]
private string _title;
private string GetTitle(Tab tab)
{
return tab switch
{
Tab.LCU => "LCU",
Tab.GameClient => "Game Client",
_ => string.Empty,
};
}
private void OnClicked(ReactiveObject viewModel)
{
ActiveViewModel = viewModel;
if (viewModel is PluginViewModel endpoint)
{
Title = $"{GetTitle(_tab)} - {endpoint.Title}";
_onEndpointNavigation.Invoke(endpoint.Title, Guid);
}
}
[RelayCommand]
private void GoBack()
{
ActiveViewModel = EndpointsViewModel;
Title = GetTitle(_tab);
_onEndpointNavigation.Invoke(null, Guid);
}
}

View File

@@ -0,0 +1,29 @@
using FluentAvalonia.UI.Controls;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
namespace Needlework.Net.ViewModels.Pages.Endpoints
{
public partial class EndpointTabItemViewModel : ReactiveObject
{
public EndpointTabItemViewModel(EndpointTabItemContentViewModel content, string? header = null, IconSource? iconSource = null, bool? selected = null)
{
_content = content;
_header = header ?? string.Empty;
_iconSource = iconSource ?? new SymbolIconSource() { Symbol = Symbol.Document, FontSize = 20.0, Foreground = Avalonia.Media.Brushes.White };
_selected = selected ?? false;
}
[Reactive]
private string _header;
[Reactive]
private IconSource _iconSource;
[Reactive]
private bool _selected;
[Reactive]
private EndpointTabItemContentViewModel _content;
}
}

View File

@@ -0,0 +1,42 @@
using DynamicData;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class EndpointTabListViewModel : ReactiveObject
{
public EndpointTabListViewModel(ObservableCollection<string> plugins, Action<ReactiveObject> onClicked, Models.Document document, Tab tab)
{
Plugins = new ObservableCollection<EndpointSearchDetailsViewModel>(plugins.Select(plugin => new EndpointSearchDetailsViewModel(document, tab, onClicked, plugin)));
this.WhenAnyValue(x => x.Search)
.Subscribe(search =>
{
EndpointSearchDetails.Clear();
if (string.IsNullOrEmpty(search))
{
EndpointSearchDetails.AddRange(
plugins.Where(plugin => plugin.Contains(search, StringComparison.InvariantCultureIgnoreCase))
.Select(plugin => new EndpointSearchDetailsViewModel(document, tab, onClicked, plugin)));
}
else
{
EndpointSearchDetails.AddRange(
plugins.Select(plugin => new EndpointSearchDetailsViewModel(document, tab, onClicked, plugin)));
}
});
}
public ObservableCollection<EndpointSearchDetailsViewModel> Plugins { get; }
[Reactive]
private ObservableCollection<EndpointSearchDetailsViewModel> _endpointSearchDetails = [];
[Reactive]
private string _search = string.Empty;
}

View File

@@ -1,48 +0,0 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.Logging;
using Needlework.Net.ViewModels.Shared;
using System;
using System.Linq;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class EndpointViewModel : ObservableObject
{
public string Endpoint { get; }
public string Title => Endpoint;
public IAvaloniaReadOnlyList<PathOperationViewModel> PathOperations { get; }
[ObservableProperty] private PathOperationViewModel? _selectedPathOperation;
[ObservableProperty] private string? _search;
public IAvaloniaList<PathOperationViewModel> FilteredPathOperations { get; }
public event EventHandler<string>? PathOperationSelected;
public EndpointViewModel(string endpoint, ILogger<RequestViewModel> requestViewModelLogger, Models.Document document, Tab tab, System.Net.Http.HttpClient httpClient)
{
Endpoint = endpoint;
PathOperations = new AvaloniaList<PathOperationViewModel>(document.Plugins[endpoint].Select(x => new PathOperationViewModel(x, requestViewModelLogger, document, tab, httpClient)));
FilteredPathOperations = new AvaloniaList<PathOperationViewModel>(PathOperations);
}
partial void OnSearchChanged(string? value)
{
FilteredPathOperations.Clear();
if (string.IsNullOrWhiteSpace(value))
{
FilteredPathOperations.AddRange(PathOperations);
return;
}
FilteredPathOperations.AddRange(PathOperations.Where(o => o.Path.Contains(value, StringComparison.InvariantCultureIgnoreCase)));
}
partial void OnSelectedPathOperationChanged(PathOperationViewModel? value)
{
if (value == null) return;
PathOperationSelected?.Invoke(this, value.Operation.RequestTemplate ?? string.Empty);
}
}

View File

@@ -1,56 +0,0 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Needlework.Net.ViewModels.Shared;
using System;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class EndpointsNavigationViewModel : ObservableObject
{
public Guid Guid { get; } = Guid.NewGuid();
[ObservableProperty] private ObservableObject _activeViewModel;
[ObservableProperty] private ObservableObject _endpointsViewModel;
[ObservableProperty] private string _title;
private readonly Action<string?, Guid> _onEndpointNavigation;
private readonly Tab _tab;
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, 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)
{
ActiveViewModel = viewModel;
if (viewModel is EndpointViewModel endpoint)
{
Title = $"{GetTitle(_tab)} - {endpoint.Title}";
_onEndpointNavigation.Invoke(endpoint.Title, Guid);
}
}
[RelayCommand]
private void GoBack()
{
ActiveViewModel = EndpointsViewModel;
Title = GetTitle(_tab);
_onEndpointNavigation.Invoke(null, Guid);
}
}

View File

@@ -1,86 +0,0 @@
using Avalonia.Collections;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
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 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<RequestViewModel> _requestViewModelLogger;
private readonly DataSource _dataSource;
private readonly HttpClient _httpClient;
public EndpointsTabViewModel(ILogger<RequestViewModel> requestViewModelLogger, DataSource dataSource, IHttpClientFactory httpClientFactory) : base("Endpoints", "list-alt", -500)
{
_requestViewModelLogger = requestViewModelLogger;
_dataSource = dataSource;
_httpClient = httpClientFactory.CreateClient(nameof(EndpointsTabViewModel));
}
public override async Task InitializeAsync()
{
await Dispatcher.UIThread.Invoke(async () => await AddEndpoint(Tab.LCU));
IsBusy = false;
IsInitialized = true;
}
[RelayCommand]
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 = vm,
Header = vm.Title,
Selected = true
});
}
private void OnEndpointNavigation(string? title, Guid guid)
{
foreach (var endpoint in Endpoints)
{
if (endpoint.Content.Guid.Equals(guid))
{
endpoint.Header = endpoint.Content.Title;
break;
}
}
}
}
public partial class EndpointItem : ObservableObject
{
[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

@@ -1,55 +1,82 @@
using Avalonia.Collections; using DynamicData;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Needlework.Net.Models; using Needlework.Net.Models;
using Needlework.Net.ViewModels.Shared; using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Splat;
using System; using System;
using System.Linq; using System.Collections.ObjectModel;
using System.Net.Http; using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Endpoints; namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class EndpointsViewModel : ObservableObject public enum Tab
{ {
public IAvaloniaList<string> Plugins { get; } LCU,
public IAvaloniaList<string> Query { get; } GameClient
[ObservableProperty] private string _search = string.Empty;
[ObservableProperty] private string? _selectedQuery = string.Empty;
public Action<ObservableObject> OnClicked { get; }
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<RequestViewModel> requestViewModelLogger, Models.Document document, Tab tab, System.Net.Http.HttpClient httpClient)
{
Plugins = new AvaloniaList<string>(plugins);
Query = new AvaloniaList<string>(plugins);
OnClicked = onClicked;
_requestViewModelLogger = requestViewModelLogger;
_document = document;
_tab = tab;
_httpClient = httpClient;
}
partial void OnSearchChanged(string value)
{
Query.Clear();
if (!string.IsNullOrEmpty(Search))
Query.AddRange(Plugins.Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase)));
else
Query.AddRange(Plugins);
}
[RelayCommand]
private void OpenEndpoint(string? value)
{
if (string.IsNullOrEmpty(value)) return;
OnClicked.Invoke(new EndpointViewModel(value, _requestViewModelLogger, _document, _tab, _httpClient));
}
} }
public partial class EndpointsViewModel : PageBase, IEnableLogger
{
public record Endpoint(Document Document, Tab Tab);
private readonly DataSource _dataSource;
public EndpointsViewModel(IScreen? screen = null, DataSource? dataSource = null) : base("Endpoints", "list-alt", -500)
{
_dataSource = dataSource ?? Locator.Current.GetService<DataSource>()!;
HostScreen = screen ?? Locator.Current.GetService<IScreen>()!;
GetEndpointCommand.Subscribe(endpoint =>
{
Plugins.Clear();
Plugins.AddRange(endpoint.Document.Plugins.Keys);
var vm = new EndpointTabItemContentViewModel(Plugins, OnEndpointNavigation, endpoint.Document, endpoint.Tab);
EndpointTabItems.Add(new(vm, vm.Title, null, true));
IsBusy = false;
});
GetEndpointCommand.ThrownExceptions.Subscribe(ex =>
{
this.Log()
.Error(ex, "Failed to get endpoint.");
IsBusy = false;
});
}
public override string? UrlPathSegment => "endpoints";
public override ReactiveUI.IScreen HostScreen { get; }
[Reactive]
public ObservableCollection<string> Plugins { get; } = [];
[Reactive]
public ObservableCollection<EndpointTabItemViewModel> EndpointTabItems { get; } = [];
[Reactive]
private bool _isBusy = true;
[ReactiveCommand]
private async Task<Endpoint> GetEndpointAsync(Tab tab)
{
return tab switch
{
Tab.LCU => new(await _dataSource.GetLcuSchemaDocumentAsync(), tab),
Tab.GameClient => new(await _dataSource.GetLolClientDocumentAsync(), tab),
_ => throw new NotImplementedException(),
};
}
private void OnEndpointNavigation(string? title, Guid guid)
{
foreach (var endpoint in EndpointTabItems)
{
if (endpoint.Content.Guid.Equals(guid))
{
endpoint.Header = endpoint.Content.Title;
break;
}
}
}
}

View File

@@ -1,7 +1,6 @@
using Avalonia.Collections; using Microsoft.OpenApi.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.OpenApi.Models;
using Needlework.Net.Models; using Needlework.Net.Models;
using ReactiveUI;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
@@ -9,19 +8,8 @@ using System.Text.Json;
namespace Needlework.Net.ViewModels.Pages.Endpoints; namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class OperationViewModel : ObservableObject public partial class OperationViewModel : ReactiveObject
{ {
public string Summary { get; }
public string Description { get; }
public string ReturnType { get; }
public bool IsRequestBody { get; }
public string? RequestBodyType { get; }
public IAvaloniaReadOnlyList<PropertyClassViewModel> RequestClasses { get; }
public IAvaloniaReadOnlyList<PropertyClassViewModel> ResponseClasses { get; }
public IAvaloniaReadOnlyList<ParameterViewModel> PathParameters { get; }
public IAvaloniaReadOnlyList<ParameterViewModel> QueryParameters { get; }
public string? RequestTemplate { get; }
public OperationViewModel(OpenApiOperation operation, Models.Document document) public OperationViewModel(OpenApiOperation operation, Models.Document document)
{ {
Summary = operation.Summary ?? string.Empty; Summary = operation.Summary ?? string.Empty;
@@ -36,6 +24,26 @@ public partial class OperationViewModel : ObservableObject
RequestTemplate = GetRequestTemplate(operation.RequestBody, document); RequestTemplate = GetRequestTemplate(operation.RequestBody, document);
} }
public List<PropertyClassViewModel> RequestClasses { get; }
public List<PropertyClassViewModel> ResponseClasses { get; }
public List<ParameterViewModel> PathParameters { get; }
public List<ParameterViewModel> QueryParameters { get; }
public string? RequestTemplate { get; }
public string Summary { get; }
public string Description { get; }
public string ReturnType { get; }
public bool IsRequestBody { get; }
public string? RequestBodyType { get; }
private string? GetRequestTemplate(OpenApiRequestBody? requestBody, Document document) private string? GetRequestTemplate(OpenApiRequestBody? requestBody, Document document)
{ {
var requestClasses = GetRequestClasses(requestBody, document); var requestClasses = GetRequestClasses(requestBody, document);
@@ -50,7 +58,7 @@ public partial class OperationViewModel : ObservableObject
return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(string.Join(string.Empty, template)), App.JsonSerializerOptions); return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(string.Join(string.Empty, template)), App.JsonSerializerOptions);
} }
private List<string> CreateTemplate(AvaloniaList<PropertyClassViewModel> requestClasses) private List<string> CreateTemplate(List<PropertyClassViewModel> requestClasses)
{ {
if (requestClasses.Count == 0) return []; if (requestClasses.Count == 0) return [];
List<string> template = []; List<string> template = [];
@@ -83,7 +91,7 @@ public partial class OperationViewModel : ObservableObject
} }
else else
{ {
AvaloniaList<PropertyClassViewModel> classes = [.. requestClasses]; List<PropertyClassViewModel> classes = [.. requestClasses];
classes.Remove(rootClass); classes.Remove(rootClass);
template[i] = string.Join(string.Empty, CreateTemplate(classes)); template[i] = string.Join(string.Empty, CreateTemplate(classes));
} }
@@ -121,9 +129,9 @@ public partial class OperationViewModel : ObservableObject
return null; return null;
} }
private AvaloniaList<ParameterViewModel> GetParameters(IList<OpenApiParameter> parameters, ParameterLocation location) private List<ParameterViewModel> GetParameters(IList<OpenApiParameter> parameters, ParameterLocation location)
{ {
var pathParameters = new AvaloniaList<ParameterViewModel>(); var pathParameters = new List<ParameterViewModel>();
foreach (var parameter in parameters) foreach (var parameter in parameters)
{ {
if (parameter.In != location) continue; if (parameter.In != location) continue;
@@ -151,7 +159,7 @@ public partial class OperationViewModel : ObservableObject
} }
private AvaloniaList<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses, Document document) private List<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses, Document document)
{ {
if (!TryGetResponse(responses, out var response)) if (!TryGetResponse(responses, out var response))
return []; return [];
@@ -162,7 +170,7 @@ public partial class OperationViewModel : ObservableObject
var schema = media.Schema; var schema = media.Schema;
if (schema == null) return []; if (schema == null) return [];
AvaloniaList<PropertyClassViewModel> propertyClasses = []; List<PropertyClassViewModel> propertyClasses = [];
WalkSchema(schema, propertyClasses, rawDocument); WalkSchema(schema, propertyClasses, rawDocument);
return propertyClasses; return propertyClasses;
} }
@@ -170,7 +178,7 @@ public partial class OperationViewModel : ObservableObject
return []; return [];
} }
private void WalkSchema(OpenApiSchema schema, AvaloniaList<PropertyClassViewModel> propertyClasses, OpenApiDocument document) private void WalkSchema(OpenApiSchema schema, List<PropertyClassViewModel> propertyClasses, OpenApiDocument document)
{ {
var type = GetSchemaType(schema); var type = GetSchemaType(schema);
if (IsComponent(type)) if (IsComponent(type))
@@ -209,7 +217,7 @@ public partial class OperationViewModel : ObservableObject
|| type.Contains("number")); || type.Contains("number"));
} }
private AvaloniaList<PropertyClassViewModel> GetRequestClasses(OpenApiRequestBody? requestBody, Document document) private List<PropertyClassViewModel> GetRequestClasses(OpenApiRequestBody? requestBody, Document document)
{ {
if (requestBody == null) return []; if (requestBody == null) return [];
if (requestBody.Content.TryGetValue("application/json", out var media)) if (requestBody.Content.TryGetValue("application/json", out var media))
@@ -223,7 +231,7 @@ public partial class OperationViewModel : ObservableObject
{ {
var componentId = GetComponentId(schema); var componentId = GetComponentId(schema);
var componentSchema = rawDocument.Components.Schemas[componentId]; var componentSchema = rawDocument.Components.Schemas[componentId];
AvaloniaList<PropertyClassViewModel> propertyClasses = []; List<PropertyClassViewModel> propertyClasses = [];
WalkSchema(componentSchema, propertyClasses, rawDocument); WalkSchema(componentSchema, propertyClasses, rawDocument);
return propertyClasses; return propertyClasses;
} }

View File

@@ -1,14 +1,10 @@
using CommunityToolkit.Mvvm.ComponentModel; using ReactiveUI;
using ReactiveUI.SourceGenerators;
namespace Needlework.Net.ViewModels.Pages.Endpoints; namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class ParameterViewModel : ObservableObject public partial class ParameterViewModel : ReactiveObject
{ {
public string Name { get; }
public string Type { get; }
public bool IsRequired { get; }
[ObservableProperty] private string? _value = null;
public ParameterViewModel(string name, string type, bool isRequired, string? value = null) public ParameterViewModel(string name, string type, bool isRequired, string? value = null)
{ {
Name = name; Name = name;
@@ -16,4 +12,13 @@ public partial class ParameterViewModel : ObservableObject
IsRequired = isRequired; IsRequired = isRequired;
Value = value; Value = value;
} }
public string Name { get; }
public string Type { get; }
public bool IsRequired { get; }
[Reactive]
private string? _value;
} }

View File

@@ -1,36 +1,42 @@
using CommunityToolkit.Mvvm.ComponentModel; using Needlework.Net.Models;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Needlework.Net.Models;
using Needlework.Net.ViewModels.Shared; using Needlework.Net.ViewModels.Shared;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System; using System;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Endpoints; namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class PathOperationViewModel : ObservableObject public partial class PathOperationViewModel : ReactiveObject
{ {
public PathOperationViewModel(PathOperation pathOperation, Document document, Tab tab)
{
Path = pathOperation.Path;
Operation = new OperationViewModel(pathOperation.Operation, document);
Request = new(() => new RequestViewModel(tab)
{
Method = pathOperation.Method.ToUpper()
});
Url = $"https://swagger.dysolix.dev/lcu/#/{Uri.EscapeDataString(pathOperation.Tag)}/{pathOperation.Operation.OperationId}";
Markdown = $"[{pathOperation.Method.ToUpper()} {Path}]({Url})";
}
public string Path { get; } public string Path { get; }
public OperationViewModel Operation { get; } public OperationViewModel Operation { get; }
public string Url { get; } public string Url { get; }
[ObservableProperty] private bool _isBusy; public string Markdown { get; }
[ObservableProperty] private Lazy<RequestViewModel> _request;
public PathOperationViewModel(PathOperation pathOperation, ILogger<RequestViewModel> requestViewModelLogger, Document document, Tab tab, System.Net.Http.HttpClient httpClient) [Reactive]
{ private bool _isBusy;
Path = pathOperation.Path;
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] [Reactive]
private Lazy<RequestViewModel> _request;
[ReactiveCommand]
private async Task SendRequest() private async Task SendRequest()
{ {
var sb = new StringBuilder(Path); var sb = new StringBuilder(Path);
@@ -54,9 +60,15 @@ public partial class PathOperationViewModel : ObservableObject
await Request.Value.ExecuteAsync(); await Request.Value.ExecuteAsync();
} }
[RelayCommand] [ReactiveCommand]
private void CopyUrl() private void CopyUrl()
{ {
App.MainWindow?.Clipboard?.SetTextAsync(Url); App.MainWindow?.Clipboard?.SetTextAsync(Url);
} }
[ReactiveCommand]
private void CopyMarkdown()
{
App.MainWindow?.Clipboard?.SetTextAsync(Markdown);
}
} }

View File

@@ -0,0 +1,59 @@
using DynamicData;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Subjects;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class PluginViewModel : ReactiveObject
{
private readonly Subject<string> _pathOperationSelectedSubject = new();
public PluginViewModel(string endpoint, Models.Document document, Tab tab)
{
Endpoint = endpoint;
PathOperations = [.. document.Plugins[endpoint].Select(x => new PathOperationViewModel(x, document, tab))];
FilteredPathOperations = new ObservableCollection<PathOperationViewModel>(PathOperations);
this.WhenAnyValue(x => x.Search)
.Subscribe(search =>
{
FilteredPathOperations.Clear();
if (string.IsNullOrWhiteSpace(search))
{
FilteredPathOperations.AddRange(PathOperations);
return;
}
FilteredPathOperations.AddRange(PathOperations.Where(o => o.Path.Contains(search, StringComparison.InvariantCultureIgnoreCase)));
});
this.WhenAnyValue(x => x.SelectedPathOperation)
.Subscribe(pathOperation =>
{
if (pathOperation == null) return;
_pathOperationSelectedSubject.OnNext(pathOperation.Operation.RequestTemplate ?? string.Empty);
});
}
public IObservable<string> PathOperationSelected { get { return _pathOperationSelectedSubject; } }
public string Endpoint { get; }
public string Title => Endpoint;
public ObservableCollection<PathOperationViewModel> PathOperations { get; } = [];
[Reactive]
private ObservableCollection<PathOperationViewModel> _filteredPathOperations = [];
[Reactive]
private PathOperationViewModel? _selectedPathOperation;
[Reactive]
private string? _search;
}

View File

@@ -1,22 +1,18 @@
using Avalonia.Collections; using Microsoft.OpenApi.Any;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using ReactiveUI;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
namespace Needlework.Net.ViewModels.Pages.Endpoints; namespace Needlework.Net.ViewModels.Pages.Endpoints;
public class PropertyClassViewModel : ObservableObject public class PropertyClassViewModel : ReactiveObject
{ {
public string Id { get; }
public IAvaloniaReadOnlyList<PropertyFieldViewModel> PropertyFields { get; } = new AvaloniaList<PropertyFieldViewModel>();
public IAvaloniaReadOnlyList<PropertyEnumViewModel> PropertyEnums { get; } = new AvaloniaList<PropertyEnumViewModel>();
public PropertyClassViewModel(string id, IDictionary<string, OpenApiSchema> properties, IList<IOpenApiAny> enumValue) public PropertyClassViewModel(string id, IDictionary<string, OpenApiSchema> properties, IList<IOpenApiAny> enumValue)
{ {
AvaloniaList<PropertyFieldViewModel> propertyFields = []; List<PropertyFieldViewModel> propertyFields = [];
AvaloniaList<PropertyEnumViewModel> propertyEnums = []; List<PropertyEnumViewModel> propertyEnums = [];
foreach ((var propertyName, var propertySchema) in properties) foreach ((var propertyName, var propertySchema) in properties)
{ {
var type = OperationViewModel.GetSchemaType(propertySchema); var type = OperationViewModel.GetSchemaType(propertySchema);
@@ -28,8 +24,14 @@ public class PropertyClassViewModel : ObservableObject
var propertyEnum = new PropertyEnumViewModel(enumValue); var propertyEnum = new PropertyEnumViewModel(enumValue);
propertyEnums.Add(propertyEnum); propertyEnums.Add(propertyEnum);
} }
PropertyFields = propertyFields; PropertyFields = [.. propertyFields];
PropertyEnums = propertyEnums; PropertyEnums = [.. propertyEnums];
Id = id; Id = id;
} }
public string Id { get; }
public ObservableCollection<PropertyFieldViewModel> PropertyFields { get; } = [];
public ObservableCollection<PropertyEnumViewModel> PropertyEnums { get; } = [];
} }

View File

@@ -6,11 +6,13 @@ namespace Needlework.Net.ViewModels.Pages.Endpoints;
public class PropertyEnumViewModel public class PropertyEnumViewModel
{ {
public string Type { get; } = "Enum";
public string Values { get; }
public PropertyEnumViewModel(IList<IOpenApiAny> enumValue) public PropertyEnumViewModel(IList<IOpenApiAny> enumValue)
{ {
Values = $"[{string.Join(", ", enumValue.Select(x => $"\"{((OpenApiString)x).Value}\"").ToList())}]"; Values = $"[{string.Join(", ", enumValue.Select(x => $"\"{((OpenApiString)x).Value}\"").ToList())}]";
} }
public string Type { get; } = "Enum";
public string Values { get; }
} }

View File

@@ -2,12 +2,14 @@
public class PropertyFieldViewModel public class PropertyFieldViewModel
{ {
public string Name { get; }
public string Type { get; }
public PropertyFieldViewModel(string name, string type) public PropertyFieldViewModel(string name, string type)
{ {
Name = name; Name = name;
Type = type; Type = type;
} }
public string Name { get; }
public string Type { get; }
} }

View File

@@ -1,17 +1,11 @@
using BlossomiShymae.GrrrLCU; using BlossomiShymae.Briar.Utils;
using CommunityToolkit.Mvvm.ComponentModel; using ReactiveUI;
using ReactiveUI.SourceGenerators;
namespace Needlework.Net.ViewModels.Pages.Endpoints; namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class ResponseViewModel : ObservableObject public partial class ResponseViewModel : ReactiveObject
{ {
[ObservableProperty] private string? _path;
[ObservableProperty] private string? _status;
[ObservableProperty] private string? _authentication;
[ObservableProperty] private string? _username;
[ObservableProperty] private string? _password;
[ObservableProperty] private string? _authorization;
public ResponseViewModel(string path) public ResponseViewModel(string path)
{ {
Path = path; Path = path;
@@ -26,6 +20,24 @@ public partial class ResponseViewModel : ObservableObject
} }
} }
[Reactive]
private string? _path;
[Reactive]
private string? _status;
[Reactive]
private string? _authentication;
[Reactive]
private string? _username;
[Reactive]
private string? _password;
[Reactive]
private string? _authorization;
private static ProcessInfo? GetProcessInfo() private static ProcessInfo? GetProcessInfo()
{ {
if (ProcessFinder.IsActive()) return ProcessFinder.GetProcessInfo(); if (ProcessFinder.IsActive()) return ProcessFinder.GetProcessInfo();

View File

@@ -0,0 +1,26 @@
using Avalonia.Platform;
using Needlework.Net.Models;
using ReactiveUI;
using Splat;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
namespace Needlework.Net.ViewModels.Pages.Home;
public partial class HomeViewModel : PageBase
{
public HomeViewModel(IScreen? screen = null) : base("Home", "home", int.MinValue)
{
Libraries = JsonSerializer.Deserialize<List<Library>>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json")))!
.Select(library => new LibraryViewModel(library))
.ToList();
HostScreen = screen ?? Locator.Current.GetService<IScreen>()!;
}
public List<LibraryViewModel> Libraries { get; }
public override string? UrlPathSegment => "home";
public override IScreen HostScreen { get; }
}

View File

@@ -0,0 +1,15 @@
using Needlework.Net.Models;
using ReactiveUI;
namespace Needlework.Net.ViewModels.Pages.Home
{
public class LibraryViewModel : ReactiveObject
{
public LibraryViewModel(Library library)
{
Library = library;
}
public Library Library { get; }
}
}

View File

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

View File

@@ -1,15 +1,17 @@
using CommunityToolkit.Mvvm.ComponentModel; using ReactiveUI;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages; namespace Needlework.Net.ViewModels.Pages;
public abstract partial class PageBase(string displayName, string icon, int index = 0) : ObservableValidator public abstract partial class PageBase(string displayName, string icon, int index = 0) : ReactiveObject, IRoutableViewModel
{ {
[ObservableProperty] private string _displayName = displayName; public string DisplayName { get; } = displayName;
[ObservableProperty] private string _icon = icon;
[ObservableProperty] private int _index = index;
[ObservableProperty] private bool _isInitialized;
public abstract Task InitializeAsync(); public string Icon { get; } = icon;
public int Index { get; } = index;
public abstract string? UrlPathSegment { get; }
public abstract IScreen HostScreen { get; }
} }

View File

@@ -1,10 +1,10 @@
using BlossomiShymae.GrrrLCU; using BlossomiShymae.Briar.WebSocket.Events;
using CommunityToolkit.Mvvm.ComponentModel; using ReactiveUI;
using System; using System;
namespace Needlework.Net.ViewModels.Pages.Websocket; namespace Needlework.Net.ViewModels.Pages.WebSocket;
public class EventViewModel : ObservableObject public class EventViewModel : ReactiveObject
{ {
public string Time { get; } public string Time { get; }
public string Type { get; } public string Type { get; }

View File

@@ -1,83 +1,93 @@
using Avalonia.Collections; using BlossomiShymae.Briar;
using BlossomiShymae.GrrrLCU; using BlossomiShymae.Briar.WebSocket.Events;
using CommunityToolkit.Mvvm.ComponentModel; using Flurl.Http;
using CommunityToolkit.Mvvm.Input; using Flurl.Http.Configuration;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Needlework.Net.Messages; using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Splat;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Websocket.Client; using Websocket.Client;
namespace Needlework.Net.ViewModels.Pages.Websocket; namespace Needlework.Net.ViewModels.Pages.WebSocket;
public partial class WebsocketViewModel : PageBase public partial class WebSocketViewModel : PageBase, IEnableLogger
{ {
public ObservableCollection<EventViewModel> EventLog { get; } = [];
public SemaphoreSlim EventLogLock { get; } = new(1, 1);
[NotifyPropertyChangedFor(nameof(FilteredEventLog))]
[ObservableProperty] private string _search = string.Empty;
[ObservableProperty] private bool _isAttach = true;
[ObservableProperty] private bool _isTail = false;
[ObservableProperty] private EventViewModel? _selectedEventLog = null;
[ObservableProperty] private IAvaloniaList<string> _eventTypes = new AvaloniaList<string>();
[ObservableProperty] private string _eventType = "OnJsonApiEvent";
private Dictionary<string, EventMessage> _events = []; private Dictionary<string, EventMessage> _events = [];
private readonly object _tokenLock = new();
private readonly IFlurlClient _githubUserContentClient;
// public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
public WebSocketViewModel(IScreen? screen = null, IFlurlClientCache? clients = null) : base("Event Viewer", "plug", -100)
{
_githubUserContentClient = clients?.Get("GithubUserContentClient") ?? Locator.Current.GetService<IFlurlClientCache>()?.Get("GithubUserContentClient")!;
HostScreen = screen ?? Locator.Current.GetService<IScreen>()!;
//EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
//Task.Run(async () =>
//{
// await InitializeEventTypes();
// InitializeWebsocket();
//});
}
public override string? UrlPathSegment => "websocket";
public override ReactiveUI.IScreen HostScreen { get; }
public CancellationTokenSource TokenSource { get; set; } = new();
public WebsocketClient? Client { get; set; } public WebsocketClient? Client { get; set; }
public List<IDisposable> ClientDisposables = []; public List<IDisposable> ClientDisposables = [];
private readonly object _tokenLock = new(); public SemaphoreSlim EventLogLock { get; } = new(1, 1);
public CancellationTokenSource TokenSource { get; set; } = new();
public HttpClient HttpClient { get; } [Reactive]
public ObservableCollection<EventViewModel> EventLog { get; } = [];
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))]; [Reactive]
private string _search = string.Empty;
private readonly ILogger<WebsocketViewModel> _logger; [Reactive]
private bool _isAttach = true;
public WebsocketViewModel(HttpClient httpClient, ILogger<WebsocketViewModel> logger) : base("Event Viewer", "plug", -100) [Reactive]
private bool _isTail;
[Reactive]
private EventViewModel? _selectedEventLog;
[Reactive]
private ObservableCollection<string> _eventTypes = [];
[Reactive]
private string _eventType = "OnJsonApiEvent";
[ObservableAsProperty]
private ObservableCollection<EventViewModel> _filteredEventLog = [];
[ReactiveCommand]
private async Task<List<string>> GetEventTypesAsync()
{ {
_logger = logger; var file = await _githubUserContentClient.Request("/dysolix/hasagi-types/refs/heads/main/dist/lcu-events.d.ts")
HttpClient = httpClient; .GetStringAsync();
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog)); var matches = EventTypesRegex().Matches(file);
Task.Run(async () => var eventTypes = matches.Select(m => m.Groups[1].Value)
{ .ToList();
await InitializeEventTypes(); return eventTypes;
InitializeWebsocket();
});
}
public override Task InitializeAsync()
{
IsInitialized = true;
return Task.CompletedTask;
}
private async Task InitializeEventTypes()
{
try
{
var file = await HttpClient.GetStringAsync("https://raw.githubusercontent.com/dysolix/hasagi-types/refs/heads/main/dist/lcu-events.d.ts");
var matches = EventTypesRegex().Matches(file);
Avalonia.Threading.Dispatcher.UIThread.Invoke(() => EventTypes.AddRange(matches.Select(m => m.Groups[1].Value)));
}
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))));
}
} }
private void InitializeWebsocket() private void InitializeWebsocket()
@@ -86,7 +96,8 @@ public partial class WebsocketViewModel : PageBase
{ {
if (Client != null) if (Client != null)
{ {
_logger.LogDebug("Disposing old connection"); this.Log()
.Debug("Disposing old connection");
foreach (var disposable in ClientDisposables) foreach (var disposable in ClientDisposables)
disposable.Dispose(); disposable.Dispose();
ClientDisposables.Clear(); ClientDisposables.Clear();
@@ -116,23 +127,24 @@ public partial class WebsocketViewModel : PageBase
}) })
{ IsBackground = true }; { IsBackground = true };
thread.Start(); thread.Start();
_logger.LogDebug("Initialized new connection: {EventType}", EventType); this.Log()
.Debug("Initialized new connection: {EventType}", EventType);
TokenSource = tokenSource; TokenSource = tokenSource;
} }
} }
partial void OnSelectedEventLogChanged(EventViewModel? value) //partial void OnSelectedEventLogChanged(EventViewModel? value)
{ //{
if (value == null) return; // if (value == null) return;
if (_events.TryGetValue(value.Key, out var message)) // if (_events.TryGetValue(value.Key, out var message))
{ // {
var text = JsonSerializer.Serialize(message, App.JsonSerializerOptions); // var text = JsonSerializer.Serialize(message, App.JsonSerializerOptions);
if (text.Length >= App.MaxCharacters) WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(text)); // if (text.Length >= App.MaxCharacters) WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(text));
else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebsocketViewModel)); // else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebSocketViewModel));
} // }
} //}
[RelayCommand] [ReactiveCommand]
private void Clear() private void Clear()
{ {
_events.Clear(); _events.Clear();
@@ -141,19 +153,21 @@ public partial class WebsocketViewModel : PageBase
private void OnReconnection(ReconnectionInfo info) private void OnReconnection(ReconnectionInfo info)
{ {
_logger.LogTrace("Reconnected: {Type}", info.Type); this.Log()
.Debug("Reconnected: {Type}", info.Type);
} }
private void OnDisconnection(DisconnectionInfo info) private void OnDisconnection(DisconnectionInfo info)
{ {
_logger.LogTrace("Disconnected: {Type}", info.Type); this.Log()
.Debug("Disconnected: {Type}", info.Type);
InitializeWebsocket(); InitializeWebsocket();
} }
partial void OnEventTypeChanged(string value) //partial void OnEventTypeChanged(string value)
{ //{
InitializeWebsocket(); // InitializeWebsocket();
} //}
private void OnMessage(EventMessage message) private void OnMessage(EventMessage message)
{ {

View File

@@ -1,55 +1,73 @@
using Avalonia.Media; using Avalonia.Media;
using BlossomiShymae.GrrrLCU; using AvaloniaEdit.Document;
using CommunityToolkit.Mvvm.ComponentModel; using BlossomiShymae.Briar;
using CommunityToolkit.Mvvm.Messaging; using BlossomiShymae.Briar.Utils;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Needlework.Net.Messages; using Needlework.Net.Services;
using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages.Endpoints; using Needlework.Net.ViewModels.Pages.Endpoints;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Splat;
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Reactive.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Shared; namespace Needlework.Net.ViewModels.Shared;
public partial class RequestViewModel : ObservableObject public partial class RequestViewModel : ReactiveObject, IEnableLogger
{ {
[ObservableProperty] private string? _method = "GET"; private readonly NotificationService _notificationService;
[ObservableProperty] private SolidColorBrush _color = new(GetColor("GET"));
[ObservableProperty] private bool _isRequestBusy = false;
[ObservableProperty] private string? _requestPath = null;
[ObservableProperty] private string? _requestBody = null;
[ObservableProperty] private string? _responsePath = null;
[ObservableProperty] private string? _responseStatus = null;
[ObservableProperty] private string? _responseAuthentication = null;
[ObservableProperty] private string? _responseUsername = null;
[ObservableProperty] private string? _responsePassword = null;
[ObservableProperty] private string? _responseAuthorization = null;
[ObservableProperty] private string? _responseBody = null;
public event EventHandler<RequestViewModel>? RequestText;
public event EventHandler<string>? UpdateText;
private readonly ILogger<RequestViewModel> _logger;
private readonly Tab _tab; private readonly Tab _tab;
private readonly HttpClient _httpClient;
public RequestViewModel(ILogger<RequestViewModel> logger, Pages.Endpoints.Tab tab, HttpClient httpClient) public RequestViewModel(Pages.Endpoints.Tab tab, NotificationService? notificationService = null)
{ {
_logger = logger;
_tab = tab; _tab = tab;
_httpClient = httpClient; _notificationService = notificationService ?? Locator.Current.GetService<NotificationService>()!;
_colorHelper = this.WhenAnyValue(x => x.Method)
.Select(method => GetSolidCrushBrush(method ?? "GET"))
.ToProperty(this, x => x.Color);
} }
partial void OnMethodChanged(string? oldValue, string? newValue) [ObservableAsProperty]
{ private SolidColorBrush _color = GetSolidCrushBrush("GET");
if (newValue == null) return;
Color = new(GetColor(newValue)); [Reactive]
} private string? _method = "GET";
[Reactive]
private bool _isRequestBusy;
[Reactive]
private string? _requestPath;
[Reactive]
private TextDocument _requestDocument = new();
[Reactive]
private string? _responsePath;
[Reactive]
private string? _responseStatus;
[Reactive]
private string? _responseAuthentication;
[Reactive]
private string? _responseUsername;
[Reactive]
private string? _responsePassword;
[Reactive]
private string? _responseAuthorization;
[Reactive]
private TextDocument _responseDocument = new();
public async Task ExecuteAsync() public async Task ExecuteAsync()
{ {
@@ -75,34 +93,25 @@ public partial class RequestViewModel : ObservableObject
throw new Exception("Path is empty."); throw new Exception("Path is empty.");
var method = GetMethod(); var method = GetMethod();
_logger.LogDebug("Sending request: {Tuple}", (Method, RequestPath)); this.Log()
RequestText?.Invoke(this, this); .Debug("Sending request: {Tuple}", (Method, RequestPath));
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 content = new StringContent(RequestDocument.Text, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var response = await _httpClient.SendAsync(new HttpRequestMessage(method, responsePath) { Content = content }); var client = Connector.GetGameHttpClientInstance();
var response = await client.SendAsync(new HttpRequestMessage(method, RequestPath) { Content = content });
var responseBody = await response.Content.ReadAsByteArrayAsync(); var responseBody = await response.Content.ReadAsByteArrayAsync();
var body = responseBody.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), App.JsonSerializerOptions) : string.Empty; var body = responseBody.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), App.JsonSerializerOptions) : string.Empty;
if (body.Length > App.MaxCharacters)
{
WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(body));
UpdateText?.Invoke(this, string.Empty);
}
else
{
ResponseBody = body;
UpdateText?.Invoke(this, body);
}
ResponseDocument = new(body);
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}"; ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}";
ResponsePath = responsePath; ResponsePath = $"https://127.0.0.1:2999{RequestPath}";
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Request failed: {Tuple}", (Method, RequestPath)); this.Log()
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5)))); .Error(ex, "Request failed: {Tuple}", (Method, RequestPath));
UpdateText?.Invoke(this, string.Empty); _notificationService.Notify("Request Failed", ex.Message, InfoBarSeverity.Error);
ResponseStatus = null; ResponseStatus = null;
ResponsePath = null; ResponsePath = null;
@@ -110,7 +119,7 @@ public partial class RequestViewModel : ObservableObject
ResponseAuthorization = null; ResponseAuthorization = null;
ResponseUsername = null; ResponseUsername = null;
ResponsePassword = null; ResponsePassword = null;
ResponseBody = null; ResponseDocument = new();
} }
finally finally
{ {
@@ -127,28 +136,18 @@ public partial class RequestViewModel : ObservableObject
throw new Exception("Path is empty."); throw new Exception("Path is empty.");
var method = GetMethod(); var method = GetMethod();
_logger.LogDebug("Sending request: {Tuple}", (Method, RequestPath)); this.Log()
.Debug("Sending request: {Tuple}", (Method, RequestPath));
var processInfo = ProcessFinder.GetProcessInfo(); var processInfo = ProcessFinder.GetProcessInfo();
RequestText?.Invoke(this, this); var content = new StringContent(RequestDocument.Text, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var content = new StringContent(RequestBody ?? string.Empty, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var client = Connector.GetLcuHttpClientInstance(); var client = Connector.GetLcuHttpClientInstance();
var response = await client.SendAsync(new(method, RequestPath) { Content = content }); var response = await client.SendAsync(new(method, RequestPath) { Content = content });
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken); var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
var responseBody = await response.Content.ReadAsByteArrayAsync(); var responseBody = await response.Content.ReadAsByteArrayAsync();
var body = responseBody.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), App.JsonSerializerOptions) : string.Empty; var body = responseBody.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), App.JsonSerializerOptions) : string.Empty;
if (body.Length >= App.MaxCharacters)
{
WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(body));
UpdateText?.Invoke(this, string.Empty);
}
else
{
ResponseBody = body;
UpdateText?.Invoke(this, body);
}
ResponseDocument = new(body);
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}"; ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}";
ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{RequestPath}"; ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{RequestPath}";
ResponseAuthentication = riotAuthentication.Value; ResponseAuthentication = riotAuthentication.Value;
@@ -158,9 +157,9 @@ public partial class RequestViewModel : ObservableObject
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Request failed: {Tuple}", (Method, RequestPath)); this.Log()
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5)))); .Error(ex, "Request failed: {Tuple}", (Method, RequestPath));
UpdateText?.Invoke(this, string.Empty); _notificationService.Notify("Request Failed", ex.Message, InfoBarSeverity.Error);
ResponseStatus = null; ResponseStatus = null;
ResponsePath = null; ResponsePath = null;
@@ -168,7 +167,7 @@ public partial class RequestViewModel : ObservableObject
ResponseAuthorization = null; ResponseAuthorization = null;
ResponseUsername = null; ResponseUsername = null;
ResponsePassword = null; ResponsePassword = null;
ResponseBody = null; ResponseDocument = new();
} }
finally finally
{ {
@@ -192,14 +191,14 @@ public partial class RequestViewModel : ObservableObject
}; };
} }
private static Color GetColor(string method) => method switch private static SolidColorBrush GetSolidCrushBrush(string? method = null) => new(method switch
{ {
"GET" => Avalonia.Media.Color.FromRgb(95, 99, 186), "GET" or null => Avalonia.Media.Color.FromRgb(95, 99, 186),
"POST" => Avalonia.Media.Color.FromRgb(103, 186, 95), "POST" => Avalonia.Media.Color.FromRgb(103, 186, 95),
"PUT" => Avalonia.Media.Color.FromRgb(186, 139, 95), "PUT" => Avalonia.Media.Color.FromRgb(186, 139, 95),
"DELETE" => Avalonia.Media.Color.FromRgb(186, 95, 95), "DELETE" => Avalonia.Media.Color.FromRgb(186, 95, 95),
"HEAD" => Avalonia.Media.Color.FromRgb(136, 95, 186), "HEAD" => Avalonia.Media.Color.FromRgb(136, 95, 186),
"PATCH" => Avalonia.Media.Color.FromRgb(95, 186, 139), "PATCH" => Avalonia.Media.Color.FromRgb(95, 186, 139),
_ => throw new InvalidOperationException("Method does not have assigned color.") _ => throw new InvalidOperationException("Method does not have assigned color.")
}; });
} }

View File

@@ -6,10 +6,11 @@
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives" xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:i="https://github.com/projektanker/icons.avalonia" xmlns:i="https://github.com/projektanker/icons.avalonia"
xmlns:vm="using:Needlework.Net.ViewModels.MainWindow" xmlns:vm="using:Needlework.Net.ViewModels.MainWindow"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.MainWindow.MainWindowView" x:Class="Needlework.Net.Views.MainWindow.MainWindow"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
Title="Needlework.Net" Title="Needlework.Net"
Icon="/Assets/app.ico" Icon="/Assets/app.ico"
@@ -34,14 +35,13 @@
Needlework.Net Needlework.Net
</TextBlock> </TextBlock>
</Grid> </Grid>
<ui:NavigationView AlwaysShowHeader="False" <ui:NavigationView Name="NavigationView"
AlwaysShowHeader="False"
PaneDisplayMode="Left" PaneDisplayMode="Left"
IsSettingsVisible="False" IsSettingsVisible="False"
IsPaneOpen="False" IsPaneOpen="False"
OpenPaneLength="200" OpenPaneLength="200"
Grid.Row="1" Grid.Row="1">
MenuItemsSource="{Binding MenuItems}"
SelectedItem="{Binding SelectedMenuItem}">
<ui:NavigationView.PaneFooter> <ui:NavigationView.PaneFooter>
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<StackPanel.Styles> <StackPanel.Styles>
@@ -53,20 +53,16 @@
<Setter Property="FontSize" Value="20" /> <Setter Property="FontSize" Value="20" />
</Style> </Style>
</StackPanel.Styles> </StackPanel.Styles>
<Button <Button Name="GithubButton"
Theme="{StaticResource TransparentButton}" Theme="{StaticResource TransparentButton}"
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding OpenUrlCommand}"
CommandParameter="https://github.com/BlossomiShymae/Needlework.Net"
ToolTip.Tip="Open on GitHub." ToolTip.Tip="Open on GitHub."
Margin="4"> Margin="4">
<materialIcons:MaterialIcon Kind="Github" /> <materialIcons:MaterialIcon Kind="Github" />
</Button> </Button>
<Button <Button Name="DiscordButton"
Theme="{StaticResource TransparentButton}" Theme="{StaticResource TransparentButton}"
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding OpenUrlCommand}"
CommandParameter="https://discord.gg/chEvEX5J4E"
ToolTip.Tip="Open Discord server." ToolTip.Tip="Open Discord server."
Margin="4"> Margin="4">
<i:Icon Value="fa-brand fa-discord" /> <i:Icon Value="fa-brand fa-discord" />
@@ -74,28 +70,14 @@
</StackPanel> </StackPanel>
</ui:NavigationView.PaneFooter> </ui:NavigationView.PaneFooter>
<Grid> <Grid>
<TransitioningContentControl Content="{Binding CurrentPage}"/> <reactiveUi:RoutedViewHost Name="RoutedViewHost"/>
<Button Content="{Binding Version}" <Button Name="VersionButton"
Background="RoyalBlue" Background="RoyalBlue"
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Margin="16"/> Margin="16"/>
<ItemsRepeater ItemsSource="{Binding InfoBarItems}" <ItemsControl Name="NotificationItemsControl"
VerticalAlignment="Bottom"> VerticalAlignment="Bottom"/>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Border Margin="4">
<ui:InfoBar
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
Title="{Binding Title}"
IsOpen="{Binding IsOpen}"
Severity="{Binding Severity}"
Message="{Binding Message}"
ActionButton="{Binding ActionButton}"/>
</Border>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Grid> </Grid>
</ui:NavigationView> </ui:NavigationView>
</Grid> </Grid>

View File

@@ -0,0 +1,70 @@
using Avalonia;
using Avalonia.Controls;
using FluentAvalonia.UI.Windowing;
using Needlework.Net.ViewModels.MainWindow;
using ReactiveUI;
using System.Reactive.Disposables;
namespace Needlework.Net.Views.MainWindow;
public partial class MainWindow : AppWindow, IViewFor<MainWindowViewModel>
{
public MainWindow()
{
TitleBar.ExtendsContentIntoTitleBar = true;
TransparencyLevelHint = [WindowTransparencyLevel.Mica, WindowTransparencyLevel.None];
Background = IsWindows11 ? null : Background;
this.WhenActivated(disposables =>
{
this.OneWayBind(ViewModel, vm => vm.PageItems, v => v.NavigationView.MenuItemsSource)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedPageItem, v => v.NavigationView.SelectedItem)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Router, v => v.RoutedViewHost.Router)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Notifications, v => v.NotificationItemsControl.ItemsSource)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Version, v => v.VersionButton.Content)
.DisposeWith(disposables);
});
InitializeComponent();
}
public static readonly StyledProperty<MainWindowViewModel?> ViewModelProperty = AvaloniaProperty
.Register<MainWindow, MainWindowViewModel?>(nameof(ViewModel));
public MainWindowViewModel? ViewModel
{
get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object? IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (MainWindowViewModel?)value;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == DataContextProperty)
{
if (ReferenceEquals(change.OldValue, ViewModel)
&& change.NewValue is null or MainWindowViewModel)
{
SetCurrentValue(ViewModelProperty, change.NewValue);
}
}
else if (change.Property == ViewModelProperty)
{
if (ReferenceEquals(change.OldValue, DataContext))
{
SetCurrentValue(DataContextProperty, change.NewValue);
}
}
}
}

View File

@@ -1,30 +0,0 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using FluentAvalonia.UI.Windowing;
namespace Needlework.Net.Views.MainWindow;
public partial class MainWindowView : AppWindow
{
public MainWindowView()
{
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

@@ -0,0 +1,20 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
xmlns:vm="using:Needlework.Net.ViewModels.MainWindow"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.MainWindow.NotificationView"
x:DataType="vm:NotificationViewModel">
<Border Margin="4">
<ui:InfoBar Name="InfoBar"
IsOpen="True">
<ui:InfoBar.ActionButton>
<Button Name="InfoBarButton"/>
</ui:InfoBar.ActionButton>
</ui:InfoBar>
</Border>
</UserControl>

View File

@@ -0,0 +1,28 @@
using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.MainWindow;
using ReactiveUI;
using System.Reactive.Disposables;
namespace Needlework.Net.Views.MainWindow;
public partial class NotificationView : ReactiveUserControl<NotificationViewModel>
{
public NotificationView()
{
this.WhenActivated(disposables =>
{
this.OneWayBind(ViewModel, vm => vm.Notification.Title, v => v.InfoBar.Title)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Notification.Message, v => v.InfoBar.Message)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Notification.InfoBarSeverity, v => v.InfoBar.Severity)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.IsButtonVisible, v => v.InfoBarButton.IsVisible)
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.OpenUrlCommand, v => v.InfoBarButton);
});
InitializeComponent();
}
}

View File

@@ -2,11 +2,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels.Pages" xmlns:vm="using:Needlework.Net.ViewModels.Pages.About"
xmlns:controls="using:Needlework.Net.Controls" xmlns:controls="using:Needlework.Net.Controls"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:i="https://github.com/projektanker/icons.avalonia" xmlns:i="https://github.com/projektanker/icons.avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.AboutView" x:Class="Needlework.Net.Views.Pages.About.AboutPage"
x:DataType="vm:AboutViewModel"> x:DataType="vm:AboutViewModel">
<ScrollViewer Margin="8"> <ScrollViewer Margin="8">
<WrapPanel HorizontalAlignment="Center"> <WrapPanel HorizontalAlignment="Center">

View File

@@ -0,0 +1,15 @@
using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.About;
using ReactiveUI;
namespace Needlework.Net.Views.Pages.About;
public partial class AboutPage : ReactiveUserControl<AboutViewModel>
{
public AboutPage()
{
this.WhenActivated(disposables => { });
InitializeComponent();
}
}

View File

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

View File

@@ -3,14 +3,15 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit" xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives" xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
xmlns:vm="using:Needlework.Net.ViewModels.Pages" xmlns:vm="using:Needlework.Net.ViewModels.Pages.Console"
xmlns:controls="using:Needlework.Net.Controls" xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.ConsoleView" x:Class="Needlework.Net.Views.Pages.Console.ConsolePage"
x:DataType="vm:ConsoleViewModel"> x:DataType="vm:ConsoleViewModel">
<controls:BusyArea IsBusy="{Binding IsBusy}" <controls:BusyArea Name="BusyArea"
BusyText="Loading..."> BusyText="Loading...">
<Grid Margin="16" RowDefinitions="auto,*" ColumnDefinitions="*,*"> <Grid Margin="16" RowDefinitions="auto,*" ColumnDefinitions="*,*">
<Grid Grid.Row="0" <Grid Grid.Row="0"
@@ -18,25 +19,31 @@
Grid.ColumnSpan="2"> Grid.ColumnSpan="2">
<StackPanel Margin="0 0 0 8"> <StackPanel Margin="0 0 0 8">
<Grid RowDefinitions="auto" ColumnDefinitions="auto,*,auto"> <Grid RowDefinitions="auto" ColumnDefinitions="auto,*,auto">
<ComboBox ItemsSource="{Binding RequestMethods}" <!-- Worst behavior of Avalonia with ReactiveUI, but works for now.
SelectedItem="{Binding Request.Method}" https://stackoverflow.com/a/78409519
https://github.com/AvaloniaUI/Avalonia/discussions/17736#discussioncomment-11525997 -->
<ComboBox Name="RequestMethodsComboBox"
Margin="0 0 8 0" Margin="0 0 8 0"
Grid.Row="0" Grid.Row="0"
Grid.Column="0"/> Grid.Column="0">
<AutoCompleteBox <ComboBox.ItemTemplate>
ItemsSource="{Binding RequestPaths}" <DataTemplate>
Text="{Binding Request.RequestPath}" <TextBlock Text="{Binding}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<AutoCompleteBox Name="RequestPathsAutoCompleteBox"
MaxDropDownHeight="400" MaxDropDownHeight="400"
FilterMode="StartsWith" FilterMode="StartsWith"
Grid.Row="0" Grid.Row="0"
Grid.Column="1"/> Grid.Column="1"/>
<Button Margin="8 0 0 0" <Button Name="SendRequestButton"
Margin="8 0 0 0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
FontWeight="DemiBold" FontWeight="DemiBold"
Grid.Row="0" Grid.Row="0"
Grid.Column="2" Grid.Column="2">
Command="{Binding SendRequestCommand}">
Send Send
</Button> </Button>
</Grid> </Grid>
@@ -46,10 +53,10 @@
Grid.Column="0" Grid.Column="0"
RowDefinitions="auto,*" RowDefinitions="auto,*"
ColumnDefinitions="*"> ColumnDefinitions="*">
<TextBox IsReadOnly="True" <TextBox Name="ResponsePathTextBox"
IsReadOnly="True"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"/>
Text="{Binding Request.ResponsePath}"/>
<avaloniaEdit:TextEditor <avaloniaEdit:TextEditor
Name="RequestEditor" Name="RequestEditor"
Text="" Text=""
@@ -69,7 +76,7 @@
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Grid.Row="0" Grid.Row="0"
Grid.Column="0"> Grid.Column="0">
<Button Content="{Binding Request.ResponseStatus}" <Button Name="ResponseStatusButton"
FontSize="12" FontSize="12"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
</StackPanel> </StackPanel>

View File

@@ -0,0 +1,59 @@
using Avalonia;
using Avalonia.ReactiveUI;
using Avalonia.Styling;
using Needlework.Net.Extensions;
using Needlework.Net.ViewModels.Pages.Console;
using ReactiveUI;
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using TextMateSharp.Grammars;
namespace Needlework.Net.Views.Pages.Console;
public partial class ConsolePage : ReactiveUserControl<ConsoleViewModel>
{
public ConsolePage()
{
this.WhenAnyValue(x => x.ViewModel!.GetRequestPathsCommand)
.SelectMany(x => x.Execute())
.Subscribe();
this.WhenActivated(disposables =>
{
ResponseEditor.ApplyJsonEditorSettings();
RequestEditor.ApplyJsonEditorSettings();
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
this.OneWayBind(ViewModel, vm => vm.IsBusy, v => v.BusyArea.IsBusy)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.RequestMethods, v => v.RequestMethodsComboBox.ItemsSource)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Request.Method, v => v.RequestMethodsComboBox.SelectedItem)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.RequestPaths, v => v.RequestPathsAutoCompleteBox.ItemsSource)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Request.RequestPath, v => v.RequestPathsAutoCompleteBox.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Request.ResponsePath, v => v.ResponsePathTextBox.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Request.ResponseStatus, v => v.ResponseStatusButton.Content)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Request.RequestDocument, v => v.RequestEditor.Document)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Request.ResponseDocument, v => v.ResponseEditor.Document)
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SendRequestCommand, v => v.SendRequestButton)
.DisposeWith(disposables);
});
InitializeComponent();
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)
{
var registryOptions = new RegistryOptions(
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
}
}

View File

@@ -1,61 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Styling;
using AvaloniaEdit;
using Needlework.Net.Extensions;
using Needlework.Net.ViewModels.Pages;
using TextMateSharp.Grammars;
namespace Needlework.Net.Views.Pages;
public partial class ConsoleView : UserControl
{
private TextEditor? _responseEditor;
private TextEditor? _requestEditor;
public ConsoleView()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_responseEditor = this.FindControl<TextEditor>("ResponseEditor");
_requestEditor = this.FindControl<TextEditor>("RequestEditor");
_responseEditor?.ApplyJsonEditorSettings();
_requestEditor?.ApplyJsonEditorSettings();
var vm = (ConsoleViewModel)DataContext!;
vm.Request.RequestText += LcuRequest_RequestText; ;
vm.Request.UpdateText += LcuRequest_UpdateText;
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
}
private void LcuRequest_RequestText(object? sender, ViewModels.Shared.RequestViewModel e)
{
e.RequestBody = _requestEditor!.Text;
}
private void LcuRequest_UpdateText(object? sender, string e)
{
_responseEditor!.Text = e;
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
var vm = (ConsoleViewModel)DataContext!;
vm.Request.RequestText -= LcuRequest_RequestText;
vm.Request.UpdateText -= LcuRequest_UpdateText;
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)
{
var registryOptions = new RegistryOptions(
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
}
}

View File

@@ -0,0 +1,16 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointSearchDetailsView"
x:DataType="vm:EndpointSearchDetailsViewModel">
<Button Name="DetailsButton"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Theme="{StaticResource TransparentButton}"/>
</UserControl>

View File

@@ -0,0 +1,23 @@
using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.Endpoints;
using ReactiveUI;
using System.Reactive.Disposables;
namespace Needlework.Net.Views.Pages.Endpoints;
public partial class EndpointSearchDetailsView : ReactiveUserControl<EndpointSearchDetailsViewModel>
{
public EndpointSearchDetailsView()
{
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.Plugin, v => v.DetailsButton.Content)
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.OpenEndpointCommand, v => v.DetailsButton)
.DisposeWith(disposables);
});
InitializeComponent();
}
}

View File

@@ -0,0 +1,48 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:avalonEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:i="https://github.com/projektanker/icons.avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointTabItemContentView"
x:DataType="vm:EndpointTabItemContentViewModel">
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
<Menu Grid.Row="0"
Grid.Column="0">
<MenuItem Header="_New tab">
<MenuItem Header="LCU"
CommandParameter="{x:Static vm:Tab.LCU}"/>
<MenuItem Header="Game Client"
CommandParameter="{x:Static vm:Tab.GameClient}"/>
</MenuItem>
</Menu>
<Separator Grid.Row="1"
Grid.Column="0"/>
<Grid Grid.Row="2"
Grid.Column="0"
Margin="16"
RowDefinitions="auto,*"
ColumnDefinitions="*">
<StackPanel Orientation="Horizontal"
Grid.Row="0"
Grid.Column="0"
Margin="0 0 0 8">
<Button Name="GoBackButton"
Theme="{StaticResource TransparentButton}"
Margin="0 0 8 0">
<i:Icon Value="fa-arrow-left"
FontSize="20"/>
</Button>
<TextBlock Name="TitleTextBlock"
Theme="{StaticResource TitleTextBlockStyle}"
Text="{Binding Title}"/>
</StackPanel>
<reactiveUi:ViewModelViewHost Name="ViewModelViewHost"
Grid.Row="1"
Grid.Column="0"/>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,25 @@
using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.Endpoints;
using ReactiveUI;
using System.Reactive.Disposables;
namespace Needlework.Net.Views.Pages.Endpoints;
public partial class EndpointTabItemContentView : ReactiveUserControl<EndpointTabItemContentViewModel>
{
public EndpointTabItemContentView()
{
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.Title, v => v.TitleTextBlock.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ActiveViewModel, v => v.ViewModelViewHost.ViewModel)
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GoBackCommand, v => v.GoBackButton)
.DisposeWith(disposables);
});
InitializeComponent();
}
}

View File

@@ -0,0 +1,30 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:views="using:Needlework.Net.Views.Pages.Endpoints"
xmlns:controls="using:Needlework.Net.Controls"
xmlns:reactiveUi="http://reactiveui.net"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Name="EndpointsControl"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointTabListView"
x:DataType="vm:EndpointTabListViewModel">
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
<TextBox Name="SearchTextBox"
Watermark="Search"
Margin="0 4"
Grid.Row="1"
Grid.Column="0"/>
<ScrollViewer Grid.Row="2" Grid.Column="0">
<ItemsControl Name="EndpointSearchDetailItemsControl"
ItemsSource="{Binding Plugins}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:EndpointSearchDetailsViewModel">
<reactiveUi:ViewModelViewHost ViewModel="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -0,0 +1,20 @@
using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.Endpoints;
using ReactiveUI;
using System.Reactive.Disposables;
namespace Needlework.Net.Views.Pages.Endpoints;
public partial class EndpointTabListView : ReactiveUserControl<EndpointTabListViewModel>
{
public EndpointTabListView()
{
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.Search, v => v.SearchTextBox.Text)
.DisposeWith(disposables);
});
InitializeComponent();
}
}

View File

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

View File

@@ -1,32 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:avalonEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:i="https://github.com/projektanker/icons.avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsNavigationView"
x:DataType="vm:EndpointsNavigationViewModel">
<Grid RowDefinitions="auto,*"
ColumnDefinitions="*"
Margin="16">
<StackPanel Orientation="Horizontal"
Grid.Row="0"
Grid.Column="0"
Margin="0 0 0 8">
<Button Command="{Binding GoBackCommand}"
Theme="{StaticResource TransparentButton}"
Margin="0 0 8 0">
<i:Icon Value="fa-arrow-left"
FontSize="20"/>
</Button>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
Text="{Binding Title}"/>
</StackPanel>
<TransitioningContentControl
Grid.Row="1"
Grid.Column="0"
Content="{Binding ActiveViewModel}"/>
</Grid>
</UserControl>

View File

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

View File

@@ -5,18 +5,18 @@
xmlns:i="https://github.com/projektanker/icons.avalonia" xmlns:i="https://github.com/projektanker/icons.avalonia"
Name="EndpointsTab" Name="EndpointsTab"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints" xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:reactiveUi="http://reactiveui.net"
xmlns:views="using:Needlework.Net.Views.Pages.Endpoints"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:controls="using:Needlework.Net.Controls" xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsTabView" x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsPage"
x:DataType="vm:EndpointsTabViewModel"> x:DataType="vm:EndpointsViewModel">
<controls:BusyArea IsBusy="{Binding IsBusy}" <controls:BusyArea Name="BusyArea"
BusyText="Loading..."> BusyText="Loading...">
<Grid> <Grid>
<ui:TabView TabItems="{Binding Endpoints}" <ui:TabView Name="TabView"
AddTabButtonCommand="{Binding AddEndpointCommand}" TabItems="{Binding EndpointTabItems}">
AddTabButtonCommandParameter="{x:Static vm:Tab.LCU}"
TabCloseRequested="TabView_TabCloseRequested">
<!--Need to override Tab header for Mica theme...--> <!--Need to override Tab header for Mica theme...-->
<ui:TabView.Resources> <ui:TabView.Resources>
<ResourceDictionary> <ResourceDictionary>
@@ -39,31 +39,15 @@
</Style> </Style>
</ui:TabView.Styles> </ui:TabView.Styles>
<ui:TabView.TabItemTemplate> <ui:TabView.TabItemTemplate>
<DataTemplate DataType="vm:EndpointItem"> <!--We have to item template with XAML bindings here due to FluentAvalonia generating a TabViewItem control for reactive bindings...-->
<DataTemplate x:DataType="vm:EndpointTabItemViewModel">
<ui:TabViewItem Header="{Binding Header}" <ui:TabViewItem Header="{Binding Header}"
IconSource="{Binding IconSource}" IconSource="{Binding IconSource}"
IsSelected="{Binding Selected}" IsSelected="{Binding Selected}"
Content="{Binding}"> Content="{Binding Content}">
<ui:TabViewItem.ContentTemplate> <ui:TabViewItem.ContentTemplate>
<DataTemplate DataType="vm:EndpointItem"> <DataTemplate x:DataType="vm:EndpointTabItemContentViewModel">
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*"> <reactiveUi:ViewModelViewHost ViewModel="{Binding}"/>
<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> </DataTemplate>
</ui:TabViewItem.ContentTemplate> </ui:TabViewItem.ContentTemplate>
</ui:TabViewItem> </ui:TabViewItem>

View File

@@ -0,0 +1,38 @@
using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.Endpoints;
using ReactiveUI;
using System;
using System.Collections;
using System.Reactive.Disposables;
using System.Reactive.Linq;
namespace Needlework.Net.Views.Pages.Endpoints;
public partial class EndpointsPage : ReactiveUserControl<EndpointsViewModel>
{
public EndpointsPage()
{
this.WhenAnyValue(x => x.ViewModel!.GetEndpointCommand)
.SelectMany(x => x.Execute())
.Subscribe();
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.IsBusy, v => v.BusyArea.IsBusy)
.DisposeWith(disposables);
});
InitializeComponent();
}
private void TabView_TabCloseRequested(FluentAvalonia.UI.Controls.TabView sender, FluentAvalonia.UI.Controls.TabViewTabCloseRequestedEventArgs args)
{
if (args.Tab.Content is EndpointTabItemViewModel item && sender.TabItems is IList tabItems)
{
if (tabItems.Count > 1)
{
tabItems.Remove(item);
}
}
}
}

View File

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

View File

@@ -1,29 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Name="EndpointsControl"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsView"
x:DataType="vm:EndpointsViewModel">
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
<TextBox Watermark="Search" Margin="0 4" Text="{Binding Search}" Grid.Row="1" Grid.Column="0"/>
<ScrollViewer Grid.Row="2" Grid.Column="0">
<ItemsRepeater ItemsSource="{Binding Query}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Command="{Binding #EndpointsControl.((vm:EndpointsViewModel)DataContext).OpenEndpointCommand}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
CommandParameter="{Binding}"
Content="{Binding}"
Theme="{StaticResource TransparentButton}"/>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Grid>
</UserControl>

View File

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

View File

@@ -0,0 +1,37 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:controls="using:Needlework.Net.Controls"
x:Class="Needlework.Net.Views.Pages.Endpoints.PathOperationView"
x:DataType="vm:PathOperationViewModel">
<Grid RowDefinitions="*"
ColumnDefinitions="auto,*">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Copy Swagger URL" Command="{Binding CopyUrlCommand}"/>
<MenuItem Header="Copy Markdown" Command="{Binding CopyMarkdownCommand}"/>
</MenuFlyout>
</Grid.ContextFlyout>
<TextBlock
VerticalAlignment="Center"
TextAlignment="Center"
Margin="0 0 8 0"
Text="{Binding Request.Value.Method}"
Background="{Binding Request.Value.Color}"
FontSize="8"
Width="50"
Padding="10 2 10 2"
Grid.Row="0"
Grid.Column="0"/>
<TextBlock
VerticalAlignment="Center"
Text="{Binding Path}"
FontSize="11"
Grid.Row="0"
Grid.Column="1"/>
</Grid>
</UserControl>

View File

@@ -0,0 +1,18 @@
using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.Endpoints;
using ReactiveUI;
namespace Needlework.Net.Views.Pages.Endpoints;
public partial class PathOperationView : ReactiveUserControl<PathOperationViewModel>
{
public PathOperationView()
{
this.WhenActivated(disposables =>
{
// Add any activation logic here if needed
});
InitializeComponent();
}
}

View File

@@ -7,8 +7,8 @@
xmlns:avalonEdit="https://github.com/avaloniaui/avaloniaedit" xmlns:avalonEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:controls="using:Needlework.Net.Controls" xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointView" x:Class="Needlework.Net.Views.Pages.Endpoints.PluginView"
x:DataType="vm:EndpointViewModel"> x:DataType="vm:PluginViewModel">
<UserControl.Styles> <UserControl.Styles>
<Style Selector="DataGrid"> <Style Selector="DataGrid">
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ControlElevationBorderBrush}"/> <Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ControlElevationBorderBrush}"/>
@@ -35,7 +35,7 @@
Grid.Column="0" Grid.Column="0"
RowDefinitions="*" RowDefinitions="*"
ColumnDefinitions="auto,*"> ColumnDefinitions="auto,*">
<TextBox Text="{Binding Search}" <TextBox Name="SearchTextBox"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="2"/> Grid.ColumnSpan="2"/>
@@ -44,43 +44,11 @@
Grid.Column="0" Grid.Column="0"
RowDefinitions="*" RowDefinitions="*"
ColumnDefinitions="*"> ColumnDefinitions="*">
<ListBox ItemsSource="{Binding FilteredPathOperations}" <ListBox Name="PathOperationListBox"
SelectedItem="{Binding SelectedPathOperation}"
ScrollViewer.HorizontalScrollBarVisibility="Visible" ScrollViewer.HorizontalScrollBarVisibility="Visible"
Margin="0 0 0 0" Margin="0 0 0 0"
Grid.Row="1" Grid.Row="1"
Grid.Column="0"> Grid.Column="0"/>
<ListBox.ItemTemplate>
<DataTemplate>
<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 Request.Value.Method}"
Background="{Binding Request.Value.Color}"
FontSize="8"
Width="50"
Padding="10 2 10 2"
Grid.Row="0"
Grid.Column="0"/>
<TextBlock
VerticalAlignment="Center"
Text="{Binding Path}"
FontSize="11"
Grid.Row="0"
Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid> </Grid>
<GridSplitter Background="Gray" <GridSplitter Background="Gray"
Margin="8 0 8 0" Margin="8 0 8 0"
@@ -91,44 +59,43 @@
Grid.Column="2" Grid.Column="2"
RowDefinitions="*" RowDefinitions="*"
ColumnDefinitions="auto,*,auto"> ColumnDefinitions="auto,*,auto">
<TextBox Grid.Row="0" <TextBox Name="RequestMethodTextBox"
Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Text="{Binding SelectedPathOperation.Request.Value.Method}"
FontSize="12" FontSize="12"
IsReadOnly="True" IsReadOnly="True"
Margin="0 0 8 0"/> Margin="0 0 8 0"/>
<TextBox Grid.Row="0" <TextBox Name="RequestResponsePathTextBox"
Grid.Row="0"
Grid.Column="1" Grid.Column="1"
FontSize="12" FontSize="12"
Text="{Binding SelectedPathOperation.Request.Value.ResponsePath}"
IsReadOnly="True"/> IsReadOnly="True"/>
<StackPanel Grid.Row="0" <StackPanel Grid.Row="0"
Grid.Column="2" Grid.Column="2"
Orientation="Horizontal"> Orientation="Horizontal">
<Button Classes="Flat" <Button Name="SendRequestButton"
Classes="Flat"
Margin="4" Margin="4"
FontSize="12" FontSize="12"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Padding="12 4 12 4" Padding="12 4 12 4"
VerticalAlignment="Center" VerticalAlignment="Center">Send</Button>
Command="{Binding SelectedPathOperation.SendRequestCommand}">Send</Button>
</StackPanel> </StackPanel>
</Grid> </Grid>
<Grid Grid.Row="1" Grid.Column="2"> <Grid Grid.Row="1" Grid.Column="2">
<TabControl> <TabControl>
<TabItem Header="Params"> <TabItem Header="Params">
<ScrollViewer> <ScrollViewer>
<StackPanel IsVisible="{Binding SelectedPathOperation, Converter={StaticResource NullBoolConverter}}"> <StackPanel Name="ParamsStackPanel">
<controls:Card <controls:Card Name="PathParametersCard"
Margin="0 4" Margin="0 4">
IsVisible="{Binding SelectedPathOperation.Operation.PathParameters, Converter={StaticResource EnumerableBoolConverter}}">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" <TextBlock FontSize="14"
FontWeight="DemiBold">Path Parameters</TextBlock> FontWeight="DemiBold">Path Parameters</TextBlock>
<DataGrid <DataGrid Name="PathParametersDataGrid"
ItemsSource="{Binding SelectedPathOperation.Operation.PathParameters}" IsReadOnly="True"
IsReadOnly="True" GridLinesVisibility="All"
GridLinesVisibility="All"> x:DataType="vm:ParameterViewModel">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/> <DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridCheckBoxColumn Header="Required" Binding="{Binding IsRequired}"/> <DataGridCheckBoxColumn Header="Required" Binding="{Binding IsRequired}"/>
@@ -144,16 +111,15 @@
</DataGrid> </DataGrid>
</StackPanel> </StackPanel>
</controls:Card> </controls:Card>
<controls:Card <controls:Card Name="QueryParametersCard"
Margin="0 4" Margin="0 4">
IsVisible="{Binding SelectedPathOperation.Operation.QueryParameters, Converter={StaticResource EnumerableBoolConverter}}">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" <TextBlock FontSize="14"
FontWeight="DemiBold">Query Parameters</TextBlock> FontWeight="DemiBold">Query Parameters</TextBlock>
<DataGrid <DataGrid Name="QueryParametersDataGrid"
ItemsSource="{Binding SelectedPathOperation.Operation.QueryParameters}" IsReadOnly="True"
IsReadOnly="True" GridLinesVisibility="Horizontal"
GridLinesVisibility="Horizontal"> x:DataType="vm:ParameterViewModel">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/> <DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridCheckBoxColumn Header="Required" Binding="{Binding IsRequired}"/> <DataGridCheckBoxColumn Header="Required" Binding="{Binding IsRequired}"/>
@@ -189,110 +155,64 @@
VerticalAlignment="Center"> VerticalAlignment="Center">
Username Username
</TextBlock> </TextBlock>
<TextBox FontSize="12" <TextBox Name="UsernameTextBox"
FontSize="12"
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
Margin="0 0 0 8" Margin="0 0 0 8"
IsReadOnly="True" IsReadOnly="True"/>
Text="{Binding SelectedPathOperation.Request.Value.ResponseUsername}" />
<TextBlock FontSize="12" <TextBlock FontSize="12"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center"> VerticalAlignment="Center">
Password Password
</TextBlock> </TextBlock>
<TextBox FontSize="12" <TextBox Name="PasswordTextBox"
FontSize="12"
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
Margin="0 0 0 8" Margin="0 0 0 8"
IsReadOnly="True" IsReadOnly="True"/>
Text="{Binding SelectedPathOperation.Request.Value.ResponsePassword}"/>
<TextBlock FontSize="12" <TextBlock FontSize="12"
Grid.Row="2" Grid.Row="2"
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center"> VerticalAlignment="Center">
Authorization Authorization
</TextBlock> </TextBlock>
<TextBox FontSize="12" <TextBox Name="AuthorizationTextBox" FontSize="12"
Grid.Row="2" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
IsReadOnly="True" IsReadOnly="True"/>
Text="{Binding SelectedPathOperation.Request.Value.ResponseAuthorization}"/>
</Grid> </Grid>
</TabItem> </TabItem>
<TabItem Header="Schemas"> <TabItem Header="Schemas">
<ScrollViewer> <ScrollViewer>
<StackPanel> <StackPanel>
<controls:Card Margin="0 4" IsVisible="{Binding SelectedPathOperation.Operation.RequestBodyType, Converter={StaticResource NullBoolConverter}}"> <controls:Card Name="RequestBodyTypeCard"
Margin="0 4">
<TextBlock> <TextBlock>
<Run Text="Request body: " FontWeight="DemiBold" FontSize="12"/> <Run Text="Request body: " FontWeight="DemiBold" FontSize="12"/>
<Run Text="{Binding SelectedPathOperation.Operation.RequestBodyType}" FontSize="12"/> <Run Name="RequestBodyTypeRun" FontSize="12"/>
</TextBlock> </TextBlock>
</controls:Card> </controls:Card>
<Border Margin="0 4" IsVisible="{Binding SelectedPathOperation.Operation.RequestClasses, Converter={StaticResource EnumerableBoolConverter}}"> <Border Name="RequestClassesBorder" Margin="0 4">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" FontWeight="DemiBold" Margin="0 0 0 4">Request Classes</TextBlock> <TextBlock FontSize="14" FontWeight="DemiBold" Margin="0 0 0 4">Request Classes</TextBlock>
<ItemsRepeater ItemsSource="{Binding SelectedPathOperation.Operation.RequestClasses}"> <ItemsControl Name="RequestClassItemsControl"
<ItemsRepeater.ItemTemplate> Margin="0 4 0 8"/>
<DataTemplate>
<StackPanel Margin="0 4 0 8">
<TextBlock FontSize="12" FontWeight="DemiBold" Text="{Binding Id}"/>
<controls:Card IsVisible="{Binding PropertyFields, Converter={StaticResource EnumerableBoolConverter}}">
<DataGrid
ItemsSource="{Binding PropertyFields}"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
</DataGrid>
</controls:Card>
<controls:Card Margin="0 0 0 8" IsVisible="{Binding PropertyEnums, Converter={StaticResource EnumerableBoolConverter}}">
<DataGrid
ItemsSource="{Binding PropertyEnums}"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
</DataGrid>
</controls:Card>
</StackPanel>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel> </StackPanel>
</Border> </Border>
<controls:Card Margin="0 4"> <controls:Card Margin="0 4">
<TextBlock> <TextBlock>
<Run Text="Return value: " FontWeight="DemiBold" FontSize="12"/> <Run Text="Return value: " FontWeight="DemiBold" FontSize="12"/>
<Run Text="{Binding SelectedPathOperation.Operation.ReturnType}" FontSize="12"/> <Run Name="ReturnTypeRun" FontSize="12"/>
</TextBlock> </TextBlock>
</controls:Card> </controls:Card>
<Border Margin="0 4" IsVisible="{Binding SelectedPathOperation.Operation.ResponseClasses, Converter={StaticResource EnumerableBoolConverter}}"> <Border Name="ResponseClassesBorder" Margin="0 4">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" FontWeight="DemiBold">Response Classes</TextBlock> <TextBlock FontSize="14" FontWeight="DemiBold">Response Classes</TextBlock>
<ItemsRepeater ItemsSource="{Binding SelectedPathOperation.Operation.ResponseClasses}"> <ItemsControl Name="ResponseClassItemsControl"
<ItemsRepeater.ItemTemplate> Margin="0 4 0 8"/>
<DataTemplate>
<StackPanel Margin="0 4 0 8">
<TextBlock FontSize="12" FontWeight="DemiBold" Text="{Binding Id}" Margin="0 0 0 4"/>
<controls:Card IsVisible="{Binding PropertyFields, Converter={StaticResource EnumerableBoolConverter}}">
<DataGrid
ItemsSource="{Binding PropertyFields}"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
</DataGrid>
</controls:Card>
<controls:Card Margin="0 0 0 8" IsVisible="{Binding PropertyEnums, Converter={StaticResource EnumerableBoolConverter}}">
<DataGrid
ItemsSource="{Binding PropertyEnums}"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
</DataGrid>
</controls:Card>
</StackPanel>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel> </StackPanel>
</Border> </Border>
</StackPanel> </StackPanel>
@@ -303,18 +223,18 @@
<GridSplitter Grid.Row="0" Grid.Column="3" Grid.RowSpan="2" Background="Gray" <GridSplitter Grid.Row="0" Grid.Column="3" Grid.RowSpan="2" Background="Gray"
Margin="8 0 8 0"/> Margin="8 0 8 0"/>
<StackPanel Grid.Row="0" Grid.Column="4" Orientation="Horizontal"> <StackPanel Grid.Row="0" Grid.Column="4" Orientation="Horizontal">
<Button HorizontalAlignment="Left" <Button Name="ResponseStatusButton"
HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
Margin="4" Margin="4"
FontSize="10" FontSize="10"
Padding="12 4 12 4" Padding="12 4 12 4"
Classes="Flat" Classes="Flat"/>
Content="{Binding SelectedPathOperation.Request.Value.ResponseStatus}"/>
</StackPanel> </StackPanel>
<Grid Grid.Row="1" Grid.Column="4"> <Grid Grid.Row="1" Grid.Column="4">
<controls:BusyArea BusyText="Loading..." <controls:BusyArea Name="SelectedPathOperationBusyArea"
IsBusy="{Binding SelectedPathOperation.IsBusy}"> BusyText="Loading...">
<TabControl> <TabControl>
<TabItem Header="Preview"> <TabItem Header="Preview">
<avalonEdit:TextEditor <avalonEdit:TextEditor

View File

@@ -0,0 +1,79 @@
using Avalonia;
using Avalonia.ReactiveUI;
using Avalonia.Styling;
using Needlework.Net.Extensions;
using Needlework.Net.ViewModels.Pages.Endpoints;
using ReactiveUI;
using System.Reactive.Disposables;
using TextMateSharp.Grammars;
namespace Needlework.Net.Views.Pages.Endpoints;
public partial class PluginView : ReactiveUserControl<PluginViewModel>
{
public PluginView()
{
this.WhenActivated(disposables =>
{
EndpointRequestEditor?.ApplyJsonEditorSettings();
EndpointResponseEditor?.ApplyJsonEditorSettings();
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
this.OneWayBind(ViewModel, vm => vm.Search, v => v.SearchTextBox.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.FilteredPathOperations, v => v.PathOperationListBox.ItemsSource)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedPathOperation, v => v.PathOperationListBox.SelectedItem)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Request.Value.Method, v => v.RequestMethodTextBox.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Request.Value.ResponsePath, v => v.RequestResponsePathTextBox.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation, v => v.ParamsStackPanel.IsVisible)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Operation.PathParameters, v => v.PathParametersCard.IsVisible)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Operation.PathParameters, v => v.PathParametersDataGrid.ItemsSource)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Operation.QueryParameters, v => v.QueryParametersCard.IsVisible)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Operation.QueryParameters, v => v.QueryParametersDataGrid.ItemsSource)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Request.Value.ResponseUsername, v => v.UsernameTextBox.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Request.Value.ResponsePassword, v => v.PasswordTextBox.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Request.Value.ResponseAuthorization, v => v.AuthorizationTextBox.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Operation.RequestBodyType, v => v.RequestBodyTypeCard.IsVisible)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Operation.RequestBodyType, v => v.RequestBodyTypeRun.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Operation.RequestClasses, v => v.RequestClassesBorder.IsVisible)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Operation.RequestClasses, v => v.RequestClassItemsControl.ItemsSource)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Operation.ReturnType, v => v.ReturnTypeRun.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Operation.ResponseClasses, v => v.ResponseClassesBorder.IsVisible)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Operation.ResponseClasses, v => v.ResponseClassItemsControl.ItemsSource)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.Request.Value.ResponseStatus, v => v.ResponseStatusButton.Content)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.SelectedPathOperation.IsBusy, v => v.SelectedPathOperationBusyArea.IsBusy)
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SelectedPathOperation.SendRequestCommand, v => v.SendRequestButton)
.DisposeWith(disposables);
});
InitializeComponent();
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)
{
var registryOptions = new RegistryOptions(
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
}
}

View File

@@ -0,0 +1,31 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:Needlework.Net.Controls"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:ui="using:FluentAvalonia.UI.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Endpoints.PropertyClassView"
x:DataType="vm:PropertyClassViewModel">
<StackPanel>
<TextBlock Name="IdTextBlock"
FontSize="12"
FontWeight="DemiBold"
Margin="0 0 0 4"/>
<controls:Card Name="PropertyFieldsCard">
<DataGrid Name="PropertyFieldsDataGrid"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
</DataGrid>
</controls:Card>
<controls:Card Margin="0 0 0 8" Name="PropertyEnumsCard">
<DataGrid Name="PropertyEnumsDataGrid"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
</DataGrid>
</controls:Card>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,28 @@
using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.Endpoints;
using ReactiveUI;
using System.Reactive.Disposables;
namespace Needlework.Net.Views.Pages.Endpoints;
public partial class PropertyClassView : ReactiveUserControl<PropertyClassViewModel>
{
public PropertyClassView()
{
this.WhenActivated(disposables =>
{
this.OneWayBind(ViewModel, vm => vm.Id, v => v.IdTextBlock.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.PropertyFields, v => v.PropertyFieldsCard.IsVisible)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.PropertyFields, v => v.PropertyFieldsDataGrid.ItemsSource)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.PropertyEnums, v => v.PropertyEnumsCard.IsVisible)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.PropertyEnums, v => v.PropertyEnumsDataGrid.ItemsSource)
.DisposeWith(disposables);
});
InitializeComponent();
}
}

View File

@@ -2,29 +2,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Needlework.Net.ViewModels.Pages" xmlns:vm="using:Needlework.Net.ViewModels.Pages.Home"
xmlns:controls="using:Needlework.Net.Controls" xmlns:controls="using:Needlework.Net.Controls"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:views="using:Needlework.Net.Views.Pages.Home"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Name="HomeControl" Name="HomeControl"
x:Class="Needlework.Net.Views.Pages.HomeView" x:Class="Needlework.Net.Views.Pages.Home.HomePage"
x:DataType="vm:HomeViewModel"> x:DataType="vm:HomeViewModel">
<UserControl.Styles>
<Style Selector="Button">
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
</Style>
<Style Selector="DataGrid">
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ControlElevationBorderBrush}"/>
</Style>
<Style Selector="DataGridColumnHeader TextBlock">
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}"/>
</Style>
<Style Selector="DataGridRow DataGridCell">
<Setter Property="FontSize" Value="12"></Setter>
</Style>
<Style Selector="DataGridRow">
<Setter Property="Margin" Value="0 0 0 4"></Setter>
</Style>
</UserControl.Styles>
<!-- TOP LEVEL --> <!-- TOP LEVEL -->
<Grid ColumnDefinitions="*,400" <Grid ColumnDefinitions="*,400"
RowDefinitions="*"> RowDefinitions="*">
@@ -97,31 +82,18 @@
<ScrollViewer Grid.Column="0" <ScrollViewer Grid.Column="0"
Grid.Row="1" Grid.Row="1"
HorizontalScrollBarVisibility="Disabled"> HorizontalScrollBarVisibility="Disabled">
<ItemsRepeater ItemsSource="{Binding Libraries}"> <ItemsControl Name="LibraryItemsControl" ItemsSource="{Binding Libraries}">
<ItemsRepeater.ItemTemplate> <ItemsControl.Styles>
<DataTemplate> <Style Selector="views|LibraryView">
<StackPanel Margin="0 12 0 0"> <Setter Property="Margin" Value="0 12 0 0"/>
<TextBlock> </Style>
<Run Text="{Binding Language}" </ItemsControl.Styles>
FontWeight="Bold"/> <ItemsControl.ItemTemplate>
<Bold> - </Bold> <DataTemplate x:DataType="vm:LibraryViewModel">
<Run Text="{Binding Repo}" <views:LibraryView ViewModel="{Binding}"/>
FontWeight="Bold"/>
</TextBlock>
<TextBlock Text="{Binding Description}"
IsVisible="{Binding Description, Converter={StaticResource NullBoolConverter}}"
TextAlignment="Left"
TextWrapping="WrapWithOverflow"
Width="350"/>
<Button Command="{Binding #HomeControl.((vm:HomeViewModel)DataContext).OpenUrlCommand}"
CommandParameter="{Binding Link}"
Margin="0 4 0 0">
<TextBlock Text="{Binding Link}"/>
</Button>
</StackPanel>
</DataTemplate> </DataTemplate>
</ItemsRepeater.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsRepeater> </ItemsControl>
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -0,0 +1,15 @@
using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.Home;
using ReactiveUI;
namespace Needlework.Net.Views.Pages.Home;
public partial class HomePage : ReactiveUserControl<HomeViewModel>
{
public HomePage()
{
this.WhenActivated(disposables => { });
InitializeComponent();
}
}

View File

@@ -0,0 +1,25 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveUi="http://reactiveui.net"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Home.LibraryView">
<StackPanel>
<TextBlock>
<Run Name="LanguageRun"
FontWeight="Bold"/>
<Bold> - </Bold>
<Run Name="RepoRun"
FontWeight="Bold"/>
</TextBlock>
<TextBlock Name="DescriptionTextBlock"
TextAlignment="Left"
TextWrapping="WrapWithOverflow"
Width="350"/>
<Button Name="OpenUrlButton"
Margin="0 4 0 0">
<TextBlock Name="OpenUrlTextBlock" />
</Button>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,28 @@
using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.Home;
using ReactiveUI;
using System.Reactive.Disposables;
namespace Needlework.Net.Views.Pages.Home;
public partial class LibraryView : ReactiveUserControl<LibraryViewModel>
{
public LibraryView()
{
this.WhenActivated(disposables =>
{
this.OneWayBind(ViewModel, vm => vm.Library.Language, v => v.LanguageRun.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Library.Repo, v => v.RepoRun.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Library.Description, v => v.DescriptionTextBlock.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Library.Description, v => v.DescriptionTextBlock.IsVisible)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Library.Link, v => v.OpenUrlButton.Content)
.DisposeWith(disposables);
});
InitializeComponent();
}
}

View File

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

View File

@@ -0,0 +1,25 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.WebSocket"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.WebSocket.EventView"
x:DataType="vm:EventViewModel">
<Grid ColumnDefinitions="auto,auto,*">
<TextBlock Name="TimeTextBlock"
Margin="0 0 4 0"
Grid.Column="0"
Foreground="#8be9fd"/>
<TextBlock Name="TypeTextBlock"
Grid.Column="1"
Margin="0 0 4 0"
Foreground="#ffb86c"/>
<TextBlock Name="UriTextBlock"
Grid.Column="2"
Foreground="#f8f8f2"/>
</Grid>
</UserControl>

View File

@@ -0,0 +1,24 @@
using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.WebSocket;
using ReactiveUI;
using System.Reactive.Disposables;
namespace Needlework.Net.Views.Pages.WebSocket;
public partial class EventView : ReactiveUserControl<EventViewModel>
{
public EventView()
{
this.WhenActivated(disposables =>
{
this.OneWayBind(ViewModel, vm => vm.Time, v => v.TimeTextBlock.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Type, v => v.TypeTextBlock.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Uri, v => v.UriTextBlock.Text)
.DisposeWith(disposables);
});
InitializeComponent();
}
}

View File

@@ -0,0 +1,67 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.WebSocket"
xmlns:reactiveUi="http://reactiveui.net"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.WebSocket.WebSocketPage"
x:DataType="vm:WebSocketViewModel">
<Grid RowDefinitions="*,auto,*" Margin="16">
<Border Grid.Row="0"
Padding="0 0 0 8">
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
<Grid Grid.Row="0"
Grid.Column="0"
RowDefinitions="*">
<ComboBox Name="EventTypesComboBox"
Grid.Row="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"/>
</Grid>
<Grid
Grid.Row="1"
Grid.Column="0"
RowDefinitions="*"
ColumnDefinitions="auto,*,auto,auto"
Margin="0 8 0 0">
<Button Name="ClearButton"
Margin="0 0 8 0"
Grid.Row="0"
Grid.Column="0">Clear</Button>
<TextBox Name="SearchTextBox"
Grid.Row="0"
Grid.Column="1"
MaxLines="1"
Margin="0 0 8 0"/>
<CheckBox Name="IsAttachTextBox"
Margin="0 0 8 0"
Grid.Row="0"
Grid.Column="2"
Content="Attach"/>
<CheckBox Name="IsTailCheckBox"
Grid.Row="0"
Grid.Column="3"
Content="Tail"/>
</Grid>
<ListBox Name="EventListBox"
Grid.Row="2"
Grid.Column="0"
Margin="0 8 0 0"
ScrollViewer.HorizontalScrollBarVisibility="Auto"/>
</Grid>
</Border>
<GridSplitter Grid.Row="1" ResizeDirection="Rows" Background="Gray"/>
<Border Grid.Row="2"
Padding="0 8 0 0">
<avaloniaEdit:TextEditor
Name="ResponseEditor"
ShowLineNumbers="True"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible"
Text=""
FontSize="12"/>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,93 @@
using Avalonia;
using Avalonia.ReactiveUI;
using Avalonia.Styling;
using Needlework.Net.ViewModels.Pages.WebSocket;
using ReactiveUI;
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using TextMateSharp.Grammars;
namespace Needlework.Net.Views.Pages.WebSocket;
public partial class WebSocketPage : ReactiveUserControl<WebSocketViewModel>
{
public WebSocketPage()
{
this.WhenActivated(disposables =>
{
this.OneWayBind(ViewModel, vm => vm.EventTypes, v => v.EventTypesComboBox.ItemsSource)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.EventType, v => v.EventTypesComboBox.SelectedItem)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Search, v => v.SearchTextBox.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IsAttach, v => v.IsAttachTextBox.IsChecked)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IsTail, v => v.IsTailCheckBox.IsChecked)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.FilteredEventLog, v => v.EventListBox.ItemsSource)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedEventLog, v => v.EventListBox.SelectedItem)
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ClearCommand, v => v.ClearButton)
.DisposeWith(disposables);
this.WhenAnyValue(x => x.ViewModel!.GetEventTypesCommand)
.SelectMany(x => x.Execute())
.Subscribe()
.DisposeWith(disposables);
});
InitializeComponent();
}
//public void Receive(ResponseUpdatedMessage message)
//{
// _responseEditor!.Text = message.Value;
//}
//protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
//{
// base.OnApplyTemplate(e);
// _viewModel = (WebSocketViewModel)DataContext!;
// _viewer = this.FindControl<ListBox>("EventViewer");
// _viewModel.EventLog.CollectionChanged += EventLog_CollectionChanged; ;
// _responseEditor = this.FindControl<TextEditor>("ResponseEditor");
// _responseEditor?.ApplyJsonEditorSettings();
// WeakReferenceMessenger.Default.Register(this, nameof(WebSocketViewModel));
// OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
//}
//private void EventLog_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
//{
// Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
// {
// if (_viewModel!.IsTail)
// {
// await _viewModel.EventLogLock.WaitAsync();
// try
// {
// _viewer!.ScrollIntoView(_viewModel.EventLog.Count - 1);
// }
// catch (InvalidOperationException) { }
// finally
// {
// _viewModel.EventLogLock.Release();
// }
// }
// });
//}
private void OnBaseThemeChanged(ThemeVariant currentTheme)
{
var registryOptions = new RegistryOptions(
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
}
}

View File

@@ -1,89 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Websocket"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.WebsocketView"
x:DataType="vm:WebsocketViewModel">
<Grid RowDefinitions="*,auto,*" Margin="16">
<Border Grid.Row="0"
Padding="0 0 0 8">
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
<Grid Grid.Row="0"
Grid.Column="0"
RowDefinitions="*">
<ComboBox ItemsSource="{Binding EventTypes}"
SelectedItem="{Binding EventType}"
Grid.Row="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"/>
</Grid>
<Grid
Grid.Row="1"
Grid.Column="0"
RowDefinitions="*"
ColumnDefinitions="auto,*,auto,auto"
Margin="0 8 0 0">
<Button Margin="0 0 8 0"
Grid.Row="0"
Grid.Column="0"
Command="{Binding ClearCommand}">Clear</Button>
<TextBox Grid.Row="0"
Grid.Column="1"
Text="{Binding Search}"
MaxLines="1"
Margin="0 0 8 0"/>
<CheckBox
Margin="0 0 8 0"
Grid.Row="0"
Grid.Column="2"
Content="Attach"
IsChecked="{Binding IsAttach}"/>
<CheckBox
Grid.Row="0"
Grid.Column="3"
Content="Tail"
IsChecked="{Binding IsTail}"/>
</Grid>
<ListBox Grid.Row="2"
Grid.Column="0"
Name="EventViewer"
Margin="0 8 0 0"
ItemsSource="{Binding FilteredEventLog}"
SelectedItem="{Binding SelectedEventLog}"
ScrollViewer.HorizontalScrollBarVisibility="Auto">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="auto,auto,*">
<TextBlock Text="{Binding Time}"
Margin="0 0 4 0"
Grid.Column="0"
Foreground="#8be9fd"/>
<TextBlock Text="{Binding Type}"
Grid.Column="1"
Margin="0 0 4 0"
Foreground="#ffb86c"/>
<TextBlock Text="{Binding Uri}"
Grid.Column="2"
Foreground="#f8f8f2"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
<GridSplitter Grid.Row="1" ResizeDirection="Rows" Background="Gray"/>
<Border Grid.Row="2"
Padding="0 8 0 0">
<avaloniaEdit:TextEditor
Name="ResponseEditor"
ShowLineNumbers="True"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible"
Text=""
FontSize="12"/>
</Border>
</Grid>
</UserControl>

View File

@@ -1,73 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using AvaloniaEdit;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Extensions;
using Needlework.Net.Messages;
using Needlework.Net.ViewModels.Pages.Websocket;
using System;
using TextMateSharp.Grammars;
namespace Needlework.Net.Views.Pages;
public partial class WebsocketView : UserControl, IRecipient<ResponseUpdatedMessage>
{
private TextEditor? _responseEditor;
public WebsocketViewModel? _viewModel;
private ListBox? _viewer;
public WebsocketView()
{
InitializeComponent();
}
public void Receive(ResponseUpdatedMessage message)
{
_responseEditor!.Text = message.Value;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_viewModel = (WebsocketViewModel)DataContext!;
_viewer = this.FindControl<ListBox>("EventViewer");
_viewModel.EventLog.CollectionChanged += EventLog_CollectionChanged; ;
_responseEditor = this.FindControl<TextEditor>("ResponseEditor");
_responseEditor?.ApplyJsonEditorSettings();
WeakReferenceMessenger.Default.Register(this, nameof(WebsocketViewModel));
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
}
private void EventLog_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
{
if (_viewModel!.IsTail)
{
await _viewModel.EventLogLock.WaitAsync();
try
{
_viewer!.ScrollIntoView(_viewModel.EventLog.Count - 1);
}
catch (InvalidOperationException) { }
finally
{
_viewModel.EventLogLock.Release();
}
}
});
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)
{
var registryOptions = new RegistryOptions(
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
}
}