diff --git a/Needlework.Net/App.axaml b/Needlework.Net/App.axaml index a0e5438..1829c04 100644 --- a/Needlework.Net/App.axaml +++ b/Needlework.Net/App.axaml @@ -7,12 +7,8 @@ xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" RequestedThemeVariant="Dark"> - - - - - + diff --git a/Needlework.Net/App.axaml.cs b/Needlework.Net/App.axaml.cs index a1bee0d..aa0c9d4 100644 --- a/Needlework.Net/App.axaml.cs +++ b/Needlework.Net/App.axaml.cs @@ -2,18 +2,31 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; -using Microsoft.Extensions.DependencyInjection; +using Flurl.Http.Configuration; +using Needlework.Net.Converters; +using Needlework.Net.Services; using Needlework.Net.ViewModels.MainWindow; +using Needlework.Net.ViewModels.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 System; +using Needlework.Net.Views.Pages.About; +using Needlework.Net.Views.Pages.Console; +using Needlework.Net.Views.Pages.Endpoints; +using Needlework.Net.Views.Pages.Home; +using Needlework.Net.Views.Pages.WebSocket; +using ReactiveUI; +using Splat; +using Splat.Serilog; using System.Text.Json; namespace Needlework.Net; -public partial class App(IServiceProvider serviceProvider) : Application +public partial class App : Application { - private readonly IServiceProvider _serviceProvider = serviceProvider; - public static JsonSerializerOptions JsonSerializerOptions { get; } = new() { WriteIndented = true, @@ -31,15 +44,62 @@ public partial class App(IServiceProvider serviceProvider) : Application public override void OnFrameworkInitializationCompleted() { + RegisterValueConverters(); + RegisterAppServices(); + RegisterViews(); + RegisterViewModels(); + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow = new MainWindowView() - { - DataContext = _serviceProvider.GetRequiredService() - }; + desktop.MainWindow = new MainWindow() { DataContext = Locator.Current.GetService() }; MainWindow = desktop.MainWindow; } base.OnFrameworkInitializationCompleted(); } + + private void RegisterValueConverters() + { + Locator.CurrentMutable.RegisterConstant(new NullableToVisibilityConverter()); + Locator.CurrentMutable.RegisterConstant(new EnumerableToVisibilityConverter()); + } + + private static void RegisterViewModels() + { + Locator.CurrentMutable.RegisterConstant(new HomeViewModel()); + Locator.CurrentMutable.RegisterConstant(new EndpointsViewModel()); + Locator.CurrentMutable.RegisterConstant(new ConsoleViewModel()); + Locator.CurrentMutable.RegisterConstant(new WebSocketViewModel()); + Locator.CurrentMutable.RegisterConstant(new AboutViewModel()); + Locator.CurrentMutable.RegisterConstant(new MainWindowViewModel()); + } + + private static void RegisterViews() + { + Locator.CurrentMutable.Register>(() => new LibraryView()); + Locator.CurrentMutable.Register>(() => new NotificationView()); + Locator.CurrentMutable.Register>(() => new EventView()); + Locator.CurrentMutable.Register>(() => new EndpointTabListView()); + Locator.CurrentMutable.Register>(() => new EndpointTabItemContentView()); + Locator.CurrentMutable.Register>(() => new EndpointSearchDetailsView()); + Locator.CurrentMutable.Register>(() => new PluginView()); + Locator.CurrentMutable.Register>(() => new PropertyClassView()); + Locator.CurrentMutable.Register>(() => new PathOperationView()); + + Locator.CurrentMutable.RegisterConstant>(new HomePage()); + Locator.CurrentMutable.RegisterConstant>(new EndpointsPage()); + Locator.CurrentMutable.RegisterConstant>(new ConsolePage()); + Locator.CurrentMutable.RegisterConstant>(new WebSocketPage()); + Locator.CurrentMutable.RegisterConstant>(new AboutPage()); + } + + private static void RegisterAppServices() + { + Locator.CurrentMutable.UseSerilogFullLogger(Logger.Setup()); + Locator.CurrentMutable.RegisterConstant(new FlurlClientCache() + .Add("GithubClient", "https://api.github.com") + .Add("GithubUserContentClient", "https://raw.githubusercontent.com")); + Locator.CurrentMutable.RegisterConstant(new NotificationService()); + Locator.CurrentMutable.RegisterConstant(new DataSource()); + } } \ No newline at end of file diff --git a/Needlework.Net/Converters/EnumerableToVisibilityConverter.cs b/Needlework.Net/Converters/EnumerableToVisibilityConverter.cs new file mode 100644 index 0000000..7668abd --- /dev/null +++ b/Needlework.Net/Converters/EnumerableToVisibilityConverter.cs @@ -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).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 values && values.Any(); + return true; + } + catch (Exception) + { + result = null; + return false; + } + } + } +} diff --git a/Needlework.Net/Converters/NullableToVisibilityConverter.cs b/Needlework.Net/Converters/NullableToVisibilityConverter.cs new file mode 100644 index 0000000..28d8cab --- /dev/null +++ b/Needlework.Net/Converters/NullableToVisibilityConverter.cs @@ -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; + } + } +} diff --git a/Needlework.Net/DataSource.cs b/Needlework.Net/DataSource.cs index b9eba09..f28a63d 100644 --- a/Needlework.Net/DataSource.cs +++ b/Needlework.Net/DataSource.cs @@ -1,60 +1,53 @@ -using Microsoft.Extensions.Logging; +using FastCache; +using Flurl.Http; +using Flurl.Http.Configuration; using Microsoft.OpenApi.Readers; using Needlework.Net.Models; +using Splat; using System; -using System.Net.Http; using System.Threading.Tasks; namespace Needlework.Net { - public class DataSource + public class DataSource : IEnableLogger { - private readonly ILogger _logger; - private readonly HttpClient _httpClient; - private Document? _lcuSchemaDocument; - private Document? _lolClientDocument; - private readonly TaskCompletionSource _taskCompletionSource = new(); + private readonly OpenApiStreamReader _reader = new(); + private readonly IFlurlClient _githubUserContentClient; - public DataSource(HttpClient httpClient, ILogger logger) + public DataSource(IFlurlClientCache? clients = null) { - _httpClient = httpClient; - _logger = logger; + _githubUserContentClient = clients?.Get("GithubUserContentClient") ?? Locator.Current.GetService()!.Get("GithubUserContentClient")!; } public async Task GetLcuSchemaDocumentAsync() { - await _taskCompletionSource.Task; - return _lcuSchemaDocument ?? throw new InvalidOperationException(); + if (Cached.TryGet(nameof(GetLcuSchemaDocumentAsync), out var cached)) + { + return cached; + } + + var lcuSchemaStream = await _githubUserContentClient.Request("/dysolix/hasagi-types/main/swagger.json") + .GetStreamAsync(); + var lcuSchemaRaw = _reader.Read(lcuSchemaStream, out var _); + var document = new Document(lcuSchemaRaw); + + return cached.Save(document, TimeSpan.FromMinutes(60)); } public async Task GetLolClientDocumentAsync() { - await _taskCompletionSource.Task; - return _lolClientDocument ?? throw new InvalidOperationException(); - } + if (Cached.TryGet(nameof(GetLolClientDocumentAsync), out var cached)) + { + return cached; + } - public async Task InitializeAsync() - { - try - { - var reader = new OpenApiStreamReader(); - var lcuSchemaStream = await _httpClient.GetStreamAsync("https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json"); - var lcuSchemaRaw = reader.Read(lcuSchemaStream, out var _); - _lcuSchemaDocument = new Document(lcuSchemaRaw); + var lolClientStream = await _githubUserContentClient.Request("/AlsoSylv/Irelia/refs/heads/master/schemas/game_schema.json") + .GetStreamAsync(); + var lolClientRaw = _reader.Read(lolClientStream, out var _); + var document = new Document(lolClientRaw); - var lolClientStream = await _httpClient.GetStreamAsync("https://raw.githubusercontent.com/AlsoSylv/Irelia/refs/heads/master/schemas/game_schema.json"); - var lolClientRaw = reader.Read(lolClientStream, out var _); - _lolClientDocument = new Document(lolClientRaw); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to initialize DataSource"); - } - finally - { - _taskCompletionSource.SetResult(true); - } + return cached.Save(document, TimeSpan.FromMinutes(60)); } } } diff --git a/Needlework.Net/Extensions/ServiceCollectionExtensions.cs b/Needlework.Net/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index a2f50af..0000000 --- a/Needlework.Net/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; - -namespace Needlework.Net.Extensions -{ - public static class ServiceCollectionExtensions - { - public static IServiceCollection AddSingletonsFromAssemblies(this ServiceCollection services) - { - var types = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(s => s.GetTypes()) - .Where(p => !p.IsAbstract && typeof(T).IsAssignableFrom(p)); - - foreach (var type in types) services.AddSingleton(typeof(T), type); - - return services; - } - } -} \ No newline at end of file diff --git a/Needlework.Net/Logger.cs b/Needlework.Net/Logger.cs index eb75e86..6f02cd6 100644 --- a/Needlework.Net/Logger.cs +++ b/Needlework.Net/Logger.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Logging; -using Serilog; +using Serilog; using System; using System.IO; using System.Reflection; @@ -8,15 +7,17 @@ namespace Needlework.Net { public static class Logger { - public static void Setup(ILoggingBuilder builder) + 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); - builder.AddSerilog(logger); + + return logger; } public static void LogFatal(UnhandledExceptionEventArgs e) diff --git a/Needlework.Net/Messages/InfoBarUpdateMessage.cs b/Needlework.Net/Messages/InfoBarUpdateMessage.cs deleted file mode 100644 index ed2d354..0000000 --- a/Needlework.Net/Messages/InfoBarUpdateMessage.cs +++ /dev/null @@ -1,9 +0,0 @@ -using CommunityToolkit.Mvvm.Messaging.Messages; -using Needlework.Net.ViewModels.MainWindow; - -namespace Needlework.Net.Messages -{ - public class InfoBarUpdateMessage(InfoBarViewModel vm) : ValueChangedMessage(vm) - { - } -} diff --git a/Needlework.Net/Messages/OopsiesDialogRequestedMessage.cs b/Needlework.Net/Messages/OopsiesDialogRequestedMessage.cs deleted file mode 100644 index 8659bad..0000000 --- a/Needlework.Net/Messages/OopsiesDialogRequestedMessage.cs +++ /dev/null @@ -1,8 +0,0 @@ -using CommunityToolkit.Mvvm.Messaging.Messages; - -namespace Needlework.Net.Messages -{ - public class OopsiesDialogRequestedMessage(string text) : ValueChangedMessage(text) - { - } -} diff --git a/Needlework.Net/Messages/ResponseUpdatedMessage.cs b/Needlework.Net/Messages/ResponseUpdatedMessage.cs deleted file mode 100644 index 847a4c7..0000000 --- a/Needlework.Net/Messages/ResponseUpdatedMessage.cs +++ /dev/null @@ -1,8 +0,0 @@ -using CommunityToolkit.Mvvm.Messaging.Messages; - -namespace Needlework.Net.Messages -{ - public class ResponseUpdatedMessage(string data) : ValueChangedMessage(data) - { - } -} diff --git a/Needlework.Net/Models/GithubRelease.cs b/Needlework.Net/Models/GithubRelease.cs index 55f58d3..1f00833 100644 --- a/Needlework.Net/Models/GithubRelease.cs +++ b/Needlework.Net/Models/GithubRelease.cs @@ -7,6 +7,6 @@ namespace Needlework.Net.Models [JsonPropertyName("tag_name")] public string TagName { get; set; } = string.Empty; - public bool IsLatest(int version) => int.Parse(TagName.Replace(".", "")) > version; + public bool IsLatest(string assemblyVersion) => int.Parse(TagName.Replace(".", string.Empty)) > int.Parse(assemblyVersion.Replace(".", string.Empty)); } } diff --git a/Needlework.Net/Models/Notification.cs b/Needlework.Net/Models/Notification.cs new file mode 100644 index 0000000..6423c30 --- /dev/null +++ b/Needlework.Net/Models/Notification.cs @@ -0,0 +1,9 @@ +using FluentAvalonia.UI.Controls; +using System; + +namespace Needlework.Net.Models +{ + public record Notification(string Title, string Message, InfoBarSeverity InfoBarSeverity, TimeSpan? Duration = null, string? Url = null) + { + } +} diff --git a/Needlework.Net/Needlework.Net.csproj b/Needlework.Net/Needlework.Net.csproj index 4b40101..9b74112 100644 --- a/Needlework.Net/Needlework.Net.csproj +++ b/Needlework.Net/Needlework.Net.csproj @@ -30,6 +30,7 @@ + @@ -71,14 +72,32 @@ BusyArea.axaml - - MainWindowView.axaml + + MainWindow.axaml - - EndpointsNavigationView.axaml + + AboutPage.axaml - - EndpointView.axaml + + ConsolePage.axaml + + + EndpointTabListView.axaml + + + EndpointTabItemContentView.axaml + + + EndpointsPage.axaml + + + PluginView.axaml + + + HomePage.axaml + + + WebSocketPage.axaml diff --git a/Needlework.Net/Program.cs b/Needlework.Net/Program.cs index 470d40c..386d436 100644 --- a/Needlework.Net/Program.cs +++ b/Needlework.Net/Program.cs @@ -1,13 +1,8 @@ using Avalonia; -using Microsoft.Extensions.DependencyInjection; -using Needlework.Net.Extensions; -using Needlework.Net.Services; -using Needlework.Net.ViewModels.MainWindow; -using Needlework.Net.ViewModels.Pages; +using Avalonia.ReactiveUI; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; using System; -using System.Threading.Tasks; namespace Needlework.Net; @@ -30,42 +25,14 @@ class Program { IconProvider.Current .Register(); - var services = BuildServices(); - Task.Run(async () => await InitializeDataSourceAsync(services)); - return AppBuilder.Configure(() => new App(services)) + return AppBuilder.Configure() .UsePlatformDetect() .WithInterFont() - .With(new Win32PlatformOptions - { - CompositionMode = [Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition] - }) - .With(new MacOSPlatformOptions - { - ShowInDock = true, - }) - .LogToTrace(); - } - - private static async Task InitializeDataSourceAsync(IServiceProvider services) - { - var dataSource = services.GetRequiredService(); - await dataSource.InitializeAsync(); - } - - private static IServiceProvider BuildServices() - { - var builder = new ServiceCollection(); - - builder.AddSingleton(); - builder.AddSingleton(); - builder.AddSingleton(); - builder.AddSingletonsFromAssemblies(); - builder.AddHttpClient(); - builder.AddLogging(Logger.Setup); - - var services = builder.BuildServiceProvider(); - return services; + .With(new Win32PlatformOptions { CompositionMode = [Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition] }) + .With(new MacOSPlatformOptions { ShowInDock = true, }) + .LogToTrace() + .UseReactiveUI(); } private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e) diff --git a/Needlework.Net/Services/NotificationService.cs b/Needlework.Net/Services/NotificationService.cs new file mode 100644 index 0000000..8966042 --- /dev/null +++ b/Needlework.Net/Services/NotificationService.cs @@ -0,0 +1,19 @@ +using FluentAvalonia.UI.Controls; +using Needlework.Net.Models; +using System; +using System.Reactive.Subjects; + +namespace Needlework.Net.Services +{ + public class NotificationService + { + private readonly Subject _notificationSubject = new(); + + public IObservable Notifications { get { return _notificationSubject; } } + + public void Notify(string title, string message, InfoBarSeverity severity, TimeSpan? duration = null, string? url = null) + { + _notificationSubject.OnNext(new Notification(title, message, severity, duration, url)); + } + } +} diff --git a/Needlework.Net/ViewLocator.cs b/Needlework.Net/ViewLocator.cs deleted file mode 100644 index 14f5a17..0000000 --- a/Needlework.Net/ViewLocator.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Controls.Templates; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Reflection; - -namespace Needlework.Net -{ - public class ViewLocator : IDataTemplate - { - private readonly Dictionary _controlCache = []; - - public Control Build(object? data) - { - var name = data?.GetType().Name; - if (name is null) - { - return new TextBlock { Text = "Data is null or has no name." }; - } - if (!name.Contains("ViewModel")) - { - return new TextBlock { Text = "Data name must end with ViewModel." }; - } - - name = name.Replace("ViewModel", "View"); - var type = Assembly.GetExecutingAssembly() - .GetTypes() - .Where(t => t.Name == name) - .FirstOrDefault(); - - if (type is null) - { - return new TextBlock { Text = $"No view for {name}." }; - } - - if (!_controlCache.TryGetValue(data!, out var res)) - { - res ??= (Control)Activator.CreateInstance(type)!; - _controlCache[data!] = res; - } - - res.DataContext = data; - return res; - } - - public bool Match(object? data) => data is INotifyPropertyChanged; - } -} diff --git a/Needlework.Net/ViewModels/MainWindow/InfoBarViewModel.cs b/Needlework.Net/ViewModels/MainWindow/InfoBarViewModel.cs deleted file mode 100644 index 9ccf17d..0000000 --- a/Needlework.Net/ViewModels/MainWindow/InfoBarViewModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Avalonia.Controls; -using CommunityToolkit.Mvvm.ComponentModel; -using FluentAvalonia.UI.Controls; -using System; - -namespace Needlework.Net.ViewModels.MainWindow; - -public partial class InfoBarViewModel : ObservableObject -{ - [ObservableProperty] private string _title; - [ObservableProperty] private bool _isOpen; - [ObservableProperty] private string _message; - [ObservableProperty] private InfoBarSeverity _severity; - [ObservableProperty] private TimeSpan _duration; - [ObservableProperty] private Control? _actionButton; - - public InfoBarViewModel(string title, bool isOpen, string message, InfoBarSeverity severity, TimeSpan duration, Control? actionButton = null) - { - _title = title; - _isOpen = isOpen; - _message = message; - _severity = severity; - _duration = duration; - _actionButton = actionButton; - } -} diff --git a/Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs b/Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs index e1e84bd..f682168 100644 --- a/Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs +++ b/Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs @@ -1,218 +1,162 @@ -using Avalonia.Collections; -using BlossomiShymae.Briar; +using BlossomiShymae.Briar; using BlossomiShymae.Briar.Utils; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; using FluentAvalonia.UI.Controls; +using Flurl.Http; +using Flurl.Http.Configuration; using Microsoft.Extensions.Logging; -using Needlework.Net.Messages; using Needlework.Net.Models; using Needlework.Net.Services; using Needlework.Net.ViewModels.Pages; -using Needlework.Net.Views.MainWindow; +using ReactiveUI; +using ReactiveUI.SourceGenerators; +using Splat; 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.Reactive; +using System.Reactive.Linq; using System.Reflection; using System.Threading.Tasks; -using System.Timers; namespace Needlework.Net.ViewModels.MainWindow; public partial class MainWindowViewModel - : ObservableObject, IRecipient, IRecipient + : ReactiveObject, IScreen, IEnableLogger { - public IAvaloniaReadOnlyList MenuItems { get; } - [ObservableProperty] private NavigationViewItem _selectedMenuItem; - [ObservableProperty] private PageBase _currentPage; + private readonly IEnumerable _pages; - public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0"; - [ObservableProperty] private bool _isUpdateShown = false; + private readonly IFlurlClient _githubClient; - [ObservableProperty] private string _schemaVersion = "N/A"; - [ObservableProperty] private string _schemaVersionLatest = "N/A"; - - public HttpClient HttpClient { get; } - public DialogService DialogService { get; } + private readonly NotificationService _notificationService; private readonly DataSource _dataSource; - [ObservableProperty] private bool _isBusy = true; + private readonly IDisposable _checkForUpdatesDisposable; - [ObservableProperty] private ObservableCollection _infoBarItems = []; + private readonly IDisposable _checkForSchemaVersionDisposable; - private readonly ILogger _logger; - - private readonly System.Timers.Timer _latestUpdateTimer = new() + public MainWindowViewModel(IEnumerable? pages = null, IFlurlClientCache? clients = null, NotificationService? notificationService = null, DataSource? dataSource = null) { - Interval = TimeSpan.FromMinutes(10).TotalMilliseconds, - Enabled = true - }; + _pages = pages ?? Locator.Current.GetServices(); + _githubClient = clients?.Get("GithubClient") ?? Locator.Current.GetService()!.Get("GithubClient"); + _notificationService = notificationService ?? Locator.Current.GetService()!; + _dataSource = dataSource ?? Locator.Current.GetService()!; - private readonly System.Timers.Timer _schemaVersionTimer = new() - { - Interval = TimeSpan.FromSeconds(5).TotalMilliseconds, - Enabled = true - }; - private bool _isSchemaVersionChecked = false; - - public MainWindowViewModel(IEnumerable pages, HttpClient httpClient, DialogService dialogService, ILogger logger, DataSource dataSource) - { - _logger = logger; - _dataSource = dataSource; - - MenuItems = new AvaloniaList(pages + PageItems = _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]; - CurrentPage = (PageBase)MenuItems[0].Tag!; + .Select(ToNavigationViewItem) + .ToList(); - HttpClient = httpClient; - DialogService = dialogService; + SelectedPageItem = PageItems.First(); - WeakReferenceMessenger.Default.RegisterAll(this); + this.WhenAnyValue(x => x.SelectedPageItem) + .Subscribe(x => Router.Navigate.Execute((IRoutableViewModel)x.Tag!)); - _latestUpdateTimer.Elapsed += OnLatestUpdateTimerElapsed; - _schemaVersionTimer.Elapsed += OnSchemaVersionTimerElapsed; - _latestUpdateTimer.Start(); - _schemaVersionTimer.Start(); - OnLatestUpdateTimerElapsed(null, null); - OnSchemaVersionTimerElapsed(null, null); + _notificationService.Notifications.Subscribe(async notification => + { + var vm = new NotificationViewModel(notification); + Notifications.Add(vm); + await Task.Delay(notification.Duration ?? TimeSpan.FromSeconds(10)); + Notifications.Remove(vm); + }); + CheckForUpdatesCommand.ThrownExceptions.Subscribe(ex => + { + var message = "Failed to check for updates. Please check your internet connection or try again later."; + this.Log() + .Error(ex, message); + _notificationService.Notify("Needlework.Net", message, InfoBarSeverity.Error); + _checkForUpdatesDisposable?.Dispose(); + }); + + _checkForUpdatesDisposable = Observable.Timer(TimeSpan.Zero, TimeSpan.FromMinutes(10)) + .Select(time => Unit.Default) + .InvokeCommand(this, x => x.CheckForUpdatesCommand); + + CheckForSchemaVersionCommand.ThrownExceptions.Subscribe(ex => + { + var message = "Failed to check for schema version. Please check your internet connection or try again later."; + this.Log() + .Error(ex, message); + _notificationService.Notify("Needlework.Net", message, InfoBarSeverity.Error); + _checkForSchemaVersionDisposable?.Dispose(); + }); + + _checkForSchemaVersionDisposable = Observable.Timer(TimeSpan.Zero, TimeSpan.FromMinutes(10)) + .Select(time => Unit.Default) + .InvokeCommand(this, x => x.CheckForSchemaVersionCommand); } - partial void OnSelectedMenuItemChanged(NavigationViewItem value) + [Reactive] + private RoutingState _router = new(); + + [Reactive] + private ObservableCollection _notifications = []; + + [Reactive] + private NavigationViewItem _selectedPageItem; + + public List PageItems = []; + + public bool IsSchemaVersionChecked { get; private set; } = false; + + public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0"; + + private NavigationViewItem ToNavigationViewItem(PageBase page) => new() { - if (value.Tag is PageBase page) + Content = page.DisplayName, + Tag = page, + IconSource = new BitmapIconSource() { UriSource = new Uri($"avares://NeedleworkDotNet/Assets/Icons/{page.Icon}.png") } + }; + + [ReactiveCommand] + private async Task CheckForUpdatesAsync() + { + var release = await _githubClient + .Request("/repos/BlossomiShymae/Needlework.Net/releases/latest") + .WithHeader("User-Agent", $"Needlework.Net/{Version}") + .GetJsonAsync(); + + if (release.IsLatest(Version)) { - CurrentPage = page; - if (!page.IsInitialized) - { - Task.Run(page.InitializeAsync); - } + 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"); + _checkForUpdatesDisposable?.Dispose(); } } - private async void OnSchemaVersionTimerElapsed(object? sender, ElapsedEventArgs? e) + + [ReactiveCommand] + private async Task CheckForSchemaVersionAsync() { if (!ProcessFinder.IsPortOpen()) return; + var lcuSchemaDocument = await _dataSource.GetLcuSchemaDocumentAsync(); + var client = Connector.GetLcuHttpClientInstance(); + var currentSemVer = lcuSchemaDocument.Info.Version.Split('.'); + var systemBuild = await client.GetFromJsonAsync("/system/v1/builds") ?? throw new NullReferenceException(); + var latestSemVer = systemBuild.Version.Split('.'); - try + if (!IsSchemaVersionChecked) { - var client = Connector.GetLcuHttpClientInstance(); - - var currentSemVer = lcuSchemaDocument.Info.Version.Split('.'); - var systemBuild = await client.GetFromJsonAsync("/system/v1/builds") ?? throw new NullReferenceException(); - var latestSemVer = systemBuild.Version.Split('.'); - - if (!_isSchemaVersionChecked) - { - _logger.LogInformation("LCU Schema (current): {Version}", lcuSchemaDocument.Info.Version); - _logger.LogInformation("LCU Schema (latest): {Version}", systemBuild.Version); - _isSchemaVersionChecked = true; - } - - bool isVersionMatching = currentSemVer[0] == latestSemVer[0] && currentSemVer[1] == latestSemVer[1]; // Compare major and minor versions - if (!isVersionMatching) - { - Avalonia.Threading.Dispatcher.UIThread.Post(async () => - { - await ShowInfoBarAsync(new("Newer System Build", true, $"LCU Schema is possibly outdated compared to latest system build. Consider submitting a pull request on dysolix/hasagi-types.\nCurrent: {string.Join(".", currentSemVer)}\nLatest: {string.Join(".", latestSemVer)}", InfoBarSeverity.Warning, TimeSpan.FromSeconds(60), new Avalonia.Controls.Button() - { - Command = OpenUrlCommand, - CommandParameter = "https://github.com/dysolix/hasagi-types#updating-the-types", - Content = "Submit PR" - })); - }); - - _schemaVersionTimer.Elapsed -= OnSchemaVersionTimerElapsed; - _schemaVersionTimer.Stop(); - } + this.Log() + .Info("LCU Schema (current): {Version}", lcuSchemaDocument.Info.Version); + this.Log() + .Info("LCU Schema (latest): {Version}", systemBuild.Version); + IsSchemaVersionChecked = true; } - catch (Exception ex) + + bool isVersionMatching = currentSemVer[0] == latestSemVer[0] && currentSemVer[1] == latestSemVer[1]; // Compare major and minor versions + if (!isVersionMatching) { - _logger.LogError(ex, "Schema version check failed"); + this.Log() + .Warn("LCU Schema version mismatch: Current {CurrentVersion}, Latest {LatestVersion}", lcuSchemaDocument.Info.Version, systemBuild.Version); + _notificationService.Notify("Needlework.Net", $"LCU Schema is possibly outdated compared to latest system build. Consider submitting a pull request on dysolix/hasagi-types.\nCurrent: {string.Join(".", currentSemVer)}\nLatest: {string.Join(".", latestSemVer)}", InfoBarSeverity.Warning, null, "https://github.com/dysolix/hasagi-types#updating-the-types"); + _checkForSchemaVersionDisposable?.Dispose(); } } - - private async void OnLatestUpdateTimerElapsed(object? sender, ElapsedEventArgs? e) - { - try - { - var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/BlossomiShymae/Needlework.Net/releases/latest"); - request.Headers.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("Needlework.Net", Version)); - - var response = await HttpClient.SendAsync(request); - var release = await response.Content.ReadFromJsonAsync(); - if (release == null) - { - _logger.LogWarning("Release response is null"); - return; - } - - var currentVersion = int.Parse(Version.Replace(".", "")); - - if (release.IsLatest(currentVersion)) - { - Avalonia.Threading.Dispatcher.UIThread.Post(async () => - { - await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(30), new Avalonia.Controls.Button() - { - Command = OpenUrlCommand, - CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases", - Content = "Download" - })); - }); - - _latestUpdateTimer.Elapsed -= OnLatestUpdateTimerElapsed; - _latestUpdateTimer.Stop(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to check for latest version"); - } - } - - [RelayCommand] - private void OpenUrl(string url) - { - var process = new Process() - { - StartInfo = new ProcessStartInfo(url) - { - UseShellExecute = true - } - }; - process.Start(); - } - - public void Receive(InfoBarUpdateMessage message) - { - Avalonia.Threading.Dispatcher.UIThread.Post(async () => await ShowInfoBarAsync(message.Value)); - } - - private async Task ShowInfoBarAsync(InfoBarViewModel vm) - { - InfoBarItems.Add(vm); - await Task.Delay(vm.Duration); - InfoBarItems.Remove(vm); - } - - public void Receive(OopsiesDialogRequestedMessage message) - { - Avalonia.Threading.Dispatcher.UIThread.Invoke(async () => await DialogService.ShowAsync(message.Value)); - } } diff --git a/Needlework.Net/ViewModels/MainWindow/NotificationViewModel.cs b/Needlework.Net/ViewModels/MainWindow/NotificationViewModel.cs new file mode 100644 index 0000000..7a3345a --- /dev/null +++ b/Needlework.Net/ViewModels/MainWindow/NotificationViewModel.cs @@ -0,0 +1,36 @@ +using ReactiveUI; +using ReactiveUI.SourceGenerators; +using System; +using System.Diagnostics; +using System.Linq; +using System.Reactive.Linq; + +namespace Needlework.Net.ViewModels.MainWindow +{ + public partial class NotificationViewModel : ReactiveObject + { + private IObservable _canExecute; + + public NotificationViewModel(Needlework.Net.Models.Notification notification) + { + Notification = notification; + + _canExecute = this.WhenAnyValue(x => x.Notification.Url) + .Select(url => !string.IsNullOrEmpty(url)); + + _isButtonVisibleHelper = _canExecute.ToProperty(this, x => x.IsButtonVisible); + } + + [ObservableAsProperty] + private bool _isButtonVisible = false; + + public Needlework.Net.Models.Notification Notification { get; } + + [ReactiveCommand(CanExecute = nameof(_canExecute))] + public void OpenUrl() + { + var process = new Process() { StartInfo = new() { UseShellExecute = true } }; + process.Start(); + } + } +} diff --git a/Needlework.Net/ViewModels/Pages/About/AboutViewModel.cs b/Needlework.Net/ViewModels/Pages/About/AboutViewModel.cs new file mode 100644 index 0000000..30a03fa --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/About/AboutViewModel.cs @@ -0,0 +1,16 @@ +using ReactiveUI; +using Splat; + +namespace Needlework.Net.ViewModels.Pages.About; + +public partial class AboutViewModel : PageBase +{ + public AboutViewModel(IScreen? screen = null) : base("About", "info-circle") + { + HostScreen = screen ?? Locator.Current.GetService()!; + } + + public override string? UrlPathSegment => "about"; + + public override IScreen HostScreen { get; } +} diff --git a/Needlework.Net/ViewModels/Pages/AboutViewModel.cs b/Needlework.Net/ViewModels/Pages/AboutViewModel.cs deleted file mode 100644 index 7b43894..0000000 --- a/Needlework.Net/ViewModels/Pages/AboutViewModel.cs +++ /dev/null @@ -1,32 +0,0 @@ -using CommunityToolkit.Mvvm.Input; -using System.Diagnostics; -using System.Net.Http; -using System.Threading.Tasks; - -namespace Needlework.Net.ViewModels.Pages; - -public partial class AboutViewModel : PageBase -{ - public HttpClient HttpClient { get; } - - public AboutViewModel(HttpClient httpClient) : base("About", "info-circle") - { - HttpClient = httpClient; - } - - public override Task InitializeAsync() - { - IsInitialized = true; - return Task.CompletedTask; - } - - [RelayCommand] - private void OpenUrl(string url) - { - var process = new Process() - { - StartInfo = new ProcessStartInfo(url) { UseShellExecute = true } - }; - process.Start(); - } -} diff --git a/Needlework.Net/ViewModels/Pages/Console/ConsoleViewModel.cs b/Needlework.Net/ViewModels/Pages/Console/ConsoleViewModel.cs new file mode 100644 index 0000000..e0744d0 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Console/ConsoleViewModel.cs @@ -0,0 +1,66 @@ +using DynamicData; +using Needlework.Net.ViewModels.Shared; +using ReactiveUI; +using ReactiveUI.SourceGenerators; +using Splat; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +namespace Needlework.Net.ViewModels.Pages.Console; + +public partial class ConsoleViewModel : PageBase +{ + private readonly DataSource _dataSource; + + public ConsoleViewModel(IScreen? screen = null, DataSource? dataSource = null) : base("Console", "terminal", -200) + { + _dataSource = dataSource ?? Locator.Current.GetService()!; + _request = new(Endpoints.Tab.LCU); + + HostScreen = screen ?? Locator.Current.GetService()!; + + GetRequestPathsCommand.Subscribe(paths => + { + RequestPaths.Clear(); + RequestPaths.AddRange(paths); + IsBusy = false; + }); + GetRequestPathsCommand.ThrownExceptions.Subscribe(ex => + { + this.Log() + .Error(ex, "Failed to load request paths from LCU Schema document."); + IsBusy = false; + }); + } + + public ObservableCollection RequestMethods { get; } = ["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE"]; + + public override string? UrlPathSegment => "console"; + + public override ReactiveUI.IScreen HostScreen { get; } + + [Reactive] + private ObservableCollection _requestPaths = []; + + [Reactive] + private bool _isBusy = true; + + [Reactive] + private RequestViewModel _request; + + + [ReactiveCommand] + public async Task> GetRequestPathsAsync() + { + var document = await _dataSource.GetLcuSchemaDocumentAsync(); + return document.Paths; + } + + [ReactiveCommand] + private async Task SendRequestAsync() + { + await Request.ExecuteAsync(); + } +} diff --git a/Needlework.Net/ViewModels/Pages/ConsoleViewModel.cs b/Needlework.Net/ViewModels/Pages/ConsoleViewModel.cs deleted file mode 100644 index 18efa6b..0000000 --- a/Needlework.Net/ViewModels/Pages/ConsoleViewModel.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Avalonia.Collections; -using Avalonia.Threading; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.Extensions.Logging; -using Needlework.Net.ViewModels.Shared; -using System.Threading.Tasks; - -namespace Needlework.Net.ViewModels.Pages; - -public partial class ConsoleViewModel : PageBase -{ - 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 RequestViewModel _request; - - private readonly DataSource _dataSource; - - public ConsoleViewModel(ILogger requestViewModelLogger, DataSource dataSource) : base("Console", "terminal", -200) - { - _request = new(requestViewModelLogger, Endpoints.Tab.LCU); - _dataSource = dataSource; - } - - public override async Task InitializeAsync() - { - var document = await _dataSource.GetLcuSchemaDocumentAsync(); - Dispatcher.UIThread.Invoke(() => - { - RequestPaths.Clear(); - RequestPaths.AddRange(document.Paths); - }); - IsBusy = false; - IsInitialized = true; - } - - [RelayCommand] - private async Task SendRequest() - { - await Request.ExecuteAsync(); - } -} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointSearchDetailsViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointSearchDetailsViewModel.cs new file mode 100644 index 0000000..0d50e88 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointSearchDetailsViewModel.cs @@ -0,0 +1,35 @@ +using Needlework.Net.Models; +using ReactiveUI; +using ReactiveUI.SourceGenerators; +using System; + +namespace Needlework.Net.ViewModels.Pages.Endpoints +{ + public partial class EndpointSearchDetailsViewModel : ReactiveObject + { + private readonly Document _document; + + private readonly Tab _tab; + + private readonly Action _onClicked; + + + public EndpointSearchDetailsViewModel(Document document, Tab tab, Action onClicked, string? plugin) + { + _document = document; + _tab = tab; + _onClicked = onClicked; + _plugin = plugin; + } + + [Reactive] + private string? _plugin; + + [ReactiveCommand] + private void OpenEndpoint() + { + if (string.IsNullOrEmpty(_plugin)) return; + _onClicked.Invoke(new PluginViewModel(_plugin, _document, _tab)); + } + } +} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsNavigationViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointTabItemContentViewModel.cs similarity index 51% rename from Needlework.Net/ViewModels/Pages/Endpoints/EndpointsNavigationViewModel.cs rename to Needlework.Net/ViewModels/Pages/Endpoints/EndpointTabItemContentViewModel.cs index e1b326d..9cd8615 100644 --- a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsNavigationViewModel.cs +++ b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointTabItemContentViewModel.cs @@ -1,31 +1,36 @@ -using Avalonia.Collections; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.Extensions.Logging; -using Needlework.Net.ViewModels.Shared; +using CommunityToolkit.Mvvm.Input; +using ReactiveUI; +using ReactiveUI.SourceGenerators; using System; +using System.Collections.ObjectModel; namespace Needlework.Net.ViewModels.Pages.Endpoints; -public partial class EndpointsNavigationViewModel : ObservableObject +public partial class EndpointTabItemContentViewModel : ReactiveObject { - public Guid Guid { get; } = Guid.NewGuid(); - - [ObservableProperty] private ObservableObject _activeViewModel; - [ObservableProperty] private ObservableObject _endpointsViewModel; - [ObservableProperty] private string _title; - private readonly Action _onEndpointNavigation; + private readonly Tab _tab; - public EndpointsNavigationViewModel(IAvaloniaList plugins, Action onEndpointNavigation, ILogger requestViewModelLogger, Models.Document document, Tab tab) + public EndpointTabItemContentViewModel(ObservableCollection plugins, Action onEndpointNavigation, Models.Document document, Tab tab) { - _activeViewModel = _endpointsViewModel = new EndpointsViewModel(plugins, OnClicked, requestViewModelLogger, document, tab); + _activeViewModel = _endpointsViewModel = new EndpointTabListViewModel(plugins, OnClicked, document, tab); _onEndpointNavigation = onEndpointNavigation; _tab = tab; _title = GetTitle(tab); } + public Guid Guid { get; } = Guid.NewGuid(); + + [Reactive] + private ReactiveObject _activeViewModel; + + [Reactive] + private ReactiveObject _endpointsViewModel; + + [Reactive] + private string _title; + private string GetTitle(Tab tab) { return tab switch @@ -36,10 +41,10 @@ public partial class EndpointsNavigationViewModel : ObservableObject }; } - private void OnClicked(ObservableObject viewModel) + private void OnClicked(ReactiveObject viewModel) { ActiveViewModel = viewModel; - if (viewModel is EndpointViewModel endpoint) + if (viewModel is PluginViewModel endpoint) { Title = $"{GetTitle(_tab)} - {endpoint.Title}"; _onEndpointNavigation.Invoke(endpoint.Title, Guid); diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointTabItemViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointTabItemViewModel.cs new file mode 100644 index 0000000..49ac7f7 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointTabItemViewModel.cs @@ -0,0 +1,29 @@ +using FluentAvalonia.UI.Controls; +using ReactiveUI; +using ReactiveUI.SourceGenerators; + +namespace Needlework.Net.ViewModels.Pages.Endpoints +{ + public partial class EndpointTabItemViewModel : ReactiveObject + { + public EndpointTabItemViewModel(EndpointTabItemContentViewModel content, string? header = null, IconSource? iconSource = null, bool? selected = null) + { + _content = content; + _header = header ?? string.Empty; + _iconSource = iconSource ?? new SymbolIconSource() { Symbol = Symbol.Document, FontSize = 20.0, Foreground = Avalonia.Media.Brushes.White }; + _selected = selected ?? false; + } + + [Reactive] + private string _header; + + [Reactive] + private IconSource _iconSource; + + [Reactive] + private bool _selected; + + [Reactive] + private EndpointTabItemContentViewModel _content; + } +} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointTabListViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointTabListViewModel.cs new file mode 100644 index 0000000..beac114 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointTabListViewModel.cs @@ -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 plugins, Action onClicked, Models.Document document, Tab tab) + { + Plugins = new ObservableCollection(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 Plugins { get; } + + [Reactive] + private ObservableCollection _endpointSearchDetails = []; + + [Reactive] + private string _search = string.Empty; +} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointViewModel.cs deleted file mode 100644 index c641347..0000000 --- a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointViewModel.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Avalonia.Collections; -using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.Extensions.Logging; -using Needlework.Net.ViewModels.Shared; -using System; -using System.Linq; - -namespace Needlework.Net.ViewModels.Pages.Endpoints; - -public partial class EndpointViewModel : ObservableObject -{ - public string Endpoint { get; } - public string Title => Endpoint; - - - public IAvaloniaReadOnlyList PathOperations { get; } - [ObservableProperty] private PathOperationViewModel? _selectedPathOperation; - - [ObservableProperty] private string? _search; - public IAvaloniaList FilteredPathOperations { get; } - - public event EventHandler? PathOperationSelected; - - public EndpointViewModel(string endpoint, ILogger requestViewModelLogger, Models.Document document, Tab tab) - { - Endpoint = endpoint; - PathOperations = new AvaloniaList(document.Plugins[endpoint].Select(x => new PathOperationViewModel(x, requestViewModelLogger, document, tab))); - 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; - PathOperationSelected?.Invoke(this, value.Operation.RequestTemplate ?? string.Empty); - } -} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsTabViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsTabViewModel.cs deleted file mode 100644 index 79093f7..0000000 --- a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsTabViewModel.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Avalonia.Collections; -using Avalonia.Threading; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using FluentAvalonia.UI.Controls; -using Microsoft.Extensions.Logging; -using Needlework.Net.Models; -using Needlework.Net.ViewModels.Shared; -using System; -using System.Threading.Tasks; - -namespace Needlework.Net.ViewModels.Pages.Endpoints; - -public enum Tab -{ - LCU, - GameClient -} - -public partial class EndpointsTabViewModel : PageBase -{ - public IAvaloniaList Plugins { get; } = new AvaloniaList(); - public IAvaloniaList Endpoints { get; } = new AvaloniaList(); - - [ObservableProperty] private bool _isBusy = true; - - private readonly ILogger _requestViewModelLogger; - private readonly DataSource _dataSource; - - public EndpointsTabViewModel(ILogger requestViewModelLogger, DataSource dataSource) : base("Endpoints", "list-alt", -500) - { - _requestViewModelLogger = requestViewModelLogger; - _dataSource = dataSource; - } - public override async Task InitializeAsync() - { - await Dispatcher.UIThread.Invoke(async () => await AddEndpoint(Tab.LCU)); - IsBusy = false; - IsInitialized = true; - } - - [RelayCommand] - private async Task AddEndpoint(Tab tab) - { - Document document = tab switch - { - Tab.LCU => await _dataSource.GetLcuSchemaDocumentAsync(), - Tab.GameClient => await _dataSource.GetLolClientDocumentAsync(), - _ => throw new NotImplementedException(), - }; - - Plugins.Clear(); - Plugins.AddRange(document.Plugins.Keys); - - var vm = new EndpointsNavigationViewModel(Plugins, OnEndpointNavigation, _requestViewModelLogger, document, tab); - Endpoints.Add(new() - { - Content = vm, - Header = vm.Title, - Selected = true - }); - } - - private void OnEndpointNavigation(string? title, Guid guid) - { - foreach (var endpoint in Endpoints) - { - if (endpoint.Content.Guid.Equals(guid)) - { - endpoint.Header = endpoint.Content.Title; - break; - } - } - } -} - -public partial class EndpointItem : ObservableObject -{ - [ObservableProperty] private string _header = string.Empty; - public IconSource IconSource { get; set; } = new SymbolIconSource() { Symbol = Symbol.Document, FontSize = 20.0, Foreground = Avalonia.Media.Brushes.White }; - public bool Selected { get; set; } = false; - public required EndpointsNavigationViewModel Content { get; init; } -} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsViewModel.cs index 8a921b1..31eec29 100644 --- a/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsViewModel.cs +++ b/Needlework.Net/ViewModels/Pages/Endpoints/EndpointsViewModel.cs @@ -1,52 +1,82 @@ -using Avalonia.Collections; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.Extensions.Logging; +using DynamicData; using Needlework.Net.Models; -using Needlework.Net.ViewModels.Shared; +using ReactiveUI; +using ReactiveUI.SourceGenerators; +using Splat; using System; -using System.Linq; +using System.Collections.ObjectModel; +using System.Threading.Tasks; namespace Needlework.Net.ViewModels.Pages.Endpoints; -public partial class EndpointsViewModel : ObservableObject +public enum Tab { - public IAvaloniaList Plugins { get; } - public IAvaloniaList Query { get; } - - [ObservableProperty] private string _search = string.Empty; - [ObservableProperty] private string? _selectedQuery = string.Empty; - - public Action OnClicked { get; } - - private readonly ILogger _requestViewModelLogger; - private readonly Document _document; - private readonly Tab _tab; - - public EndpointsViewModel(IAvaloniaList plugins, Action onClicked, ILogger requestViewModelLogger, Models.Document document, Tab tab) - { - Plugins = new AvaloniaList(plugins); - Query = new AvaloniaList(plugins); - OnClicked = onClicked; - _requestViewModelLogger = requestViewModelLogger; - _document = document; - _tab = tab; - } - - partial void OnSearchChanged(string value) - { - Query.Clear(); - if (!string.IsNullOrEmpty(Search)) - Query.AddRange(Plugins.Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase))); - else - Query.AddRange(Plugins); - } - - [RelayCommand] - private void OpenEndpoint(string? value) - { - if (string.IsNullOrEmpty(value)) return; - - OnClicked.Invoke(new EndpointViewModel(value, _requestViewModelLogger, _document, _tab)); - } + LCU, + GameClient } + +public partial class EndpointsViewModel : PageBase, IEnableLogger +{ + public record Endpoint(Document Document, Tab Tab); + + private readonly DataSource _dataSource; + + public EndpointsViewModel(IScreen? screen = null, DataSource? dataSource = null) : base("Endpoints", "list-alt", -500) + { + _dataSource = dataSource ?? Locator.Current.GetService()!; + + HostScreen = screen ?? Locator.Current.GetService()!; + + GetEndpointCommand.Subscribe(endpoint => + { + Plugins.Clear(); + Plugins.AddRange(endpoint.Document.Plugins.Keys); + + var vm = new EndpointTabItemContentViewModel(Plugins, OnEndpointNavigation, endpoint.Document, endpoint.Tab); + EndpointTabItems.Add(new(vm, vm.Title, null, true)); + IsBusy = false; + }); + GetEndpointCommand.ThrownExceptions.Subscribe(ex => + { + this.Log() + .Error(ex, "Failed to get endpoint."); + IsBusy = false; + }); + } + + public override string? UrlPathSegment => "endpoints"; + + public override ReactiveUI.IScreen HostScreen { get; } + + [Reactive] + public ObservableCollection Plugins { get; } = []; + + [Reactive] + public ObservableCollection EndpointTabItems { get; } = []; + + [Reactive] + private bool _isBusy = true; + + [ReactiveCommand] + private async Task GetEndpointAsync(Tab tab) + { + return tab switch + { + Tab.LCU => new(await _dataSource.GetLcuSchemaDocumentAsync(), tab), + Tab.GameClient => new(await _dataSource.GetLolClientDocumentAsync(), tab), + _ => throw new NotImplementedException(), + }; + } + + private void OnEndpointNavigation(string? title, Guid guid) + { + foreach (var endpoint in EndpointTabItems) + { + if (endpoint.Content.Guid.Equals(guid)) + { + endpoint.Header = endpoint.Content.Title; + break; + } + } + } +} \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/OperationViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/OperationViewModel.cs index e45be2f..a89bb3d 100644 --- a/Needlework.Net/ViewModels/Pages/Endpoints/OperationViewModel.cs +++ b/Needlework.Net/ViewModels/Pages/Endpoints/OperationViewModel.cs @@ -1,7 +1,6 @@ -using Avalonia.Collections; -using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models; using Needlework.Net.Models; +using ReactiveUI; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -9,19 +8,8 @@ using System.Text.Json; namespace Needlework.Net.ViewModels.Pages.Endpoints; -public partial class OperationViewModel : ObservableObject +public partial class OperationViewModel : ReactiveObject { - public string Summary { get; } - public string Description { get; } - public string ReturnType { get; } - public bool IsRequestBody { get; } - public string? RequestBodyType { get; } - public IAvaloniaReadOnlyList RequestClasses { get; } - public IAvaloniaReadOnlyList ResponseClasses { get; } - public IAvaloniaReadOnlyList PathParameters { get; } - public IAvaloniaReadOnlyList QueryParameters { get; } - public string? RequestTemplate { get; } - public OperationViewModel(OpenApiOperation operation, Models.Document document) { Summary = operation.Summary ?? string.Empty; @@ -36,6 +24,26 @@ public partial class OperationViewModel : ObservableObject RequestTemplate = GetRequestTemplate(operation.RequestBody, document); } + public List RequestClasses { get; } + + public List ResponseClasses { get; } + + public List PathParameters { get; } + + public List QueryParameters { get; } + + public string? RequestTemplate { get; } + + public string Summary { get; } + + public string Description { get; } + + public string ReturnType { get; } + + public bool IsRequestBody { get; } + + public string? RequestBodyType { get; } + private string? GetRequestTemplate(OpenApiRequestBody? requestBody, Document document) { var requestClasses = GetRequestClasses(requestBody, document); @@ -50,7 +58,7 @@ public partial class OperationViewModel : ObservableObject return JsonSerializer.Serialize(JsonSerializer.Deserialize(string.Join(string.Empty, template)), App.JsonSerializerOptions); } - private List CreateTemplate(AvaloniaList requestClasses) + private List CreateTemplate(List requestClasses) { if (requestClasses.Count == 0) return []; List template = []; @@ -83,7 +91,7 @@ public partial class OperationViewModel : ObservableObject } else { - AvaloniaList classes = [.. requestClasses]; + List classes = [.. requestClasses]; classes.Remove(rootClass); template[i] = string.Join(string.Empty, CreateTemplate(classes)); } @@ -121,9 +129,9 @@ public partial class OperationViewModel : ObservableObject return null; } - private AvaloniaList GetParameters(IList parameters, ParameterLocation location) + private List GetParameters(IList parameters, ParameterLocation location) { - var pathParameters = new AvaloniaList(); + var pathParameters = new List(); foreach (var parameter in parameters) { if (parameter.In != location) continue; @@ -151,7 +159,7 @@ public partial class OperationViewModel : ObservableObject } - private AvaloniaList GetResponseClasses(OpenApiResponses responses, Document document) + private List GetResponseClasses(OpenApiResponses responses, Document document) { if (!TryGetResponse(responses, out var response)) return []; @@ -162,7 +170,7 @@ public partial class OperationViewModel : ObservableObject var schema = media.Schema; if (schema == null) return []; - AvaloniaList propertyClasses = []; + List propertyClasses = []; WalkSchema(schema, propertyClasses, rawDocument); return propertyClasses; } @@ -170,7 +178,7 @@ public partial class OperationViewModel : ObservableObject return []; } - private void WalkSchema(OpenApiSchema schema, AvaloniaList propertyClasses, OpenApiDocument document) + private void WalkSchema(OpenApiSchema schema, List propertyClasses, OpenApiDocument document) { var type = GetSchemaType(schema); if (IsComponent(type)) @@ -209,7 +217,7 @@ public partial class OperationViewModel : ObservableObject || type.Contains("number")); } - private AvaloniaList GetRequestClasses(OpenApiRequestBody? requestBody, Document document) + private List GetRequestClasses(OpenApiRequestBody? requestBody, Document document) { if (requestBody == null) return []; if (requestBody.Content.TryGetValue("application/json", out var media)) @@ -223,7 +231,7 @@ public partial class OperationViewModel : ObservableObject { var componentId = GetComponentId(schema); var componentSchema = rawDocument.Components.Schemas[componentId]; - AvaloniaList propertyClasses = []; + List propertyClasses = []; WalkSchema(componentSchema, propertyClasses, rawDocument); return propertyClasses; } diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/ParameterViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/ParameterViewModel.cs index d72b027..33c986d 100644 --- a/Needlework.Net/ViewModels/Pages/Endpoints/ParameterViewModel.cs +++ b/Needlework.Net/ViewModels/Pages/Endpoints/ParameterViewModel.cs @@ -1,14 +1,10 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using ReactiveUI; +using ReactiveUI.SourceGenerators; namespace Needlework.Net.ViewModels.Pages.Endpoints; -public partial class ParameterViewModel : ObservableObject +public partial class ParameterViewModel : ReactiveObject { - public string Name { get; } - public string Type { get; } - public bool IsRequired { get; } - [ObservableProperty] private string? _value = null; - public ParameterViewModel(string name, string type, bool isRequired, string? value = null) { Name = name; @@ -16,4 +12,13 @@ public partial class ParameterViewModel : ObservableObject IsRequired = isRequired; Value = value; } + + public string Name { get; } + + public string Type { get; } + + public bool IsRequired { get; } + + [Reactive] + private string? _value; } diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/PathOperationViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/PathOperationViewModel.cs index d7acaff..6dbdbe7 100644 --- a/Needlework.Net/ViewModels/Pages/Endpoints/PathOperationViewModel.cs +++ b/Needlework.Net/ViewModels/Pages/Endpoints/PathOperationViewModel.cs @@ -1,30 +1,20 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Microsoft.Extensions.Logging; -using Needlework.Net.Models; +using Needlework.Net.Models; using Needlework.Net.ViewModels.Shared; +using ReactiveUI; +using ReactiveUI.SourceGenerators; using System; using System.Text; using System.Threading.Tasks; namespace Needlework.Net.ViewModels.Pages.Endpoints; -public partial class PathOperationViewModel : ObservableObject +public partial class PathOperationViewModel : ReactiveObject { - public string Path { get; } - public OperationViewModel Operation { get; } - - public string Url { get; } - public string Markdown { get; } - - [ObservableProperty] private bool _isBusy; - [ObservableProperty] private Lazy _request; - - public PathOperationViewModel(PathOperation pathOperation, ILogger requestViewModelLogger, Document document, Tab tab) + public PathOperationViewModel(PathOperation pathOperation, Document document, Tab tab) { Path = pathOperation.Path; Operation = new OperationViewModel(pathOperation.Operation, document); - Request = new(() => new RequestViewModel(requestViewModelLogger, tab) + Request = new(() => new RequestViewModel(tab) { Method = pathOperation.Method.ToUpper() }); @@ -32,7 +22,21 @@ public partial class PathOperationViewModel : ObservableObject Markdown = $"[{pathOperation.Method.ToUpper()} {Path}]({Url})"; } - [RelayCommand] + public string Path { get; } + + public OperationViewModel Operation { get; } + + public string Url { get; } + + public string Markdown { get; } + + [Reactive] + private bool _isBusy; + + [Reactive] + private Lazy _request; + + [ReactiveCommand] private async Task SendRequest() { var sb = new StringBuilder(Path); @@ -56,13 +60,13 @@ public partial class PathOperationViewModel : ObservableObject await Request.Value.ExecuteAsync(); } - [RelayCommand] + [ReactiveCommand] private void CopyUrl() { App.MainWindow?.Clipboard?.SetTextAsync(Url); } - [RelayCommand] + [ReactiveCommand] private void CopyMarkdown() { App.MainWindow?.Clipboard?.SetTextAsync(Markdown); diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/PluginViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/PluginViewModel.cs new file mode 100644 index 0000000..3961e64 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Endpoints/PluginViewModel.cs @@ -0,0 +1,59 @@ +using DynamicData; +using ReactiveUI; +using ReactiveUI.SourceGenerators; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Subjects; + +namespace Needlework.Net.ViewModels.Pages.Endpoints; + +public partial class PluginViewModel : ReactiveObject +{ + private readonly Subject _pathOperationSelectedSubject = new(); + + public PluginViewModel(string endpoint, Models.Document document, Tab tab) + { + Endpoint = endpoint; + PathOperations = [.. document.Plugins[endpoint].Select(x => new PathOperationViewModel(x, document, tab))]; + FilteredPathOperations = new ObservableCollection(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 PathOperationSelected { get { return _pathOperationSelectedSubject; } } + + public string Endpoint { get; } + + public string Title => Endpoint; + + public ObservableCollection PathOperations { get; } = []; + + [Reactive] + private ObservableCollection _filteredPathOperations = []; + + [Reactive] + private PathOperationViewModel? _selectedPathOperation; + + [Reactive] + private string? _search; +} diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/PropertyClassViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyClassViewModel.cs index 9bdfc1f..d002ff5 100644 --- a/Needlework.Net/ViewModels/Pages/Endpoints/PropertyClassViewModel.cs +++ b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyClassViewModel.cs @@ -1,22 +1,18 @@ -using Avalonia.Collections; -using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; +using ReactiveUI; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; namespace Needlework.Net.ViewModels.Pages.Endpoints; -public class PropertyClassViewModel : ObservableObject +public class PropertyClassViewModel : ReactiveObject { - 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 = []; + List propertyFields = []; + List propertyEnums = []; foreach ((var propertyName, var propertySchema) in properties) { var type = OperationViewModel.GetSchemaType(propertySchema); @@ -28,8 +24,14 @@ public class PropertyClassViewModel : ObservableObject var propertyEnum = new PropertyEnumViewModel(enumValue); propertyEnums.Add(propertyEnum); } - PropertyFields = propertyFields; - PropertyEnums = propertyEnums; + PropertyFields = [.. propertyFields]; + PropertyEnums = [.. propertyEnums]; Id = id; } + + public string Id { get; } + + public ObservableCollection PropertyFields { get; } = []; + + public ObservableCollection PropertyEnums { get; } = []; } \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/PropertyEnumViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyEnumViewModel.cs index 5171f53..294d978 100644 --- a/Needlework.Net/ViewModels/Pages/Endpoints/PropertyEnumViewModel.cs +++ b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyEnumViewModel.cs @@ -6,11 +6,13 @@ 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())}]"; } + + public string Type { get; } = "Enum"; + + public string Values { get; } + } \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/PropertyFieldViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyFieldViewModel.cs index 92bf4fa..30b2b5b 100644 --- a/Needlework.Net/ViewModels/Pages/Endpoints/PropertyFieldViewModel.cs +++ b/Needlework.Net/ViewModels/Pages/Endpoints/PropertyFieldViewModel.cs @@ -2,12 +2,14 @@ public class PropertyFieldViewModel { - public string Name { get; } - public string Type { get; } - public PropertyFieldViewModel(string name, string type) { Name = name; Type = type; } + + public string Name { get; } + + public string Type { get; } + } \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/Endpoints/ResponseViewModel.cs b/Needlework.Net/ViewModels/Pages/Endpoints/ResponseViewModel.cs index 56b5ad2..77e392d 100644 --- a/Needlework.Net/ViewModels/Pages/Endpoints/ResponseViewModel.cs +++ b/Needlework.Net/ViewModels/Pages/Endpoints/ResponseViewModel.cs @@ -1,17 +1,11 @@ using BlossomiShymae.Briar.Utils; -using CommunityToolkit.Mvvm.ComponentModel; +using ReactiveUI; +using ReactiveUI.SourceGenerators; namespace Needlework.Net.ViewModels.Pages.Endpoints; -public partial class ResponseViewModel : ObservableObject +public partial class ResponseViewModel : ReactiveObject { - [ObservableProperty] private string? _path; - [ObservableProperty] private string? _status; - [ObservableProperty] private string? _authentication; - [ObservableProperty] private string? _username; - [ObservableProperty] private string? _password; - [ObservableProperty] private string? _authorization; - public ResponseViewModel(string path) { Path = path; @@ -26,6 +20,24 @@ public partial class ResponseViewModel : ObservableObject } } + [Reactive] + private string? _path; + + [Reactive] + private string? _status; + + [Reactive] + private string? _authentication; + + [Reactive] + private string? _username; + + [Reactive] + private string? _password; + + [Reactive] + private string? _authorization; + private static ProcessInfo? GetProcessInfo() { if (ProcessFinder.IsActive()) return ProcessFinder.GetProcessInfo(); diff --git a/Needlework.Net/ViewModels/Pages/Home/HomeViewModel.cs b/Needlework.Net/ViewModels/Pages/Home/HomeViewModel.cs new file mode 100644 index 0000000..4d71a57 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Home/HomeViewModel.cs @@ -0,0 +1,26 @@ +using Avalonia.Platform; +using Needlework.Net.Models; +using ReactiveUI; +using Splat; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +namespace Needlework.Net.ViewModels.Pages.Home; + +public partial class HomeViewModel : PageBase +{ + public HomeViewModel(IScreen? screen = null) : base("Home", "home", int.MinValue) + { + Libraries = JsonSerializer.Deserialize>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json")))! + .Select(library => new LibraryViewModel(library)) + .ToList(); + HostScreen = screen ?? Locator.Current.GetService()!; + } + public List Libraries { get; } + + public override string? UrlPathSegment => "home"; + + public override IScreen HostScreen { get; } +} diff --git a/Needlework.Net/ViewModels/Pages/Home/LibraryViewModel.cs b/Needlework.Net/ViewModels/Pages/Home/LibraryViewModel.cs new file mode 100644 index 0000000..b1ab179 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Home/LibraryViewModel.cs @@ -0,0 +1,15 @@ +using Needlework.Net.Models; +using ReactiveUI; + +namespace Needlework.Net.ViewModels.Pages.Home +{ + public class LibraryViewModel : ReactiveObject + { + public LibraryViewModel(Library library) + { + Library = library; + } + + public Library Library { get; } + } +} diff --git a/Needlework.Net/ViewModels/Pages/HomeViewModel.cs b/Needlework.Net/ViewModels/Pages/HomeViewModel.cs deleted file mode 100644 index 756a706..0000000 --- a/Needlework.Net/ViewModels/Pages/HomeViewModel.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Avalonia.Platform; -using CommunityToolkit.Mvvm.Input; -using Needlework.Net.Models; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text.Json; -using System.Threading.Tasks; - -namespace Needlework.Net.ViewModels.Pages; - -public partial class HomeViewModel : PageBase -{ - public List Libraries { get; } = JsonSerializer.Deserialize>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json")))!; - - public HomeViewModel() : base("Home", "home", int.MinValue) { } - - public override Task InitializeAsync() - { - IsInitialized = true; - return Task.CompletedTask; - } - - [RelayCommand] - private void OpenUrl(string url) - { - var process = new Process() - { - StartInfo = new ProcessStartInfo(url) { UseShellExecute = true } - }; - process.Start(); - } - -} diff --git a/Needlework.Net/ViewModels/Pages/PageBase.cs b/Needlework.Net/ViewModels/Pages/PageBase.cs index fff5f1d..bae7a4a 100644 --- a/Needlework.Net/ViewModels/Pages/PageBase.cs +++ b/Needlework.Net/ViewModels/Pages/PageBase.cs @@ -1,15 +1,17 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using System.Threading.Tasks; +using ReactiveUI; namespace Needlework.Net.ViewModels.Pages; -public abstract partial class PageBase(string displayName, string icon, int index = 0) : ObservableValidator +public abstract partial class PageBase(string displayName, string icon, int index = 0) : ReactiveObject, IRoutableViewModel { - [ObservableProperty] private string _displayName = displayName; - [ObservableProperty] private string _icon = icon; - [ObservableProperty] private int _index = index; - [ObservableProperty] private bool _isInitialized; + public string DisplayName { get; } = displayName; - public abstract Task InitializeAsync(); + public string Icon { get; } = icon; + + public int Index { get; } = index; + + public abstract string? UrlPathSegment { get; } + + public abstract IScreen HostScreen { get; } } \ No newline at end of file diff --git a/Needlework.Net/ViewModels/Pages/Websocket/EventViewModel.cs b/Needlework.Net/ViewModels/Pages/Websocket/EventViewModel.cs index bd94649..d5f90fb 100644 --- a/Needlework.Net/ViewModels/Pages/Websocket/EventViewModel.cs +++ b/Needlework.Net/ViewModels/Pages/Websocket/EventViewModel.cs @@ -1,10 +1,10 @@ using BlossomiShymae.Briar.WebSocket.Events; -using CommunityToolkit.Mvvm.ComponentModel; +using ReactiveUI; 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; } diff --git a/Needlework.Net/ViewModels/Pages/Websocket/WebsocketViewModel.cs b/Needlework.Net/ViewModels/Pages/Websocket/WebsocketViewModel.cs index 982d7e9..9a210a3 100644 --- a/Needlework.Net/ViewModels/Pages/Websocket/WebsocketViewModel.cs +++ b/Needlework.Net/ViewModels/Pages/Websocket/WebsocketViewModel.cs @@ -1,84 +1,93 @@ -using Avalonia.Collections; -using BlossomiShymae.Briar; +using BlossomiShymae.Briar; using BlossomiShymae.Briar.WebSocket.Events; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; +using Flurl.Http; +using Flurl.Http.Configuration; using Microsoft.Extensions.Logging; -using Needlework.Net.Messages; +using ReactiveUI; +using ReactiveUI.SourceGenerators; +using Splat; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Net.Http; -using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Websocket.Client; -namespace Needlework.Net.ViewModels.Pages.Websocket; +namespace Needlework.Net.ViewModels.Pages.WebSocket; -public partial class WebsocketViewModel : PageBase +public partial class WebSocketViewModel : PageBase, IEnableLogger { - public ObservableCollection EventLog { get; } = []; - public SemaphoreSlim EventLogLock { get; } = new(1, 1); - - [NotifyPropertyChangedFor(nameof(FilteredEventLog))] - [ObservableProperty] private string _search = string.Empty; - [ObservableProperty] private bool _isAttach = true; - [ObservableProperty] private bool _isTail = false; - [ObservableProperty] private EventViewModel? _selectedEventLog = null; - - [ObservableProperty] private IAvaloniaList _eventTypes = new AvaloniaList(); - [ObservableProperty] private string _eventType = "OnJsonApiEvent"; - private Dictionary _events = []; + private readonly object _tokenLock = new(); + + private readonly IFlurlClient _githubUserContentClient; + + // public IReadOnlyList 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()?.Get("GithubUserContentClient")!; + + HostScreen = screen ?? Locator.Current.GetService()!; + + //EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog)); + //Task.Run(async () => + //{ + // await InitializeEventTypes(); + // InitializeWebsocket(); + //}); + } + + public override string? UrlPathSegment => "websocket"; + + public override ReactiveUI.IScreen HostScreen { get; } + + + public CancellationTokenSource TokenSource { get; set; } = new(); + public WebsocketClient? Client { get; set; } public List ClientDisposables = []; - private readonly object _tokenLock = new(); - public CancellationTokenSource TokenSource { get; set; } = new(); + public SemaphoreSlim EventLogLock { get; } = new(1, 1); - public HttpClient HttpClient { get; } + [Reactive] + public ObservableCollection EventLog { get; } = []; - public IReadOnlyList FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))]; + [Reactive] + private string _search = string.Empty; - private readonly ILogger _logger; + [Reactive] + private bool _isAttach = true; - public WebsocketViewModel(HttpClient httpClient, ILogger logger) : base("Event Viewer", "plug", -100) + [Reactive] + private bool _isTail; + + [Reactive] + private EventViewModel? _selectedEventLog; + + [Reactive] + private ObservableCollection _eventTypes = []; + + [Reactive] + private string _eventType = "OnJsonApiEvent"; + + [ObservableAsProperty] + private ObservableCollection _filteredEventLog = []; + + [ReactiveCommand] + private async Task> GetEventTypesAsync() { - _logger = logger; - HttpClient = httpClient; - EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog)); - Task.Run(async () => - { - await InitializeEventTypes(); - InitializeWebsocket(); - }); - } - - public override Task InitializeAsync() - { - IsInitialized = true; - return Task.CompletedTask; - } - - private async Task InitializeEventTypes() - { - try - { - var file = await HttpClient.GetStringAsync("https://raw.githubusercontent.com/dysolix/hasagi-types/refs/heads/main/dist/lcu-events.d.ts"); - var matches = EventTypesRegex().Matches(file); - Avalonia.Threading.Dispatcher.UIThread.Invoke(() => EventTypes.AddRange(matches.Select(m => m.Groups[1].Value))); - } - catch (HttpRequestException ex) - { - _logger.LogError(ex, "Failed to get event types"); - WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new("Failed to get event types", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(10)))); - } + var file = await _githubUserContentClient.Request("/dysolix/hasagi-types/refs/heads/main/dist/lcu-events.d.ts") + .GetStringAsync(); + var matches = EventTypesRegex().Matches(file); + var eventTypes = matches.Select(m => m.Groups[1].Value) + .ToList(); + return eventTypes; } private void InitializeWebsocket() @@ -87,7 +96,8 @@ public partial class WebsocketViewModel : PageBase { if (Client != null) { - _logger.LogDebug("Disposing old connection"); + this.Log() + .Debug("Disposing old connection"); foreach (var disposable in ClientDisposables) disposable.Dispose(); ClientDisposables.Clear(); @@ -117,23 +127,24 @@ public partial class WebsocketViewModel : PageBase }) { IsBackground = true }; thread.Start(); - _logger.LogDebug("Initialized new connection: {EventType}", EventType); + this.Log() + .Debug("Initialized new connection: {EventType}", EventType); TokenSource = tokenSource; } } - 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)); - } - } + //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] + [ReactiveCommand] private void Clear() { _events.Clear(); @@ -142,19 +153,21 @@ public partial class WebsocketViewModel : PageBase private void OnReconnection(ReconnectionInfo info) { - _logger.LogTrace("Reconnected: {Type}", info.Type); + this.Log() + .Debug("Reconnected: {Type}", info.Type); } private void OnDisconnection(DisconnectionInfo info) { - _logger.LogTrace("Disconnected: {Type}", info.Type); + this.Log() + .Debug("Disconnected: {Type}", info.Type); InitializeWebsocket(); } - partial void OnEventTypeChanged(string value) - { - InitializeWebsocket(); - } + //partial void OnEventTypeChanged(string value) + //{ + // InitializeWebsocket(); + //} private void OnMessage(EventMessage message) { diff --git a/Needlework.Net/ViewModels/Shared/RequestViewModel.cs b/Needlework.Net/ViewModels/Shared/RequestViewModel.cs index 75e4a2a..f58e6eb 100644 --- a/Needlework.Net/ViewModels/Shared/RequestViewModel.cs +++ b/Needlework.Net/ViewModels/Shared/RequestViewModel.cs @@ -1,54 +1,73 @@ using Avalonia.Media; +using AvaloniaEdit.Document; using BlossomiShymae.Briar; using BlossomiShymae.Briar.Utils; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Messaging; +using FluentAvalonia.UI.Controls; using Microsoft.Extensions.Logging; -using Needlework.Net.Messages; -using Needlework.Net.ViewModels.MainWindow; +using Needlework.Net.Services; using Needlework.Net.ViewModels.Pages.Endpoints; +using ReactiveUI; +using ReactiveUI.SourceGenerators; +using Splat; using System; using System.Net.Http; +using System.Reactive.Linq; using System.Text.Json; using System.Threading.Tasks; namespace Needlework.Net.ViewModels.Shared; -public partial class RequestViewModel : ObservableObject +public partial class RequestViewModel : ReactiveObject, IEnableLogger { - [ObservableProperty] private string? _method = "GET"; - [ObservableProperty] private SolidColorBrush _color = new(GetColor("GET")); + private readonly NotificationService _notificationService; - [ObservableProperty] private bool _isRequestBusy = false; - [ObservableProperty] private string? _requestPath = null; - [ObservableProperty] private string? _requestBody = null; - - [ObservableProperty] private string? _responsePath = null; - [ObservableProperty] private string? _responseStatus = null; - [ObservableProperty] private string? _responseAuthentication = null; - [ObservableProperty] private string? _responseUsername = null; - [ObservableProperty] private string? _responsePassword = null; - [ObservableProperty] private string? _responseAuthorization = null; - [ObservableProperty] private string? _responseBody = null; - - public event EventHandler? RequestText; - public event EventHandler? UpdateText; - - private readonly ILogger _logger; private readonly Tab _tab; - public RequestViewModel(ILogger logger, Pages.Endpoints.Tab tab) + public RequestViewModel(Pages.Endpoints.Tab tab, NotificationService? notificationService = null) { - _logger = logger; _tab = tab; + _notificationService = notificationService ?? Locator.Current.GetService()!; + + _colorHelper = this.WhenAnyValue(x => x.Method) + .Select(method => GetSolidCrushBrush(method ?? "GET")) + .ToProperty(this, x => x.Color); } - partial void OnMethodChanged(string? oldValue, string? newValue) - { - if (newValue == null) return; + [ObservableAsProperty] + private SolidColorBrush _color = GetSolidCrushBrush("GET"); - Color = new(GetColor(newValue)); - } + [Reactive] + private string? _method = "GET"; + + [Reactive] + private bool _isRequestBusy; + + [Reactive] + private string? _requestPath; + + [Reactive] + private TextDocument _requestDocument = new(); + + [Reactive] + private string? _responsePath; + + [Reactive] + private string? _responseStatus; + + [Reactive] + private string? _responseAuthentication; + + [Reactive] + private string? _responseUsername; + + [Reactive] + private string? _responsePassword; + + [Reactive] + private string? _responseAuthorization; + + [Reactive] + private TextDocument _responseDocument = new(); public async Task ExecuteAsync() { @@ -74,34 +93,25 @@ public partial class RequestViewModel : ObservableObject throw new Exception("Path is empty."); var method = GetMethod(); - _logger.LogDebug("Sending request: {Tuple}", (Method, RequestPath)); - RequestText?.Invoke(this, this); - var content = new StringContent(RequestBody ?? string.Empty, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")); + this.Log() + .Debug("Sending request: {Tuple}", (Method, RequestPath)); + + var content = new StringContent(RequestDocument.Text, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")); var client = Connector.GetGameHttpClientInstance(); var response = await client.SendAsync(new HttpRequestMessage(method, RequestPath) { Content = content }); 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)); - UpdateText?.Invoke(this, string.Empty); - } - else - { - ResponseBody = body; - UpdateText?.Invoke(this, body); - } + ResponseDocument = new(body); ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}"; ResponsePath = $"https://127.0.0.1:2999{RequestPath}"; } catch (Exception ex) { - _logger.LogError(ex, "Request failed: {Tuple}", (Method, RequestPath)); - WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5)))); - UpdateText?.Invoke(this, string.Empty); + this.Log() + .Error(ex, "Request failed: {Tuple}", (Method, RequestPath)); + _notificationService.Notify("Request Failed", ex.Message, InfoBarSeverity.Error); ResponseStatus = null; ResponsePath = null; @@ -109,7 +119,7 @@ public partial class RequestViewModel : ObservableObject ResponseAuthorization = null; ResponseUsername = null; ResponsePassword = null; - ResponseBody = null; + ResponseDocument = new(); } finally { @@ -126,28 +136,18 @@ public partial class RequestViewModel : ObservableObject throw new Exception("Path is empty."); var method = GetMethod(); - _logger.LogDebug("Sending request: {Tuple}", (Method, RequestPath)); + this.Log() + .Debug("Sending request: {Tuple}", (Method, RequestPath)); var processInfo = ProcessFinder.GetProcessInfo(); - RequestText?.Invoke(this, this); - var content = new StringContent(RequestBody ?? string.Empty, 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.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)); - UpdateText?.Invoke(this, string.Empty); - } - else - { - ResponseBody = body; - UpdateText?.Invoke(this, body); - } + ResponseDocument = new(body); ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}"; ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{RequestPath}"; ResponseAuthentication = riotAuthentication.Value; @@ -157,9 +157,9 @@ public partial class RequestViewModel : ObservableObject } catch (Exception ex) { - _logger.LogError(ex, "Request failed: {Tuple}", (Method, RequestPath)); - WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new InfoBarViewModel("Request Failed", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(5)))); - UpdateText?.Invoke(this, string.Empty); + this.Log() + .Error(ex, "Request failed: {Tuple}", (Method, RequestPath)); + _notificationService.Notify("Request Failed", ex.Message, InfoBarSeverity.Error); ResponseStatus = null; ResponsePath = null; @@ -167,7 +167,7 @@ public partial class RequestViewModel : ObservableObject ResponseAuthorization = null; ResponseUsername = null; ResponsePassword = null; - ResponseBody = null; + ResponseDocument = new(); } finally { @@ -191,14 +191,14 @@ public partial class RequestViewModel : ObservableObject }; } - private static Color GetColor(string method) => method switch + private static SolidColorBrush GetSolidCrushBrush(string? method = null) => new(method switch { - "GET" => Avalonia.Media.Color.FromRgb(95, 99, 186), + "GET" or null => Avalonia.Media.Color.FromRgb(95, 99, 186), "POST" => Avalonia.Media.Color.FromRgb(103, 186, 95), "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/Views/MainWindow/MainWindowView.axaml b/Needlework.Net/Views/MainWindow/MainWindow.axaml similarity index 68% rename from Needlework.Net/Views/MainWindow/MainWindowView.axaml rename to Needlework.Net/Views/MainWindow/MainWindow.axaml index 812a6c7..7e2f6e0 100644 --- a/Needlework.Net/Views/MainWindow/MainWindowView.axaml +++ b/Needlework.Net/Views/MainWindow/MainWindow.axaml @@ -6,10 +6,11 @@ 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.MainWindowView" + x:Class="Needlework.Net.Views.MainWindow.MainWindow" x:DataType="vm:MainWindowViewModel" Title="Needlework.Net" Icon="/Assets/app.ico" @@ -34,14 +35,13 @@ Needlework.Net - + Grid.Row="1"> @@ -53,20 +53,16 @@ - - @@ -46,10 +53,10 @@ Grid.Column="0" RowDefinitions="auto,*" ColumnDefinitions="*"> - + Grid.Column="0"/> - + + + + + + diff --git a/Needlework.Net/Views/Pages/Endpoints/EndpointTabItemContentView.axaml.cs b/Needlework.Net/Views/Pages/Endpoints/EndpointTabItemContentView.axaml.cs new file mode 100644 index 0000000..97d3430 --- /dev/null +++ b/Needlework.Net/Views/Pages/Endpoints/EndpointTabItemContentView.axaml.cs @@ -0,0 +1,25 @@ +using Avalonia.ReactiveUI; +using Needlework.Net.ViewModels.Pages.Endpoints; +using ReactiveUI; +using System.Reactive.Disposables; + +namespace Needlework.Net.Views.Pages.Endpoints; + +public partial class EndpointTabItemContentView : ReactiveUserControl +{ + 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(); + } +} \ No newline at end of file diff --git a/Needlework.Net/Views/Pages/Endpoints/EndpointTabListView.axaml b/Needlework.Net/Views/Pages/Endpoints/EndpointTabListView.axaml new file mode 100644 index 0000000..71b5ed9 --- /dev/null +++ b/Needlework.Net/Views/Pages/Endpoints/EndpointTabListView.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Needlework.Net/Views/Pages/Endpoints/EndpointTabListView.axaml.cs b/Needlework.Net/Views/Pages/Endpoints/EndpointTabListView.axaml.cs new file mode 100644 index 0000000..01758a6 --- /dev/null +++ b/Needlework.Net/Views/Pages/Endpoints/EndpointTabListView.axaml.cs @@ -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 +{ + public EndpointTabListView() + { + this.WhenActivated(disposables => + { + this.Bind(ViewModel, vm => vm.Search, v => v.SearchTextBox.Text) + .DisposeWith(disposables); + }); + + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Needlework.Net/Views/Pages/Endpoints/EndpointView.axaml.cs b/Needlework.Net/Views/Pages/Endpoints/EndpointView.axaml.cs deleted file mode 100644 index 7d6db77..0000000 --- a/Needlework.Net/Views/Pages/Endpoints/EndpointView.axaml.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Styling; -using AvaloniaEdit; -using Needlework.Net.Extensions; -using Needlework.Net.ViewModels.Pages.Endpoints; -using Needlework.Net.ViewModels.Shared; -using TextMateSharp.Grammars; - -namespace Needlework.Net.Views.Pages.Endpoints; - -public partial class EndpointView : UserControl -{ - private TextEditor? _requestEditor; - private TextEditor? _responseEditor; - private RequestViewModel? _lcuRequestVm; - - public EndpointView() - { - InitializeComponent(); - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - _requestEditor = this.FindControl("EndpointRequestEditor"); - _responseEditor = this.FindControl("EndpointResponseEditor"); - _requestEditor?.ApplyJsonEditorSettings(); - _responseEditor?.ApplyJsonEditorSettings(); - - var vm = (EndpointViewModel)DataContext!; - vm.PathOperationSelected += Vm_PathOperationSelected; - - if (vm.SelectedPathOperation != null) - { - _lcuRequestVm = vm.SelectedPathOperation.Request.Value; - vm.SelectedPathOperation.Request.Value.RequestText += LcuRequest_RequestText; - vm.SelectedPathOperation.Request.Value.UpdateText += LcuRequest_UpdateText; - } - - OnBaseThemeChanged(Application.Current!.ActualThemeVariant); - } - - private void Vm_PathOperationSelected(object? sender, string e) - { - var vm = (EndpointViewModel)DataContext!; - if (vm.SelectedPathOperation != null) - { - _requestEditor!.Text = e; - if (_lcuRequestVm != null) - { - _lcuRequestVm.RequestText -= LcuRequest_RequestText; - _lcuRequestVm.UpdateText -= LcuRequest_UpdateText; - } - vm.SelectedPathOperation.Request.Value.RequestText += LcuRequest_RequestText; - vm.SelectedPathOperation.Request.Value.UpdateText += LcuRequest_UpdateText; - _lcuRequestVm = vm.SelectedPathOperation.Request.Value; - _responseEditor!.Text = vm.SelectedPathOperation.Request.Value.ResponseBody ?? string.Empty; - } - } - - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - - var vm = (EndpointViewModel)DataContext!; - vm.PathOperationSelected -= Vm_PathOperationSelected; - - if (_lcuRequestVm != null) - { - _lcuRequestVm.RequestText -= LcuRequest_RequestText; - _lcuRequestVm.UpdateText -= LcuRequest_UpdateText; - _lcuRequestVm = null; - } - } - - private void OnBaseThemeChanged(ThemeVariant currentTheme) - { - var registryOptions = new RegistryOptions( - currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus); - } - - private void LcuRequest_RequestText(object? sender, RequestViewModel e) - { - e.RequestBody = _requestEditor!.Text; - } - - private void LcuRequest_UpdateText(object? sender, string e) - { - _responseEditor!.Text = e; - } - -} \ No newline at end of file diff --git a/Needlework.Net/Views/Pages/Endpoints/EndpointsNavigationView.axaml b/Needlework.Net/Views/Pages/Endpoints/EndpointsNavigationView.axaml deleted file mode 100644 index e3eed3c..0000000 --- a/Needlework.Net/Views/Pages/Endpoints/EndpointsNavigationView.axaml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - diff --git a/Needlework.Net/Views/Pages/Endpoints/EndpointsNavigationView.axaml.cs b/Needlework.Net/Views/Pages/Endpoints/EndpointsNavigationView.axaml.cs deleted file mode 100644 index f82c8e4..0000000 --- a/Needlework.Net/Views/Pages/Endpoints/EndpointsNavigationView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace Needlework.Net.Views.Pages.Endpoints; - -public partial class EndpointsNavigationView : UserControl -{ - public EndpointsNavigationView() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/Needlework.Net/Views/Pages/Endpoints/EndpointsTabView.axaml b/Needlework.Net/Views/Pages/Endpoints/EndpointsPage.axaml similarity index 59% rename from Needlework.Net/Views/Pages/Endpoints/EndpointsTabView.axaml rename to Needlework.Net/Views/Pages/Endpoints/EndpointsPage.axaml index 90d8bbb..3054881 100644 --- a/Needlework.Net/Views/Pages/Endpoints/EndpointsTabView.axaml +++ b/Needlework.Net/Views/Pages/Endpoints/EndpointsPage.axaml @@ -5,18 +5,18 @@ xmlns:i="https://github.com/projektanker/icons.avalonia" Name="EndpointsTab" xmlns:vm="using:Needlework.Net.ViewModels.Pages.Endpoints" - xmlns:ui="using:FluentAvalonia.UI.Controls" + xmlns:reactiveUi="http://reactiveui.net" + xmlns:views="using:Needlework.Net.Views.Pages.Endpoints" + xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:controls="using:Needlework.Net.Controls" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsTabView" - x:DataType="vm:EndpointsTabViewModel"> - + x:Class="Needlework.Net.Views.Pages.Endpoints.EndpointsPage" + x:DataType="vm:EndpointsViewModel"> + - + @@ -39,31 +39,15 @@ - + + + Content="{Binding Content}"> - - - - - - - - - - - + + diff --git a/Needlework.Net/Views/Pages/Endpoints/EndpointsPage.axaml.cs b/Needlework.Net/Views/Pages/Endpoints/EndpointsPage.axaml.cs new file mode 100644 index 0000000..4f70f06 --- /dev/null +++ b/Needlework.Net/Views/Pages/Endpoints/EndpointsPage.axaml.cs @@ -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 +{ + 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); + } + } + } +} \ No newline at end of file diff --git a/Needlework.Net/Views/Pages/Endpoints/EndpointsTabView.axaml.cs b/Needlework.Net/Views/Pages/Endpoints/EndpointsTabView.axaml.cs deleted file mode 100644 index 980961a..0000000 --- a/Needlework.Net/Views/Pages/Endpoints/EndpointsTabView.axaml.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Avalonia.Controls; -using Needlework.Net.ViewModels.Pages.Endpoints; -using System.Collections; - -namespace Needlework.Net.Views.Pages.Endpoints; - -public partial class EndpointsTabView : UserControl -{ - public EndpointsTabView() - { - InitializeComponent(); - } - - private void TabView_TabCloseRequested(FluentAvalonia.UI.Controls.TabView sender, FluentAvalonia.UI.Controls.TabViewTabCloseRequestedEventArgs args) - { - if (args.Tab.Content is EndpointItem item && sender.TabItems is IList tabItems) - { - if (tabItems.Count > 1) - { - tabItems.Remove(item); - } - } - } -} \ No newline at end of file diff --git a/Needlework.Net/Views/Pages/Endpoints/EndpointsView.axaml b/Needlework.Net/Views/Pages/Endpoints/EndpointsView.axaml deleted file mode 100644 index e0f6a12..0000000 --- a/Needlework.Net/Views/Pages/Endpoints/EndpointsView.axaml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - + + + + + + + - - + + diff --git a/Needlework.Net/Views/Pages/Home/HomePage.axaml.cs b/Needlework.Net/Views/Pages/Home/HomePage.axaml.cs new file mode 100644 index 0000000..a075ed6 --- /dev/null +++ b/Needlework.Net/Views/Pages/Home/HomePage.axaml.cs @@ -0,0 +1,15 @@ +using Avalonia.ReactiveUI; +using Needlework.Net.ViewModels.Pages.Home; +using ReactiveUI; + +namespace Needlework.Net.Views.Pages.Home; + +public partial class HomePage : ReactiveUserControl +{ + public HomePage() + { + this.WhenActivated(disposables => { }); + + InitializeComponent(); + } +} diff --git a/Needlework.Net/Views/Pages/Home/LibraryView.axaml b/Needlework.Net/Views/Pages/Home/LibraryView.axaml new file mode 100644 index 0000000..6aef8d0 --- /dev/null +++ b/Needlework.Net/Views/Pages/Home/LibraryView.axaml @@ -0,0 +1,25 @@ + + + + + - + + + + + + diff --git a/Needlework.Net/Views/Pages/Home/LibraryView.axaml.cs b/Needlework.Net/Views/Pages/Home/LibraryView.axaml.cs new file mode 100644 index 0000000..ccc62fd --- /dev/null +++ b/Needlework.Net/Views/Pages/Home/LibraryView.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia.ReactiveUI; +using Needlework.Net.ViewModels.Pages.Home; +using ReactiveUI; +using System.Reactive.Disposables; + +namespace Needlework.Net.Views.Pages.Home; + +public partial class LibraryView : ReactiveUserControl +{ + public LibraryView() + { + this.WhenActivated(disposables => + { + this.OneWayBind(ViewModel, vm => vm.Library.Language, v => v.LanguageRun.Text) + .DisposeWith(disposables); + this.OneWayBind(ViewModel, vm => vm.Library.Repo, v => v.RepoRun.Text) + .DisposeWith(disposables); + this.OneWayBind(ViewModel, vm => vm.Library.Description, v => v.DescriptionTextBlock.Text) + .DisposeWith(disposables); + this.OneWayBind(ViewModel, vm => vm.Library.Description, v => v.DescriptionTextBlock.IsVisible) + .DisposeWith(disposables); + this.OneWayBind(ViewModel, vm => vm.Library.Link, v => v.OpenUrlButton.Content) + .DisposeWith(disposables); + }); + + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Needlework.Net/Views/Pages/HomeView.axaml.cs b/Needlework.Net/Views/Pages/HomeView.axaml.cs deleted file mode 100644 index b05c02a..0000000 --- a/Needlework.Net/Views/Pages/HomeView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace Needlework.Net.Views.Pages; - -public partial class HomeView : UserControl -{ - public HomeView() - { - InitializeComponent(); - } -} diff --git a/Needlework.Net/Views/Pages/WebSocket/EventView.axaml b/Needlework.Net/Views/Pages/WebSocket/EventView.axaml new file mode 100644 index 0000000..28da342 --- /dev/null +++ b/Needlework.Net/Views/Pages/WebSocket/EventView.axaml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/Needlework.Net/Views/Pages/WebSocket/EventView.axaml.cs b/Needlework.Net/Views/Pages/WebSocket/EventView.axaml.cs new file mode 100644 index 0000000..202f96c --- /dev/null +++ b/Needlework.Net/Views/Pages/WebSocket/EventView.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia.ReactiveUI; +using Needlework.Net.ViewModels.Pages.WebSocket; +using ReactiveUI; +using System.Reactive.Disposables; + +namespace Needlework.Net.Views.Pages.WebSocket; + +public partial class EventView : ReactiveUserControl +{ + public EventView() + { + this.WhenActivated(disposables => + { + this.OneWayBind(ViewModel, vm => vm.Time, v => v.TimeTextBlock.Text) + .DisposeWith(disposables); + this.OneWayBind(ViewModel, vm => vm.Type, v => v.TypeTextBlock.Text) + .DisposeWith(disposables); + this.OneWayBind(ViewModel, vm => vm.Uri, v => v.UriTextBlock.Text) + .DisposeWith(disposables); + }); + + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Needlework.Net/Views/Pages/WebSocket/WebSocketPage.axaml b/Needlework.Net/Views/Pages/WebSocket/WebSocketPage.axaml new file mode 100644 index 0000000..f85b6c6 --- /dev/null +++ b/Needlework.Net/Views/Pages/WebSocket/WebSocketPage.axaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Needlework.Net/Views/Pages/WebSocket/WebSocketPage.axaml.cs b/Needlework.Net/Views/Pages/WebSocket/WebSocketPage.axaml.cs new file mode 100644 index 0000000..d9f2f28 --- /dev/null +++ b/Needlework.Net/Views/Pages/WebSocket/WebSocketPage.axaml.cs @@ -0,0 +1,93 @@ +using Avalonia; +using Avalonia.ReactiveUI; +using Avalonia.Styling; +using Needlework.Net.ViewModels.Pages.WebSocket; +using ReactiveUI; +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using TextMateSharp.Grammars; + +namespace Needlework.Net.Views.Pages.WebSocket; + +public partial class WebSocketPage : ReactiveUserControl +{ + public WebSocketPage() + { + this.WhenActivated(disposables => + { + this.OneWayBind(ViewModel, vm => vm.EventTypes, v => v.EventTypesComboBox.ItemsSource) + .DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.EventType, v => v.EventTypesComboBox.SelectedItem) + .DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Search, v => v.SearchTextBox.Text) + .DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.IsAttach, v => v.IsAttachTextBox.IsChecked) + .DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.IsTail, v => v.IsTailCheckBox.IsChecked) + .DisposeWith(disposables); + this.OneWayBind(ViewModel, vm => vm.FilteredEventLog, v => v.EventListBox.ItemsSource) + .DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedEventLog, v => v.EventListBox.SelectedItem) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.ClearCommand, v => v.ClearButton) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ViewModel!.GetEventTypesCommand) + .SelectMany(x => x.Execute()) + .Subscribe() + .DisposeWith(disposables); + }); + + InitializeComponent(); + } + + //public void Receive(ResponseUpdatedMessage message) + //{ + // _responseEditor!.Text = message.Value; + //} + + //protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + //{ + // base.OnApplyTemplate(e); + + // _viewModel = (WebSocketViewModel)DataContext!; + // _viewer = this.FindControl("EventViewer"); + // _viewModel.EventLog.CollectionChanged += EventLog_CollectionChanged; ; + + // _responseEditor = this.FindControl("ResponseEditor"); + // _responseEditor?.ApplyJsonEditorSettings(); + + // WeakReferenceMessenger.Default.Register(this, nameof(WebSocketViewModel)); + + // OnBaseThemeChanged(Application.Current!.ActualThemeVariant); + //} + + //private void EventLog_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + //{ + // Avalonia.Threading.Dispatcher.UIThread.Post(async () => + // { + // if (_viewModel!.IsTail) + // { + // await _viewModel.EventLogLock.WaitAsync(); + // try + // { + // _viewer!.ScrollIntoView(_viewModel.EventLog.Count - 1); + // } + // catch (InvalidOperationException) { } + // finally + // { + // _viewModel.EventLogLock.Release(); + // } + // } + // }); + //} + + private void OnBaseThemeChanged(ThemeVariant currentTheme) + { + + var registryOptions = new RegistryOptions( + currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus); + } +} \ No newline at end of file diff --git a/Needlework.Net/Views/Pages/WebsocketView.axaml b/Needlework.Net/Views/Pages/WebsocketView.axaml deleted file mode 100644 index 284ee97..0000000 --- a/Needlework.Net/Views/Pages/WebsocketView.axaml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Needlework.Net/Views/Pages/WebsocketView.axaml.cs b/Needlework.Net/Views/Pages/WebsocketView.axaml.cs deleted file mode 100644 index 7de08f4..0000000 --- a/Needlework.Net/Views/Pages/WebsocketView.axaml.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Styling; -using AvaloniaEdit; -using CommunityToolkit.Mvvm.Messaging; -using Needlework.Net.Extensions; -using Needlework.Net.Messages; -using Needlework.Net.ViewModels.Pages.Websocket; -using System; -using TextMateSharp.Grammars; - -namespace Needlework.Net.Views.Pages; - -public partial class WebsocketView : UserControl, IRecipient -{ - private TextEditor? _responseEditor; - public WebsocketViewModel? _viewModel; - private ListBox? _viewer; - - public WebsocketView() - { - InitializeComponent(); - } - - public void Receive(ResponseUpdatedMessage message) - { - _responseEditor!.Text = message.Value; - } - - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - base.OnApplyTemplate(e); - - _viewModel = (WebsocketViewModel)DataContext!; - _viewer = this.FindControl("EventViewer"); - _viewModel.EventLog.CollectionChanged += EventLog_CollectionChanged; ; - - _responseEditor = this.FindControl("ResponseEditor"); - _responseEditor?.ApplyJsonEditorSettings(); - - WeakReferenceMessenger.Default.Register(this, nameof(WebsocketViewModel)); - - OnBaseThemeChanged(Application.Current!.ActualThemeVariant); - } - - private void EventLog_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) - { - Avalonia.Threading.Dispatcher.UIThread.Post(async () => - { - if (_viewModel!.IsTail) - { - await _viewModel.EventLogLock.WaitAsync(); - try - { - _viewer!.ScrollIntoView(_viewModel.EventLog.Count - 1); - } - catch (InvalidOperationException) { } - finally - { - _viewModel.EventLogLock.Release(); - } - } - }); - } - - private void OnBaseThemeChanged(ThemeVariant currentTheme) - { - - var registryOptions = new RegistryOptions( - currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus); - } -} \ No newline at end of file