diff --git a/Needlework.Net/App.axaml.cs b/Needlework.Net/App.axaml.cs index 2ee5261..5a5c7e4 100644 --- a/Needlework.Net/App.axaml.cs +++ b/Needlework.Net/App.axaml.cs @@ -3,8 +3,8 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Microsoft.Extensions.DependencyInjection; -using Needlework.Net.ViewModels; -using Needlework.Net.Views; +using Needlework.Net.ViewModels.MainWindow; +using Needlework.Net.Views.MainWindow; using System; using System.Text.Json; @@ -33,7 +33,7 @@ public partial class App(IServiceProvider serviceProvider) : Application { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow = new MainWindow() + desktop.MainWindow = new MainWindowView() { DataContext = _serviceProvider.GetRequiredService() }; diff --git a/Needlework.Net/Messages/InfoBarUpdateMessage.cs b/Needlework.Net/Messages/InfoBarUpdateMessage.cs index 39a8965..ed2d354 100644 --- a/Needlework.Net/Messages/InfoBarUpdateMessage.cs +++ b/Needlework.Net/Messages/InfoBarUpdateMessage.cs @@ -1,5 +1,5 @@ using CommunityToolkit.Mvvm.Messaging.Messages; -using Needlework.Net.ViewModels; +using Needlework.Net.ViewModels.MainWindow; namespace Needlework.Net.Messages { diff --git a/Needlework.Net/Needlework.Net.csproj b/Needlework.Net/Needlework.Net.csproj index 1a7beb8..081ece2 100644 --- a/Needlework.Net/Needlework.Net.csproj +++ b/Needlework.Net/Needlework.Net.csproj @@ -52,7 +52,10 @@ BusyArea.axaml - + + MainWindowView.axaml + + EndpointView.axaml diff --git a/Needlework.Net/Program.cs b/Needlework.Net/Program.cs index 38c1470..8d4c623 100644 --- a/Needlework.Net/Program.cs +++ b/Needlework.Net/Program.cs @@ -2,7 +2,8 @@ using Microsoft.Extensions.DependencyInjection; using Needlework.Net.Extensions; using Needlework.Net.Services; -using Needlework.Net.ViewModels; +using Needlework.Net.ViewModels.MainWindow; +using Needlework.Net.ViewModels.Pages; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; using System; diff --git a/Needlework.Net/ViewLocator.cs b/Needlework.Net/ViewLocator.cs index a4cfcaf..76985f6 100644 --- a/Needlework.Net/ViewLocator.cs +++ b/Needlework.Net/ViewLocator.cs @@ -3,6 +3,8 @@ using Avalonia.Controls.Templates; using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; +using System.Reflection; namespace Needlework.Net { @@ -12,17 +14,21 @@ namespace Needlework.Net public Control Build(object? data) { - var fullName = data?.GetType().FullName; - if (fullName is null) + var name = data?.GetType().Name; + if (name is null) { return new TextBlock { Text = "Data is null or has no name." }; } - var name = fullName.Replace("ViewModel", "View"); - var type = Type.GetType(name); + name = name.Replace("ViewModel", "View"); + var type = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => t.Name == name) + .FirstOrDefault(); + if (type is null) { - return new TextBlock { Text = $"No View For {name}." }; + return new TextBlock { Text = $"No view for {name}." }; } if (!_controlCache.TryGetValue(data!, out var res)) diff --git a/Needlework.Net/ViewModels/AboutViewModel.cs b/Needlework.Net/ViewModels/AboutViewModel.cs deleted file mode 100644 index bb34427..0000000 --- a/Needlework.Net/ViewModels/AboutViewModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using CommunityToolkit.Mvvm.Input; -using System.Diagnostics; -using System.Net.Http; - -namespace Needlework.Net.ViewModels -{ - public partial class AboutViewModel : PageBase - { - public HttpClient HttpClient { get; } - - public AboutViewModel(HttpClient httpClient) : base("About", "info-circle") - { - HttpClient = httpClient; - } - - [RelayCommand] - private void OpenUrl(string url) - { - var process = new Process() - { - StartInfo = new ProcessStartInfo(url) { UseShellExecute = true } - }; - process.Start(); - } - } -} diff --git a/Needlework.Net/ViewModels/ConsoleViewModel.cs b/Needlework.Net/ViewModels/ConsoleViewModel.cs deleted file mode 100644 index 8ac1fcb..0000000 --- a/Needlework.Net/ViewModels/ConsoleViewModel.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Avalonia.Collections; -using BlossomiShymae.GrrrLCU; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using Needlework.Net.Messages; -using System; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; - -namespace Needlework.Net.ViewModels -{ - public partial class ConsoleViewModel : PageBase, IRecipient - { - public IAvaloniaReadOnlyList RequestMethods { get; } = new AvaloniaList(["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE"]); - public IAvaloniaList RequestPaths { get; } = new AvaloniaList(); - - [ObservableProperty] private bool _isBusy = true; - [ObservableProperty] private bool _isRequestBusy = false; - [ObservableProperty] private string? _requestMethodSelected = "GET"; - [ObservableProperty] private string? _requestPath = null; - [ObservableProperty] private string? _requestBody = null; - [ObservableProperty] private string? _responsePath = null; - [ObservableProperty] private string? _responseStatus = null; - [ObservableProperty] private string? _responseAuthorization = null; - - public ConsoleViewModel() : base("Console", "terminal", -200) - { - WeakReferenceMessenger.Default.Register(this); - } - - [RelayCommand] - private async Task SendRequest() - { - try - { - IsRequestBusy = true; - if (string.IsNullOrEmpty(RequestPath)) throw new Exception("Path is empty."); - - var method = RequestMethodSelected switch - { - "GET" => HttpMethod.Get, - "POST" => HttpMethod.Post, - "PUT" => HttpMethod.Put, - "DELETE" => HttpMethod.Delete, - "HEAD" => HttpMethod.Head, - "PATCH" => HttpMethod.Patch, - "OPTIONS" => HttpMethod.Options, - "TRACE" => HttpMethod.Trace, - _ => throw new Exception("Method is not selected."), - }; - - var processInfo = ProcessFinder.Get(); - var requestBody = WeakReferenceMessenger.Default.Send(new ContentRequestMessage(), "ConsoleRequestEditor").Response; - var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")); - var client = Connector.GetLcuHttpClientInstance(); - var response = await client.SendAsync(new(method, RequestPath) { Content = content }); - var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken); - var responseBody = await response.Content.ReadAsByteArrayAsync(); - - var body = responseBody.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize(responseBody), App.JsonSerializerOptions) : string.Empty; - if (body.Length >= App.MaxCharacters) - { - WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(body)); - WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(string.Empty), nameof(ConsoleViewModel)); - } - else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(body), nameof(ConsoleViewModel)); - - ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}"; - ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{RequestPath}"; - ResponseAuthorization = $"Basic {riotAuthentication.Value}"; - } - catch (Exception ex) - { - WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5)))); - ResponseStatus = null; - ResponsePath = null; - ResponseAuthorization = null; - WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(string.Empty), nameof(ConsoleViewModel)); - } - finally - { - IsRequestBusy = false; - } - } - - public void Receive(DataReadyMessage message) - { - Avalonia.Threading.Dispatcher.UIThread.Invoke(() => - { - RequestPaths.Clear(); - RequestPaths.AddRange(message.Value.Paths); - IsBusy = false; - }); - } - } -} diff --git a/Needlework.Net/ViewModels/EndpointViewModel.cs b/Needlework.Net/ViewModels/EndpointViewModel.cs deleted file mode 100644 index 19fddba..0000000 --- a/Needlework.Net/ViewModels/EndpointViewModel.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Avalonia.Collections; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Messaging; -using Needlework.Net.Messages; -using System; -using System.Linq; - -namespace Needlework.Net.ViewModels -{ - public partial class EndpointViewModel : ObservableObject - { - public string Endpoint { get; } - public string Title => Endpoint; - - - public IAvaloniaReadOnlyList PathOperations { get; } - [ObservableProperty] private PathOperationViewModel? _selectedPathOperation; - - [ObservableProperty] private string? _search; - public IAvaloniaList FilteredPathOperations { get; } - - public EndpointViewModel(string endpoint) - { - Endpoint = endpoint; - - var handler = WeakReferenceMessenger.Default.Send().Response; - PathOperations = new AvaloniaList(handler.Plugins[endpoint].Select(x => new PathOperationViewModel(x))); - FilteredPathOperations = new AvaloniaList(PathOperations); - } - - partial void OnSearchChanged(string? value) - { - FilteredPathOperations.Clear(); - - if (string.IsNullOrWhiteSpace(value)) - { - FilteredPathOperations.AddRange(PathOperations); - return; - } - FilteredPathOperations.AddRange(PathOperations.Where(o => o.Path.Contains(value, StringComparison.InvariantCultureIgnoreCase))); - } - - partial void OnSelectedPathOperationChanged(PathOperationViewModel? value) - { - if (value == null) return; - WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(value.Operation.RequestTemplate ?? string.Empty, "EndpointRequestEditor"))); - } - } -} diff --git a/Needlework.Net/ViewModels/EndpointsContainerViewModel.cs b/Needlework.Net/ViewModels/EndpointsContainerViewModel.cs deleted file mode 100644 index 626f665..0000000 --- a/Needlework.Net/ViewModels/EndpointsContainerViewModel.cs +++ /dev/null @@ -1,31 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using System.Net.Http; - -namespace Needlework.Net.ViewModels -{ - public partial class EndpointsContainerViewModel : PageBase - { - [ObservableProperty] private ObservableObject _activeViewModel; - [ObservableProperty] private ObservableObject _endpointsViewModel; - [ObservableProperty] private string _title = string.Empty; - - public EndpointsContainerViewModel(HttpClient httpClient) : base("Endpoints", "list-alt", -500) - { - _activeViewModel = _endpointsViewModel = new EndpointsViewModel(httpClient, OnClicked); - } - - private void OnClicked(ObservableObject viewModel) - { - ActiveViewModel = viewModel; - if (viewModel is EndpointViewModel endpoint) Title = endpoint.Title; - } - - [RelayCommand] - private void GoBack() - { - ActiveViewModel = EndpointsViewModel; - Title = string.Empty; - } - } -} diff --git a/Needlework.Net/ViewModels/EndpointsViewModel.cs b/Needlework.Net/ViewModels/EndpointsViewModel.cs deleted file mode 100644 index c04c3b9..0000000 --- a/Needlework.Net/ViewModels/EndpointsViewModel.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Avalonia.Collections; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using Needlework.Net.Messages; -using System; -using System.Linq; -using System.Net.Http; - -namespace Needlework.Net.ViewModels -{ - public partial class EndpointsViewModel : ObservableObject, IRecipient - { - public HttpClient HttpClient { get; } - - public string Title => "Endpoints"; - public Action OnClicked; - public IAvaloniaList Plugins { get; } = new AvaloniaList(); - public IAvaloniaList Query { get; } = new AvaloniaList(); - - [ObservableProperty] private bool _isBusy = true; - [ObservableProperty] private string _search = string.Empty; - [ObservableProperty] private string? _selectedQuery = string.Empty; - - public EndpointsViewModel(HttpClient httpClient, Action onClicked) - { - HttpClient = httpClient; - OnClicked = onClicked; - - WeakReferenceMessenger.Default.Register(this); - } - - public void Receive(DataReadyMessage message) - { - IsBusy = false; - Plugins.Clear(); - Plugins.AddRange(message.Value.Plugins.Keys); - Query.Clear(); - Query.AddRange(Plugins); - } - - partial void OnSearchChanged(string value) - { - Query.Clear(); - if (!string.IsNullOrEmpty(Search)) - Query.AddRange(Plugins.Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase))); - else - Query.AddRange(Plugins); - } - - [RelayCommand] - private void OpenEndpoint(string? value) - { - if (string.IsNullOrEmpty(value)) return; - - OnClicked.Invoke(new EndpointViewModel(value)); - } - } -} diff --git a/Needlework.Net/ViewModels/EventViewModel.cs b/Needlework.Net/ViewModels/EventViewModel.cs deleted file mode 100644 index 0b59415..0000000 --- a/Needlework.Net/ViewModels/EventViewModel.cs +++ /dev/null @@ -1,22 +0,0 @@ -using BlossomiShymae.GrrrLCU; -using CommunityToolkit.Mvvm.ComponentModel; -using System; - -namespace Needlework.Net.ViewModels -{ - public class EventViewModel : ObservableObject - { - public string Time { get; } - public string Type { get; } - public string Uri { get; } - - public string Key => $"{Time} {Type} {Uri}"; - - public EventViewModel(EventData eventData) - { - Time = $"{DateTime.Now:HH:mm:ss.fff}"; - Type = eventData?.EventType.ToUpper() ?? string.Empty; - Uri = eventData?.Uri ?? string.Empty; - } - } -} diff --git a/Needlework.Net/ViewModels/HomeViewModel.cs b/Needlework.Net/ViewModels/HomeViewModel.cs deleted file mode 100644 index 6ca93f5..0000000 --- a/Needlework.Net/ViewModels/HomeViewModel.cs +++ /dev/null @@ -1,20 +0,0 @@ -using CommunityToolkit.Mvvm.Input; -using System.Diagnostics; - -namespace Needlework.Net.ViewModels -{ - public partial class HomeViewModel : PageBase - { - public HomeViewModel() : base("Home", "home", int.MinValue) { } - - [RelayCommand] - private void OpenUrl(string url) - { - var process = new Process() - { - StartInfo = new ProcessStartInfo(url) { UseShellExecute = true } - }; - process.Start(); - } - } -} diff --git a/Needlework.Net/ViewModels/InfoBarViewModel.cs b/Needlework.Net/ViewModels/InfoBarViewModel.cs deleted file mode 100644 index 91c54c6..0000000 --- a/Needlework.Net/ViewModels/InfoBarViewModel.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Avalonia.Controls; -using CommunityToolkit.Mvvm.ComponentModel; -using FluentAvalonia.UI.Controls; -using System; - -namespace Needlework.Net.ViewModels -{ - public partial class InfoBarViewModel : ObservableObject - { - [ObservableProperty] private string _title; - [ObservableProperty] private bool _isOpen; - [ObservableProperty] private string _message; - [ObservableProperty] private InfoBarSeverity _severity; - [ObservableProperty] private TimeSpan _duration; - [ObservableProperty] private Control? _actionButton; - - public InfoBarViewModel(string title, bool isOpen, string message, InfoBarSeverity severity, TimeSpan duration, Control? actionButton = null) - { - _title = title; - _isOpen = isOpen; - _message = message; - _severity = severity; - _duration = duration; - _actionButton = actionButton; - } - } -} diff --git a/Needlework.Net/ViewModels/MainWindow/InfoBarViewModel.cs b/Needlework.Net/ViewModels/MainWindow/InfoBarViewModel.cs new file mode 100644 index 0000000..9ccf17d --- /dev/null +++ b/Needlework.Net/ViewModels/MainWindow/InfoBarViewModel.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using FluentAvalonia.UI.Controls; +using System; + +namespace Needlework.Net.ViewModels.MainWindow; + +public partial class InfoBarViewModel : ObservableObject +{ + [ObservableProperty] private string _title; + [ObservableProperty] private bool _isOpen; + [ObservableProperty] private string _message; + [ObservableProperty] private InfoBarSeverity _severity; + [ObservableProperty] private TimeSpan _duration; + [ObservableProperty] private Control? _actionButton; + + public InfoBarViewModel(string title, bool isOpen, string message, InfoBarSeverity severity, TimeSpan duration, Control? actionButton = null) + { + _title = title; + _isOpen = isOpen; + _message = message; + _severity = severity; + _duration = duration; + _actionButton = actionButton; + } +} diff --git a/Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs b/Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs new file mode 100644 index 0000000..3f47b28 --- /dev/null +++ b/Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs @@ -0,0 +1,157 @@ +using Avalonia.Collections; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using FluentAvalonia.UI.Controls; +using Microsoft.OpenApi.Models; +using Needlework.Net.Messages; +using Needlework.Net.Models; +using Needlework.Net.Services; +using Needlework.Net.ViewModels.Pages; +using Needlework.Net.Views.MainWindow; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Needlework.Net.ViewModels.MainWindow; + +public partial class MainWindowViewModel + : ObservableObject, IRecipient, IRecipient, IRecipient, IRecipient +{ + public IAvaloniaReadOnlyList MenuItems { get; } + [NotifyPropertyChangedFor(nameof(CurrentPage))] + [ObservableProperty] private NavigationViewItem _selectedMenuItem; + public PageBase CurrentPage => (PageBase)SelectedMenuItem.Tag!; + + public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0"; + [ObservableProperty] private bool _isUpdateShown = false; + + public HttpClient HttpClient { get; } + public DialogService DialogService { get; } + public OpenApiDocumentWrapper? OpenApiDocumentWrapper { get; set; } + public OpenApiDocument? HostDocument { get; set; } + + [ObservableProperty] private bool _isBusy = true; + + [ObservableProperty] private ObservableCollection _infoBarItems = []; + + public MainWindowViewModel(IEnumerable pages, HttpClient httpClient, DialogService dialogService) + { + MenuItems = new AvaloniaList(pages + .OrderBy(p => p.Index) + .ThenBy(p => p.DisplayName) + .Select(p => new NavigationViewItem() + { + Content = p.DisplayName, + Tag = p, + IconSource = new BitmapIconSource() { UriSource = new Uri($"avares://NeedleworkDotNet/Assets/Icons/{p.Icon}.png") } + })); + SelectedMenuItem = MenuItems[0]; + + HttpClient = httpClient; + DialogService = dialogService; + + WeakReferenceMessenger.Default.RegisterAll(this); + + Task.Run(FetchDataAsync); + new Thread(ProcessEvents) { IsBackground = true }.Start(); + } + + private void ProcessEvents(object? obj) + { + while (!IsUpdateShown) + { + Task.Run(CheckLatestVersionAsync); + + Thread.Sleep(TimeSpan.FromSeconds(60)); + } + } + + private async Task CheckLatestVersionAsync() + { + try + { + var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/BlossomiShymae/Needlework.Net/releases/latest"); + request.Headers.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("Needlework.Net", Version)); + + var response = await HttpClient.SendAsync(request); + var release = await response.Content.ReadFromJsonAsync(); + if (release == null) return; + + var currentVersion = int.Parse(Version.Replace(".", "")); + + if (release.IsLatest(currentVersion)) + { + Avalonia.Threading.Dispatcher.UIThread.Post(async () => + { + await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(10), new Avalonia.Controls.Button() + { + Command = OpenUrlCommand, + CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases", + Content = "Download" + })); + IsUpdateShown = true; + }); + } + } + catch (Exception) { } + } + + private async Task FetchDataAsync() + { + var document = await Resources.GetOpenApiDocumentAsync(HttpClient); + HostDocument = document; + var handler = new OpenApiDocumentWrapper(document); + OpenApiDocumentWrapper = handler; + + WeakReferenceMessenger.Default.Send(new DataReadyMessage(handler)); + IsBusy = false; + } + + public void Receive(DataRequestMessage message) + { + message.Reply(OpenApiDocumentWrapper!); + } + + public void Receive(HostDocumentRequestMessage message) + { + message.Reply(HostDocument!); + } + + [RelayCommand] + private void OpenUrl(string url) + { + var process = new Process() + { + StartInfo = new ProcessStartInfo(url) + { + UseShellExecute = true + } + }; + process.Start(); + } + + public void Receive(InfoBarUpdateMessage message) + { + Avalonia.Threading.Dispatcher.UIThread.Post(async () => await ShowInfoBarAsync(message.Value)); + } + + private async Task ShowInfoBarAsync(InfoBarViewModel vm) + { + InfoBarItems.Add(vm); + await Task.Delay(vm.Duration); + InfoBarItems.Remove(vm); + } + + public void Receive(OopsiesDialogRequestedMessage message) + { + Avalonia.Threading.Dispatcher.UIThread.Invoke(async () => await DialogService.ShowAsync(message.Value)); + } +} diff --git a/Needlework.Net/ViewModels/MainWindowViewModel.cs b/Needlework.Net/ViewModels/MainWindowViewModel.cs deleted file mode 100644 index 711d1dd..0000000 --- a/Needlework.Net/ViewModels/MainWindowViewModel.cs +++ /dev/null @@ -1,157 +0,0 @@ -using Avalonia.Collections; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using FluentAvalonia.UI.Controls; -using Microsoft.OpenApi.Models; -using Needlework.Net.Messages; -using Needlework.Net.Models; -using Needlework.Net.Services; -using Needlework.Net.Views; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Json; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; - -namespace Needlework.Net.ViewModels -{ - public partial class MainWindowViewModel - : ObservableObject, IRecipient, IRecipient, IRecipient, IRecipient - { - public IAvaloniaReadOnlyList MenuItems { get; } - [NotifyPropertyChangedFor(nameof(CurrentPage))] - [ObservableProperty] private NavigationViewItem _selectedMenuItem; - public PageBase CurrentPage => (PageBase)SelectedMenuItem.Tag!; - - public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0"; - [ObservableProperty] private bool _isUpdateShown = false; - - public HttpClient HttpClient { get; } - public DialogService DialogService { get; } - public OpenApiDocumentWrapper? OpenApiDocumentWrapper { get; set; } - public OpenApiDocument? HostDocument { get; set; } - - [ObservableProperty] private bool _isBusy = true; - - [ObservableProperty] private ObservableCollection _infoBarItems = []; - - public MainWindowViewModel(IEnumerable pages, HttpClient httpClient, DialogService dialogService) - { - MenuItems = new AvaloniaList(pages - .OrderBy(p => p.Index) - .ThenBy(p => p.DisplayName) - .Select(p => new NavigationViewItem() - { - Content = p.DisplayName, - Tag = p, - IconSource = new BitmapIconSource() { UriSource = new Uri($"avares://NeedleworkDotNet/Assets/Icons/{p.Icon}.png") } - })); - SelectedMenuItem = MenuItems[0]; - - HttpClient = httpClient; - DialogService = dialogService; - - WeakReferenceMessenger.Default.RegisterAll(this); - - Task.Run(FetchDataAsync); - new Thread(ProcessEvents) { IsBackground = true }.Start(); - } - - private void ProcessEvents(object? obj) - { - while (!IsUpdateShown) - { - Task.Run(CheckLatestVersionAsync); - - Thread.Sleep(TimeSpan.FromSeconds(60)); - } - } - - private async Task CheckLatestVersionAsync() - { - try - { - var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/BlossomiShymae/Needlework.Net/releases/latest"); - request.Headers.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("Needlework.Net", Version)); - - var response = await HttpClient.SendAsync(request); - var release = await response.Content.ReadFromJsonAsync(); - if (release == null) return; - - var currentVersion = int.Parse(Version.Replace(".", "")); - - if (release.IsLatest(currentVersion)) - { - Avalonia.Threading.Dispatcher.UIThread.Post(async () => - { - await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(10), new Avalonia.Controls.Button() - { - Command = OpenUrlCommand, - CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases", - Content = "Download" - })); - IsUpdateShown = true; - }); - } - } - catch (Exception) { } - } - - private async Task FetchDataAsync() - { - var document = await Resources.GetOpenApiDocumentAsync(HttpClient); - HostDocument = document; - var handler = new OpenApiDocumentWrapper(document); - OpenApiDocumentWrapper = handler; - - WeakReferenceMessenger.Default.Send(new DataReadyMessage(handler)); - IsBusy = false; - } - - public void Receive(DataRequestMessage message) - { - message.Reply(OpenApiDocumentWrapper!); - } - - public void Receive(HostDocumentRequestMessage message) - { - message.Reply(HostDocument!); - } - - [RelayCommand] - private void OpenUrl(string url) - { - var process = new Process() - { - StartInfo = new ProcessStartInfo(url) - { - UseShellExecute = true - } - }; - process.Start(); - } - - public void Receive(InfoBarUpdateMessage message) - { - Avalonia.Threading.Dispatcher.UIThread.Post(async () => await ShowInfoBarAsync(message.Value)); - } - - private async Task ShowInfoBarAsync(InfoBarViewModel vm) - { - InfoBarItems.Add(vm); - await Task.Delay(vm.Duration); - InfoBarItems.Remove(vm); - } - - public void Receive(OopsiesDialogRequestedMessage message) - { - Avalonia.Threading.Dispatcher.UIThread.Invoke(async () => await DialogService.ShowAsync(message.Value)); - } - } -} diff --git a/Needlework.Net/ViewModels/OperationViewModel.cs b/Needlework.Net/ViewModels/OperationViewModel.cs deleted file mode 100644 index 8efb1db..0000000 --- a/Needlework.Net/ViewModels/OperationViewModel.cs +++ /dev/null @@ -1,233 +0,0 @@ -using Avalonia.Collections; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Messaging; -using Microsoft.OpenApi.Models; -using Needlework.Net.Messages; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; - -namespace Needlework.Net.ViewModels -{ - public partial class OperationViewModel : ObservableObject - { - public string Summary { get; } - public string Description { get; } - public string ReturnType { get; } - public bool IsRequestBody { get; } - public string? RequestBodyType { get; } - public IAvaloniaReadOnlyList RequestClasses { get; } - public IAvaloniaReadOnlyList ResponseClasses { get; } - public IAvaloniaReadOnlyList PathParameters { get; } - public IAvaloniaReadOnlyList QueryParameters { get; } - public string? RequestTemplate { get; } - - public OperationViewModel(OpenApiOperation operation) - { - Summary = operation.Summary ?? string.Empty; - Description = operation.Description ?? string.Empty; - IsRequestBody = operation.RequestBody != null; - ReturnType = GetReturnType(operation.Responses); - RequestClasses = GetRequestClasses(operation.RequestBody); - ResponseClasses = GetResponseClasses(operation.Responses); - PathParameters = GetParameters(operation.Parameters, ParameterLocation.Path); - QueryParameters = GetParameters(operation.Parameters, ParameterLocation.Query); - RequestBodyType = GetRequestBodyType(operation.RequestBody); - RequestTemplate = GetRequestTemplate(operation.RequestBody); - } - - private string? GetRequestTemplate(OpenApiRequestBody? requestBody) - { - var requestClasses = GetRequestClasses(requestBody); - if (requestClasses.Count == 0) - { - var type = GetRequestBodyType(requestBody); - if (type == null) return null; - return GetRequestDefaultValue(type); - } - - var template = CreateTemplate(requestClasses); - return JsonSerializer.Serialize(JsonSerializer.Deserialize(string.Join(string.Empty, template)), App.JsonSerializerOptions); - } - - private List CreateTemplate(AvaloniaList requestClasses) - { - if (requestClasses.Count == 0) return []; - List template = []; - template.Add("{"); - - var rootClass = requestClasses.First(); - if (rootClass.PropertyEnums.Any()) return [rootClass.PropertyEnums.First().Values]; - var propertyFields = rootClass.PropertyFields; - for (int i = 0; i < propertyFields.Count; i++) - { - template.Add($"\"{propertyFields[i].Name}\""); - template.Add(":"); - template.Add($"#{propertyFields[i].Type}"); - - if (i == propertyFields.Count - 1) template.Add("}"); - else template.Add(","); - } - - for (int i = 0; i < template.Count; i++) - { - var type = template[i]; - if (!type.Contains("#")) continue; - - var foundClass = requestClasses.Where(c => c.Id == type.Replace("#", string.Empty)); - if (foundClass.Any()) - { - if (foundClass.First().PropertyEnums.Any()) - { - template[i] = string.Join(string.Empty, CreateTemplate([.. foundClass])); - } - else - { - AvaloniaList classes = [.. requestClasses]; - classes.Remove(rootClass); - template[i] = string.Join(string.Empty, CreateTemplate(classes)); - } - } - else - { - template[i] = GetRequestDefaultValue(type); - } - } - - return template; - } - - private static string GetRequestDefaultValue(string type) - { - var defaultValue = string.Empty; - if (type.Contains("[]")) defaultValue = "[]"; - else if (type.Contains("string")) defaultValue = "\"\""; - else if (type.Contains("boolean")) defaultValue = "false"; - else if (type.Contains("integer")) defaultValue = "0"; - else if (type.Contains("double") || type.Contains("float")) defaultValue = "0.0"; - else if (type.Contains("object")) defaultValue = "{}"; - return defaultValue; - } - - private string? GetRequestBodyType(OpenApiRequestBody? requestBody) - { - if (requestBody == null) return null; - if (requestBody.Content.TryGetValue("application/json", out var media)) - { - var schema = media.Schema; - if (schema == null) return null; // Because "PostLolAccountVerificationV1SendDeactivationPin" exists where the media body is empty... - return GetSchemaType(schema); - } - return null; - } - - private AvaloniaList GetParameters(IList parameters, ParameterLocation location) - { - var pathParameters = new AvaloniaList(); - foreach (var parameter in parameters) - { - if (parameter.In != location) continue; - pathParameters.Add(new ParameterViewModel(parameter.Name, GetSchemaType(parameter.Schema), parameter.Required)); - } - - return pathParameters; - } - - private AvaloniaList GetResponseClasses(OpenApiResponses responses) - { - if (responses.TryGetValue("2XX", out var response) - && response.Content.TryGetValue("application/json", out var media)) - { - var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response; - var schema = media.Schema; - AvaloniaList propertyClasses = []; - WalkSchema(schema, propertyClasses, document); - return propertyClasses; - } - return []; - } - - private void WalkSchema(OpenApiSchema schema, AvaloniaList propertyClasses, OpenApiDocument document) - { - var type = GetSchemaType(schema); - if (IsComponent(type)) - { - string componentId = GetComponentId(schema); - var componentSchema = document.Components.Schemas[componentId]; - var responseClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum); - - if (propertyClasses.Where(c => c.Id == componentId).Any()) return; // Avoid adding duplicate schemas in classes - propertyClasses.Add(responseClass); - - foreach ((var _, var property) in componentSchema.Properties) - // Check for self-references like "LolLootLootOddsResponse" - // I blame dubble - if (IsComponent(GetSchemaType(property)) && componentId != GetComponentId(property)) - WalkSchema(property, propertyClasses, document); - } - } - - private static string GetComponentId(OpenApiSchema schema) - { - string componentId; - if (schema.Reference != null) componentId = schema.Reference.Id; - else if (schema.Items != null) componentId = schema.Items.Reference.Id; - else componentId = schema.AdditionalProperties.Reference.Id; - return componentId; - } - - private static bool IsComponent(string type) - { - return !(type.Contains("object") - || type.Contains("array") - || type.Contains("bool") - || type.Contains("string") - || type.Contains("integer") - || type.Contains("number")); - } - - private AvaloniaList GetRequestClasses(OpenApiRequestBody? requestBody) - { - if (requestBody == null) return []; - if (requestBody.Content.TryGetValue("application/json", out var media)) - { - var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response; - var schema = media.Schema; - if (schema == null) return []; - - var type = GetSchemaType(media.Schema); - if (IsComponent(type)) - { - var componentId = GetComponentId(schema); - var componentSchema = document.Components.Schemas[componentId]; - AvaloniaList propertyClasses = []; - WalkSchema(componentSchema, propertyClasses, document); - return propertyClasses; - } - } - return []; - } - - private string GetReturnType(OpenApiResponses responses) - { - if (responses.TryGetValue("2XX", out var response) - && response.Content.TryGetValue("application/json", out var media)) - { - var schema = media.Schema; - return GetSchemaType(schema); - } - return "none"; - } - - public static string GetSchemaType(OpenApiSchema schema) - { - if (schema.Reference != null) return schema.Reference.Id; - if (schema.Type == "object" && schema.AdditionalProperties?.Reference != null) return schema.AdditionalProperties.Reference.Id; - if (schema.Type == "integer" || schema.Type == "number") return $"{schema.Type}:{schema.Format}"; - if (schema.Type == "array" && schema.Items.Reference != null) return $"{schema.Items.Reference.Id}[]"; - if (schema.Type == "array" && (schema.Items.Type == "integer" || schema.Items.Type == "number")) return $"{schema.Items.Type}:{schema.Items.Format}[]"; - if (schema.Type == "array") return $"{schema.Items.Type}[]"; - return schema.Type; - } - } -} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/PageBase.cs b/Needlework.Net/ViewModels/PageBase.cs deleted file mode 100644 index d88d8ed..0000000 --- a/Needlework.Net/ViewModels/PageBase.cs +++ /dev/null @@ -1,12 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; - -namespace Needlework.Net.ViewModels -{ - - public abstract partial class PageBase(string displayName, string icon, int index = 0) : ObservableValidator - { - [ObservableProperty] private string _displayName = displayName; - [ObservableProperty] private string _icon = icon; - [ObservableProperty] private int _index = index; - } -} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/AboutViewModel.cs b/Needlework.Net/ViewModels/Pages/AboutViewModel.cs new file mode 100644 index 0000000..7574eec --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/AboutViewModel.cs @@ -0,0 +1,25 @@ +using CommunityToolkit.Mvvm.Input; +using System.Diagnostics; +using System.Net.Http; + +namespace Needlework.Net.ViewModels.Pages; + +public partial class AboutViewModel : PageBase +{ + public HttpClient HttpClient { get; } + + public AboutViewModel(HttpClient httpClient) : base("About", "info-circle") + { + HttpClient = httpClient; + } + + [RelayCommand] + private void OpenUrl(string url) + { + var process = new Process() + { + StartInfo = new ProcessStartInfo(url) { UseShellExecute = true } + }; + process.Start(); + } +} diff --git a/Needlework.Net/ViewModels/Pages/ConsoleViewModel.cs b/Needlework.Net/ViewModels/Pages/ConsoleViewModel.cs new file mode 100644 index 0000000..90d5984 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/ConsoleViewModel.cs @@ -0,0 +1,98 @@ +using Avalonia.Collections; +using BlossomiShymae.GrrrLCU; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Needlework.Net.Messages; +using Needlework.Net.ViewModels.MainWindow; +using System; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Needlework.Net.ViewModels.Pages; + +public partial class ConsoleViewModel : PageBase, IRecipient +{ + public IAvaloniaReadOnlyList RequestMethods { get; } = new AvaloniaList(["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE"]); + public IAvaloniaList RequestPaths { get; } = new AvaloniaList(); + + [ObservableProperty] private bool _isBusy = true; + [ObservableProperty] private bool _isRequestBusy = false; + [ObservableProperty] private string? _requestMethodSelected = "GET"; + [ObservableProperty] private string? _requestPath = null; + [ObservableProperty] private string? _requestBody = null; + [ObservableProperty] private string? _responsePath = null; + [ObservableProperty] private string? _responseStatus = null; + [ObservableProperty] private string? _responseAuthorization = null; + + public ConsoleViewModel() : base("Console", "terminal", -200) + { + WeakReferenceMessenger.Default.Register(this); + } + + [RelayCommand] + private async Task SendRequest() + { + try + { + IsRequestBusy = true; + if (string.IsNullOrEmpty(RequestPath)) throw new Exception("Path is empty."); + + var method = RequestMethodSelected switch + { + "GET" => HttpMethod.Get, + "POST" => HttpMethod.Post, + "PUT" => HttpMethod.Put, + "DELETE" => HttpMethod.Delete, + "HEAD" => HttpMethod.Head, + "PATCH" => HttpMethod.Patch, + "OPTIONS" => HttpMethod.Options, + "TRACE" => HttpMethod.Trace, + _ => throw new Exception("Method is not selected."), + }; + + var processInfo = ProcessFinder.Get(); + var requestBody = WeakReferenceMessenger.Default.Send(new ContentRequestMessage(), "ConsoleRequestEditor").Response; + var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")); + var client = Connector.GetLcuHttpClientInstance(); + var response = await client.SendAsync(new(method, RequestPath) { Content = content }); + var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken); + var responseBody = await response.Content.ReadAsByteArrayAsync(); + + var body = responseBody.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize(responseBody), App.JsonSerializerOptions) : string.Empty; + if (body.Length >= App.MaxCharacters) + { + WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(body)); + WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(string.Empty), nameof(ConsoleViewModel)); + } + else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(body), nameof(ConsoleViewModel)); + + ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}"; + ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{RequestPath}"; + ResponseAuthorization = $"Basic {riotAuthentication.Value}"; + } + catch (Exception ex) + { + WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5)))); + ResponseStatus = null; + ResponsePath = null; + ResponseAuthorization = null; + WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(string.Empty), nameof(ConsoleViewModel)); + } + finally + { + IsRequestBusy = false; + } + } + + public void Receive(DataReadyMessage message) + { + Avalonia.Threading.Dispatcher.UIThread.Invoke(() => + { + RequestPaths.Clear(); + RequestPaths.AddRange(message.Value.Paths); + IsBusy = false; + }); + } +} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointViewModel.cs new file mode 100644 index 0000000..7de549c --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointViewModel.cs @@ -0,0 +1,48 @@ +using Avalonia.Collections; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using Needlework.Net.Messages; +using System; +using System.Linq; + +namespace Needlework.Net.ViewModels.Pages.Endpoints; + +public partial class EndpointViewModel : ObservableObject +{ + public string Endpoint { get; } + public string Title => Endpoint; + + + public IAvaloniaReadOnlyList PathOperations { get; } + [ObservableProperty] private PathOperationViewModel? _selectedPathOperation; + + [ObservableProperty] private string? _search; + public IAvaloniaList FilteredPathOperations { get; } + + public EndpointViewModel(string endpoint) + { + Endpoint = endpoint; + + var handler = WeakReferenceMessenger.Default.Send().Response; + PathOperations = new AvaloniaList(handler.Plugins[endpoint].Select(x => new PathOperationViewModel(x))); + FilteredPathOperations = new AvaloniaList(PathOperations); + } + + partial void OnSearchChanged(string? value) + { + FilteredPathOperations.Clear(); + + if (string.IsNullOrWhiteSpace(value)) + { + FilteredPathOperations.AddRange(PathOperations); + return; + } + FilteredPathOperations.AddRange(PathOperations.Where(o => o.Path.Contains(value, StringComparison.InvariantCultureIgnoreCase))); + } + + partial void OnSelectedPathOperationChanged(PathOperationViewModel? value) + { + if (value == null) return; + WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(value.Operation.RequestTemplate ?? string.Empty, "EndpointRequestEditor"))); + } +} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsContainerViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsContainerViewModel.cs new file mode 100644 index 0000000..225f84e --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsContainerViewModel.cs @@ -0,0 +1,30 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System.Net.Http; + +namespace Needlework.Net.ViewModels.Pages.Endpoints; + +public partial class EndpointsContainerViewModel : PageBase +{ + [ObservableProperty] private ObservableObject _activeViewModel; + [ObservableProperty] private ObservableObject _endpointsViewModel; + [ObservableProperty] private string _title = string.Empty; + + public EndpointsContainerViewModel(HttpClient httpClient) : base("Endpoints", "list-alt", -500) + { + _activeViewModel = _endpointsViewModel = new EndpointsViewModel(httpClient, OnClicked); + } + + private void OnClicked(ObservableObject viewModel) + { + ActiveViewModel = viewModel; + if (viewModel is EndpointViewModel endpoint) Title = endpoint.Title; + } + + [RelayCommand] + private void GoBack() + { + ActiveViewModel = EndpointsViewModel; + Title = string.Empty; + } +} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsViewModel.cs new file mode 100644 index 0000000..69863fd --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsViewModel.cs @@ -0,0 +1,58 @@ +using Avalonia.Collections; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Needlework.Net.Messages; +using System; +using System.Linq; +using System.Net.Http; + +namespace Needlework.Net.ViewModels.Pages.Endpoints; + +public partial class EndpointsViewModel : ObservableObject, IRecipient +{ + public HttpClient HttpClient { get; } + + public string Title => "Endpoints"; + public Action OnClicked; + public IAvaloniaList Plugins { get; } = new AvaloniaList(); + public IAvaloniaList Query { get; } = new AvaloniaList(); + + [ObservableProperty] private bool _isBusy = true; + [ObservableProperty] private string _search = string.Empty; + [ObservableProperty] private string? _selectedQuery = string.Empty; + + public EndpointsViewModel(HttpClient httpClient, Action onClicked) + { + HttpClient = httpClient; + OnClicked = onClicked; + + WeakReferenceMessenger.Default.Register(this); + } + + public void Receive(DataReadyMessage message) + { + IsBusy = false; + Plugins.Clear(); + Plugins.AddRange(message.Value.Plugins.Keys); + Query.Clear(); + Query.AddRange(Plugins); + } + + partial void OnSearchChanged(string value) + { + Query.Clear(); + if (!string.IsNullOrEmpty(Search)) + Query.AddRange(Plugins.Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase))); + else + Query.AddRange(Plugins); + } + + [RelayCommand] + private void OpenEndpoint(string? value) + { + if (string.IsNullOrEmpty(value)) return; + + OnClicked.Invoke(new EndpointViewModel(value)); + } +} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/OperationViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/OperationViewModel.cs new file mode 100644 index 0000000..2e3f899 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/OperationViewModel.cs @@ -0,0 +1,232 @@ +using Avalonia.Collections; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.OpenApi.Models; +using Needlework.Net.Messages; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +namespace Needlework.Net.ViewModels.Pages.Endpoints; + +public partial class OperationViewModel : ObservableObject +{ + public string Summary { get; } + public string Description { get; } + public string ReturnType { get; } + public bool IsRequestBody { get; } + public string? RequestBodyType { get; } + public IAvaloniaReadOnlyList RequestClasses { get; } + public IAvaloniaReadOnlyList ResponseClasses { get; } + public IAvaloniaReadOnlyList PathParameters { get; } + public IAvaloniaReadOnlyList QueryParameters { get; } + public string? RequestTemplate { get; } + + public OperationViewModel(OpenApiOperation operation) + { + Summary = operation.Summary ?? string.Empty; + Description = operation.Description ?? string.Empty; + IsRequestBody = operation.RequestBody != null; + ReturnType = GetReturnType(operation.Responses); + RequestClasses = GetRequestClasses(operation.RequestBody); + ResponseClasses = GetResponseClasses(operation.Responses); + PathParameters = GetParameters(operation.Parameters, ParameterLocation.Path); + QueryParameters = GetParameters(operation.Parameters, ParameterLocation.Query); + RequestBodyType = GetRequestBodyType(operation.RequestBody); + RequestTemplate = GetRequestTemplate(operation.RequestBody); + } + + private string? GetRequestTemplate(OpenApiRequestBody? requestBody) + { + var requestClasses = GetRequestClasses(requestBody); + if (requestClasses.Count == 0) + { + var type = GetRequestBodyType(requestBody); + if (type == null) return null; + return GetRequestDefaultValue(type); + } + + var template = CreateTemplate(requestClasses); + return JsonSerializer.Serialize(JsonSerializer.Deserialize(string.Join(string.Empty, template)), App.JsonSerializerOptions); + } + + private List CreateTemplate(AvaloniaList requestClasses) + { + if (requestClasses.Count == 0) return []; + List template = []; + template.Add("{"); + + var rootClass = requestClasses.First(); + if (rootClass.PropertyEnums.Any()) return [rootClass.PropertyEnums.First().Values]; + var propertyFields = rootClass.PropertyFields; + for (int i = 0; i < propertyFields.Count; i++) + { + template.Add($"\"{propertyFields[i].Name}\""); + template.Add(":"); + template.Add($"#{propertyFields[i].Type}"); + + if (i == propertyFields.Count - 1) template.Add("}"); + else template.Add(","); + } + + for (int i = 0; i < template.Count; i++) + { + var type = template[i]; + if (!type.Contains("#")) continue; + + var foundClass = requestClasses.Where(c => c.Id == type.Replace("#", string.Empty)); + if (foundClass.Any()) + { + if (foundClass.First().PropertyEnums.Any()) + { + template[i] = string.Join(string.Empty, CreateTemplate([.. foundClass])); + } + else + { + AvaloniaList classes = [.. requestClasses]; + classes.Remove(rootClass); + template[i] = string.Join(string.Empty, CreateTemplate(classes)); + } + } + else + { + template[i] = GetRequestDefaultValue(type); + } + } + + return template; + } + + private static string GetRequestDefaultValue(string type) + { + var defaultValue = string.Empty; + if (type.Contains("[]")) defaultValue = "[]"; + else if (type.Contains("string")) defaultValue = "\"\""; + else if (type.Contains("boolean")) defaultValue = "false"; + else if (type.Contains("integer")) defaultValue = "0"; + else if (type.Contains("double") || type.Contains("float")) defaultValue = "0.0"; + else if (type.Contains("object")) defaultValue = "{}"; + return defaultValue; + } + + private string? GetRequestBodyType(OpenApiRequestBody? requestBody) + { + if (requestBody == null) return null; + if (requestBody.Content.TryGetValue("application/json", out var media)) + { + var schema = media.Schema; + if (schema == null) return null; // Because "PostLolAccountVerificationV1SendDeactivationPin" exists where the media body is empty... + return GetSchemaType(schema); + } + return null; + } + + private AvaloniaList GetParameters(IList parameters, ParameterLocation location) + { + var pathParameters = new AvaloniaList(); + foreach (var parameter in parameters) + { + if (parameter.In != location) continue; + pathParameters.Add(new ParameterViewModel(parameter.Name, GetSchemaType(parameter.Schema), parameter.Required)); + } + + return pathParameters; + } + + private AvaloniaList GetResponseClasses(OpenApiResponses responses) + { + if (responses.TryGetValue("2XX", out var response) + && response.Content.TryGetValue("application/json", out var media)) + { + var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response; + var schema = media.Schema; + AvaloniaList propertyClasses = []; + WalkSchema(schema, propertyClasses, document); + return propertyClasses; + } + return []; + } + + private void WalkSchema(OpenApiSchema schema, AvaloniaList propertyClasses, OpenApiDocument document) + { + var type = GetSchemaType(schema); + if (IsComponent(type)) + { + string componentId = GetComponentId(schema); + var componentSchema = document.Components.Schemas[componentId]; + var responseClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum); + + if (propertyClasses.Where(c => c.Id == componentId).Any()) return; // Avoid adding duplicate schemas in classes + propertyClasses.Add(responseClass); + + foreach ((var _, var property) in componentSchema.Properties) + // Check for self-references like "LolLootLootOddsResponse" + // I blame dubble + if (IsComponent(GetSchemaType(property)) && componentId != GetComponentId(property)) + WalkSchema(property, propertyClasses, document); + } + } + + private static string GetComponentId(OpenApiSchema schema) + { + string componentId; + if (schema.Reference != null) componentId = schema.Reference.Id; + else if (schema.Items != null) componentId = schema.Items.Reference.Id; + else componentId = schema.AdditionalProperties.Reference.Id; + return componentId; + } + + private static bool IsComponent(string type) + { + return !(type.Contains("object") + || type.Contains("array") + || type.Contains("bool") + || type.Contains("string") + || type.Contains("integer") + || type.Contains("number")); + } + + private AvaloniaList GetRequestClasses(OpenApiRequestBody? requestBody) + { + if (requestBody == null) return []; + if (requestBody.Content.TryGetValue("application/json", out var media)) + { + var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response; + var schema = media.Schema; + if (schema == null) return []; + + var type = GetSchemaType(media.Schema); + if (IsComponent(type)) + { + var componentId = GetComponentId(schema); + var componentSchema = document.Components.Schemas[componentId]; + AvaloniaList propertyClasses = []; + WalkSchema(componentSchema, propertyClasses, document); + return propertyClasses; + } + } + return []; + } + + private string GetReturnType(OpenApiResponses responses) + { + if (responses.TryGetValue("2XX", out var response) + && response.Content.TryGetValue("application/json", out var media)) + { + var schema = media.Schema; + return GetSchemaType(schema); + } + return "none"; + } + + public static string GetSchemaType(OpenApiSchema schema) + { + if (schema.Reference != null) return schema.Reference.Id; + if (schema.Type == "object" && schema.AdditionalProperties?.Reference != null) return schema.AdditionalProperties.Reference.Id; + if (schema.Type == "integer" || schema.Type == "number") return $"{schema.Type}:{schema.Format}"; + if (schema.Type == "array" && schema.Items.Reference != null) return $"{schema.Items.Reference.Id}[]"; + if (schema.Type == "array" && (schema.Items.Type == "integer" || schema.Items.Type == "number")) return $"{schema.Items.Type}:{schema.Items.Format}[]"; + if (schema.Type == "array") return $"{schema.Items.Type}[]"; + return schema.Type; + } +} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/ParameterViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/ParameterViewModel.cs new file mode 100644 index 0000000..d72b027 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/ParameterViewModel.cs @@ -0,0 +1,19 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Needlework.Net.ViewModels.Pages.Endpoints; + +public partial class ParameterViewModel : ObservableObject +{ + public string Name { get; } + public string Type { get; } + public bool IsRequired { get; } + [ObservableProperty] private string? _value = null; + + public ParameterViewModel(string name, string type, bool isRequired, string? value = null) + { + Name = name; + Type = type; + IsRequired = isRequired; + Value = value; + } +} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/PathOperationViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/PathOperationViewModel.cs new file mode 100644 index 0000000..92f6293 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/PathOperationViewModel.cs @@ -0,0 +1,121 @@ +using Avalonia.Media; +using BlossomiShymae.GrrrLCU; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Needlework.Net.Messages; +using Needlework.Net.Models; +using Needlework.Net.ViewModels.MainWindow; +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Needlework.Net.ViewModels.Pages.Endpoints; + +public partial class PathOperationViewModel : ObservableObject +{ + public string Method { get; } + public SolidColorBrush Color { get; } + public string Path { get; } + public OperationViewModel Operation { get; } + + public ProcessInfo? ProcessInfo { get; } + + [ObservableProperty] private bool _isBusy; + + [ObservableProperty] private Lazy _response; + + public PathOperationViewModel(PathOperation pathOperation) + { + Method = pathOperation.Method.ToUpper(); + Color = new SolidColorBrush(GetColor(Method)); + Path = pathOperation.Path; + Operation = new OperationViewModel(pathOperation.Operation); + Response = new(() => new ResponseViewModel(pathOperation.Path)); + } + + [RelayCommand] + public async Task SendRequest() + { + try + { + IsBusy = true; + + var method = Method switch + { + "GET" => HttpMethod.Get, + "POST" => HttpMethod.Post, + "PUT" => HttpMethod.Put, + "DELETE" => HttpMethod.Delete, + "HEAD" => HttpMethod.Head, + "PATCH" => HttpMethod.Patch, + "OPTIONS" => HttpMethod.Options, + "TRACE" => HttpMethod.Trace, + _ => throw new Exception("Method is missing.") + }; + + var processInfo = ProcessFinder.Get(); + var sb = new StringBuilder(Path); + foreach (var pathParameter in Operation.PathParameters) + { + sb.Replace($"{{{pathParameter.Name}}}", pathParameter.Value); + } + + var firstQueryAdded = false; + foreach (var queryParameter in Operation.QueryParameters) + { + if (!string.IsNullOrWhiteSpace(queryParameter.Value)) + { + sb.Append(firstQueryAdded ? '&' : '?'); + firstQueryAdded = true; + sb.Append($"{queryParameter.Name}={Uri.EscapeDataString(queryParameter.Value)}"); + } + } + var uri = sb.ToString(); + + var requestBody = WeakReferenceMessenger.Default.Send(new ContentRequestMessage(), "EndpointRequestEditor").Response; + var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")); + + var client = Connector.GetLcuHttpClientInstance(); + var response = await client.SendAsync(new(method, uri) { Content = content }); + var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken); + var responseBytes = await response.Content.ReadAsByteArrayAsync(); + + var responseBody = responseBytes.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize(responseBytes), App.JsonSerializerOptions) : string.Empty; + if (responseBody.Length >= App.MaxCharacters) + { + WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(responseBody)); + WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(string.Empty, "EndpointResponseEditor"))); + } + else WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(responseBody, "EndpointResponseEditor"))); + + Response.Value.Status = $"{(int)response.StatusCode} {response.StatusCode}"; + Response.Value.Path = $"https://127.0.0.1:{processInfo.AppPort}{uri}"; + Response.Value.Authentication = Response.Value.Authorization = $"Basic {riotAuthentication.Value}"; + Response.Value.Username = riotAuthentication.Username; + Response.Value.Password = riotAuthentication.Password; + } + catch (Exception ex) + { + WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5)))); + WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(string.Empty, "EndpointResponseEditor"))); + } + finally + { + IsBusy = false; + } + } + + public static Color GetColor(string method) => method switch + { + "GET" => Avalonia.Media.Color.FromRgb(95, 99, 186), + "POST" => Avalonia.Media.Color.FromRgb(103, 186, 95), + "PUT" => Avalonia.Media.Color.FromRgb(186, 139, 95), + "DELETE" => Avalonia.Media.Color.FromRgb(186, 95, 95), + "HEAD" => Avalonia.Media.Color.FromRgb(136, 95, 186), + "PATCH" => Avalonia.Media.Color.FromRgb(95, 186, 139), + _ => throw new InvalidOperationException("Method does not have assigned color.") + }; +} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/PropertyClassViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyClassViewModel.cs new file mode 100644 index 0000000..9bdfc1f --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyClassViewModel.cs @@ -0,0 +1,35 @@ +using Avalonia.Collections; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using System.Collections.Generic; +using System.Linq; + +namespace Needlework.Net.ViewModels.Pages.Endpoints; + +public class PropertyClassViewModel : ObservableObject +{ + public string Id { get; } + public IAvaloniaReadOnlyList PropertyFields { get; } = new AvaloniaList(); + public IAvaloniaReadOnlyList PropertyEnums { get; } = new AvaloniaList(); + + public PropertyClassViewModel(string id, IDictionary properties, IList enumValue) + { + AvaloniaList propertyFields = []; + AvaloniaList propertyEnums = []; + foreach ((var propertyName, var propertySchema) in properties) + { + var type = OperationViewModel.GetSchemaType(propertySchema); + var field = new PropertyFieldViewModel(propertyName, type); + propertyFields.Add(field); + } + if (enumValue != null && enumValue.Any()) + { + var propertyEnum = new PropertyEnumViewModel(enumValue); + propertyEnums.Add(propertyEnum); + } + PropertyFields = propertyFields; + PropertyEnums = propertyEnums; + Id = id; + } +} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/PropertyEnumViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyEnumViewModel.cs new file mode 100644 index 0000000..5171f53 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyEnumViewModel.cs @@ -0,0 +1,16 @@ +using Microsoft.OpenApi.Any; +using System.Collections.Generic; +using System.Linq; + +namespace Needlework.Net.ViewModels.Pages.Endpoints; + +public class PropertyEnumViewModel +{ + public string Type { get; } = "Enum"; + public string Values { get; } + + public PropertyEnumViewModel(IList enumValue) + { + Values = $"[{string.Join(", ", enumValue.Select(x => $"\"{((OpenApiString)x).Value}\"").ToList())}]"; + } +} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/PropertyFieldViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyFieldViewModel.cs new file mode 100644 index 0000000..92bf4fa --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyFieldViewModel.cs @@ -0,0 +1,13 @@ +namespace Needlework.Net.ViewModels.Pages.Endpoints; + +public class PropertyFieldViewModel +{ + public string Name { get; } + public string Type { get; } + + public PropertyFieldViewModel(string name, string type) + { + Name = name; + Type = type; + } +} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/ResponseViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/ResponseViewModel.cs new file mode 100644 index 0000000..3e31434 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/ResponseViewModel.cs @@ -0,0 +1,34 @@ +using BlossomiShymae.GrrrLCU; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Needlework.Net.ViewModels.Pages.Endpoints; + +public partial class ResponseViewModel : ObservableObject +{ + [ObservableProperty] private string? _path; + [ObservableProperty] private string? _status; + [ObservableProperty] private string? _authentication; + [ObservableProperty] private string? _username; + [ObservableProperty] private string? _password; + [ObservableProperty] private string? _authorization; + + public ResponseViewModel(string path) + { + Path = path; + var processInfo = GetProcessInfo(); + if (processInfo != null) + { + var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken); + Path = $"https://127.0.0.1:{processInfo.AppPort}{path}"; + Username = riotAuthentication.Username; + Password = riotAuthentication.Password; + Authorization = $"Basic {riotAuthentication.RawValue}"; + } + } + + private static ProcessInfo? GetProcessInfo() + { + if (ProcessFinder.IsActive()) return ProcessFinder.Get(); + return null; + } +} diff --git a/Needlework.Net/ViewModels/Pages/HomeViewModel.cs b/Needlework.Net/ViewModels/Pages/HomeViewModel.cs new file mode 100644 index 0000000..a12150a --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/HomeViewModel.cs @@ -0,0 +1,19 @@ +using CommunityToolkit.Mvvm.Input; +using System.Diagnostics; + +namespace Needlework.Net.ViewModels.Pages; + +public partial class HomeViewModel : PageBase +{ + public HomeViewModel() : base("Home", "home", int.MinValue) { } + + [RelayCommand] + private void OpenUrl(string url) + { + var process = new Process() + { + StartInfo = new ProcessStartInfo(url) { UseShellExecute = true } + }; + process.Start(); + } +} diff --git a/Needlework.Net/ViewModels/Pages/PageBase.cs b/Needlework.Net/ViewModels/Pages/PageBase.cs new file mode 100644 index 0000000..f172026 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/PageBase.cs @@ -0,0 +1,11 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Needlework.Net.ViewModels.Pages; + + +public abstract partial class PageBase(string displayName, string icon, int index = 0) : ObservableValidator +{ + [ObservableProperty] private string _displayName = displayName; + [ObservableProperty] private string _icon = icon; + [ObservableProperty] private int _index = index; +} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/Websocket/EventViewModel.cs b/Needlework.Net/ViewModels/Pages/Websocket/EventViewModel.cs new file mode 100644 index 0000000..feac14b --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Websocket/EventViewModel.cs @@ -0,0 +1,21 @@ +using BlossomiShymae.GrrrLCU; +using CommunityToolkit.Mvvm.ComponentModel; +using System; + +namespace Needlework.Net.ViewModels.Pages.Websocket; + +public class EventViewModel : ObservableObject +{ + public string Time { get; } + public string Type { get; } + public string Uri { get; } + + public string Key => $"{Time} {Type} {Uri}"; + + public EventViewModel(EventData eventData) + { + Time = $"{DateTime.Now:HH:mm:ss.fff}"; + Type = eventData?.EventType.ToUpper() ?? string.Empty; + Uri = eventData?.Uri ?? string.Empty; + } +} diff --git a/Needlework.Net/ViewModels/Pages/Websocket/WebsocketViewModel.cs b/Needlework.Net/ViewModels/Pages/Websocket/WebsocketViewModel.cs new file mode 100644 index 0000000..fc36805 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Websocket/WebsocketViewModel.cs @@ -0,0 +1,125 @@ +using BlossomiShymae.GrrrLCU; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Needlework.Net.Messages; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Text.Json; +using System.Threading; +using Websocket.Client; + +namespace Needlework.Net.ViewModels.Pages.Websocket; + +public partial class WebsocketViewModel : PageBase +{ + public ObservableCollection EventLog { get; } = []; + public SemaphoreSlim EventLogLock { get; } = new(1, 1); + + [NotifyPropertyChangedFor(nameof(FilteredEventLog))] + [ObservableProperty] private string _search = string.Empty; + [ObservableProperty] private bool _isAttach = true; + [ObservableProperty] private bool _isTail = false; + [ObservableProperty] private EventViewModel? _selectedEventLog = null; + + private Dictionary _events = []; + + public WebsocketClient? Client { get; set; } + + public IReadOnlyList FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))]; + + public WebsocketViewModel() : base("Event Viewer", "plug", -100) + { + EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog)); + var thread = new Thread(InitializeWebsocket) { IsBackground = true }; + thread.Start(); + } + + private void InitializeWebsocket() + { + while (true) + { + try + { + var client = Connector.CreateLcuWebsocketClient(); + client.EventReceived.Subscribe(OnMessage); + client.DisconnectionHappened.Subscribe(OnDisconnection); + client.ReconnectionHappened.Subscribe(OnReconnection); + + client.Start(); + client.Send(new EventMessage(RequestType.Subscribe, EventMessage.Kinds.OnJsonApiEvent)); + Client = client; + return; + } + catch (Exception) { } + Thread.Sleep(TimeSpan.FromSeconds(5)); + } + } + + partial void OnSelectedEventLogChanged(EventViewModel? value) + { + if (value == null) return; + if (_events.TryGetValue(value.Key, out var message)) + { + var text = JsonSerializer.Serialize(message, App.JsonSerializerOptions); + if (text.Length >= App.MaxCharacters) WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(text)); + else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebsocketViewModel)); + } + } + + [RelayCommand] + private void Clear() + { + _events.Clear(); + EventLog.Clear(); + } + + private void OnReconnection(ReconnectionInfo info) + { + Trace.WriteLine($"-- Reconnection --\nType{info.Type}"); + } + + private void OnDisconnection(DisconnectionInfo info) + { + Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}"); + Client?.Dispose(); + var thread = new Thread(InitializeWebsocket) { IsBackground = true }; + thread.Start(); + } + + private void OnMessage(EventMessage message) + { + Avalonia.Threading.Dispatcher.UIThread.Invoke(async () => + { + if (!IsAttach) return; + + var line = new EventViewModel(message.Data!); + + await EventLogLock.WaitAsync(); + try + { + if (EventLog.Count < 1000) + { + EventLog.Add(line); + _events[line.Key] = message; + } + else + { + var _event = EventLog[0]; + EventLog.RemoveAt(0); + _events.Remove(_event.Key); + + EventLog.Add(line); + _events[line.Key] = message; + } + } + finally + { + EventLogLock.Release(); + } + }); + } +} diff --git a/Needlework.Net/ViewModels/ParameterViewModel.cs b/Needlework.Net/ViewModels/ParameterViewModel.cs deleted file mode 100644 index c0fad5b..0000000 --- a/Needlework.Net/ViewModels/ParameterViewModel.cs +++ /dev/null @@ -1,20 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; - -namespace Needlework.Net.ViewModels -{ - public partial class ParameterViewModel : ObservableObject - { - public string Name { get; } - public string Type { get; } - public bool IsRequired { get; } - [ObservableProperty] private string? _value = null; - - public ParameterViewModel(string name, string type, bool isRequired, string? value = null) - { - Name = name; - Type = type; - IsRequired = isRequired; - Value = value; - } - } -} diff --git a/Needlework.Net/ViewModels/PathOperationViewModel.cs b/Needlework.Net/ViewModels/PathOperationViewModel.cs deleted file mode 100644 index aadd3c7..0000000 --- a/Needlework.Net/ViewModels/PathOperationViewModel.cs +++ /dev/null @@ -1,121 +0,0 @@ -using Avalonia.Media; -using BlossomiShymae.GrrrLCU; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using Needlework.Net.Messages; -using Needlework.Net.Models; -using System; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; - -namespace Needlework.Net.ViewModels -{ - public partial class PathOperationViewModel : ObservableObject - { - public string Method { get; } - public SolidColorBrush Color { get; } - public string Path { get; } - public OperationViewModel Operation { get; } - - public ProcessInfo? ProcessInfo { get; } - - [ObservableProperty] private bool _isBusy; - - [ObservableProperty] private Lazy _response; - - public PathOperationViewModel(PathOperation pathOperation) - { - Method = pathOperation.Method.ToUpper(); - Color = new SolidColorBrush(GetColor(Method)); - Path = pathOperation.Path; - Operation = new OperationViewModel(pathOperation.Operation); - Response = new(() => new ResponseViewModel(pathOperation.Path)); - } - - [RelayCommand] - public async Task SendRequest() - { - try - { - IsBusy = true; - - var method = Method switch - { - "GET" => HttpMethod.Get, - "POST" => HttpMethod.Post, - "PUT" => HttpMethod.Put, - "DELETE" => HttpMethod.Delete, - "HEAD" => HttpMethod.Head, - "PATCH" => HttpMethod.Patch, - "OPTIONS" => HttpMethod.Options, - "TRACE" => HttpMethod.Trace, - _ => throw new Exception("Method is missing.") - }; - - var processInfo = ProcessFinder.Get(); - var sb = new StringBuilder(Path); - foreach (var pathParameter in Operation.PathParameters) - { - sb.Replace($"{{{pathParameter.Name}}}", pathParameter.Value); - } - - var firstQueryAdded = false; - foreach (var queryParameter in Operation.QueryParameters) - { - if (!string.IsNullOrWhiteSpace(queryParameter.Value)) - { - sb.Append(firstQueryAdded ? '&' : '?'); - firstQueryAdded = true; - sb.Append($"{queryParameter.Name}={Uri.EscapeDataString(queryParameter.Value)}"); - } - } - var uri = sb.ToString(); - - var requestBody = WeakReferenceMessenger.Default.Send(new ContentRequestMessage(), "EndpointRequestEditor").Response; - var content = new StringContent(requestBody, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")); - - var client = Connector.GetLcuHttpClientInstance(); - var response = await client.SendAsync(new(method, uri) { Content = content }); - var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken); - var responseBytes = await response.Content.ReadAsByteArrayAsync(); - - var responseBody = responseBytes.Length > 0 ? JsonSerializer.Serialize(JsonSerializer.Deserialize(responseBytes), App.JsonSerializerOptions) : string.Empty; - if (responseBody.Length >= App.MaxCharacters) - { - WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(responseBody)); - WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(string.Empty, "EndpointResponseEditor"))); - } - else WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(responseBody, "EndpointResponseEditor"))); - - Response.Value.Status = $"{(int)response.StatusCode} {response.StatusCode}"; - Response.Value.Path = $"https://127.0.0.1:{processInfo.AppPort}{uri}"; - Response.Value.Authentication = Response.Value.Authorization = $"Basic {riotAuthentication.Value}"; - Response.Value.Username = riotAuthentication.Username; - Response.Value.Password = riotAuthentication.Password; - } - catch (Exception ex) - { - WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5)))); - WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(string.Empty, "EndpointResponseEditor"))); - } - finally - { - IsBusy = false; - } - } - - public static Color GetColor(string method) => method switch - { - "GET" => Avalonia.Media.Color.FromRgb(95, 99, 186), - "POST" => Avalonia.Media.Color.FromRgb(103, 186, 95), - "PUT" => Avalonia.Media.Color.FromRgb(186, 139, 95), - "DELETE" => Avalonia.Media.Color.FromRgb(186, 95, 95), - "HEAD" => Avalonia.Media.Color.FromRgb(136, 95, 186), - "PATCH" => Avalonia.Media.Color.FromRgb(95, 186, 139), - _ => throw new InvalidOperationException("Method does not have assigned color.") - }; - } -} diff --git a/Needlework.Net/ViewModels/PropertyClassViewModel.cs b/Needlework.Net/ViewModels/PropertyClassViewModel.cs deleted file mode 100644 index 6d3df9f..0000000 --- a/Needlework.Net/ViewModels/PropertyClassViewModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Avalonia.Collections; -using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; -using System.Collections.Generic; -using System.Linq; - -namespace Needlework.Net.ViewModels -{ - public class PropertyClassViewModel : ObservableObject - { - public string Id { get; } - public IAvaloniaReadOnlyList PropertyFields { get; } = new AvaloniaList(); - public IAvaloniaReadOnlyList PropertyEnums { get; } = new AvaloniaList(); - - public PropertyClassViewModel(string id, IDictionary properties, IList enumValue) - { - AvaloniaList propertyFields = []; - AvaloniaList propertyEnums = []; - foreach ((var propertyName, var propertySchema) in properties) - { - var type = OperationViewModel.GetSchemaType(propertySchema); - var field = new PropertyFieldViewModel(propertyName, type); - propertyFields.Add(field); - } - if (enumValue != null && enumValue.Any()) - { - var propertyEnum = new PropertyEnumViewModel(enumValue); - propertyEnums.Add(propertyEnum); - } - PropertyFields = propertyFields; - PropertyEnums = propertyEnums; - Id = id; - } - } -} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/PropertyEnumViewModel.cs b/Needlework.Net/ViewModels/PropertyEnumViewModel.cs deleted file mode 100644 index 9cb9bc0..0000000 --- a/Needlework.Net/ViewModels/PropertyEnumViewModel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.OpenApi.Any; -using System.Collections.Generic; -using System.Linq; - -namespace Needlework.Net.ViewModels -{ - public class PropertyEnumViewModel - { - public string Type { get; } = "Enum"; - public string Values { get; } - - public PropertyEnumViewModel(IList enumValue) - { - Values = $"[{string.Join(", ", enumValue.Select(x => $"\"{((OpenApiString)x).Value}\"").ToList())}]"; - } - } -} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/PropertyFieldViewModel.cs b/Needlework.Net/ViewModels/PropertyFieldViewModel.cs deleted file mode 100644 index 5631598..0000000 --- a/Needlework.Net/ViewModels/PropertyFieldViewModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Needlework.Net.ViewModels -{ - public class PropertyFieldViewModel - { - public string Name { get; } - public string Type { get; } - - public PropertyFieldViewModel(string name, string type) - { - Name = name; - Type = type; - } - } -} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/ResponseViewModel.cs b/Needlework.Net/ViewModels/ResponseViewModel.cs deleted file mode 100644 index 815cb47..0000000 --- a/Needlework.Net/ViewModels/ResponseViewModel.cs +++ /dev/null @@ -1,35 +0,0 @@ -using BlossomiShymae.GrrrLCU; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace Needlework.Net.ViewModels -{ - public partial class ResponseViewModel : ObservableObject - { - [ObservableProperty] private string? _path; - [ObservableProperty] private string? _status; - [ObservableProperty] private string? _authentication; - [ObservableProperty] private string? _username; - [ObservableProperty] private string? _password; - [ObservableProperty] private string? _authorization; - - public ResponseViewModel(string path) - { - Path = path; - var processInfo = GetProcessInfo(); - if (processInfo != null) - { - var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken); - Path = $"https://127.0.0.1:{processInfo.AppPort}{path}"; - Username = riotAuthentication.Username; - Password = riotAuthentication.Password; - Authorization = $"Basic {riotAuthentication.RawValue}"; - } - } - - private static ProcessInfo? GetProcessInfo() - { - if (ProcessFinder.IsActive()) return ProcessFinder.Get(); - return null; - } - } -} diff --git a/Needlework.Net/ViewModels/WebsocketViewModel.cs b/Needlework.Net/ViewModels/WebsocketViewModel.cs deleted file mode 100644 index f3f3478..0000000 --- a/Needlework.Net/ViewModels/WebsocketViewModel.cs +++ /dev/null @@ -1,126 +0,0 @@ -using BlossomiShymae.GrrrLCU; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using Needlework.Net.Messages; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Linq; -using System.Text.Json; -using System.Threading; -using Websocket.Client; - -namespace Needlework.Net.ViewModels -{ - public partial class WebsocketViewModel : PageBase - { - public ObservableCollection EventLog { get; } = []; - public SemaphoreSlim EventLogLock { get; } = new(1, 1); - - [NotifyPropertyChangedFor(nameof(FilteredEventLog))] - [ObservableProperty] private string _search = string.Empty; - [ObservableProperty] private bool _isAttach = true; - [ObservableProperty] private bool _isTail = false; - [ObservableProperty] private EventViewModel? _selectedEventLog = null; - - private Dictionary _events = []; - - public WebsocketClient? Client { get; set; } - - public IReadOnlyList FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))]; - - public WebsocketViewModel() : base("Event Viewer", "plug", -100) - { - EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog)); - var thread = new Thread(InitializeWebsocket) { IsBackground = true }; - thread.Start(); - } - - private void InitializeWebsocket() - { - while (true) - { - try - { - var client = Connector.CreateLcuWebsocketClient(); - client.EventReceived.Subscribe(OnMessage); - client.DisconnectionHappened.Subscribe(OnDisconnection); - client.ReconnectionHappened.Subscribe(OnReconnection); - - client.Start(); - client.Send(new EventMessage(RequestType.Subscribe, EventMessage.Kinds.OnJsonApiEvent)); - Client = client; - return; - } - catch (Exception) { } - Thread.Sleep(TimeSpan.FromSeconds(5)); - } - } - - partial void OnSelectedEventLogChanged(EventViewModel? value) - { - if (value == null) return; - if (_events.TryGetValue(value.Key, out var message)) - { - var text = JsonSerializer.Serialize(message, App.JsonSerializerOptions); - if (text.Length >= App.MaxCharacters) WeakReferenceMessenger.Default.Send(new OopsiesDialogRequestedMessage(text)); - else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebsocketViewModel)); - } - } - - [RelayCommand] - private void Clear() - { - _events.Clear(); - EventLog.Clear(); - } - - private void OnReconnection(ReconnectionInfo info) - { - Trace.WriteLine($"-- Reconnection --\nType{info.Type}"); - } - - private void OnDisconnection(DisconnectionInfo info) - { - Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}"); - Client?.Dispose(); - var thread = new Thread(InitializeWebsocket) { IsBackground = true }; - thread.Start(); - } - - private void OnMessage(EventMessage message) - { - Avalonia.Threading.Dispatcher.UIThread.Invoke(async () => - { - if (!IsAttach) return; - - var line = new EventViewModel(message.Data!); - - await EventLogLock.WaitAsync(); - try - { - if (EventLog.Count < 1000) - { - EventLog.Add(line); - _events[line.Key] = message; - } - else - { - var _event = EventLog[0]; - EventLog.RemoveAt(0); - _events.Remove(_event.Key); - - EventLog.Add(line); - _events[line.Key] = message; - } - } - finally - { - EventLogLock.Release(); - } - }); - } - } -} diff --git a/Needlework.Net/Views/EndpointsView.axaml.cs b/Needlework.Net/Views/EndpointsView.axaml.cs deleted file mode 100644 index fa0b3ac..0000000 --- a/Needlework.Net/Views/EndpointsView.axaml.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Avalonia.Controls; - -namespace Needlework.Net.Views -{ - public partial class EndpointsView : UserControl - { - public EndpointsView() - { - InitializeComponent(); - } - } -} diff --git a/Needlework.Net/Views/HomeView.axaml.cs b/Needlework.Net/Views/HomeView.axaml.cs deleted file mode 100644 index e56972e..0000000 --- a/Needlework.Net/Views/HomeView.axaml.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Avalonia.Controls; - -namespace Needlework.Net.Views -{ - public partial class HomeView : UserControl - { - public HomeView() - { - InitializeComponent(); - } - } -} diff --git a/Needlework.Net/Views/MainWindow.axaml b/Needlework.Net/Views/MainWindow/MainWindowView.axaml similarity index 96% rename from Needlework.Net/Views/MainWindow.axaml rename to Needlework.Net/Views/MainWindow/MainWindowView.axaml index 6eebe20..f954cba 100644 --- a/Needlework.Net/Views/MainWindow.axaml +++ b/Needlework.Net/Views/MainWindow/MainWindowView.axaml @@ -7,9 +7,9 @@ xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives" xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:i="https://github.com/projektanker/icons.avalonia" - xmlns:vm="using:Needlework.Net.ViewModels" + xmlns:vm="using:Needlework.Net.ViewModels.MainWindow" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Needlework.Net.Views.MainWindow" + x:Class="Needlework.Net.Views.MainWindow.MainWindowView" x:DataType="vm:MainWindowViewModel" Title="Needlework.Net" Icon="/Assets/app.ico" diff --git a/Needlework.Net/Views/MainWindow.axaml.cs b/Needlework.Net/Views/MainWindow/MainWindowView.axaml.cs similarity index 53% rename from Needlework.Net/Views/MainWindow.axaml.cs rename to Needlework.Net/Views/MainWindow/MainWindowView.axaml.cs index 80eb8ac..ec6a532 100644 --- a/Needlework.Net/Views/MainWindow.axaml.cs +++ b/Needlework.Net/Views/MainWindow/MainWindowView.axaml.cs @@ -1,10 +1,10 @@ using FluentAvalonia.UI.Windowing; -namespace Needlework.Net.Views; +namespace Needlework.Net.Views.MainWindow; -public partial class MainWindow : AppWindow +public partial class MainWindowView : AppWindow { - public MainWindow() + public MainWindowView() { InitializeComponent(); diff --git a/Needlework.Net/Views/MainWindow/OopsiesDialog.cs b/Needlework.Net/Views/MainWindow/OopsiesDialog.cs new file mode 100644 index 0000000..b2b306d --- /dev/null +++ b/Needlework.Net/Views/MainWindow/OopsiesDialog.cs @@ -0,0 +1,64 @@ +using FluentAvalonia.UI.Controls; +using Needlework.Net.Services; +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; + +namespace Needlework.Net.Views.MainWindow; + +public class OopsiesDialog : IDialog, IDisposable +{ + private bool _isDisposing; + private string? _text; + private ContentDialog _dialog; + + public OopsiesDialog() + { + _dialog = new ContentDialog + { + PrimaryButtonText = "Open", + CloseButtonText = "Cancel", + Title = "Oopsies", + Content = "This response is too large to handle for performance reasons.\nIt can be viewed in an external editor or viewer.", + IsPrimaryButtonEnabled = true, + IsSecondaryButtonEnabled = false, + DefaultButton = ContentDialogButton.Primary + }; + _dialog.PrimaryButtonClick += OnPrimaryButtonClick; + } + + public async Task ShowAsync(object data) + { + _text = (string)data; + var result = await _dialog.ShowAsync(); + return result; + } + + private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + var temp = Path.GetTempFileName().Replace(".tmp", ".json"); + File.WriteAllText(temp, _text); + Process.Start("explorer", "\"" + temp + "\""); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposing) + { + if (disposing) + { + _text = null; + _dialog.PrimaryButtonClick -= OnPrimaryButtonClick; + } + + _isDisposing = true; + } + } +} \ No newline at end of file diff --git a/Needlework.Net/Views/OopsiesDialog.cs b/Needlework.Net/Views/OopsiesDialog.cs deleted file mode 100644 index 8e55a07..0000000 --- a/Needlework.Net/Views/OopsiesDialog.cs +++ /dev/null @@ -1,65 +0,0 @@ -using FluentAvalonia.UI.Controls; -using Needlework.Net.Services; -using System; -using System.Diagnostics; -using System.IO; -using System.Threading.Tasks; - -namespace Needlework.Net.Views -{ - public class OopsiesDialog : IDialog, IDisposable - { - private bool _isDisposing; - private string? _text; - private ContentDialog _dialog; - - public OopsiesDialog() - { - _dialog = new ContentDialog - { - PrimaryButtonText = "Open", - CloseButtonText = "Cancel", - Title = "Oopsies", - Content = "This response is too large to handle for performance reasons.\nIt can be viewed in an external editor or viewer.", - IsPrimaryButtonEnabled = true, - IsSecondaryButtonEnabled = false, - DefaultButton = ContentDialogButton.Primary - }; - _dialog.PrimaryButtonClick += OnPrimaryButtonClick; - } - - public async Task ShowAsync(object data) - { - _text = (string)data; - var result = await _dialog.ShowAsync(); - return result; - } - - private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) - { - var temp = Path.GetTempFileName().Replace(".tmp", ".json"); - File.WriteAllText(temp, _text); - Process.Start("explorer", "\"" + temp + "\""); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_isDisposing) - { - if (disposing) - { - _text = null; - _dialog.PrimaryButtonClick -= OnPrimaryButtonClick; - } - - _isDisposing = true; - } - } - } -} \ No newline at end of file diff --git a/Needlework.Net/Views/AboutView.axaml b/Needlework.Net/Views/Pages/AboutView.axaml similarity index 98% rename from Needlework.Net/Views/AboutView.axaml rename to Needlework.Net/Views/Pages/AboutView.axaml index 9f22b2e..beea255 100644 --- a/Needlework.Net/Views/AboutView.axaml +++ b/Needlework.Net/Views/Pages/AboutView.axaml @@ -2,11 +2,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:vm="using:Needlework.Net.ViewModels" + xmlns:vm="using:Needlework.Net.ViewModels.Pages" xmlns:controls="using:Needlework.Net.Controls" xmlns:i="https://github.com/projektanker/icons.avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Needlework.Net.Views.AboutView" + x:Class="Needlework.Net.Views.Pages.AboutView" x:DataType="vm:AboutViewModel">