mirror of
https://github.com/BlossomiShymae/Needlework.Net.git
synced 2025-12-06 18:20:47 +01:00
Compare commits
18 Commits
0.8.1.0
...
375d5a2ff8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
375d5a2ff8 | ||
|
|
2aa77f3e02 | ||
|
|
576863bd72 | ||
|
|
68e5abd1d1 | ||
|
|
b18f425257 | ||
|
|
5ebed22ae3 | ||
|
|
dc44cf72df | ||
|
|
01cb8886c6 | ||
|
|
38e4a64bb8 | ||
|
|
b63713f054 | ||
|
|
6a776dfd5f | ||
|
|
9270c6d1f1 | ||
|
|
f65c6f1b09 | ||
|
|
bd6589c310 | ||
|
|
cf947f3af4 | ||
|
|
2e4637f533 | ||
|
|
7aaa79956c | ||
|
|
e9d4615ecf |
Binary file not shown.
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 5.1 KiB |
1
Needlework.Net/Assets/libraries.json
Normal file
1
Needlework.Net/Assets/libraries.json
Normal file
@@ -0,0 +1 @@
|
||||
[{"Repo":"GrrrLCU","Description":"A simple wrapper for the LCU. Grrr. x3","Language":"C#","Link":"https://github.com/BlossomiShymae/GrrrLCU"},{"Repo":"Kunc.RiotGames","Description":null,"Language":"C#","Link":"https://github.com/AoshiW/Kunc.RiotGames"},{"Repo":"rito","Description":"Rito is a simple, crossplatform (Windows and Linux) C++20 library interfacing with Riot services (i.e. Riot REST API and League of Legends client).","Language":"cpp","Link":"https://github.com/bartekprtc/rito"},{"Repo":"R4J","Description":"A Java library containing the API for every Riot game","Language":"Java","Link":"https://github.com/stelar7/R4J"},{"Repo":"hasagi-core","Description":"LCU library with auto-generated types for request parameters and responses","Language":"JavaScript","Link":"https://github.com/dysolix/hasagi-core"},{"Repo":"lcu-driver","Description":"Python3 helper for the League of Legends LCU API.","Language":"Python","Link":"https://github.com/sousa-andre/lcu-driver"},{"Repo":"willump","Description":"Python3 helper for the League of Legends LCU API.","Language":"Python","Link":"https://github.com/elliejs/Willump"},{"Repo":"Irelia","Description":"LoL LCU Wrapper for Rust, built on top of hyper!","Language":"Rust","Link":"https://github.com/AlsoSylv/Irelia"},{"Repo":"Shaco","Description":"League of Legends LCU wrapper for rust","Language":"Rust","Link":"https://github.com/Leastrio/Shaco"},{"Repo":"hasagi-core","Description":"LCU library with auto-generated types for request parameters and responses","Language":"TypeScript","Link":"https://github.com/dysolix/hasagi-core"},{"Repo":"hexgate","Description":"LCU API wrapper for League of Legends","Language":"TypeScript","Link":"https://github.com/cuppachino/hexgate"}]
|
||||
9
Needlework.Net/Models/Library.cs
Normal file
9
Needlework.Net/Models/Library.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Needlework.Net.Models;
|
||||
|
||||
public class Library
|
||||
{
|
||||
public required string Repo { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public required string Language { get; init; }
|
||||
public required string Link { get; init; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Needlework.Net.Models;
|
||||
@@ -35,10 +36,10 @@ public class OpenApiDocumentWrapper
|
||||
{
|
||||
pluginsKey = "default";
|
||||
if (plugins.TryGetValue(pluginsKey, out var p))
|
||||
p.Add(new(method.ToString(), path, operation));
|
||||
p.Add(new(method.ToString(), path, pluginsKey, operation));
|
||||
else
|
||||
{
|
||||
operations.Add(new(method.ToString(), path, operation));
|
||||
operations.Add(new(method.ToString(), path, pluginsKey, operation));
|
||||
plugins[pluginsKey] = operations;
|
||||
}
|
||||
}
|
||||
@@ -46,19 +47,16 @@ public class OpenApiDocumentWrapper
|
||||
{
|
||||
foreach (var tag in operation.Tags)
|
||||
{
|
||||
var lowercaseTag = tag.Name.ToLower();
|
||||
if (lowercaseTag == "plugins")
|
||||
if (tag.Name == "plugins")
|
||||
continue;
|
||||
else if (lowercaseTag.Contains("plugin "))
|
||||
pluginsKey = lowercaseTag.Replace("plugin ", "");
|
||||
else
|
||||
pluginsKey = lowercaseTag;
|
||||
pluginsKey = tag.Name;
|
||||
|
||||
if (plugins.TryGetValue(pluginsKey, out var p))
|
||||
p.Add(new(method.ToString(), path, operation));
|
||||
p.Add(new(method.ToString(), path, tag.Name, operation));
|
||||
else
|
||||
{
|
||||
operations.Add(new(method.ToString(), path, operation));
|
||||
operations.Add(new(method.ToString(), path, tag.Name, operation));
|
||||
plugins[pluginsKey] = operations;
|
||||
}
|
||||
}
|
||||
@@ -66,6 +64,10 @@ public class OpenApiDocumentWrapper
|
||||
}
|
||||
}
|
||||
|
||||
plugins = new(plugins.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value.OrderBy(x => x.Path).ToList()));
|
||||
|
||||
Plugins = plugins;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Needlework.Net.Models;
|
||||
|
||||
public record PathOperation(string Method, string Path, OpenApiOperation Operation);
|
||||
public record PathOperation(string Method, string Path, string Tag, OpenApiOperation Operation);
|
||||
10
Needlework.Net/Models/SystemBuild.cs
Normal file
10
Needlework.Net/Models/SystemBuild.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Needlework.Net.Models
|
||||
{
|
||||
public class SystemBuild
|
||||
{
|
||||
public string Branch { get; set; } = string.Empty;
|
||||
public string Patchline { get; set; } = string.Empty;
|
||||
public string PatchlineVisibleName { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
|
||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||
<AssemblyName>NeedleworkDotNet</AssemblyName>
|
||||
<AssemblyVersion>0.8.1.0</AssemblyVersion>
|
||||
<AssemblyVersion>0.11.0.0</AssemblyVersion>
|
||||
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
|
||||
</PropertyGroup>
|
||||
@@ -31,13 +31,17 @@
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0" />
|
||||
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="1.6.22" />
|
||||
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.22" />
|
||||
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.4.0" />
|
||||
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
|
||||
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.64" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.65" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -6,8 +6,10 @@ using Needlework.Net.ViewModels.MainWindow;
|
||||
using Needlework.Net.ViewModels.Pages;
|
||||
using Projektanker.Icons.Avalonia;
|
||||
using Projektanker.Icons.Avalonia.FontAwesome;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Needlework.Net;
|
||||
|
||||
@@ -34,7 +36,11 @@ class Program
|
||||
return AppBuilder.Configure(() => new App(BuildServices()))
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
.LogToTrace()
|
||||
.With(new Win32PlatformOptions
|
||||
{
|
||||
CompositionMode = [ Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition ]
|
||||
});
|
||||
}
|
||||
|
||||
private static IServiceProvider BuildServices()
|
||||
@@ -44,9 +50,16 @@ class Program
|
||||
builder.AddSingleton<MainWindowViewModel>();
|
||||
builder.AddSingleton<DialogService>();
|
||||
builder.AddSingletonsFromAssemblies<PageBase>();
|
||||
|
||||
builder.AddHttpClient();
|
||||
|
||||
var logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.File("Logs/NeedleworkDotNet.log", rollingInterval: RollingInterval.Day, shared: true)
|
||||
.CreateLogger();
|
||||
logger.Debug("NeedleworkDotNet version: {Version}", Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0");
|
||||
logger.Debug("OS description: {Description}", System.Runtime.InteropServices.RuntimeInformation.OSDescription);
|
||||
builder.AddLogging(builder => builder.AddSerilog(logger));
|
||||
|
||||
var services = builder.BuildServiceProvider();
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Avalonia.Collections;
|
||||
using BlossomiShymae.GrrrLCU;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Needlework.Net.Messages;
|
||||
using Needlework.Net.Models;
|
||||
@@ -17,8 +19,10 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
|
||||
namespace Needlework.Net.ViewModels.MainWindow;
|
||||
|
||||
@@ -33,6 +37,9 @@ public partial class MainWindowViewModel
|
||||
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
|
||||
[ObservableProperty] private bool _isUpdateShown = false;
|
||||
|
||||
[ObservableProperty] private string _schemaVersion = "N/A";
|
||||
[ObservableProperty] private string _schemaVersionLatest = "N/A";
|
||||
|
||||
public HttpClient HttpClient { get; }
|
||||
public DialogService DialogService { get; }
|
||||
public OpenApiDocumentWrapper? OpenApiDocumentWrapper { get; set; }
|
||||
@@ -42,8 +49,25 @@ public partial class MainWindowViewModel
|
||||
|
||||
[ObservableProperty] private ObservableCollection<InfoBarViewModel> _infoBarItems = [];
|
||||
|
||||
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, DialogService dialogService)
|
||||
private readonly ILogger<MainWindowViewModel> _logger;
|
||||
|
||||
private readonly System.Timers.Timer _latestUpdateTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMinutes(10).TotalMilliseconds,
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
private readonly System.Timers.Timer _schemaVersionTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(5).TotalMilliseconds,
|
||||
Enabled = true
|
||||
};
|
||||
private bool _isSchemaVersionChecked = false;
|
||||
|
||||
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, DialogService dialogService, ILogger<MainWindowViewModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
MenuItems = new AvaloniaList<NavigationViewItem>(pages
|
||||
.OrderBy(p => p.Index)
|
||||
.ThenBy(p => p.DisplayName)
|
||||
@@ -61,20 +85,60 @@ public partial class MainWindowViewModel
|
||||
WeakReferenceMessenger.Default.RegisterAll(this);
|
||||
|
||||
Task.Run(FetchDataAsync);
|
||||
new Thread(ProcessEvents) { IsBackground = true }.Start();
|
||||
|
||||
_latestUpdateTimer.Elapsed += OnLatestUpdateTimerElapsed;
|
||||
_schemaVersionTimer.Elapsed += OnSchemaVersionTimerElapsed;
|
||||
_latestUpdateTimer.Start();
|
||||
_schemaVersionTimer.Start();
|
||||
OnLatestUpdateTimerElapsed(null, null);
|
||||
OnSchemaVersionTimerElapsed(null, null);
|
||||
|
||||
}
|
||||
|
||||
private void ProcessEvents(object? obj)
|
||||
private async void OnSchemaVersionTimerElapsed(object? sender, ElapsedEventArgs? e)
|
||||
{
|
||||
while (!IsUpdateShown)
|
||||
if (OpenApiDocumentWrapper == null) return;
|
||||
if (!ProcessFinder.IsPortOpen()) return;
|
||||
|
||||
try
|
||||
{
|
||||
Task.Run(CheckLatestVersionAsync);
|
||||
var client = Connector.GetLcuHttpClientInstance();
|
||||
|
||||
Thread.Sleep(TimeSpan.FromMinutes(10)); // Avoid tripping unauthenticated rate limits
|
||||
var currentSemVer = OpenApiDocumentWrapper.Info.Version.Split('.');
|
||||
var systemBuild = await client.GetFromJsonAsync<SystemBuild>("/system/v1/builds") ?? throw new NullReferenceException();
|
||||
var latestSemVer = systemBuild.Version.Split('.');
|
||||
|
||||
if (!_isSchemaVersionChecked)
|
||||
{
|
||||
_logger.LogInformation("LCU Schema (current): {Version}", OpenApiDocumentWrapper.Info.Version);
|
||||
_logger.LogInformation("LCU Schema (latest): {Version}", systemBuild.Version);
|
||||
_isSchemaVersionChecked = true;
|
||||
}
|
||||
|
||||
bool isVersionMatching = currentSemVer[0] == latestSemVer[0] && currentSemVer[1] == latestSemVer[1]; // Compare major and minor versions
|
||||
if (!isVersionMatching)
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ShowInfoBarAsync(new("Newer System Build", true, $"LCU Schema is possibly outdated compared to latest system build. Consider submitting a pull request on dysolix/hasagi-types.\nCurrent: {string.Join(".", currentSemVer)}\nLatest: {string.Join(".", latestSemVer)}", InfoBarSeverity.Warning, TimeSpan.FromSeconds(60), new Avalonia.Controls.Button()
|
||||
{
|
||||
Command = OpenUrlCommand,
|
||||
CommandParameter = "https://github.com/dysolix/hasagi-types#updating-the-types",
|
||||
Content = "Submit PR"
|
||||
}));
|
||||
});
|
||||
|
||||
_schemaVersionTimer.Elapsed -= OnSchemaVersionTimerElapsed;
|
||||
_schemaVersionTimer.Stop();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Schema version check failed");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckLatestVersionAsync()
|
||||
private async void OnLatestUpdateTimerElapsed(object? sender, ElapsedEventArgs? e)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -83,7 +147,11 @@ public partial class MainWindowViewModel
|
||||
|
||||
var response = await HttpClient.SendAsync(request);
|
||||
var release = await response.Content.ReadFromJsonAsync<GithubRelease>();
|
||||
if (release == null) return;
|
||||
if (release == null)
|
||||
{
|
||||
_logger.LogWarning("Release response is null");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentVersion = int.Parse(Version.Replace(".", ""));
|
||||
|
||||
@@ -91,20 +159,27 @@ public partial class MainWindowViewModel
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(10), new Avalonia.Controls.Button()
|
||||
await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(30), new Avalonia.Controls.Button()
|
||||
{
|
||||
Command = OpenUrlCommand,
|
||||
CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases",
|
||||
Content = "Download"
|
||||
}));
|
||||
IsUpdateShown = true;
|
||||
});
|
||||
|
||||
_latestUpdateTimer.Elapsed -= OnLatestUpdateTimerElapsed;
|
||||
_latestUpdateTimer.Stop();
|
||||
}
|
||||
}
|
||||
catch (Exception) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to check for latest version");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FetchDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var document = await Resources.GetOpenApiDocumentAsync(HttpClient);
|
||||
HostDocument = document;
|
||||
@@ -114,6 +189,11 @@ public partial class MainWindowViewModel
|
||||
WeakReferenceMessenger.Default.Send(new DataReadyMessage(handler));
|
||||
IsBusy = false;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch OpenAPI data");
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(DataRequestMessage message)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.Messages;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using System.Threading.Tasks;
|
||||
@@ -14,10 +15,11 @@ public partial class ConsoleViewModel : PageBase, IRecipient<DataReadyMessage>
|
||||
public IAvaloniaList<string> RequestPaths { get; } = new AvaloniaList<string>();
|
||||
|
||||
[ObservableProperty] private bool _isBusy = true;
|
||||
[ObservableProperty] private LcuRequestViewModel _lcuRequest = new();
|
||||
[ObservableProperty] private LcuRequestViewModel _lcuRequest;
|
||||
|
||||
public ConsoleViewModel() : base("Console", "terminal", -200)
|
||||
public ConsoleViewModel(ILogger<LcuRequestViewModel> lcuRequestViewModelLogger) : base("Console", "terminal", -200)
|
||||
{
|
||||
_lcuRequest = new(lcuRequestViewModelLogger);
|
||||
WeakReferenceMessenger.Default.Register<DataReadyMessage>(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.Messages;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
@@ -21,12 +23,12 @@ public partial class EndpointViewModel : ObservableObject
|
||||
|
||||
public event EventHandler<string>? PathOperationSelected;
|
||||
|
||||
public EndpointViewModel(string endpoint)
|
||||
public EndpointViewModel(string endpoint, ILogger<LcuRequestViewModel> lcuRequestViewModelLogger)
|
||||
{
|
||||
Endpoint = endpoint;
|
||||
|
||||
var handler = WeakReferenceMessenger.Default.Send<DataRequestMessage>().Response;
|
||||
PathOperations = new AvaloniaList<PathOperationViewModel>(handler.Plugins[endpoint].Select(x => new PathOperationViewModel(x)));
|
||||
PathOperations = new AvaloniaList<PathOperationViewModel>(handler.Plugins[endpoint].Select(x => new PathOperationViewModel(x, lcuRequestViewModelLogger)));
|
||||
FilteredPathOperations = new AvaloniaList<PathOperationViewModel>(PathOperations);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using System;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
@@ -15,9 +17,9 @@ public partial class EndpointsNavigationViewModel : ObservableObject
|
||||
|
||||
private readonly Action<string?, Guid> _onEndpointNavigation;
|
||||
|
||||
public EndpointsNavigationViewModel(IAvaloniaList<string> plugins, Action<string?, Guid> onEndpointNavigation)
|
||||
public EndpointsNavigationViewModel(IAvaloniaList<string> plugins, Action<string?, Guid> onEndpointNavigation, ILogger<LcuRequestViewModel> lcuRequestViewModelLogger)
|
||||
{
|
||||
_activeViewModel = _endpointsViewModel = new EndpointsViewModel(plugins, OnClicked);
|
||||
_activeViewModel = _endpointsViewModel = new EndpointsViewModel(plugins, OnClicked, lcuRequestViewModelLogger);
|
||||
_onEndpointNavigation = onEndpointNavigation;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.Messages;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using System;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages.Endpoints;
|
||||
@@ -16,8 +18,11 @@ public partial class EndpointsTabViewModel : PageBase, IRecipient<DataReadyMessa
|
||||
|
||||
[ObservableProperty] private bool _isBusy = true;
|
||||
|
||||
public EndpointsTabViewModel() : base("Endpoints", "list-alt", -500)
|
||||
private readonly ILogger<LcuRequestViewModel> _lcuRequestViewModelLogger;
|
||||
|
||||
public EndpointsTabViewModel(ILogger<LcuRequestViewModel> lcuRequestViewModelLogger) : base("Endpoints", "list-alt", -500)
|
||||
{
|
||||
_lcuRequestViewModelLogger = lcuRequestViewModelLogger;
|
||||
WeakReferenceMessenger.Default.RegisterAll(this);
|
||||
}
|
||||
|
||||
@@ -35,7 +40,7 @@ public partial class EndpointsTabViewModel : PageBase, IRecipient<DataReadyMessa
|
||||
{
|
||||
Endpoints.Add(new()
|
||||
{
|
||||
Content = new EndpointsNavigationViewModel(Plugins, OnEndpointNavigation),
|
||||
Content = new EndpointsNavigationViewModel(Plugins, OnEndpointNavigation, _lcuRequestViewModelLogger),
|
||||
Selected = true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Avalonia.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
@@ -16,11 +18,14 @@ public partial class EndpointsViewModel : ObservableObject
|
||||
|
||||
public Action<ObservableObject> OnClicked { get; }
|
||||
|
||||
public EndpointsViewModel(IAvaloniaList<string> plugins, Action<ObservableObject> onClicked)
|
||||
private readonly ILogger<LcuRequestViewModel> _lcuRequestViewModelLogger;
|
||||
|
||||
public EndpointsViewModel(IAvaloniaList<string> plugins, Action<ObservableObject> onClicked, ILogger<LcuRequestViewModel> lcuRequestViewModelLogger)
|
||||
{
|
||||
Plugins = new AvaloniaList<string>(plugins);
|
||||
Query = new AvaloniaList<string>(plugins);
|
||||
OnClicked = onClicked;
|
||||
_lcuRequestViewModelLogger = lcuRequestViewModelLogger;
|
||||
}
|
||||
|
||||
partial void OnSearchChanged(string value)
|
||||
@@ -37,6 +42,6 @@ public partial class EndpointsViewModel : ObservableObject
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return;
|
||||
|
||||
OnClicked.Invoke(new EndpointViewModel(value));
|
||||
OnClicked.Invoke(new EndpointViewModel(value, _lcuRequestViewModelLogger));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Avalonia.Controls;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.Models;
|
||||
using Needlework.Net.ViewModels.Shared;
|
||||
using System;
|
||||
@@ -13,17 +15,20 @@ public partial class PathOperationViewModel : ObservableObject
|
||||
public string Path { get; }
|
||||
public OperationViewModel Operation { get; }
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
[ObservableProperty] private Lazy<LcuRequestViewModel> _lcuRequest;
|
||||
|
||||
public PathOperationViewModel(PathOperation pathOperation)
|
||||
public PathOperationViewModel(PathOperation pathOperation, ILogger<LcuRequestViewModel> lcuRequestViewModelLogger)
|
||||
{
|
||||
Path = pathOperation.Path;
|
||||
Operation = new OperationViewModel(pathOperation.Operation);
|
||||
LcuRequest = new(() => new LcuRequestViewModel()
|
||||
LcuRequest = new(() => new LcuRequestViewModel(lcuRequestViewModelLogger)
|
||||
{
|
||||
Method = pathOperation.Method.ToUpper()
|
||||
});
|
||||
Url = $"https://swagger.dysolix.dev/lcu/#/{pathOperation.Tag}/{pathOperation.Operation.OperationId}";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -49,4 +54,10 @@ public partial class PathOperationViewModel : ObservableObject
|
||||
LcuRequest.Value.RequestPath = sb.ToString();
|
||||
await LcuRequest.Value.ExecuteAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CopyUrl()
|
||||
{
|
||||
App.MainWindow?.Clipboard?.SetTextAsync(Url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Avalonia.Platform;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Needlework.Net.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Needlework.Net.ViewModels.Pages;
|
||||
|
||||
public partial class HomeViewModel : PageBase
|
||||
{
|
||||
public List<Library> Libraries { get; } = JsonSerializer.Deserialize<List<Library>>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json")))!;
|
||||
|
||||
public HomeViewModel() : base("Home", "home", int.MinValue) { }
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@@ -15,7 +15,7 @@ public class EventViewModel : ObservableObject
|
||||
public EventViewModel(EventData eventData)
|
||||
{
|
||||
Time = $"{DateTime.Now:HH:mm:ss.fff}";
|
||||
Type = eventData?.EventType.ToUpper() ?? string.Empty;
|
||||
Type = eventData?.EventType?.ToUpper() ?? string.Empty;
|
||||
Uri = eventData?.Uri ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
using BlossomiShymae.GrrrLCU;
|
||||
using Avalonia.Collections;
|
||||
using BlossomiShymae.GrrrLCU;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.Messages;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
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;
|
||||
@@ -25,38 +29,90 @@ public partial class WebsocketViewModel : PageBase
|
||||
[ObservableProperty] private bool _isTail = false;
|
||||
[ObservableProperty] private EventViewModel? _selectedEventLog = null;
|
||||
|
||||
[ObservableProperty] private IAvaloniaList<string> _eventTypes = new AvaloniaList<string>();
|
||||
[ObservableProperty] private string _eventType = "OnJsonApiEvent";
|
||||
|
||||
private Dictionary<string, EventMessage> _events = [];
|
||||
|
||||
public WebsocketClient? Client { get; set; }
|
||||
|
||||
public List<IDisposable> ClientDisposables = [];
|
||||
|
||||
private readonly object _tokenLock = new();
|
||||
public CancellationTokenSource TokenSource { get; set; } = new();
|
||||
|
||||
public HttpClient HttpClient { get; }
|
||||
|
||||
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
|
||||
|
||||
public WebsocketViewModel() : base("Event Viewer", "plug", -100)
|
||||
private readonly ILogger<WebsocketViewModel> _logger;
|
||||
|
||||
public WebsocketViewModel(HttpClient httpClient, ILogger<WebsocketViewModel> logger) : base("Event Viewer", "plug", -100)
|
||||
{
|
||||
_logger = logger;
|
||||
HttpClient = httpClient;
|
||||
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
|
||||
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
|
||||
thread.Start();
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await InitializeEventTypes();
|
||||
InitializeWebsocket();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task InitializeEventTypes()
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = await HttpClient.GetStringAsync("https://raw.githubusercontent.com/dysolix/hasagi-types/refs/heads/main/dist/lcu-events.d.ts");
|
||||
var matches = EventTypesRegex().Matches(file);
|
||||
Avalonia.Threading.Dispatcher.UIThread.Invoke(() => EventTypes.AddRange(matches.Select(m => m.Groups[1].Value)));
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get event types");
|
||||
WeakReferenceMessenger.Default.Send(new InfoBarUpdateMessage(new("Failed to get event types", true, ex.Message, FluentAvalonia.UI.Controls.InfoBarSeverity.Error, TimeSpan.FromSeconds(10))));
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeWebsocket()
|
||||
{
|
||||
while (true)
|
||||
lock (_tokenLock)
|
||||
{
|
||||
if (Client != null)
|
||||
{
|
||||
_logger.LogDebug("Disposing old connection");
|
||||
foreach (var disposable in ClientDisposables)
|
||||
disposable.Dispose();
|
||||
ClientDisposables.Clear();
|
||||
Client.Dispose();
|
||||
}
|
||||
TokenSource.Cancel();
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
while (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = Connector.CreateLcuWebsocketClient();
|
||||
client.EventReceived.Subscribe(OnMessage);
|
||||
client.DisconnectionHappened.Subscribe(OnDisconnection);
|
||||
client.ReconnectionHappened.Subscribe(OnReconnection);
|
||||
ClientDisposables.Add(client.EventReceived.Subscribe(OnMessage));
|
||||
ClientDisposables.Add(client.DisconnectionHappened.Subscribe(OnDisconnection));
|
||||
ClientDisposables.Add(client.ReconnectionHappened.Subscribe(OnReconnection));
|
||||
|
||||
client.Start();
|
||||
client.Send(new EventMessage(EventRequestType.Subscribe, EventKinds.OnJsonApiEvent));
|
||||
client.Send(new EventMessage(EventRequestType.Subscribe, new EventKind() { Prefix = EventType }));
|
||||
Client = client;
|
||||
return;
|
||||
}
|
||||
catch (Exception) { }
|
||||
Thread.Sleep(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
})
|
||||
{ IsBackground = true };
|
||||
thread.Start();
|
||||
_logger.LogDebug("Initialized new connection: {EventType}", EventType);
|
||||
TokenSource = tokenSource;
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedEventLogChanged(EventViewModel? value)
|
||||
@@ -79,15 +135,18 @@ public partial class WebsocketViewModel : PageBase
|
||||
|
||||
private void OnReconnection(ReconnectionInfo info)
|
||||
{
|
||||
Trace.WriteLine($"-- Reconnection --\nType{info.Type}");
|
||||
_logger.LogTrace("Reconnected: {Type}", info.Type);
|
||||
}
|
||||
|
||||
private void OnDisconnection(DisconnectionInfo info)
|
||||
{
|
||||
Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}");
|
||||
Client?.Dispose();
|
||||
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
|
||||
thread.Start();
|
||||
_logger.LogTrace("Disconnected: {Type}", info.Type);
|
||||
InitializeWebsocket();
|
||||
}
|
||||
|
||||
partial void OnEventTypeChanged(string value)
|
||||
{
|
||||
InitializeWebsocket();
|
||||
}
|
||||
|
||||
private void OnMessage(EventMessage message)
|
||||
@@ -122,4 +181,7 @@ public partial class WebsocketViewModel : PageBase
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[GeneratedRegex("\"(.*?)\":")]
|
||||
public static partial Regex EventTypesRegex();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using BlossomiShymae.GrrrLCU;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Needlework.Net.Messages;
|
||||
using Needlework.Net.ViewModels.MainWindow;
|
||||
using System;
|
||||
@@ -31,6 +32,13 @@ public partial class LcuRequestViewModel : ObservableObject
|
||||
public event EventHandler<LcuRequestViewModel>? RequestText;
|
||||
public event EventHandler<string>? UpdateText;
|
||||
|
||||
private readonly ILogger<LcuRequestViewModel> _logger;
|
||||
|
||||
public LcuRequestViewModel(ILogger<LcuRequestViewModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
partial void OnMethodChanged(string? oldValue, string? newValue)
|
||||
{
|
||||
if (newValue == null) return;
|
||||
@@ -59,6 +67,8 @@ public partial class LcuRequestViewModel : ObservableObject
|
||||
_ => throw new Exception("Method is not selected or missing."),
|
||||
};
|
||||
|
||||
_logger.LogDebug("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"));
|
||||
@@ -88,6 +98,7 @@ public partial class LcuRequestViewModel : 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);
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<Grid>
|
||||
<TransitioningContentControl Content="{Binding CurrentPage}"/>
|
||||
<Button Content="{Binding Version}"
|
||||
Background="RoyalBlue"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="16"/>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia.Controls;
|
||||
using FluentAvalonia.UI.Windowing;
|
||||
|
||||
namespace Needlework.Net.Views.MainWindow;
|
||||
@@ -9,5 +12,19 @@ public partial class MainWindowView : AppWindow
|
||||
InitializeComponent();
|
||||
|
||||
TitleBar.ExtendsContentIntoTitleBar = true;
|
||||
TransparencyLevelHint = [WindowTransparencyLevel.Mica, WindowTransparencyLevel.None];
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
if (IsWindows11OrNewer())
|
||||
{
|
||||
Background = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsWindows11OrNewer()
|
||||
{
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build >= 22000;
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,11 @@
|
||||
<Grid
|
||||
RowDefinitions="*"
|
||||
ColumnDefinitions="auto,*">
|
||||
<Grid.ContextFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuItem Header="Copy Swagger URL" Command="{Binding CopyUrlCommand}"/>
|
||||
</MenuFlyout>
|
||||
</Grid.ContextFlyout>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
|
||||
@@ -15,6 +15,27 @@
|
||||
<ui:TabView TabItems="{Binding Endpoints}"
|
||||
AddTabButtonCommand="{Binding AddEndpointCommand}"
|
||||
TabCloseRequested="TabView_TabCloseRequested">
|
||||
<!--Need to override Tab header for Mica theme...-->
|
||||
<ui:TabView.Resources>
|
||||
<ResourceDictionary>
|
||||
<SolidColorBrush x:Key="TabViewItemHeaderBackgroundSelected" Color="{DynamicResource ControlFillColorTransparent}"/>
|
||||
</ResourceDictionary>
|
||||
</ui:TabView.Resources>
|
||||
<!--We need to hack this style for Mica theme since there is no way to explicity set style priority in Avalonia...-->
|
||||
<ui:TabView.Styles>
|
||||
<Style Selector="Grid > ContentPresenter#TabContentPresenter">
|
||||
<Style.Animations>
|
||||
<Animation IterationCount="1" Duration="0:0:1" FillMode="Both">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillColorTransparent}"/>
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillColorTransparent}"/>
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
</ui:TabView.Styles>
|
||||
<ui:TabView.TabItemTemplate>
|
||||
<DataTemplate DataType="vm:EndpointItem">
|
||||
<ui:TabViewItem Header="{Binding Header}"
|
||||
|
||||
@@ -5,10 +5,32 @@
|
||||
xmlns:vm="using:Needlework.Net.ViewModels.Pages"
|
||||
xmlns:controls="using:Needlework.Net.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
Name="HomeControl"
|
||||
x:Class="Needlework.Net.Views.Pages.HomeView"
|
||||
x:DataType="vm:HomeViewModel">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
|
||||
</Style>
|
||||
<Style Selector="DataGrid">
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ControlElevationBorderBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="DataGridColumnHeader TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="DataGridRow DataGridCell">
|
||||
<Setter Property="FontSize" Value="12"></Setter>
|
||||
</Style>
|
||||
<Style Selector="DataGridRow">
|
||||
<Setter Property="Margin" Value="0 0 0 4"></Setter>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<!-- TOP LEVEL -->
|
||||
<ScrollViewer>
|
||||
<Grid ColumnDefinitions="*,400"
|
||||
RowDefinitions="*">
|
||||
<!-- MAIN AREA -->
|
||||
<ScrollViewer Grid.Column="0"
|
||||
Grid.Row="0">
|
||||
<WrapPanel Margin="8"
|
||||
Orientation="Horizontal">
|
||||
<!-- WELCOME -->
|
||||
@@ -32,24 +54,24 @@
|
||||
<TextBlock
|
||||
Theme="{StaticResource SubtitleTextBlockStyle}"
|
||||
Margin="0 0 0 8">Resources</TextBlock>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<Button CommandParameter="https://hextechdocs.dev/tag/lcu/" Margin="0 0 8 0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button CommandParameter="https://hextechdocs.dev/tag/lcu/" Margin="4">
|
||||
Hextech Docs
|
||||
</Button>
|
||||
<Button CommandParameter="https://hextechdocs.dev/getting-started-with-the-lcu-api/">
|
||||
<Button CommandParameter="https://hextechdocs.dev/getting-started-with-the-lcu-api/" Margin="4">
|
||||
Getting Started
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button CommandParameter="https://discord.com/channels/187652476080488449/516802588805431296" Margin="4">
|
||||
#lcu-api
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
<controls:Card Margin="12" Width="300">
|
||||
<StackPanel>
|
||||
<TextBlock>© 2024 - Blossomi Shymae</TextBlock>
|
||||
<TextBlock>© 2025 - Blossomi Shymae</TextBlock>
|
||||
<TextBlock>MIT License</TextBlock>
|
||||
</StackPanel>
|
||||
</controls:Card>
|
||||
@@ -60,4 +82,44 @@
|
||||
</controls:Card>
|
||||
</WrapPanel>
|
||||
</ScrollViewer>
|
||||
<!-- LIBRARIES -->
|
||||
<Grid Margin="20"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
ColumnDefinitions="*"
|
||||
RowDefinitions="auto,*">
|
||||
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0">Libraries</TextBlock>
|
||||
<ScrollViewer Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<ItemsRepeater ItemsSource="{Binding Libraries}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="0 12 0 0">
|
||||
<TextBlock>
|
||||
<Run Text="{Binding Language}"
|
||||
FontWeight="Bold"/>
|
||||
<Bold> - </Bold>
|
||||
<Run Text="{Binding Repo}"
|
||||
FontWeight="Bold"/>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Description}"
|
||||
IsVisible="{Binding Description, Converter={StaticResource NullBoolConverter}}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="WrapWithOverflow"
|
||||
Width="350"/>
|
||||
<Button Command="{Binding #HomeControl.((vm:HomeViewModel)DataContext).OpenUrlCommand}"
|
||||
CommandParameter="{Binding Link}"
|
||||
Margin="0 4 0 0">
|
||||
<TextBlock Text="{Binding Link}"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -10,12 +10,22 @@
|
||||
<Grid RowDefinitions="*,auto,*" Margin="16">
|
||||
<Border Grid.Row="0"
|
||||
Padding="0 0 0 8">
|
||||
<Grid RowDefinitions="auto,*" ColumnDefinitions="*">
|
||||
<Grid
|
||||
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
|
||||
<Grid Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
RowDefinitions="*">
|
||||
<ComboBox ItemsSource="{Binding EventTypes}"
|
||||
SelectedItem="{Binding EventType}"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"/>
|
||||
</Grid>
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
RowDefinitions="*"
|
||||
ColumnDefinitions="auto,*,auto,auto">
|
||||
ColumnDefinitions="auto,*,auto,auto"
|
||||
Margin="0 8 0 0">
|
||||
<Button Margin="0 0 8 0"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
@@ -37,7 +47,7 @@
|
||||
Content="Tail"
|
||||
IsChecked="{Binding IsTail}"/>
|
||||
</Grid>
|
||||
<ListBox Grid.Row="1"
|
||||
<ListBox Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Name="EventViewer"
|
||||
Margin="0 8 0 0"
|
||||
|
||||
BIN
app-preview.png
BIN
app-preview.png
Binary file not shown.
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 397 KiB |
Reference in New Issue
Block a user