4 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
121 changed files with 1855 additions and 4765 deletions

5
.gitignore vendored
View File

@@ -34,7 +34,6 @@ bld/
[Oo]bj/ [Oo]bj/
[Ll]og/ [Ll]og/
[Ll]ogs/ [Ll]ogs/
[Dd]ata/
# Visual Studio 2015/2017 cache/options directory # Visual Studio 2015/2017 cache/options directory
.vs/ .vs/
@@ -483,7 +482,3 @@ $RECYCLE.BIN/
# Vim temporary swap files # Vim temporary swap files
*.swp *.swp
*.sqlite
*.sqlite-shm
*.sqlite-wal

View File

@@ -8,7 +8,7 @@
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.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"/>
@@ -17,7 +17,7 @@
</Application.Styles> </Application.Styles>
<Application.Resources> <Application.Resources>
<converters:EnumerableToVisibility x:Key="EnumerableToVisibilityConverter"/> <converters:EnumerableBoolConverter x:Key="EnumerableBoolConverter"/>
<converters:NullableToVisibility x:Key="NullableToVisibilityConverter"/> <converters:NullBoolConverter x:Key="NullBoolConverter"/>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@@ -1,44 +1,32 @@
using Akavache;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Templates;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection; using Flurl.Http.Configuration;
using Needlework.Net.Constants; using Needlework.Net.Converters;
using Needlework.Net.Extensions; using Needlework.Net.Services;
using Needlework.Net.ViewModels.MainWindow; using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages; 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 System.Reactive.Linq; 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 : Application, IEnableLogger public partial class App : Application
{ {
private readonly IDataTemplate _viewLocator;
private readonly IBlobCache _blobCache;
private readonly PageFactory _pageFactory;
private readonly MainWindowViewModel _mainWindowViewModel;
public App(IServiceProvider serviceProvider)
{
_viewLocator = serviceProvider.GetRequiredService<IDataTemplate>();
_blobCache = serviceProvider.GetRequiredService<IBlobCache>();
_pageFactory = serviceProvider.GetRequiredService<PageFactory>();
_mainWindowViewModel = serviceProvider.GetRequiredService<MainWindowViewModel>();
this.Log()
.Debug("NeedleworkDotNet version: {Version}", AppInfo.Version);
this.Log()
.Debug("OS description: {Description}", System.Runtime.InteropServices.RuntimeInformation.OSDescription);
}
public static JsonSerializerOptions JsonSerializerOptions { get; } = new() public static JsonSerializerOptions JsonSerializerOptions { get; } = new()
{ {
WriteIndented = true, WriteIndented = true,
@@ -51,23 +39,67 @@ public partial class App : Application, IEnableLogger
public override void Initialize() public override void Initialize()
{ {
DataTemplates.Add(_viewLocator);
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
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(_mainWindowViewModel, _pageFactory); desktop.MainWindow = new MainWindow() { DataContext = Locator.Current.GetService<IScreen>() };
MainWindow = desktop.MainWindow; MainWindow = desktop.MainWindow;
desktop.ShutdownRequested += (_, _) =>
{
_blobCache.Flush().Wait();
_blobCache.Dispose();
};
} }
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());
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
using System.Reflection;
namespace Needlework.Net.Constants
{
public static class AppInfo
{
public static readonly string Name = "Needlework.Net";
public static readonly string Version = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
}
}

View File

@@ -1,9 +0,0 @@
namespace Needlework.Net.Constants
{
public static class BlobCacheKeys
{
public static readonly string GithubLatestRelease = nameof(GithubLatestRelease);
public static readonly string AppSettings = nameof(AppSettings);
}
}

View File

@@ -1,11 +0,0 @@
namespace Needlework.Net.Constants
{
public static class FlurlClientKeys
{
public static readonly string GithubClient = nameof(GithubClient);
public static readonly string GithubUserContentClient = nameof(GithubUserContentClient);
public static readonly string Client = nameof(Client);
}
}

View File

@@ -1,9 +0,0 @@
using System;
namespace Needlework.Net.Constants
{
public static class Intervals
{
public static readonly TimeSpan CheckForUpdates = TimeSpan.FromMinutes(60);
}
}

View File

@@ -9,8 +9,8 @@
<!-- Set Defaults --> <!-- Set Defaults -->
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border Padding="12" <Border Padding="16"
CornerRadius="4" CornerRadius="16,16,16,16"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"> Background="{DynamicResource CardBackgroundFillColorDefaultBrush}">
<ContentPresenter Content="{TemplateBinding Content}"/> <ContentPresenter Content="{TemplateBinding Content}"/>
</Border> </Border>

View File

@@ -21,7 +21,7 @@
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Grid> <Grid>
<Border CornerRadius="4" <Border CornerRadius="16,16,16,16"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}" Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
Margin="0 50 0 0" Margin="0 50 0 0"
Padding="16 66 16 16"> Padding="16 66 16 16">

View File

@@ -6,7 +6,7 @@ using System.Linq;
namespace Needlework.Net.Converters namespace Needlework.Net.Converters
{ {
public class EnumerableToVisibility : IValueConverter public class EnumerableBoolConverter : IValueConverter
{ {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {

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

@@ -4,7 +4,7 @@ using System.Globalization;
namespace Needlework.Net.Converters namespace Needlework.Net.Converters
{ {
public class NullableToVisibility : IValueConverter public class NullBoolConverter : IValueConverter
{ {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {

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,13 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.DataModels
{
public partial class AppSettings : ObservableObject
{
[ObservableProperty]
private bool _isCheckForUpdates = true;
[ObservableProperty]
private bool _isCheckForSchema = true;
}
}

View File

@@ -1,13 +0,0 @@
namespace Needlework.Net.DataModels
{
public class HextechDocsPost
{
public required string Path { get; init; }
public required string Title { get; init; }
public required string Excerpt { get; init; }
public string Url => $"https://hextechdocs.dev{Path}";
}
}

View File

@@ -2,27 +2,25 @@
using Flurl.Http; using Flurl.Http;
using Flurl.Http.Configuration; using Flurl.Http.Configuration;
using Microsoft.OpenApi.Readers; using Microsoft.OpenApi.Readers;
using Needlework.Net.Constants;
using Needlework.Net.Extensions;
using Needlework.Net.Models; using Needlework.Net.Models;
using Splat;
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Needlework.Net namespace Needlework.Net
{ {
public class DocumentService : IEnableLogger public class DataSource : IEnableLogger
{ {
private readonly OpenApiStreamReader _reader = new(); private readonly OpenApiStreamReader _reader = new();
private readonly IFlurlClient _githubUserContentClient; private readonly IFlurlClient _githubUserContentClient;
public DocumentService(IFlurlClientCache clients) public DataSource(IFlurlClientCache? clients = null)
{ {
_githubUserContentClient = clients.Get(FlurlClientKeys.GithubUserContentClient); _githubUserContentClient = clients?.Get("GithubUserContentClient") ?? Locator.Current.GetService<IFlurlClientCache>()!.Get("GithubUserContentClient")!;
} }
public async Task<Document> GetLcuSchemaDocumentAsync(CancellationToken cancellationToken = default) public async Task<Document> GetLcuSchemaDocumentAsync()
{ {
if (Cached<Document>.TryGet(nameof(GetLcuSchemaDocumentAsync), out var cached)) if (Cached<Document>.TryGet(nameof(GetLcuSchemaDocumentAsync), out var cached))
{ {
@@ -30,19 +28,14 @@ namespace Needlework.Net
} }
var lcuSchemaStream = await _githubUserContentClient.Request("/dysolix/hasagi-types/main/swagger.json") var lcuSchemaStream = await _githubUserContentClient.Request("/dysolix/hasagi-types/main/swagger.json")
.GetStreamAsync(cancellationToken: cancellationToken); .GetStreamAsync();
var lcuSchemaRaw = _reader.Read(lcuSchemaStream, out var diagnostic); var lcuSchemaRaw = _reader.Read(lcuSchemaStream, out var _);
foreach (var error in diagnostic.Errors)
{
this.Log()
.Warning("Diagnostic error: {Message}", error);
}
var document = new Document(lcuSchemaRaw); var document = new Document(lcuSchemaRaw);
return cached.Save(document, TimeSpan.FromMinutes(60)); return cached.Save(document, TimeSpan.FromMinutes(60));
} }
public async Task<Document> GetLolClientDocumentAsync(CancellationToken cancellationToken = default) public async Task<Document> GetLolClientDocumentAsync()
{ {
if (Cached<Document>.TryGet(nameof(GetLolClientDocumentAsync), out var cached)) if (Cached<Document>.TryGet(nameof(GetLolClientDocumentAsync), out var cached))
{ {
@@ -50,13 +43,8 @@ namespace Needlework.Net
} }
var lolClientStream = await _githubUserContentClient.Request("/AlsoSylv/Irelia/refs/heads/master/schemas/game_schema.json") var lolClientStream = await _githubUserContentClient.Request("/AlsoSylv/Irelia/refs/heads/master/schemas/game_schema.json")
.GetStreamAsync(cancellationToken: cancellationToken); .GetStreamAsync();
var lolClientRaw = _reader.Read(lolClientStream, out var diagnostic); var lolClientRaw = _reader.Read(lolClientStream, out var _);
foreach (var error in diagnostic.Errors)
{
this.Log()
.Warning("Diagnostic error: {Message}", error);
}
var document = new Document(lolClientRaw); var document = new Document(lolClientRaw);
return cached.Save(document, TimeSpan.FromMinutes(60)); return cached.Save(document, TimeSpan.FromMinutes(60));

View File

@@ -1,16 +0,0 @@
using Serilog;
namespace Needlework.Net.Extensions
{
public static class EnableLoggerExtensions
{
private static readonly ILogger _logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}", path: "Logs/debug-.log", rollingInterval: RollingInterval.Day, shared: true)
.CreateLogger();
public static ILogger Log(this IEnableLogger? context) => _logger.ForContext(context?.GetType() ?? typeof(Program));
}
public interface IEnableLogger;
}

View File

@@ -1,288 +0,0 @@
using Microsoft.OpenApi.Models;
using Needlework.Net.Models;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
namespace Needlework.Net.Helpers
{
public static class OpenApiHelpers
{
public static string GetReturnType(OpenApiResponses responses)
{
if (!TryGetResponse(responses, out var response))
return "none";
if (TryGetApplicationJsonMedia(response, out var media))
{
var schema = media.Schema;
return GetSchemaType(schema);
}
return "none";
}
public static bool TryGetApplicationJsonMedia(OpenApiResponse response, [NotNullWhen(true)] out OpenApiMediaType? media) // Because GetLolGameflowV1SpectateDelayedLaunch has an empty schema with no type...
{
var flag = false;
if (response.Content.TryGetValue("application/json", out var _media))
{
if (_media?.Schema?.Type != null)
{
media = _media;
flag = true;
}
else
{
media = null;
}
}
else
{
media = null;
}
return flag;
}
public static bool TryGetApplicationJsonMedia(OpenApiRequestBody requestBody, [NotNullWhen(true)] out OpenApiMediaType? media)
{
var flag = false;
if (requestBody.Content.TryGetValue("application/json", out var _media))
{
if (_media?.Schema?.Type != null)
{
media = _media;
flag = true;
}
else
{
media = null;
}
}
else
{
media = null;
}
return flag;
}
public static string GetSchemaType(OpenApiSchema? schema)
{
if (schema == null) return "object"; // Because GetLolVanguardV1Notification exists where it has a required parameter without a type...
if (schema.Reference != null) return schema.Reference.Id;
if (schema.Type == "object" && schema.AdditionalProperties?.Reference != null) return schema.AdditionalProperties.Reference.Id;
if (schema.Type == "integer" || schema.Type == "number") return $"{schema.Type}:{schema.Format}";
if (schema.Type == "array" && schema.AdditionalProperties?.Reference != null) return $"{schema.AdditionalProperties.Reference.Id}[]";
if (schema.Type == "array" && schema.AdditionalProperties?.Type != null) return $"{schema.AdditionalProperties.Type}[]";
if (schema.Type == "array" && schema.Items.Reference != null) return $"{schema.Items.Reference.Id}[]";
if (schema.Type == "array" && (schema.Items.Type == "integer" || schema.Items.Type == "number")) return $"{schema.Items.Type}:{schema.Items.Format}[]";
if (schema.Type == "array") return $"{schema.Items.Type}[]";
return schema.Type;
}
public static List<string> CreateTemplate(List<PropertyClassViewModel> requestClasses)
{
if (requestClasses.Count == 0) return [];
List<string> template = [];
template.Add("{");
var rootClass = requestClasses.First();
if (rootClass.PropertyEnums.Any()) return [rootClass.PropertyEnums.First().Values];
var propertyFields = rootClass.PropertyFields;
for (int i = 0; i < propertyFields.Count; i++)
{
template.Add($"\"{propertyFields[i].Name}\"");
template.Add(":");
template.Add($"#{propertyFields[i].Type}");
if (i == propertyFields.Count - 1) template.Add("}");
else template.Add(",");
}
for (int i = 0; i < template.Count; i++)
{
var type = template[i];
if (!type.Contains("#")) continue;
var foundClass = requestClasses.Where(c => c.Id == type.Replace("#", string.Empty));
if (foundClass.Any())
{
if (foundClass.First().PropertyEnums.Any())
{
template[i] = string.Join(string.Empty, CreateTemplate([.. foundClass]));
}
else
{
List<PropertyClassViewModel> classes = [.. requestClasses];
classes.Remove(rootClass);
template[i] = string.Join(string.Empty, CreateTemplate(classes));
}
}
else
{
template[i] = GetRequestDefaultValue(type);
}
}
return template;
}
public static string GetComponentId(OpenApiSchema schema)
{
string componentId;
if (schema.Reference != null) componentId = schema.Reference.Id;
else if (schema.Items != null) componentId = schema.Items.Reference.Id;
else componentId = schema.AdditionalProperties.Reference.Id;
return componentId;
}
public static List<ParameterViewModel> GetParameters(List<OpenApiParameter> parameters, ParameterLocation location)
{
var pathParameters = new List<ParameterViewModel>();
foreach (var parameter in parameters)
{
if (parameter.In != location) continue;
pathParameters.Add(new ParameterViewModel(parameter.Name, GetSchemaType(parameter.Schema), parameter.Required));
}
return pathParameters;
}
public static string? GetRequestBodyType(OpenApiRequestBody? requestBody)
{
if (requestBody == null) return null;
if (requestBody.Content.TryGetValue("application/json", out var media))
{
var schema = media.Schema;
if (schema == null) return null; // Because "PostLolAccountVerificationV1SendDeactivationPin" exists where the media body is empty...
return GetSchemaType(schema);
}
return null;
}
public static List<PropertyClassViewModel> GetRequestClasses(OpenApiRequestBody? requestBody, Document document)
{
if (requestBody == null) return [];
if (TryGetApplicationJsonMedia(requestBody, out var media))
{
var rawDocument = document.OpenApiDocument;
var schema = media.Schema;
if (schema == null) return [];
var type = GetSchemaType(media.Schema);
if (IsComponent(type))
{
var componentId = GetComponentId(schema);
var componentSchema = rawDocument.Components.Schemas[componentId];
List<PropertyClassViewModel> propertyClasses = [];
WalkSchema(componentSchema, propertyClasses, rawDocument);
return propertyClasses;
}
}
return [];
}
public static string GetRequestDefaultValue(string type)
{
var defaultValue = string.Empty;
if (type.Contains("[]")) defaultValue = "[]";
else if (type.Contains("string")) defaultValue = "\"\"";
else if (type.Contains("boolean")) defaultValue = "false";
else if (type.Contains("integer")) defaultValue = "0";
else if (type.Contains("double") || type.Contains("float")) defaultValue = "0.0";
else if (type.Contains("object")) defaultValue = "{}";
return defaultValue;
}
public static string? GetRequestTemplate(OpenApiRequestBody? requestBody, Document document)
{
var requestClasses = GetRequestClasses(requestBody, document);
if (requestClasses.Count == 0)
{
var type = GetRequestBodyType(requestBody);
if (type == null) return null;
return GetRequestDefaultValue(type);
}
var template = CreateTemplate(requestClasses);
return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(string.Join(string.Empty, template)), App.JsonSerializerOptions);
}
public static List<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses, Document document)
{
if (!TryGetResponse(responses, out var response))
return [];
if (TryGetApplicationJsonMedia(response, out var media))
{
var rawDocument = document.OpenApiDocument;
var schema = media.Schema;
if (schema == null) return [];
List<PropertyClassViewModel> propertyClasses = [];
WalkSchema(schema, propertyClasses, rawDocument);
return propertyClasses;
}
return [];
}
public static bool IsComponent(string type)
{
return !(type.Contains("object")
|| type.Contains("array")
|| type.Contains("bool")
|| type.Contains("string")
|| type.Contains("integer")
|| type.Contains("number"));
}
public static bool TryGetResponse(OpenApiResponses responses, [NotNullWhen(true)] out OpenApiResponse? response)
{
response = null;
var flag = false;
if (responses.TryGetValue("2XX", out var x))
{
response = x;
flag = true;
}
else if (responses.TryGetValue("200", out var y))
{
response = y;
flag = true;
}
return flag;
}
public static void WalkSchema(OpenApiSchema schema, List<PropertyClassViewModel> propertyClasses, OpenApiDocument document)
{
var type = GetSchemaType(schema);
if (IsComponent(type))
{
string componentId = GetComponentId(schema);
var componentSchema = document.Components.Schemas[componentId];
var responseClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum);
if (propertyClasses.Where(c => c.Id == componentId).Any()) return; // Avoid adding duplicate schemas in classes
propertyClasses.Add(responseClass);
foreach ((var _, var property) in componentSchema.Properties)
// Check for self-references like "LolLootLootOddsResponse"
// I blame dubble
if (IsComponent(GetSchemaType(property)) && componentId != GetComponentId(property))
WalkSchema(property, propertyClasses, document);
}
}
public static PropertyClassViewModel WalkSchema(OpenApiSchema schema, OpenApiDocument document)
{
string componentId = GetComponentId(schema);
var componentSchema = document.Components.Schemas[componentId];
var propertyClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum);
return propertyClass;
}
}
}

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

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

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

@@ -1,12 +1,12 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Needlework.Net.DataModels namespace Needlework.Net.Models
{ {
public class GithubRelease public class GithubRelease
{ {
[JsonPropertyName("tag_name")] [JsonPropertyName("tag_name")]
public string TagName { get; set; } = string.Empty; public string TagName { get; set; } = string.Empty;
public bool IsLatest(string assemblyVersion) => int.Parse(TagName.Replace(".", "")) > int.Parse(assemblyVersion.ToString().Replace(".", "")); public bool IsLatest(string assemblyVersion) => int.Parse(TagName.Replace(".", string.Empty)) > int.Parse(assemblyVersion.Replace(".", string.Empty));
} }
} }

View File

@@ -1,31 +1,9 @@
using System.Collections.Generic; namespace Needlework.Net.Models;
using System.Text.Json.Serialization;
namespace Needlework.Net.Models;
public class Library public class Library
{ {
[JsonPropertyName("repo")]
public required string Repo { get; init; } public required string Repo { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; } public string? Description { get; init; }
[JsonPropertyName("language")]
public required string Language { get; init; } public required string Language { get; init; }
public required string Link { get; init; }
[JsonPropertyName("owner")]
public required string Owner { get; init; }
[JsonPropertyName("tags")]
public List<string> Tags { get; init; } = [];
public string Link
{
get
{
if (Owner.Equals("jellies")) return $"https://github.com/elliejs/{Repo}";
return $"https://github.com/{Owner}/{Repo}";
}
}
} }

View File

@@ -1,8 +0,0 @@
using Needlework.Net.ViewModels.Pages.Endpoints;
namespace Needlework.Net.Models
{
public record SchemaPaneItem(string Key, Tab Tab)
{
}
}

View File

@@ -17,37 +17,39 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="akavache" Version="10.2.41" /> <PackageReference Include="Avalonia" Version="11.3.1" />
<PackageReference Include="AngleSharp" Version="1.3.0" />
<PackageReference Include="Avalonia" Version="11.2.8" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.3.0" /> <PackageReference Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.8" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.1" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" /> <PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.8" /> <PackageReference Include="Avalonia.Desktop" Version="11.3.1" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.8" /> <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.2.8" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.8" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.3.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.1" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.3.0" /> <PackageReference Include="AvaloniaEdit.TextMate" Version="11.3.0" />
<PackageReference Include="BlossomiShymae.Briar" Version="0.2.2" /> <PackageReference Include="BlossomiShymae.Briar" Version="0.2.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="DebounceThrottle" Version="3.0.1" />
<PackageReference Include="FastCache.Cached" Version="1.8.2" /> <PackageReference Include="FastCache.Cached" Version="1.8.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.3.0" /> <PackageReference Include="FluentAvaloniaUI" Version="2.3.0" />
<PackageReference Include="Flurl" Version="4.0.0" /> <PackageReference Include="Flurl" Version="4.0.0" />
<PackageReference Include="Flurl.Http" Version="4.0.2" /> <PackageReference Include="Flurl.Http" Version="4.0.2" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.4.1" /> <PackageReference Include="Material.Icons.Avalonia" Version="2.4.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" /> <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.NET.ILLink.Tasks" Version="9.0.3" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.24" /> <PackageReference Include="Microsoft.OpenApi" Version="1.6.24" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.24" /> <PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.24" />
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.6.2" /> <PackageReference Include="Projektanker.Icons.Avalonia" Version="9.6.2" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.6.2" /> <PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.6.2" />
<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" Version="4.3.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" /> <PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Splat.Serilog" Version="15.3.1" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.69" /> <PackageReference Include="TextMateSharp.Grammars" Version="1.0.69" />
</ItemGroup> </ItemGroup>
@@ -70,31 +72,36 @@
<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\EndpointListView.axaml.cs"> <Compile Update="Views\Pages\About\AboutPage.axaml.cs">
<DependentUpon>EndpointListView.axaml</DependentUpon> <DependentUpon>AboutPage.axaml</DependentUpon>
</Compile> </Compile>
<Compile Update="Views\Pages\Endpoints\EndpointsView.axaml.cs"> <Compile Update="Views\Pages\Console\ConsolePage.axaml.cs">
<DependentUpon>EndpointsView.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>
<Compile Update="Views\Pages\Endpoints\PluginView.axaml.cs"> <Compile Update="Views\Pages\Endpoints\PluginView.axaml.cs">
<DependentUpon>PluginView.axaml</DependentUpon> <DependentUpon>PluginView.axaml</DependentUpon>
</Compile> </Compile>
<Compile Update="Views\Pages\Schemas\SchemaSearchDetailsView.axaml.cs"> <Compile Update="Views\Pages\Home\HomePage.axaml.cs">
<DependentUpon>SchemaSearchDetailsView.axaml</DependentUpon> <DependentUpon>HomePage.axaml</DependentUpon>
</Compile> </Compile>
<Compile Update="Views\Pages\WebSocket\WebsocketView.axaml.cs"> <Compile Update="Views\Pages\WebSocket\WebSocketPage.axaml.cs">
<DependentUpon>WebSocketView.axaml</DependentUpon> <DependentUpon>WebSocketPage.axaml</DependentUpon>
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Assets\Users\" /> <Folder Include="Assets\Users\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="ViewModels\Pages\Schemas\SchemaSearchDetailsViewModel.cs" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,34 +1,8 @@
using Akavache; using Avalonia;
using Akavache.Sqlite3; using Avalonia.ReactiveUI;
using Avalonia;
using Avalonia.Controls.Templates;
using Flurl.Http.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Needlework.Net.Constants;
using Needlework.Net.Extensions;
using Needlework.Net.Services;
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.Schemas;
using Needlework.Net.ViewModels.Pages.Settings;
using Needlework.Net.ViewModels.Pages.WebSocket;
using Needlework.Net.Views.MainWindow;
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.Schemas;
using Needlework.Net.Views.Pages.Settings;
using Needlework.Net.Views.Pages.WebSocket;
using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome; using Projektanker.Icons.Avalonia.FontAwesome;
using Serilog;
using System; using System;
using System.IO;
namespace Needlework.Net; namespace Needlework.Net;
@@ -49,104 +23,20 @@ class Program
// Avalonia configuration, don't remove; also used by visual designer. // Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
{ {
IconProvider.Current.Register<FontAwesomeIconProvider>(); IconProvider.Current
.Register<FontAwesomeIconProvider>();
return AppBuilder.Configure(() => new App(BuildServices())) return AppBuilder.Configure<App>()
.UsePlatformDetect() .UsePlatformDetect()
.WithInterFont() .WithInterFont()
.With(new Win32PlatformOptions { CompositionMode = [Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition] }) .With(new Win32PlatformOptions { CompositionMode = [Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition] })
.With(new MacOSPlatformOptions { ShowInDock = true, }) .With(new MacOSPlatformOptions { ShowInDock = true, })
.LogToTrace(); .LogToTrace()
} .UseReactiveUI();
private static IServiceProvider BuildServices()
{
var builder = new ServiceCollection();
AddViews(builder);
AddViewModels(builder);
AddServices(builder);
return builder.BuildServiceProvider();
}
private static void AddViews(ServiceCollection builder)
{
var locator = new ViewLocator();
// MAIN WINDOW
locator.Register<NotificationViewModel>(() => new NotificationView());
locator.Register<ViewModels.MainWindow.SchemaSearchDetailsViewModel>(() => new Views.MainWindow.SchemaSearchDetailsView());
locator.Register<SchemaViewModel>(() => new SchemaView());
// ABOUT
locator.Register<AboutViewModel>(() => new AboutView());
// CONSOLE
locator.Register<ConsoleViewModel>(() => new ConsoleView());
// ENDPOINTS
locator.Register<EndpointListViewModel>(() => new EndpointListView());
locator.Register<EndpointSearchDetailsViewModel>(() => new EndpointSearchDetailsView());
locator.Register<EndpointsViewModel>(() => new EndpointsView());
locator.Register<EndpointTabItemContentViewModel>(() => new EndpointTabItemContentView());
locator.Register<PathOperationViewModel>(() => new PathOperationView());
locator.Register<PluginViewModel>(() => new PluginView());
locator.Register<PropertyClassViewModel>(() => new PropertyClassView());
// HOME
locator.Register<HomeViewModel>(() => new HomeView());
locator.Register<LibraryViewModel>(() => new LibraryView());
locator.Register<HextechDocsPostViewModel>(() => new HextechDocsPostView());
// SCHEMAS
locator.Register<SchemasViewModel>(() => new SchemasView());
locator.Register<ViewModels.Pages.Schemas.SchemaSearchDetailsViewModel>(() => new Views.Pages.Schemas.SchemaSearchDetailsView());
// WEBSOCKET
locator.Register<WebSocketViewModel>(() => new WebSocketView());
locator.Register<EventViewModel>(() => new EventView());
// SETTINGS
locator.Register<SettingsViewModel>(() => new SettingsView());
builder.AddSingleton<IDataTemplate>(locator);
}
private static void AddServices(ServiceCollection builder)
{
builder.AddSingleton<DialogService>();
builder.AddSingleton<DocumentService>();
builder.AddSingleton<NotificationService>();
builder.AddSingleton<SchemaPaneService>();
builder.AddSingleton<HextechDocsService>();
builder.AddSingleton<GithubService>();
builder.AddSingleton<IBlobCache>((_) =>
{
var appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
appDataFolder = string.IsNullOrEmpty(appDataFolder) ? "AppData" : appDataFolder;
var appFolder = Path.Join(appDataFolder, AppInfo.Name);
Directory.CreateDirectory(appFolder);
var filePath = Path.Join(appFolder, "cache.sqlite");
return new SqlRawPersistentBlobCache(filePath);
});
builder.AddSingleton<IFlurlClientCache>(new FlurlClientCache()
.Add(FlurlClientKeys.GithubClient, "https://api.github.com")
.Add(FlurlClientKeys.GithubUserContentClient, "https://raw.githubusercontent.com")
.Add(FlurlClientKeys.Client));
builder.AddLogging((builder) => builder.AddSerilog(EnableLoggerExtensions.Log(null)));
}
private static void AddViewModels(ServiceCollection builder)
{
builder.AddSingleton<MainWindowViewModel>();
builder.AddSingleton<PageBase, HomeViewModel>();
builder.AddSingleton<PageBase, ConsoleViewModel>();
builder.AddSingleton<PageBase, EndpointsViewModel>();
builder.AddSingleton<PageBase, WebSocketViewModel>();
builder.AddSingleton<PageBase, SchemasViewModel>();
builder.AddSingleton<PageBase, AboutViewModel>();
builder.AddSingleton<PageBase, SettingsViewModel>();
builder.AddSingleton<PageFactory>();
} }
private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e) private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{ {
File.WriteAllText($"Logs/fatal-{DateTime.Now:yyyyMMdd}.log", e.ExceptionObject.ToString()); Logger.LogFatal(e);
} }
} }

View File

@@ -1,42 +0,0 @@
using Akavache;
using Flurl.Http;
using Flurl.Http.Configuration;
using Needlework.Net.Constants;
using Needlework.Net.DataModels;
using Needlework.Net.Extensions;
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.Services
{
public class GithubService : IEnableLogger
{
private readonly IFlurlClient _githubClient;
private readonly IFlurlClient _githubUserContentClient;
private readonly IBlobCache _blobCache;
public GithubService(IBlobCache blobCache, IFlurlClientCache clients)
{
_githubClient = clients.Get(FlurlClientKeys.GithubClient);
_githubUserContentClient = clients.Get(FlurlClientKeys.GithubUserContentClient);
_blobCache = blobCache;
}
public async Task<GithubRelease> GetLatestReleaseAsync()
{
return await _blobCache.GetOrFetchObject(BlobCacheKeys.GithubLatestRelease, async () =>
{
this.Log()
.Debug("Downloading latest release info from GitHub...");
var release = await _githubClient
.Request("/repos/BlossomiShymae/Needlework.Net/releases/latest")
.WithHeader("User-Agent", $"{AppInfo.Name}/{AppInfo.Version}")
.GetJsonAsync<GithubRelease>();
return release;
}, DateTimeOffset.Now + Intervals.CheckForUpdates);
}
}
}

View File

@@ -1,49 +0,0 @@
using Akavache;
using AngleSharp;
using Needlework.Net.DataModels;
using Needlework.Net.Extensions;
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.Services
{
public class HextechDocsService : IEnableLogger
{
private readonly IBrowsingContext _context = BrowsingContext.New(Configuration.Default.WithDefaultLoader());
private readonly IBlobCache _blobCache;
public HextechDocsService(IBlobCache blobCache)
{
_blobCache = blobCache;
}
public async Task<List<HextechDocsPost>> GetPostsAsync()
{
return await _blobCache.GetOrFetchObject("HextechDocsPosts", async () =>
{
this.Log()
.Debug("Downloading HextechDocs posts...");
var document = await _context.OpenAsync("https://hextechdocs.dev/tag/lcu/");
var elements = document.QuerySelectorAll("article.post-card");
var posts = new List<HextechDocsPost>();
foreach (var element in elements)
{
var path = element.QuerySelector("a.post-card-content-link")!.GetAttribute("href")!;
var title = element.QuerySelector(".post-card-title")!.TextContent;
var excerpt = element.QuerySelector(".post-card-excerpt > p")!.TextContent;
var post = new HextechDocsPost()
{
Path = path,
Title = title,
Excerpt = excerpt,
};
posts.Add(post);
}
return posts;
}, DateTimeOffset.Now + TimeSpan.FromHours(12));
}
}
}

View File

@@ -13,8 +13,7 @@ namespace Needlework.Net.Services
public void Notify(string title, string message, InfoBarSeverity severity, TimeSpan? duration = null, string? url = null) public void Notify(string title, string message, InfoBarSeverity severity, TimeSpan? duration = null, string? url = null)
{ {
var notification = new Notification(title, message, severity, duration, url); _notificationSubject.OnNext(new Notification(title, message, severity, duration, url));
_notificationSubject.OnNext(notification);
} }
} }
} }

View File

@@ -1,20 +0,0 @@
using Needlework.Net.Models;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System;
using System.Reactive.Subjects;
namespace Needlework.Net.Services
{
public class SchemaPaneService
{
private readonly Subject<SchemaPaneItem> _schemaPaneItemsSubject = new();
public IObservable<SchemaPaneItem> SchemaPaneItems { get { return _schemaPaneItemsSubject; } }
public void Add(string key, Tab tab)
{
var schemaPaneItem = new SchemaPaneItem(key, tab);
_schemaPaneItemsSubject.OnNext(schemaPaneItem);
}
}
}

View File

@@ -1,33 +0,0 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace Needlework.Net
{
public class ViewLocator : IDataTemplate
{
private readonly Dictionary<Type, Func<Control>> _viewRegister = [];
public void Register<T>(Func<Control> viewActivator)
where T : INotifyPropertyChanged
{
_viewRegister[typeof(T)] = viewActivator;
}
public Control Build(object? data)
{
if (!_viewRegister.TryGetValue(data!.GetType(), out var activator))
{
throw new Exception("Data type has no registered view activator.");
}
var res = activator();
res!.DataContext = data;
return res;
}
public bool Match(object? data) => data is INotifyPropertyChanged;
}
}

View File

@@ -1,41 +1,59 @@
using Avalonia.Threading; using BlossomiShymae.Briar;
using CommunityToolkit.Mvvm.ComponentModel; using BlossomiShymae.Briar.Utils;
using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls;
using CommunityToolkit.Mvvm.Messaging; using Flurl.Http;
using Needlework.Net.Constants; using Flurl.Http.Configuration;
using Needlework.Net.Extensions; using Microsoft.Extensions.Logging;
using Needlework.Net.Helpers; using Needlework.Net.Models;
using Needlework.Net.Messages;
using Needlework.Net.Services; using Needlework.Net.Services;
using Needlework.Net.Views.MainWindow; using Needlework.Net.ViewModels.Pages;
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.Json;
using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.MainWindow; namespace Needlework.Net.ViewModels.MainWindow;
public partial class MainWindowViewModel public partial class MainWindowViewModel
: ObservableObject, IRecipient<OopsiesDialogRequestedMessage>, IEnableLogger : ReactiveObject, IScreen, IEnableLogger
{ {
private readonly DocumentService _documentService; private readonly IEnumerable<PageBase> _pages;
private readonly IFlurlClient _githubClient;
private readonly NotificationService _notificationService; private readonly NotificationService _notificationService;
private readonly DialogService _dialogService; private readonly DataSource _dataSource;
private readonly SchemaPaneService _schemaPaneService; private readonly IDisposable _checkForUpdatesDisposable;
public MainWindowViewModel(DialogService dialogService, DocumentService documentService, NotificationService notificationService, SchemaPaneService schemaPaneService) private readonly IDisposable _checkForSchemaVersionDisposable;
public MainWindowViewModel(IEnumerable<PageBase>? pages = null, IFlurlClientCache? clients = null, NotificationService? notificationService = null, DataSource? dataSource = null)
{ {
_dialogService = dialogService; _pages = pages ?? Locator.Current.GetServices<PageBase>();
_documentService = documentService; _githubClient = clients?.Get("GithubClient") ?? Locator.Current.GetService<IFlurlClientCache>()!.Get("GithubClient");
_notificationService = notificationService; _notificationService = notificationService ?? Locator.Current.GetService<NotificationService>()!;
_schemaPaneService = schemaPaneService; _dataSource = dataSource ?? Locator.Current.GetService<DataSource>()!;
PageItems = _pages
.OrderBy(p => p.Index)
.ThenBy(p => p.DisplayName)
.Select(ToNavigationViewItem)
.ToList();
SelectedPageItem = PageItems.First();
this.WhenAnyValue(x => x.SelectedPageItem)
.Subscribe(x => Router.Navigate.Execute((IRoutableViewModel)x.Tag!));
_notificationService.Notifications.Subscribe(async notification => _notificationService.Notifications.Subscribe(async notification =>
{ {
@@ -45,157 +63,100 @@ public partial class MainWindowViewModel
Notifications.Remove(vm); Notifications.Remove(vm);
}); });
_schemaPaneService.SchemaPaneItems.Subscribe(async item => CheckForUpdatesCommand.ThrownExceptions.Subscribe(ex =>
{ {
var document = item.Tab switch var message = "Failed to check for updates. Please check your internet connection or try again later.";
{ this.Log()
Pages.Endpoints.Tab.LCU => await documentService.GetLcuSchemaDocumentAsync(), .Error(ex, message);
Pages.Endpoints.Tab.GameClient => await documentService.GetLolClientDocumentAsync(), _notificationService.Notify("Needlework.Net", message, InfoBarSeverity.Error);
_ => throw new NotImplementedException() _checkForUpdatesDisposable?.Dispose();
};
var propertyClassViewModel = OpenApiHelpers.WalkSchema(document.OpenApiDocument.Components.Schemas[item.Key], document.OpenApiDocument);
var schemaViewModel = new SchemaViewModel(propertyClassViewModel);
if (Schemas.ToList().Find(schema => schema.Id == schemaViewModel.Id) == null)
{
Schemas.Add(schemaViewModel);
IsPaneOpen = true;
OpenSchemaPaneCommand.NotifyCanExecuteChanged();
CloseSchemaAllCommand.NotifyCanExecuteChanged();
}
}); });
WeakReferenceMessenger.Default.RegisterAll(this); _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);
} }
[ObservableProperty] [Reactive]
private bool _isPaneOpen; private RoutingState _router = new();
[ObservableProperty] [Reactive]
private ObservableCollection<SchemaViewModel> _schemas = [];
[ObservableProperty]
private SchemaViewModel? _selectedSchema;
[ObservableProperty]
private ObservableCollection<NotificationViewModel> _notifications = []; private ObservableCollection<NotificationViewModel> _notifications = [];
[ObservableProperty] [Reactive]
private SchemaSearchDetailsViewModel? _selectedSchemaSearchDetails; private NavigationViewItem _selectedPageItem;
public string AppName => AppInfo.Name; public List<NavigationViewItem> PageItems = [];
public string Title => $"{AppInfo.Name} {AppInfo.Version}"; public bool IsSchemaVersionChecked { get; private set; } = false;
partial void OnSelectedSchemaSearchDetailsChanged(SchemaSearchDetailsViewModel? value) public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
private NavigationViewItem ToNavigationViewItem(PageBase page) => new()
{ {
if (value == null) return; Content = page.DisplayName,
Task.Run(async () => Tag = page,
{ IconSource = new BitmapIconSource() { UriSource = new Uri($"avares://NeedleworkDotNet/Assets/Icons/{page.Icon}.png") }
var document = value.Tab switch
{
Pages.Endpoints.Tab.LCU => await _documentService.GetLcuSchemaDocumentAsync(),
Pages.Endpoints.Tab.GameClient => await _documentService.GetLolClientDocumentAsync(),
_ => throw new NotImplementedException()
}; };
var propertyClassViewModel = OpenApiHelpers.WalkSchema(document.OpenApiDocument.Components.Schemas[value.Key], document.OpenApiDocument);
var schemaViewModel = new SchemaViewModel(propertyClassViewModel);
Dispatcher.UIThread.Post(() =>
{
if (Schemas.ToList().Find(schema => schema.Id == schemaViewModel.Id) == null)
{
Schemas.Add(schemaViewModel);
IsPaneOpen = true;
OpenSchemaPaneCommand.NotifyCanExecuteChanged(); [ReactiveCommand]
CloseSchemaAllCommand.NotifyCanExecuteChanged(); private async Task CheckForUpdatesAsync()
} {
}); var release = await _githubClient
}); .Request("/repos/BlossomiShymae/Needlework.Net/releases/latest")
} .WithHeader("User-Agent", $"Needlework.Net/{Version}")
.GetJsonAsync<GithubRelease>();
partial void OnSelectedSchemaChanged(SchemaViewModel? value) if (release.IsLatest(Version))
{ {
CloseSchemaCommand.NotifyCanExecuteChanged(); this.Log()
} .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");
partial void OnSchemasChanged(ObservableCollection<SchemaViewModel> value) _checkForUpdatesDisposable?.Dispose();
{
if (!value.Any())
{
IsPaneOpen = false;
} }
} }
public async Task<IEnumerable<object>> PopulateAsync(string? searchText, CancellationToken cancellationToken)
[ReactiveCommand]
private async Task CheckForSchemaVersionAsync()
{ {
if (searchText == null) return []; if (!ProcessFinder.IsPortOpen()) return;
var lcuSchemaDocument = await _documentService.GetLcuSchemaDocumentAsync(cancellationToken); var lcuSchemaDocument = await _dataSource.GetLcuSchemaDocumentAsync();
var gameClientDocument = await _documentService.GetLolClientDocumentAsync(cancellationToken); var client = Connector.GetLcuHttpClientInstance();
var lcuResults = lcuSchemaDocument.OpenApiDocument.Components.Schemas.Keys.Where(key => key.Contains(searchText, StringComparison.OrdinalIgnoreCase)) var currentSemVer = lcuSchemaDocument.Info.Version.Split('.');
.Select(key => new SchemaSearchDetailsViewModel(key, Pages.Endpoints.Tab.LCU)); var systemBuild = await client.GetFromJsonAsync<SystemBuild>("/system/v1/builds") ?? throw new NullReferenceException();
var gameClientResults = gameClientDocument.OpenApiDocument.Components.Schemas.Keys.Where(key => key.Contains(searchText, StringComparison.OrdinalIgnoreCase)) var latestSemVer = systemBuild.Version.Split('.');
.Select(key => new SchemaSearchDetailsViewModel(key, Pages.Endpoints.Tab.GameClient));
return Enumerable.Concat(lcuResults, gameClientResults); if (!IsSchemaVersionChecked)
{
this.Log()
.Info("LCU Schema (current): {Version}", lcuSchemaDocument.Info.Version);
this.Log()
.Info("LCU Schema (latest): {Version}", systemBuild.Version);
IsSchemaVersionChecked = true;
} }
[RelayCommand(CanExecute = nameof(CanOpenSchemaPane))] bool isVersionMatching = currentSemVer[0] == latestSemVer[0] && currentSemVer[1] == latestSemVer[1]; // Compare major and minor versions
private void OpenSchemaPane() if (!isVersionMatching)
{ {
IsPaneOpen = !IsPaneOpen; 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 bool CanOpenSchemaPane()
{
return Schemas.Any();
}
[RelayCommand(CanExecute = nameof(CanCloseSchema))]
private void CloseSchema()
{
if (SelectedSchema is SchemaViewModel selection)
{
SelectedSchema = null;
Schemas = new ObservableCollection<SchemaViewModel>(Schemas.Where(schema => schema != selection));
OpenSchemaPaneCommand.NotifyCanExecuteChanged();
CloseSchemaCommand.NotifyCanExecuteChanged();
CloseSchemaAllCommand.NotifyCanExecuteChanged();
}
}
private bool CanCloseSchema()
{
return SelectedSchema != null;
}
[RelayCommand(CanExecute = nameof(CanCloseSchemaAll))]
private void CloseSchemaAll()
{
SelectedSchema = null;
Schemas = [];
OpenSchemaPaneCommand.NotifyCanExecuteChanged();
CloseSchemaCommand.NotifyCanExecuteChanged();
CloseSchemaAllCommand.NotifyCanExecuteChanged();
}
private bool CanCloseSchemaAll()
{
return Schemas.Any();
}
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process() { StartInfo = new ProcessStartInfo(url) { UseShellExecute = true } };
process.Start();
}
public void Receive(OopsiesDialogRequestedMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(async () => await _dialogService.ShowAsync<OopsiesDialog>(message.Value));
} }
} }

View File

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

View File

@@ -1,25 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.ViewModels.Pages.Endpoints;
namespace Needlework.Net.ViewModels.MainWindow
{
public partial class SchemaSearchDetailsViewModel : ObservableObject
{
public SchemaSearchDetailsViewModel(string key, Tab tab)
{
Tab = tab;
Key = key;
}
public string Key { get; }
public Tab Tab { get; }
public string Document => Tab switch
{
Tab.LCU => "LCU",
Tab.GameClient => "Game Client",
_ => throw new System.NotImplementedException()
};
}
}

View File

@@ -1,22 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System.Collections.Generic;
namespace Needlework.Net.ViewModels.MainWindow
{
public partial class SchemaViewModel : ObservableObject
{
public SchemaViewModel(PropertyClassViewModel vm)
{
Id = vm.Id;
PropertyFields = vm.PropertyFields;
PropertyEnums = vm.PropertyEnums;
}
public string Id { get; }
public List<PropertyFieldViewModel> PropertyFields { get; } = [];
public List<PropertyEnumViewModel> PropertyEnums { get; } = [];
}
}

View File

@@ -1,24 +1,16 @@
using CommunityToolkit.Mvvm.Input; using ReactiveUI;
using System.Diagnostics; using Splat;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.About; namespace Needlework.Net.ViewModels.Pages.About;
public partial class AboutViewModel : PageBase public partial class AboutViewModel : PageBase
{ {
public AboutViewModel() : base("About", "fa-solid fa-circle-info") public AboutViewModel(IScreen? screen = null) : base("About", "info-circle")
{ {
HostScreen = screen ?? Locator.Current.GetService<IScreen>()!;
} }
public override Task InitializeAsync() public override string? UrlPathSegment => "about";
{
return Task.CompletedTask;
}
[RelayCommand] public override IScreen HostScreen { get; }
private void OpenUrl(string url)
{
var process = new Process() { StartInfo = new ProcessStartInfo(url) { UseShellExecute = true } };
process.Start();
}
} }

View File

@@ -1,44 +1,65 @@
using Avalonia.Threading; using DynamicData;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Services;
using Needlework.Net.ViewModels.Shared; using Needlework.Net.ViewModels.Shared;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Splat;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Console; namespace Needlework.Net.ViewModels.Pages.Console;
public partial class ConsoleViewModel : PageBase public partial class ConsoleViewModel : PageBase
{ {
private readonly DocumentService _documentService; private readonly DataSource _dataSource;
public ConsoleViewModel(DocumentService documentService, NotificationService notificationService) : base("Console", "fa-solid fa-terminal") public ConsoleViewModel(IScreen? screen = null, DataSource? dataSource = null) : base("Console", "terminal", -200)
{ {
_request = new(notificationService, Endpoints.Tab.LCU); _dataSource = dataSource ?? Locator.Current.GetService<DataSource>()!;
_documentService = documentService; _request = new(Endpoints.Tab.LCU);
}
public List<string> RequestMethods { get; } = ["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE"]; HostScreen = screen ?? Locator.Current.GetService<IScreen>()!;
public List<string> RequestPaths { get; } = []; GetRequestPathsCommand.Subscribe(paths =>
[ObservableProperty] private bool _isBusy = true;
[ObservableProperty] private RequestViewModel _request;
public override async Task InitializeAsync()
{
var document = await _documentService.GetLcuSchemaDocumentAsync();
Dispatcher.UIThread.Invoke(() =>
{ {
RequestPaths.Clear(); RequestPaths.Clear();
RequestPaths.AddRange(document.Paths); RequestPaths.AddRange(paths);
});
IsBusy = false; IsBusy = false;
});
GetRequestPathsCommand.ThrownExceptions.Subscribe(ex =>
{
this.Log()
.Error(ex, "Failed to load request paths from LCU Schema document.");
IsBusy = false;
});
} }
[RelayCommand] public ObservableCollection<string> RequestMethods { get; } = ["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE"];
private async Task SendRequest()
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(); await Request.ExecuteAsync();
} }

View File

@@ -1,68 +0,0 @@
using Avalonia;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Models;
using Needlework.Net.Services;
using System;
using System.Collections.ObjectModel;
using System.Linq;
namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class EndpointListViewModel : ObservableObject
{
private readonly Document _document;
private readonly Tab _tab;
private readonly Action<ObservableObject> _onClicked;
private readonly ObservableCollection<string> _plugins;
private readonly NotificationService _notificationService;
public EndpointListViewModel(NotificationService notificationService, ObservableCollection<string> plugins, Action<ObservableObject> onClicked, Models.Document document, Tab tab)
{
_plugins = new ObservableCollection<string>(plugins);
_document = document;
_tab = tab;
_onClicked = onClicked;
_notificationService = notificationService;
Plugins = EndpointSearchDetails = new ObservableCollection<EndpointSearchDetailsViewModel>(plugins.Select(plugin => new EndpointSearchDetailsViewModel(notificationService, document, tab, onClicked, plugin)));
}
public ObservableCollection<EndpointSearchDetailsViewModel> Plugins { get; }
[ObservableProperty]
private ObservableCollection<EndpointSearchDetailsViewModel> _endpointSearchDetails = [];
[ObservableProperty]
private string _search = string.Empty;
[ObservableProperty]
private Vector _offset = new();
partial void OnSearchChanged(string value)
{
EndpointSearchDetails.Clear();
if (!string.IsNullOrEmpty(Search))
{
EndpointSearchDetails.AddRange(_plugins.Where(plugin => plugin.Contains(value, StringComparison.InvariantCultureIgnoreCase))
.Select(plugin => new EndpointSearchDetailsViewModel(_notificationService, _document, _tab, _onClicked, plugin)));
}
else
{
EndpointSearchDetails.AddRange(
_plugins.Select(plugin => new EndpointSearchDetailsViewModel(_notificationService, _document, _tab, _onClicked, plugin)));
}
}
[RelayCommand]
private void OpenEndpoint(string? value)
{
if (string.IsNullOrEmpty(value)) return;
_onClicked.Invoke(new PluginViewModel(_notificationService, value, _document, _tab));
}
}

View File

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

View File

@@ -1,35 +1,35 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Input; using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Needlework.Net.ViewModels.Pages.Endpoints; namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class EndpointTabItemContentViewModel : ObservableObject public partial class EndpointTabItemContentViewModel : ReactiveObject
{ {
private readonly Action<string?, Guid> _onEndpointNavigation; private readonly Action<string?, Guid> _onEndpointNavigation;
private readonly Tab _tab; private readonly Tab _tab;
public EndpointTabItemContentViewModel(Services.NotificationService notificationService, ObservableCollection<string> plugins, Action<string?, Guid> onEndpointNavigation, IAsyncRelayCommand addEndpointCommand, Models.Document document, Tab tab) public EndpointTabItemContentViewModel(ObservableCollection<string> plugins, Action<string?, Guid> onEndpointNavigation, Models.Document document, Tab tab)
{ {
_activeViewModel = _endpointsViewModel = new EndpointListViewModel(notificationService, new ObservableCollection<string>(plugins), OnClicked, document, tab); _activeViewModel = _endpointsViewModel = new EndpointTabListViewModel(plugins, OnClicked, document, tab);
_onEndpointNavigation = onEndpointNavigation; _onEndpointNavigation = onEndpointNavigation;
_tab = tab; _tab = tab;
_title = GetTitle(tab); _title = GetTitle(tab);
AddEndpointCommand = addEndpointCommand;
} }
public Guid Guid { get; } = Guid.NewGuid(); public Guid Guid { get; } = Guid.NewGuid();
public IAsyncRelayCommand AddEndpointCommand { get; } [Reactive]
private ReactiveObject _activeViewModel;
[ObservableProperty] private ObservableObject _activeViewModel; [Reactive]
private ReactiveObject _endpointsViewModel;
[ObservableProperty] private ObservableObject _endpointsViewModel; [Reactive]
private string _title;
[ObservableProperty] private string _title;
private string GetTitle(Tab tab) private string GetTitle(Tab tab)
{ {
@@ -41,7 +41,7 @@ public partial class EndpointTabItemContentViewModel : ObservableObject
}; };
} }
private void OnClicked(ObservableObject viewModel) private void OnClicked(ReactiveObject viewModel)
{ {
ActiveViewModel = viewModel; ActiveViewModel = viewModel;
if (viewModel is PluginViewModel endpoint) if (viewModel is PluginViewModel endpoint)

View File

@@ -1,12 +1,29 @@
using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Controls; using ReactiveUI;
using ReactiveUI.SourceGenerators;
namespace Needlework.Net.ViewModels.Pages.Endpoints; namespace Needlework.Net.ViewModels.Pages.Endpoints
public partial class EndpointTabItemViewModel : ObservableObject
{ {
[ObservableProperty] private string _header = string.Empty; public partial class EndpointTabItemViewModel : ReactiveObject
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 EndpointTabItemViewModel(EndpointTabItemContentViewModel content, string? header = null, IconSource? iconSource = null, bool? selected = null)
public required EndpointTabItemContentViewModel Content { get; init; } {
_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,9 +1,8 @@
using Avalonia.Threading; using DynamicData;
using AvaloniaEdit.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Models; using Needlework.Net.Models;
using Needlework.Net.Services; using ReactiveUI;
using ReactiveUI.SourceGenerators;
using Splat;
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -16,57 +15,62 @@ public enum Tab
GameClient GameClient
} }
public partial class EndpointsViewModel : PageBase public partial class EndpointsViewModel : PageBase, IEnableLogger
{ {
private readonly DocumentService _documentService; public record Endpoint(Document Document, Tab Tab);
private readonly NotificationService _notificationService; private readonly DataSource _dataSource;
public EndpointsViewModel(DocumentService documentService, NotificationService notificationService) : base("Endpoints", "fa-solid fa-rectangle-list") public EndpointsViewModel(IScreen? screen = null, DataSource? dataSource = null) : base("Endpoints", "list-alt", -500)
{ {
_documentService = documentService; _dataSource = dataSource ?? Locator.Current.GetService<DataSource>()!;
_notificationService = notificationService;
}
public ObservableCollection<string> Plugins { get; } = []; HostScreen = screen ?? Locator.Current.GetService<IScreen>()!;
public ObservableCollection<EndpointTabItemViewModel> Endpoints { get; } = []; GetEndpointCommand.Subscribe(endpoint =>
[ObservableProperty] private bool _isBusy = true;
public override async Task InitializeAsync()
{
await AddEndpoint(Tab.LCU);
IsBusy = false;
}
[RelayCommand]
private async Task AddEndpoint(Tab tab)
{
Document document = tab switch
{
Tab.LCU => await _documentService.GetLcuSchemaDocumentAsync(),
Tab.GameClient => await _documentService.GetLolClientDocumentAsync(),
_ => throw new NotImplementedException(),
};
await Dispatcher.UIThread.InvokeAsync(() =>
{ {
Plugins.Clear(); Plugins.Clear();
Plugins.AddRange(document.Plugins.Keys); Plugins.AddRange(endpoint.Document.Plugins.Keys);
var vm = new EndpointTabItemContentViewModel(_notificationService, Plugins, OnEndpointNavigation, AddEndpointCommand, document, tab);
Endpoints.Add(new() var vm = new EndpointTabItemContentViewModel(Plugins, OnEndpointNavigation, endpoint.Document, endpoint.Tab);
EndpointTabItems.Add(new(vm, vm.Title, null, true));
IsBusy = false;
});
GetEndpointCommand.ThrownExceptions.Subscribe(ex =>
{ {
Content = vm, this.Log()
Header = vm.Title, .Error(ex, "Failed to get endpoint.");
Selected = true 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) private void OnEndpointNavigation(string? title, Guid guid)
{ {
foreach (var endpoint in Endpoints) foreach (var endpoint in EndpointTabItems)
{ {
if (endpoint.Content.Guid.Equals(guid)) if (endpoint.Content.Guid.Equals(guid))
{ {

View File

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

View File

@@ -1,8 +1,9 @@
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 ParameterViewModel(string name, string type, bool isRequired, string? value = null) public ParameterViewModel(string name, string type, bool isRequired, string? value = null)
{ {
@@ -18,7 +19,6 @@ public partial class ParameterViewModel : ObservableObject
public bool IsRequired { get; } public bool IsRequired { get; }
[ObservableProperty] [Reactive]
private string? _value = null; private string? _value;
} }

View File

@@ -1,23 +1,22 @@
using CommunityToolkit.Mvvm.ComponentModel; using Needlework.Net.Models;
using CommunityToolkit.Mvvm.Input;
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(Services.NotificationService notificationService, PathOperation pathOperation, Document document, Tab tab) public PathOperationViewModel(PathOperation pathOperation, Document document, Tab tab)
{ {
Path = pathOperation.Path; Path = pathOperation.Path;
Operation = new OperationViewModel(pathOperation.Operation, document); Operation = new OperationViewModel(pathOperation.Operation, document);
Request = new(() => new RequestViewModel(notificationService, tab) Request = new(() => new RequestViewModel(tab)
{ {
Method = pathOperation.Method.ToUpper(), Method = pathOperation.Method.ToUpper()
RequestDocument = new(Operation.RequestTemplate ?? string.Empty)
}); });
Url = $"https://swagger.dysolix.dev/lcu/#/{Uri.EscapeDataString(pathOperation.Tag)}/{pathOperation.Operation.OperationId}"; Url = $"https://swagger.dysolix.dev/lcu/#/{Uri.EscapeDataString(pathOperation.Tag)}/{pathOperation.Operation.OperationId}";
Markdown = $"[{pathOperation.Method.ToUpper()} {Path}]({Url})"; Markdown = $"[{pathOperation.Method.ToUpper()} {Path}]({Url})";
@@ -31,11 +30,13 @@ public partial class PathOperationViewModel : ObservableObject
public string Markdown { get; } public string Markdown { get; }
[ObservableProperty] private bool _isBusy; [Reactive]
private bool _isBusy;
[ObservableProperty] private Lazy<RequestViewModel> _request; [Reactive]
private Lazy<RequestViewModel> _request;
[RelayCommand] [ReactiveCommand]
private async Task SendRequest() private async Task SendRequest()
{ {
var sb = new StringBuilder(Path); var sb = new StringBuilder(Path);
@@ -59,13 +60,13 @@ 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);
} }
[RelayCommand] [ReactiveCommand]
private void CopyMarkdown() private void CopyMarkdown()
{ {
App.MainWindow?.Clipboard?.SetTextAsync(Markdown); App.MainWindow?.Clipboard?.SetTextAsync(Markdown);

View File

@@ -1,55 +1,59 @@
using Avalonia; using DynamicData;
using AvaloniaEdit.Utils; using ReactiveUI;
using CommunityToolkit.Mvvm.ComponentModel; using ReactiveUI.SourceGenerators;
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.Reactive.Subjects;
namespace Needlework.Net.ViewModels.Pages.Endpoints; namespace Needlework.Net.ViewModels.Pages.Endpoints;
public partial class PluginViewModel : ObservableObject public partial class PluginViewModel : ReactiveObject
{ {
public PluginViewModel(Services.NotificationService notificationService, string endpoint, Models.Document document, Tab tab) private readonly Subject<string> _pathOperationSelectedSubject = new();
public PluginViewModel(string endpoint, Models.Document document, Tab tab)
{ {
Endpoint = endpoint; Endpoint = endpoint;
PathOperations = document.Plugins[endpoint].Select(x => new PathOperationViewModel(notificationService, x, document, tab)).ToList(); PathOperations = [.. document.Plugins[endpoint].Select(x => new PathOperationViewModel(x, document, tab))];
FilteredPathOperations = new ObservableCollection<PathOperationViewModel>(PathOperations); 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 Endpoint { get; }
public string Title => Endpoint; public string Title => Endpoint;
public List<PathOperationViewModel> PathOperations { get; } public ObservableCollection<PathOperationViewModel> PathOperations { get; } = [];
[ObservableProperty] [Reactive]
private ObservableCollection<PathOperationViewModel> _filteredPathOperations; private ObservableCollection<PathOperationViewModel> _filteredPathOperations = [];
[ObservableProperty] [Reactive]
private PathOperationViewModel? _selectedPathOperation; private PathOperationViewModel? _selectedPathOperation;
[ObservableProperty] [Reactive]
private string? _search; private string? _search;
[ObservableProperty]
private Vector _offset = new();
[ObservableProperty]
private Vector _paramsOffset = new();
[ObservableProperty]
private Vector _schemasOffset = new();
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)));
}
} }

View File

@@ -1,13 +1,13 @@
using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Needlework.Net.Helpers; 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 PropertyClassViewModel(string id, IDictionary<string, OpenApiSchema> properties, IList<IOpenApiAny> enumValue) public PropertyClassViewModel(string id, IDictionary<string, OpenApiSchema> properties, IList<IOpenApiAny> enumValue)
{ {
@@ -15,7 +15,7 @@ public class PropertyClassViewModel : ObservableObject
List<PropertyEnumViewModel> propertyEnums = []; List<PropertyEnumViewModel> propertyEnums = [];
foreach ((var propertyName, var propertySchema) in properties) foreach ((var propertyName, var propertySchema) in properties)
{ {
var type = OpenApiHelpers.GetSchemaType(propertySchema); var type = OperationViewModel.GetSchemaType(propertySchema);
var field = new PropertyFieldViewModel(propertyName, type); var field = new PropertyFieldViewModel(propertyName, type);
propertyFields.Add(field); propertyFields.Add(field);
} }
@@ -24,14 +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 string Id { get; }
public List<PropertyFieldViewModel> PropertyFields { get; } = []; public ObservableCollection<PropertyFieldViewModel> PropertyFields { get; } = [];
public List<PropertyEnumViewModel> PropertyEnums { get; } = []; public ObservableCollection<PropertyEnumViewModel> PropertyEnums { get; } = [];
} }

View File

@@ -10,7 +10,9 @@ public class PropertyEnumViewModel
{ {
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 Type { get; } = "Enum";
public string Values { get; } public string Values { get; }
} }

View File

@@ -11,4 +11,5 @@ public class PropertyFieldViewModel
public string Name { get; } public string Name { get; }
public string Type { get; } public string Type { get; }
} }

View File

@@ -1,9 +1,10 @@
using BlossomiShymae.Briar.Utils; 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
{ {
public ResponseViewModel(string path) public ResponseViewModel(string path)
{ {
@@ -19,22 +20,22 @@ public partial class ResponseViewModel : ObservableObject
} }
} }
[ObservableProperty] [Reactive]
private string? _path; private string? _path;
[ObservableProperty] [Reactive]
private string? _status; private string? _status;
[ObservableProperty] [Reactive]
private string? _authentication; private string? _authentication;
[ObservableProperty] [Reactive]
private string? _username; private string? _username;
[ObservableProperty] [Reactive]
private string? _password; private string? _password;
[ObservableProperty] [Reactive]
private string? _authorization; private string? _authorization;
private static ProcessInfo? GetProcessInfo() private static ProcessInfo? GetProcessInfo()

View File

@@ -1,15 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.DataModels;
namespace Needlework.Net.ViewModels.Pages.Home
{
public partial class HextechDocsPostViewModel : ObservableObject
{
public HextechDocsPostViewModel(HextechDocsPost hextechDocsPost)
{
HextechDocsPost = hextechDocsPost;
}
public HextechDocsPost HextechDocsPost { get; }
}
}

View File

@@ -1,80 +1,26 @@
using Avalonia; using Avalonia.Platform;
using Avalonia.Platform;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Needlework.Net.Extensions;
using Needlework.Net.Models; using Needlework.Net.Models;
using Needlework.Net.Services; using ReactiveUI;
using Splat;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Home; namespace Needlework.Net.ViewModels.Pages.Home;
public partial class HomeViewModel : PageBase, IEnableLogger public partial class HomeViewModel : PageBase
{ {
private readonly HextechDocsService _hextechDocsService; public HomeViewModel(IScreen? screen = null) : base("Home", "home", int.MinValue)
private readonly IDisposable _carouselNextDisposable;
public HomeViewModel(HextechDocsService hextechDocsService) : base("Home", "fa-solid fa-house")
{ {
_hextechDocsService = hextechDocsService; Libraries = JsonSerializer.Deserialize<List<Library>>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json")))!
_carouselNextDisposable = Observable.Timer(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5))
.Select(time => Unit.Default)
.Subscribe(_ =>
{
if (SelectedHextechDocsPost is HextechDocsPostViewModel vm)
{
var index = HextechDocsPosts.IndexOf(vm);
if (index == HextechDocsPosts.Count - 1)
{
index = 0;
}
else
{
index += 1;
}
SelectedHextechDocsPost = HextechDocsPosts.ElementAt(index);
}
});
}
public List<LibraryViewModel> Libraries { get; } = JsonSerializer.Deserialize<List<Library>>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json")))
!.Where(library => library.Tags.Contains("lcu") || library.Tags.Contains("ingame"))
.Select(library => new LibraryViewModel(library)) .Select(library => new LibraryViewModel(library))
.ToList(); .ToList();
HostScreen = screen ?? Locator.Current.GetService<IScreen>()!;
[ObservableProperty]
private Vector _librariesOffset = new();
[ObservableProperty]
private List<HextechDocsPostViewModel> _hextechDocsPosts = [];
[ObservableProperty]
private HextechDocsPostViewModel? _selectedHextechDocsPost;
public override async Task InitializeAsync()
{
try
{
var posts = await _hextechDocsService.GetPostsAsync();
var hextechDocsPosts = posts.Select(post => new HextechDocsPostViewModel(post)).ToList();
Dispatcher.UIThread.Invoke(() =>
{
HextechDocsPosts = hextechDocsPosts;
SelectedHextechDocsPost = HextechDocsPosts.First();
});
}
catch (Exception ex)
{
this.Log()
.Error(ex, "Failed to get posts from HextechDocs.");
}
} }
public List<LibraryViewModel> Libraries { get; }
public override string? UrlPathSegment => "home";
public override IScreen HostScreen { get; }
} }

View File

@@ -1,9 +1,9 @@
using CommunityToolkit.Mvvm.ComponentModel; using Needlework.Net.Models;
using Needlework.Net.Models; using ReactiveUI;
namespace Needlework.Net.ViewModels.Pages.Home namespace Needlework.Net.ViewModels.Pages.Home
{ {
public partial class LibraryViewModel : ObservableObject public class LibraryViewModel : ReactiveObject
{ {
public LibraryViewModel(Library library) public LibraryViewModel(Library library)
{ {

View File

@@ -1,14 +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) : ObservableValidator public abstract partial class PageBase(string displayName, string icon, int index = 0) : ReactiveObject, IRoutableViewModel
{ {
public string DisplayName { get; } = displayName; public string DisplayName { get; } = displayName;
public string Icon { get; } = icon; public string Icon { get; } = icon;
public abstract Task InitializeAsync(); public int Index { get; } = index;
public abstract string? UrlPathSegment { get; }
public abstract IScreen HostScreen { get; }
} }

View File

@@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages
{
public class PageFactory
{
private readonly IEnumerable<PageBase> _pages;
public PageFactory(IEnumerable<PageBase> pages)
{
_pages = pages;
}
public PageBase GetPage<T>() where T : PageBase
{
var page = _pages.Where(page => typeof(T) == page.GetType())
.FirstOrDefault() ?? throw new NotSupportedException(typeof(T).FullName);
Task.Run(page.InitializeAsync);
return page;
}
}
}

View File

@@ -1,38 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Needlework.Net.Services;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System;
namespace Needlework.Net.ViewModels.Pages.Schemas
{
public partial class SchemaSearchDetailsViewModel : ObservableObject
{
private readonly SchemaPaneService _schemaPaneService;
public SchemaSearchDetailsViewModel(Tab tab, PropertyClassViewModel vm, SchemaPaneService schemaPaneService)
{
_schemaPaneService = schemaPaneService;
Tab = tab;
Id = vm.Id;
}
public string Id { get; }
public Tab Tab { get; }
public string Document => Tab switch
{
Tab.LCU => "LCU",
Tab.GameClient => "Game Client",
_ => throw new NotImplementedException()
};
[RelayCommand]
private void Display()
{
_schemaPaneService.Add(Id, Tab);
}
}
}

View File

@@ -1,77 +0,0 @@
using Avalonia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using DebounceThrottle;
using Needlework.Net.Helpers;
using Needlework.Net.Services;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Schemas
{
public partial class SchemasViewModel : PageBase
{
private readonly DebounceDispatcher _debounceDispatcher = new(TimeSpan.FromMilliseconds(500));
private readonly DocumentService _documentService;
private readonly SchemaPaneService _schemaPaneService;
private List<SchemaSearchDetailsViewModel> _schemas = [];
public SchemasViewModel(DocumentService documentService, SchemaPaneService schemaPaneService) : base("Schemas", "fa-solid fa-file-lines")
{
_documentService = documentService;
_schemaPaneService = schemaPaneService;
}
[ObservableProperty]
private bool _isBusy = true;
[ObservableProperty]
private string? _search;
[ObservableProperty]
private List<SchemaSearchDetailsViewModel> _schemaItems = [];
[ObservableProperty]
private Vector _offset = new();
partial void OnSearchChanged(string? value)
{
_debounceDispatcher.Debounce(() =>
{
if (string.IsNullOrEmpty(value))
{
Dispatcher.UIThread.Invoke(() =>
{
SchemaItems = _schemas.ToList();
});
return;
}
var items = _schemas.Where(schema => schema.Id.Contains(value, StringComparison.OrdinalIgnoreCase))
.ToList();
Dispatcher.UIThread.Invoke(() => { SchemaItems = items; });
});
}
public override async Task InitializeAsync()
{
var lcuSchemaDocument = await _documentService.GetLcuSchemaDocumentAsync();
var lolClientDocument = await _documentService.GetLolClientDocumentAsync();
Dispatcher.UIThread.Invoke(() =>
{
var schemas = Enumerable.Concat(
lcuSchemaDocument.OpenApiDocument.Components.Schemas.Values.Select(schema => new SchemaSearchDetailsViewModel(Tab.LCU, OpenApiHelpers.WalkSchema(schema, lcuSchemaDocument.OpenApiDocument), _schemaPaneService)),
lolClientDocument.OpenApiDocument.Components.Schemas.Values.Select(schema => new SchemaSearchDetailsViewModel(Tab.GameClient, OpenApiHelpers.WalkSchema(schema, lolClientDocument.OpenApiDocument), _schemaPaneService))
).ToList();
_schemas = schemas;
SchemaItems = schemas.ToList();
IsBusy = false;
});
}
}
}

View File

@@ -1,197 +0,0 @@
using Akavache;
using Avalonia.Threading;
using BlossomiShymae.Briar;
using BlossomiShymae.Briar.Utils;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Needlework.Net.Constants;
using Needlework.Net.DataModels;
using Needlework.Net.Extensions;
using Needlework.Net.Models;
using Needlework.Net.Services;
using System;
using System.Net.Http.Json;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages.Settings
{
public partial class SettingsViewModel : PageBase, IEnableLogger
{
private readonly IBlobCache _blobCache;
private readonly IDisposable _checkForUpdatesDisposable;
private readonly IDisposable _checkForSchemaVersionDisposable;
private readonly GithubService _githubService;
private readonly DocumentService _documentService;
private readonly NotificationService _notificationService;
private readonly TaskCompletionSource<bool> _initializeTaskCompletionSource = new();
public SettingsViewModel(IBlobCache blobCache, GithubService githubService, DocumentService documentService, NotificationService notificationService) : base("Settings", "fa-solid fa-gear")
{
_blobCache = blobCache;
_githubService = githubService;
_documentService = documentService;
_notificationService = notificationService;
_checkForUpdatesDisposable = Observable.Timer(TimeSpan.Zero, Intervals.CheckForUpdates)
.Select(time => Unit.Default)
.Subscribe(async _ =>
{
try
{
await _initializeTaskCompletionSource.Task;
if (AppSettings!.IsCheckForUpdates)
{
await CheckForUpdatesAsync();
}
}
catch (Exception ex)
{
var message = "Failed to check for updates. Please check your internet connection or try again later.";
this.Log()
.Error(ex, message);
_notificationService.Notify(AppInfo.Name, message, InfoBarSeverity.Error);
_checkForUpdatesDisposable?.Dispose();
}
});
_checkForSchemaVersionDisposable = Observable.Timer(TimeSpan.Zero, TimeSpan.FromMinutes(5))
.Select(time => Unit.Default)
.Subscribe(async _ =>
{
try
{
await _initializeTaskCompletionSource.Task;
if (AppSettings!.IsCheckForSchema)
{
await CheckForSchemaVersionAsync();
}
}
catch (Exception 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(AppInfo.Name, message, InfoBarSeverity.Error);
_checkForSchemaVersionDisposable?.Dispose();
}
});
}
[ObservableProperty]
private bool _isBusy = true;
[ObservableProperty]
private AppSettings? _appSettings;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(UpdateCheckTitle), nameof(UpdateCheckIconValue), nameof(UpdateCheckLastChecked))]
private Guid _upToDateGuid = Guid.Empty;
public bool IsUpToDate { get; private set; }
public bool IsSchemaVersionChecked { get; private set; }
public string UpdateCheckTitle => IsUpToDate switch
{
true => "You're up to date",
false => "You're out of date"
};
public string UpdateCheckIconValue => IsUpToDate switch
{
true => "fa-heart-circle-check",
false => "fa-heart-circle-exclamation"
};
public string UpdateCheckLastChecked => $"Last checked: {DateTime.Now:dddd}, {DateTime.Now:T}";
partial void OnAppSettingsChanged(AppSettings? value)
{
if (AppSettings is AppSettings appSettings)
{
_blobCache.InsertObject(BlobCacheKeys.AppSettings, appSettings);
}
}
public override async Task InitializeAsync()
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
try
{
AppSettings = await _blobCache.GetObject<AppSettings>(BlobCacheKeys.AppSettings);
}
catch (Exception ex)
{
this.Log()
.Warning(ex, "Failed to get application settings.");
AppSettings = new();
}
finally
{
AppSettings!.PropertyChanged += (s, e) => OnAppSettingsChanged((AppSettings?)s);
IsBusy = false;
_initializeTaskCompletionSource.SetResult(true);
}
});
}
[RelayCommand]
private async Task CheckForUpdatesAsync()
{
var release = await _githubService.GetLatestReleaseAsync();
if (release.IsLatest(AppInfo.Version))
{
this.Log()
.Information("New version available: {TagName}", release.TagName);
_notificationService.Notify(AppInfo.Name, $"New version available: {release.TagName}", InfoBarSeverity.Informational, null, "https://github.com/BlossomiShymae/Needlework.Net/releases/latest");
_checkForUpdatesDisposable?.Dispose();
IsUpToDate = false;
}
else
{
IsUpToDate = true;
}
UpToDateGuid = Guid.NewGuid();
}
private async Task CheckForSchemaVersionAsync()
{
if (!ProcessFinder.IsPortOpen()) return;
var lcuSchemaDocument = await _documentService.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('.');
if (!IsSchemaVersionChecked)
{
this.Log()
.Information("LCU Schema (current): {Version}", lcuSchemaDocument.Info.Version);
this.Log()
.Information("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)
{
this.Log()
.Warning("LCU Schema outdated: Current {CurrentVersion}, Latest {LatestVersion}", lcuSchemaDocument.Info.Version, systemBuild.Version);
_notificationService.Notify(AppInfo.Name, $"LCU Schema is 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();
}
}
}
}

View File

@@ -1,23 +1,21 @@
using BlossomiShymae.Briar.WebSocket.Events; 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 Type { get; }
public string Uri { get; }
public string Key => $"{Time} {Type} {Uri}";
public EventViewModel(EventData eventData) public EventViewModel(EventData eventData)
{ {
Time = $"{DateTime.Now:HH:mm:ss.fff}"; Time = $"{DateTime.Now:HH:mm:ss.fff}";
Type = eventData?.EventType?.ToUpper() ?? string.Empty; Type = eventData?.EventType?.ToUpper() ?? string.Empty;
Uri = eventData?.Uri ?? string.Empty; Uri = eventData?.Uri ?? string.Empty;
} }
public string Time { get; }
public string Type { get; }
public string Uri { get; }
public string Key => $"{Time} {Type} {Uri}";
} }

View File

@@ -1,23 +1,15 @@
using Avalonia; using BlossomiShymae.Briar;
using Avalonia.Collections;
using AvaloniaEdit.Document;
using BlossomiShymae.Briar;
using BlossomiShymae.Briar.WebSocket.Events; using BlossomiShymae.Briar.WebSocket.Events;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Flurl.Http; using Flurl.Http;
using Flurl.Http.Configuration; using Flurl.Http.Configuration;
using Needlework.Net.Constants; using Microsoft.Extensions.Logging;
using Needlework.Net.Extensions; using ReactiveUI;
using Needlework.Net.Messages; using ReactiveUI.SourceGenerators;
using Needlework.Net.Services; 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;
@@ -29,82 +21,73 @@ public partial class WebSocketViewModel : PageBase, IEnableLogger
{ {
private Dictionary<string, EventMessage> _events = []; private Dictionary<string, EventMessage> _events = [];
private readonly IFlurlClient _githubUserContentClient;
private readonly NotificationService _notificationService;
private readonly object _tokenLock = new(); private readonly object _tokenLock = new();
public WebSocketViewModel(IFlurlClientCache clients, NotificationService notificationService) : base("Event Viewer", "fa-solid fa-plug") private readonly IFlurlClient _githubUserContentClient;
{
_githubUserContentClient = clients.Get(FlurlClientKeys.GithubUserContentClient);
_notificationService = notificationService;
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog)); // 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 ObservableCollection<EventViewModel> EventLog { get; } = []; public override string? UrlPathSegment => "websocket";
public SemaphoreSlim EventLogLock { get; } = new(1, 1); 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 = [];
public CancellationTokenSource TokenSource { get; set; } = new(); public SemaphoreSlim EventLogLock { get; } = new(1, 1);
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))]; [Reactive]
public ObservableCollection<EventViewModel> EventLog { get; } = [];
[ObservableProperty] [Reactive]
private Vector _eventLogOffset = new();
[NotifyPropertyChangedFor(nameof(FilteredEventLog))]
[ObservableProperty]
private string _search = string.Empty; private string _search = string.Empty;
[ObservableProperty] [Reactive]
private bool _isAttach = true; private bool _isAttach = true;
[ObservableProperty] [Reactive]
private bool _isTail = false; private bool _isTail;
[ObservableProperty] [Reactive]
private EventViewModel? _selectedEventLog = null; private EventViewModel? _selectedEventLog;
[ObservableProperty] [Reactive]
private IAvaloniaList<string> _eventTypes = new AvaloniaList<string>(); private ObservableCollection<string> _eventTypes = [];
[ObservableProperty] [Reactive]
private string _eventType = "OnJsonApiEvent"; private string _eventType = "OnJsonApiEvent";
[ObservableProperty] [ObservableAsProperty]
private TextDocument _document = new(); private ObservableCollection<EventViewModel> _filteredEventLog = [];
[ObservableProperty] [ReactiveCommand]
private Vector _documentOffset = new(); private async Task<List<string>> GetEventTypesAsync()
public override async Task InitializeAsync()
{
await InitializeEventTypes();
InitializeWebsocket();
}
private async Task InitializeEventTypes()
{
try
{ {
var file = await _githubUserContentClient.Request("/dysolix/hasagi-types/refs/heads/main/dist/lcu-events.d.ts") var file = await _githubUserContentClient.Request("/dysolix/hasagi-types/refs/heads/main/dist/lcu-events.d.ts")
.GetStringAsync(); .GetStringAsync();
var matches = EventTypesRegex().Matches(file); var matches = EventTypesRegex().Matches(file);
Avalonia.Threading.Dispatcher.UIThread.Invoke(() => EventTypes.AddRange(matches.Select(m => m.Groups[1].Value))); var eventTypes = matches.Select(m => m.Groups[1].Value)
} .ToList();
catch (HttpRequestException ex) return eventTypes;
{
var message = "Failed to get event types from GitHub. Please check your internet connection or try again later.";
this.Log()
.Error(ex, message);
_notificationService.Notify(AppInfo.Name, message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error);
}
} }
private void InitializeWebsocket() private void InitializeWebsocket()
@@ -150,23 +133,22 @@ public partial class WebSocketViewModel : PageBase, IEnableLogger
} }
} }
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 Document = new(text); // else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebSocketViewModel));
} // }
} //}
[RelayCommand] [ReactiveCommand]
private void Clear() private void Clear()
{ {
_events.Clear(); _events.Clear();
EventLog.Clear(); EventLog.Clear();
Document = new();
} }
private void OnReconnection(ReconnectionInfo info) private void OnReconnection(ReconnectionInfo info)
@@ -182,10 +164,10 @@ public partial class WebSocketViewModel : PageBase, IEnableLogger
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,87 +1,73 @@
using Avalonia; using Avalonia.Media;
using Avalonia.Media;
using AvaloniaEdit.Document; using AvaloniaEdit.Document;
using BlossomiShymae.Briar; using BlossomiShymae.Briar;
using BlossomiShymae.Briar.Utils; using BlossomiShymae.Briar.Utils;
using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls;
using CommunityToolkit.Mvvm.Messaging; using Microsoft.Extensions.Logging;
using Needlework.Net.Extensions;
using Needlework.Net.Messages;
using Needlework.Net.Services; using Needlework.Net.Services;
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, IEnableLogger public partial class RequestViewModel : ReactiveObject, IEnableLogger
{ {
private readonly NotificationService _notificationService; private readonly NotificationService _notificationService;
private readonly Tab _tab; private readonly Tab _tab;
public RequestViewModel(NotificationService notificationService, Tab tab) public RequestViewModel(Pages.Endpoints.Tab tab, NotificationService? notificationService = null)
{ {
_tab = tab; _tab = tab;
_notificationService = notificationService; _notificationService = notificationService ?? Locator.Current.GetService<NotificationService>()!;
_colorHelper = this.WhenAnyValue(x => x.Method)
.Select(method => GetSolidCrushBrush(method ?? "GET"))
.ToProperty(this, x => x.Color);
} }
[ObservableProperty] [ObservableAsProperty]
private SolidColorBrush _color = GetSolidCrushBrush("GET");
[Reactive]
private string? _method = "GET"; private string? _method = "GET";
[ObservableProperty] [Reactive]
private SolidColorBrush _color = new(GetColor("GET"));
[ObservableProperty]
private bool _isRequestBusy; private bool _isRequestBusy;
[ObservableProperty] [Reactive]
private string? _requestPath; private string? _requestPath;
[ObservableProperty] [Reactive]
private TextDocument _requestDocument = new(); private TextDocument _requestDocument = new();
[ObservableProperty] [Reactive]
private Vector _requestDocumentOffset = new();
[ObservableProperty]
private TextDocument _responseDocument = new();
[ObservableProperty]
private Vector _responseDocumentOffset = new();
[ObservableProperty]
private double _responseDocumentHorizontalScrollBar;
[ObservableProperty]
private double _responseDocumentVerticalScrollBar;
[ObservableProperty]
private string? _responsePath; private string? _responsePath;
[ObservableProperty] [Reactive]
private string? _responseStatus; private string? _responseStatus;
[ObservableProperty] [Reactive]
private string? _responseAuthentication; private string? _responseAuthentication;
[ObservableProperty] [Reactive]
private string? _responseUsername; private string? _responseUsername;
[ObservableProperty] [Reactive]
private string? _responsePassword; private string? _responsePassword;
[ObservableProperty] [Reactive]
private string? _responseAuthorization; private string? _responseAuthorization;
partial void OnMethodChanged(string? oldValue, string? newValue) [Reactive]
{ private TextDocument _responseDocument = new();
if (newValue == null) return;
Color = new(GetColor(newValue));
}
public async Task ExecuteAsync() public async Task ExecuteAsync()
{ {
@@ -109,23 +95,14 @@ public partial class RequestViewModel : ObservableObject, IEnableLogger
this.Log() this.Log()
.Debug("Sending request: {Tuple}", (Method, RequestPath)); .Debug("Sending request: {Tuple}", (Method, RequestPath));
var requestBody = RequestDocument.Text;
var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")); var content = new StringContent(RequestDocument.Text, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var client = Connector.GetGameHttpClientInstance(); var client = Connector.GetGameHttpClientInstance();
var response = await client.SendAsync(new HttpRequestMessage(method, RequestPath) { Content = content }); 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));
ResponseDocument = new();
}
else
{
ResponseDocument = new(body);
}
ResponseDocument = new(body);
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}"; ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}";
ResponsePath = $"https://127.0.0.1:2999{RequestPath}"; ResponsePath = $"https://127.0.0.1:2999{RequestPath}";
@@ -134,7 +111,7 @@ public partial class RequestViewModel : ObservableObject, IEnableLogger
{ {
this.Log() this.Log()
.Error(ex, "Request failed: {Tuple}", (Method, RequestPath)); .Error(ex, "Request failed: {Tuple}", (Method, RequestPath));
_notificationService.Notify("Request failed", ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error); _notificationService.Notify("Request Failed", ex.Message, InfoBarSeverity.Error);
ResponseStatus = null; ResponseStatus = null;
ResponsePath = null; ResponsePath = null;
@@ -163,24 +140,14 @@ public partial class RequestViewModel : ObservableObject, IEnableLogger
.Debug("Sending request: {Tuple}", (Method, RequestPath)); .Debug("Sending request: {Tuple}", (Method, RequestPath));
var processInfo = ProcessFinder.GetProcessInfo(); var processInfo = ProcessFinder.GetProcessInfo();
var requestBody = RequestDocument.Text; var content = new StringContent(RequestDocument.Text, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var content = new StringContent(requestBody, 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));
ResponseDocument = new();
}
else
{
ResponseDocument = new(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;
@@ -192,7 +159,7 @@ public partial class RequestViewModel : ObservableObject, IEnableLogger
{ {
this.Log() this.Log()
.Error(ex, "Request failed: {Tuple}", (Method, RequestPath)); .Error(ex, "Request failed: {Tuple}", (Method, RequestPath));
_notificationService.Notify("Request failed", ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error); _notificationService.Notify("Request Failed", ex.Message, InfoBarSeverity.Error);
ResponseStatus = null; ResponseStatus = null;
ResponsePath = null; ResponsePath = null;
@@ -224,14 +191,14 @@ public partial class RequestViewModel : ObservableObject, IEnableLogger
}; };
} }
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

@@ -0,0 +1,84 @@
<Window
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:ui="using:FluentAvalonia.UI.Controls"
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
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:vm="using:Needlework.Net.ViewModels.MainWindow"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.MainWindow.MainWindow"
x:DataType="vm:MainWindowViewModel"
Title="Needlework.Net"
Icon="/Assets/app.ico"
Width="1280"
Height="720">
<Grid RowDefinitions="auto,*">
<Grid ColumnDefinitions="auto,auto,*,auto"
Background="Transparent"
Height="40"
Grid.Row="0">
<Image Margin="12 4"
IsHitTestVisible="False"
Source="/Assets/app.png"
Width="18"
Height="18"
DockPanel.Dock="Left"
Grid.Column="0"/>
<TextBlock FontSize="12"
IsHitTestVisible="False"
VerticalAlignment="Center"
Grid.Column="1">
Needlework.Net
</TextBlock>
</Grid>
<ui:NavigationView Name="NavigationView"
AlwaysShowHeader="False"
PaneDisplayMode="Left"
IsSettingsVisible="False"
IsPaneOpen="False"
OpenPaneLength="200"
Grid.Row="1">
<ui:NavigationView.PaneFooter>
<StackPanel Orientation="Vertical">
<StackPanel.Styles>
<Style Selector="materialIcons|MaterialIcon">
<Setter Property="Width" Value="20" />
<Setter Property="Height" Value="20" />
</Style>
<Style Selector="i|Icon">
<Setter Property="FontSize" Value="20" />
</Style>
</StackPanel.Styles>
<Button Name="GithubButton"
Theme="{StaticResource TransparentButton}"
VerticalAlignment="Center"
ToolTip.Tip="Open on GitHub."
Margin="4">
<materialIcons:MaterialIcon Kind="Github" />
</Button>
<Button Name="DiscordButton"
Theme="{StaticResource TransparentButton}"
VerticalAlignment="Center"
ToolTip.Tip="Open Discord server."
Margin="4">
<i:Icon Value="fa-brand fa-discord" />
</Button>
</StackPanel>
</ui:NavigationView.PaneFooter>
<Grid>
<reactiveUi:RoutedViewHost Name="RoutedViewHost"/>
<Button Name="VersionButton"
Background="RoyalBlue"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Margin="16"/>
<ItemsControl Name="NotificationItemsControl"
VerticalAlignment="Bottom"/>
</Grid>
</ui:NavigationView>
</Grid>
</Window>

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,150 +0,0 @@
<Window
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:ui="using:FluentAvalonia.UI.Controls"
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:i="https://github.com/projektanker/icons.avalonia"
xmlns:vm="using:Needlework.Net.ViewModels.MainWindow"
xmlns:views="using:Needlework.Net.Views.MainWindow"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.MainWindow.MainWindowView"
x:DataType="vm:MainWindowViewModel"
Title="{Binding AppName}"
Icon="/Assets/app.ico"
Width="1280"
Height="720">
<Grid RowDefinitions="auto,*">
<Grid Name="TitleBarHost"
ColumnDefinitions="auto,auto,*,auto"
Background="Transparent"
Height="40"
Grid.Row="0">
<Image Margin="12 4"
IsHitTestVisible="False"
Source="/Assets/app.png"
Width="18"
Height="18"
DockPanel.Dock="Left"
Grid.Column="0"/>
<TextBlock FontSize="12"
Text="{Binding Title}"
IsHitTestVisible="False"
VerticalAlignment="Center"
Grid.Column="1"/>
<Border Grid.Column="2" Padding="4" IsHitTestVisible="True">
<StackPanel HorizontalAlignment="Center"
Orientation="Horizontal">
<AutoCompleteBox Name="SchemaAutoCompleteBox"
Margin="0 0 8 0"
MinWidth="200"
MaxWidth="500"
MaxDropDownHeight="400"
Watermark="Search schemas"
FilterMode="None"
ValueMemberBinding="{Binding Key, DataType=vm:SchemaSearchDetailsViewModel}"
AsyncPopulator="{Binding PopulateAsync}"
SelectedItem="{Binding SelectedSchemaSearchDetails}">
<AutoCompleteBox.ItemTemplate>
<DataTemplate x:DataType="vm:SchemaSearchDetailsViewModel">
<ContentControl Content="{Binding}"/>
</DataTemplate>
</AutoCompleteBox.ItemTemplate>
</AutoCompleteBox>
<Button i:Attached.Icon="fa-solid fa-file-lines"
FontSize="20"
Command="{Binding OpenSchemaPaneCommand}"/>
</StackPanel>
</Border>
</Grid>
<SplitView Grid.Row="1"
PaneBackground="Transparent"
IsPaneOpen="{Binding IsPaneOpen}"
DisplayMode="Inline"
OpenPaneLength="350"
PanePlacement="Right">
<ui:NavigationView AlwaysShowHeader="False"
PaneDisplayMode="Left"
IsSettingsVisible="False"
IsPaneOpen="False"
OpenPaneLength="200"
Grid.Row="1"
Name="NavigationView">
<ui:NavigationView.PaneFooter>
<StackPanel Orientation="Vertical">
<StackPanel.Styles>
<Style Selector="materialIcons|MaterialIcon">
<Setter Property="Width" Value="20" />
<Setter Property="Height" Value="20" />
</Style>
<Style Selector="i|Icon">
<Setter Property="FontSize" Value="20" />
</Style>
</StackPanel.Styles>
<Button
Theme="{StaticResource TransparentButton}"
VerticalAlignment="Center"
Command="{Binding OpenUrlCommand}"
CommandParameter="https://github.com/BlossomiShymae/Needlework.Net"
ToolTip.Tip="Open on GitHub."
Margin="4">
<materialIcons:MaterialIcon Kind="Github" />
</Button>
<Button
Theme="{StaticResource TransparentButton}"
VerticalAlignment="Center"
Command="{Binding OpenUrlCommand}"
CommandParameter="https://discord.gg/chEvEX5J4E"
ToolTip.Tip="Open Discord server."
Margin="4">
<i:Icon Value="fa-brand fa-discord" />
</Button>
</StackPanel>
</ui:NavigationView.PaneFooter>
<Grid>
<TransitioningContentControl Name="CurrentPageContentControl"/>
<ItemsControl ItemsSource="{Binding Notifications}"
VerticalAlignment="Bottom">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:NotificationViewModel">
<ContentControl Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ui:NavigationView>
<SplitView.Pane>
<Grid RowDefinitions="auto,*">
<ui:CommandBar DefaultLabelPosition="Right"
Grid.Row="0">
<ui:CommandBar.PrimaryCommands>
<ui:CommandBarButton Name="CloseCommandBarButton" Label="Close" Command="{Binding CloseSchemaCommand}"/>
<ui:CommandBarButton Name="CloseAllCommandBarButton" Label="Close all" Command="{Binding CloseSchemaAllCommand}"/>
</ui:CommandBar.PrimaryCommands>
</ui:CommandBar>
<ListBox ItemsSource="{Binding Schemas}" SelectedItem="{Binding SelectedSchema}" Margin="8 0 8 0" Grid.Row="1">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
<Setter Property="Padding" Value="0"></Setter>
<Setter Property="Margin" Value="0 0 0 8"></Setter>
</Style>
</ListBox.Styles>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</SplitView.Pane>
</SplitView>
</Grid>
</Window>

View File

@@ -1,100 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Windowing;
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.Schemas;
using Needlework.Net.ViewModels.Pages.Settings;
using Needlework.Net.ViewModels.Pages.WebSocket;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Needlework.Net.Views.MainWindow;
public partial class MainWindowView : AppWindow
{
public MainWindowView()
{
InitializeComponent();
}
public MainWindowView(MainWindowViewModel mainWindowViewModel, PageFactory pageFactory)
{
InitializeComponent();
DataContext = mainWindowViewModel;
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
TransparencyLevelHint = [WindowTransparencyLevel.Mica, WindowTransparencyLevel.None];
Background = IsWindows11 ? null : Background;
NavigationView.MenuItems = [.. new List<PageBase>()
{
pageFactory.GetPage<HomeViewModel>(),
pageFactory.GetPage<EndpointsViewModel>(),
pageFactory.GetPage<ConsoleViewModel>(),
pageFactory.GetPage<WebSocketViewModel>(),
pageFactory.GetPage<SchemasViewModel>(),
pageFactory.GetPage<SettingsViewModel>(),
pageFactory.GetPage<AboutViewModel>(),
}
.Select(ToNavigationViewItem)];
NavigationView.GetObservable(NavigationView.SelectedItemProperty)
.Subscribe(value =>
{
if (value is NavigationViewItem item)
{
CurrentPageContentControl.Content = item.Tag;
}
});
NavigationView.SelectedItem = NavigationView.MenuItems.Cast<NavigationViewItem>()
.First();
SchemaAutoCompleteBox.MinimumPopulateDelay = TimeSpan.FromSeconds(1);
SchemaAutoCompleteBox.MinimumPrefixLength = 3;
App.Current!.TryGetResource("TextFillColorPrimaryBrush", ActualThemeVariant, out var brush);
CloseCommandBarButton.IconSource = new ImageIconSource
{
Source = new Projektanker.Icons.Avalonia.IconImage()
{
Value = "fa-solid fa-file-circle-xmark",
Brush = (SolidColorBrush)brush!
}
};
}
private NavigationViewItem ToNavigationViewItem(PageBase page)
{
App.Current!.TryGetResource("TextFillColorPrimaryBrush", ActualThemeVariant, out var brush);
return new NavigationViewItem()
{
Content = page.DisplayName,
Tag = page,
IconSource = new ImageIconSource
{
Source = new Projektanker.Icons.Avalonia.IconImage()
{
Value = page.Icon,
Brush = (SolidColorBrush)brush!
}
}
};
}
protected override void OnLoaded(RoutedEventArgs e)
{
if (VisualRoot is AppWindow aw)
{
TitleBarHost.ColumnDefinitions[3].Width = new GridLength(aw.TitleBar.RightInset, GridUnitType.Pixel);
}
}
}

View File

@@ -2,23 +2,18 @@
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.MainWindow" 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: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.NotificationView" x:Class="Needlework.Net.Views.MainWindow.NotificationView"
x:DataType="vm:NotificationViewModel"> x:DataType="vm:NotificationViewModel">
<Border Margin="4"> <Border Margin="4">
<ui:InfoBar <ui:InfoBar Name="InfoBar"
IsOpen="True" IsOpen="True">
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
Title="{Binding Notification.Title}"
Severity="{Binding Notification.InfoBarSeverity}"
Message="{Binding Notification.Message}">
<ui:InfoBar.ActionButton> <ui:InfoBar.ActionButton>
<Button Command="{Binding OpenUrlCommand}" <Button Name="InfoBarButton"/>
IsVisible="{Binding IsButtonVisible}">
Open URL
</Button>
</ui:InfoBar.ActionButton> </ui:InfoBar.ActionButton>
</ui:InfoBar> </ui:InfoBar>
</Border> </Border>

View File

@@ -1,11 +1,28 @@
using Avalonia.Controls; using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.MainWindow;
using ReactiveUI;
using System.Reactive.Disposables;
namespace Needlework.Net.Views.MainWindow; namespace Needlework.Net.Views.MainWindow;
public partial class NotificationView : UserControl public partial class NotificationView : ReactiveUserControl<NotificationViewModel>
{ {
public NotificationView() 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(); InitializeComponent();
} }
} }

View File

@@ -10,10 +10,8 @@ namespace Needlework.Net.Views.MainWindow;
public class OopsiesDialog : IDialog, IDisposable public class OopsiesDialog : IDialog, IDisposable
{ {
private bool _isDisposing; private bool _isDisposing;
private string? _text; private string? _text;
private ContentDialog _dialog;
private readonly ContentDialog _dialog;
public OopsiesDialog() public OopsiesDialog()
{ {

View File

@@ -1,16 +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.MainWindow"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.MainWindow.SchemaSearchDetailsView"
x:DataType="vm:SchemaSearchDetailsViewModel">
<StackPanel HorizontalAlignment="Left"
VerticalAlignment="Center">
<TextBlock Text="{Binding Key}"/>
<TextBlock Text="{Binding Document}"
Theme="{StaticResource CaptionTextBlockStyle}"
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"/>
</StackPanel>
</UserControl>

View File

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

View File

@@ -1,42 +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.MainWindow"
xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.MainWindow.SchemaView"
x:DataType="vm:SchemaViewModel">
<UserControl.Styles>
<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>
<controls:Card>
<StackPanel>
<TextBlock FontSize="12" FontWeight="DemiBold" Text="{Binding Id}" Margin="0 0 0 4"/>
<DataGrid IsVisible="{Binding PropertyFields, Converter={StaticResource EnumerableToVisibilityConverter}}"
ItemsSource="{Binding PropertyFields}"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
</DataGrid>
<DataGrid IsVisible="{Binding PropertyEnums, Converter={StaticResource EnumerableToVisibilityConverter}}"
Margin="0 0 0 8"
ItemsSource="{Binding PropertyEnums}"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
</DataGrid>
</StackPanel>
</controls:Card>
</UserControl>

View File

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

View File

@@ -4,9 +4,10 @@
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.About" 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.About.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.About;
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.Console" 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.Console.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,26 +53,20 @@
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"
Document="{Binding Request.RequestDocument}" Text=""
ShowLineNumbers="True" ShowLineNumbers="True"
HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"
Margin="0 8 0 0" Margin="0 8 0 0"
FontSize="12" FontSize="12"
Grid.Row="1" Grid.Row="1"
Grid.Column="0"> Grid.Column="0"/>
<avaloniaEdit:TextEditor.Styles>
<Style Selector="ScrollViewer#PART_ScrollViewer">
<Setter Property="Offset" Value="{Binding Request.RequestDocumentOffset, Mode=TwoWay}"/>
</Style>
</avaloniaEdit:TextEditor.Styles>
</avaloniaEdit:TextEditor>
</Grid> </Grid>
<Grid RowDefinitions="35,*" <Grid RowDefinitions="35,*"
ColumnDefinitions="*" ColumnDefinitions="*"
@@ -75,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>
@@ -83,19 +84,12 @@
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
Name="ResponseEditor" Name="ResponseEditor"
Document="{Binding Request.ResponseDocument}"
HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"
ShowLineNumbers="True" ShowLineNumbers="True"
IsReadOnly="True" IsReadOnly="True"
Text="" Text=""
FontSize="12"> FontSize="12"/>
<avaloniaEdit:TextEditor.Styles>
<Style Selector="ScrollViewer#PART_ScrollViewer">
<Setter Property="Offset" Value="{Binding Request.ResponseDocumentOffset, Mode=TwoWay}"/>
</Style>
</avaloniaEdit:TextEditor.Styles>
</avaloniaEdit:TextEditor>
</Grid> </Grid>
</Grid> </Grid>
</controls:BusyArea> </controls:BusyArea>

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,25 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Styling;
using Needlework.Net.Extensions;
using TextMateSharp.Grammars;
namespace Needlework.Net.Views.Pages.Console;
public partial class ConsoleView : UserControl
{
public ConsoleView()
{
InitializeComponent();
ResponseEditor.ApplyJsonEditorSettings();
RequestEditor.ApplyJsonEditorSettings();
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)
{
var registryOptions = new RegistryOptions(
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
}
}

View File

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

View File

@@ -3,11 +3,12 @@
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.Endpoints" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointSearchDetailsView" x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointSearchDetailsView"
x:DataType="vm:EndpointSearchDetailsViewModel"> x:DataType="vm:EndpointSearchDetailsViewModel">
<Button Content="{Binding Plugin}" <Button Name="DetailsButton"
Command="{Binding OpenEndpointCommand}"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"

View File

@@ -1,11 +1,23 @@
using Avalonia.Controls; using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.Endpoints;
using ReactiveUI;
using System.Reactive.Disposables;
namespace Needlework.Net.Views.Pages.Endpoints; namespace Needlework.Net.Views.Pages.Endpoints;
public partial class EndpointSearchDetailsView : UserControl public partial class EndpointSearchDetailsView : ReactiveUserControl<EndpointSearchDetailsViewModel>
{ {
public EndpointSearchDetailsView() 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(); InitializeComponent();
} }
} }

View File

@@ -2,8 +2,10 @@
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:i="https://github.com/projektanker/icons.avalonia"
xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointTabItemContentView" x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointTabItemContentView"
x:DataType="vm:EndpointTabItemContentViewModel"> x:DataType="vm:EndpointTabItemContentViewModel">
@@ -12,10 +14,8 @@
Grid.Column="0"> Grid.Column="0">
<MenuItem Header="_New tab"> <MenuItem Header="_New tab">
<MenuItem Header="LCU" <MenuItem Header="LCU"
Command="{Binding AddEndpointCommand}"
CommandParameter="{x:Static vm:Tab.LCU}"/> CommandParameter="{x:Static vm:Tab.LCU}"/>
<MenuItem Header="Game Client" <MenuItem Header="Game Client"
Command="{Binding AddEndpointCommand}"
CommandParameter="{x:Static vm:Tab.GameClient}"/> CommandParameter="{x:Static vm:Tab.GameClient}"/>
</MenuItem> </MenuItem>
</Menu> </Menu>
@@ -23,26 +23,26 @@
Grid.Column="0"/> Grid.Column="0"/>
<Grid Grid.Row="2" <Grid Grid.Row="2"
Grid.Column="0" Grid.Column="0"
Margin="16"
RowDefinitions="auto,*" RowDefinitions="auto,*"
ColumnDefinitions="*" ColumnDefinitions="*">
Margin="16">
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"
Margin="0 0 0 8"> Margin="0 0 0 8">
<Button Command="{Binding GoBackCommand}" <Button Name="GoBackButton"
Theme="{StaticResource TransparentButton}" Theme="{StaticResource TransparentButton}"
Margin="0 0 8 0"> Margin="0 0 8 0">
<i:Icon Value="fa-arrow-left" <i:Icon Value="fa-arrow-left"
FontSize="20"/> FontSize="20"/>
</Button> </Button>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}" <TextBlock Name="TitleTextBlock"
Theme="{StaticResource TitleTextBlockStyle}"
Text="{Binding Title}"/> Text="{Binding Title}"/>
</StackPanel> </StackPanel>
<TransitioningContentControl <reactiveUi:ViewModelViewHost Name="ViewModelViewHost"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"/>
Content="{Binding ActiveViewModel}"/>
</Grid> </Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -1,11 +1,25 @@
using Avalonia.Controls; using Avalonia.ReactiveUI;
using Needlework.Net.ViewModels.Pages.Endpoints;
using ReactiveUI;
using System.Reactive.Disposables;
namespace Needlework.Net.Views.Pages.Endpoints; namespace Needlework.Net.Views.Pages.Endpoints;
public partial class EndpointTabItemContentView : UserControl public partial class EndpointTabItemContentView : ReactiveUserControl<EndpointTabItemContentViewModel>
{ {
public EndpointTabItemContentView() 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(); InitializeComponent();
} }
} }

View File

@@ -3,23 +3,25 @@
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.Endpoints" xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints"
xmlns:views="using:Needlework.Net.Views.Pages.Endpoints"
xmlns:controls="using:Needlework.Net.Controls" xmlns:controls="using:Needlework.Net.Controls"
xmlns:reactiveUi="http://reactiveui.net"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Name="EndpointsControl" Name="EndpointsControl"
x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointListView" x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointTabListView"
x:DataType="vm:EndpointListViewModel"> x:DataType="vm:EndpointTabListViewModel">
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*"> <Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
<TextBox Watermark="Search" Margin="0 0 0 4" Text="{Binding Search}" Grid.Row="1" Grid.Column="0"/> <TextBox Name="SearchTextBox"
<ScrollViewer Grid.Row="2" Grid.Column="0" Offset="{Binding Offset, Mode=TwoWay}"> Watermark="Search"
<ItemsControl ItemsSource="{Binding EndpointSearchDetails}"> Margin="0 4"
<ItemsControl.ItemsPanel> Grid.Row="1"
<ItemsPanelTemplate> Grid.Column="0"/>
<VirtualizingStackPanel/> <ScrollViewer Grid.Row="2" Grid.Column="0">
</ItemsPanelTemplate> <ItemsControl Name="EndpointSearchDetailItemsControl"
</ItemsControl.ItemsPanel> ItemsSource="{Binding Plugins}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate x:DataType="vm:EndpointSearchDetailsViewModel">
<ContentControl Content="{Binding}"/> <reactiveUi:ViewModelViewHost ViewModel="{Binding}"/>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>

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

@@ -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:reactiveUi="http://reactiveui.net"
xmlns:views="using:Needlework.Net.Views.Pages.Endpoints"
xmlns:ui="using:FluentAvalonia.UI.Controls" 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.EndpointsView" x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsPage"
x:DataType="vm:EndpointsViewModel"> 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>
@@ -29,16 +29,17 @@
<Style.Animations> <Style.Animations>
<Animation IterationCount="1" Duration="0:0:1" FillMode="Both"> <Animation IterationCount="1" Duration="0:0:1" FillMode="Both">
<KeyFrame Cue="0%"> <KeyFrame Cue="0%">
<Setter Property="Background" Value="{DynamicResource ControlFillColorTransparentBrush}"/> <Setter Property="Background" Value="{DynamicResource ControlFillColorTransparent}"/>
</KeyFrame> </KeyFrame>
<KeyFrame Cue="100%"> <KeyFrame Cue="100%">
<Setter Property="Background" Value="{DynamicResource ControlFillColorTransparentBrush}"/> <Setter Property="Background" Value="{DynamicResource ControlFillColorTransparent}"/>
</KeyFrame> </KeyFrame>
</Animation> </Animation>
</Style.Animations> </Style.Animations>
</Style> </Style>
</ui:TabView.Styles> </ui:TabView.Styles>
<ui:TabView.TabItemTemplate> <ui:TabView.TabItemTemplate>
<!--We have to item template with XAML bindings here due to FluentAvalonia generating a TabViewItem control for reactive bindings...-->
<DataTemplate x:DataType="vm:EndpointTabItemViewModel"> <DataTemplate x:DataType="vm:EndpointTabItemViewModel">
<ui:TabViewItem Header="{Binding Header}" <ui:TabViewItem Header="{Binding Header}"
IconSource="{Binding IconSource}" IconSource="{Binding IconSource}"
@@ -46,7 +47,7 @@
Content="{Binding Content}"> Content="{Binding Content}">
<ui:TabViewItem.ContentTemplate> <ui:TabViewItem.ContentTemplate>
<DataTemplate x:DataType="vm:EndpointTabItemContentViewModel"> <DataTemplate x:DataType="vm:EndpointTabItemContentViewModel">
<ContentControl Content="{Binding}"/> <reactiveUi:ViewModelViewHost ViewModel="{Binding}"/>
</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 EndpointsView : UserControl
{
public EndpointsView()
{
InitializeComponent();
}
private void TabView_TabCloseRequested(FluentAvalonia.UI.Controls.TabView sender, FluentAvalonia.UI.Controls.TabViewTabCloseRequestedEventArgs args)
{
if (args.Tab.DataContext is EndpointTabItemViewModel item && sender.TabItems is IList tabItems)
{
if (tabItems.Count > 1)
{
tabItems.Remove(item);
}
}
}
}

View File

@@ -2,12 +2,13 @@
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.Endpoints"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" 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:Class="Needlework.Net.Views.Pages.Endpoints.PathOperationView"
x:DataType="vm:PathOperationViewModel"> x:DataType="vm:PathOperationViewModel">
<Grid <Grid RowDefinitions="*"
RowDefinitions="*"
ColumnDefinitions="auto,*"> ColumnDefinitions="auto,*">
<Grid.ContextFlyout> <Grid.ContextFlyout>
<MenuFlyout> <MenuFlyout>

View File

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

View File

@@ -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,23 +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.Styles>
<Style Selector="ScrollViewer#PART_ScrollViewer">
<Setter Property="Offset" Value="{Binding Offset, Mode=TwoWay}"/>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:PathOperationViewModel">
<ContentControl Content="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid> </Grid>
<GridSplitter Background="Gray" <GridSplitter Background="Gray"
Margin="8 0 8 0" Margin="8 0 8 0"
@@ -71,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 Offset="{Binding ParamsOffset, Mode=TwoWay}"> <ScrollViewer>
<StackPanel IsVisible="{Binding SelectedPathOperation, Converter={StaticResource NullableToVisibilityConverter}}"> <StackPanel Name="ParamsStackPanel">
<controls:Card <controls:Card Name="PathParametersCard"
Margin="0 4" Margin="0 4">
IsVisible="{Binding SelectedPathOperation.Operation.PathParameters, Converter={StaticResource EnumerableToVisibilityConverter}}">
<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}"/>
@@ -124,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 EnumerableToVisibilityConverter}}">
<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}"/>
@@ -155,18 +141,11 @@
<TabItem Header="Body"> <TabItem Header="Body">
<avalonEdit:TextEditor <avalonEdit:TextEditor
Name="EndpointRequestEditor" Name="EndpointRequestEditor"
Document="{Binding SelectedPathOperation.Request.Value.RequestDocument}"
HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"
Text="" Text=""
ShowLineNumbers="True" ShowLineNumbers="True"
FontSize="12"> FontSize="12"/>
<avalonEdit:TextEditor.Styles>
<Style Selector="ScrollViewer#PART_ScrollViewer">
<Setter Property="Offset" Value="{Binding SelectedPathOperation.Request.Value.RequestDocumentOffset, Mode=TwoWay}"/>
</Style>
</avalonEdit:TextEditor.Styles>
</avalonEdit:TextEditor>
</TabItem> </TabItem>
<TabItem Header="Auth"> <TabItem Header="Auth">
<Grid RowDefinitions="auto,auto,auto,*" ColumnDefinitions="*,4*"> <Grid RowDefinitions="auto,auto,auto,*" ColumnDefinitions="*,4*">
@@ -176,75 +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 Offset="{Binding SchemasOffset, Mode=TwoWay}"> <ScrollViewer>
<StackPanel> <StackPanel>
<controls:Card Margin="0 4" IsVisible="{Binding SelectedPathOperation.Operation.RequestBodyType, Converter={StaticResource NullableToVisibilityConverter}}"> <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 EnumerableToVisibilityConverter}}"> <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>
<ItemsControl ItemsSource="{Binding SelectedPathOperation.Operation.RequestClasses}"> <ItemsControl Name="RequestClassItemsControl"
<ItemsControl.ItemTemplate> Margin="0 4 0 8"/>
<DataTemplate x:DataType="vm:PropertyClassViewModel">
<ContentControl Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</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 EnumerableToVisibilityConverter}}"> <Border Name="ResponseClassesBorder" Margin="0 4">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" FontWeight="DemiBold">Response Classes</TextBlock> <TextBlock FontSize="14" FontWeight="DemiBold">Response Classes</TextBlock>
<ItemsControl ItemsSource="{Binding SelectedPathOperation.Operation.ResponseClasses}"> <ItemsControl Name="ResponseClassItemsControl"
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:PropertyClassViewModel">
<ContentControl Content="{Binding}"
Margin="0 4 0 8"/> Margin="0 4 0 8"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel> </StackPanel>
</Border> </Border>
</StackPanel> </StackPanel>
@@ -255,35 +223,28 @@
<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
Name="EndpointResponseEditor" Name="EndpointResponseEditor"
Document="{Binding SelectedPathOperation.Request.Value.ResponseDocument}"
HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"
ShowLineNumbers="True" ShowLineNumbers="True"
IsReadOnly="True" IsReadOnly="True"
Text="" Text=""
FontSize="12"> FontSize="12"/>
<avalonEdit:TextEditor.Styles>
<Style Selector="ScrollViewer#PART_ScrollViewer">
<Setter Property="Offset" Value="{Binding SelectedPathOperation.Request.Value.ResponseDocumentOffset, Mode=TwoWay}"/>
</Style>
</avalonEdit:TextEditor.Styles>
</avalonEdit:TextEditor>
</TabItem> </TabItem>
</TabControl> </TabControl>
</controls:BusyArea> </controls:BusyArea>

View File

@@ -1,20 +1,74 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.ReactiveUI;
using Avalonia.Styling; using Avalonia.Styling;
using Needlework.Net.Extensions; using Needlework.Net.Extensions;
using Needlework.Net.ViewModels.Pages.Endpoints;
using ReactiveUI;
using System.Reactive.Disposables;
using TextMateSharp.Grammars; using TextMateSharp.Grammars;
namespace Needlework.Net.Views.Pages.Endpoints; namespace Needlework.Net.Views.Pages.Endpoints;
public partial class PluginView : UserControl public partial class PluginView : ReactiveUserControl<PluginViewModel>
{ {
public PluginView() public PluginView()
{ {
InitializeComponent(); this.WhenActivated(disposables =>
{
EndpointRequestEditor.ApplyJsonEditorSettings(); EndpointRequestEditor?.ApplyJsonEditorSettings();
EndpointResponseEditor.ApplyJsonEditorSettings(); EndpointResponseEditor?.ApplyJsonEditorSettings();
OnBaseThemeChanged(Application.Current!.ActualThemeVariant); 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) private void OnBaseThemeChanged(ThemeVariant currentTheme)

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