Compare commits

..

148 Commits

Author SHA1 Message Date
estrogen elf 4948946f84 chore: increment version 2026-05-25 13:56:01 -05:00
estrogen elf 3a1c7376c1 fix: exception when opening latest update 2026-05-25 13:55:35 -05:00
estrogen elf c93093c0ea chore: increment version 2026-05-25 13:32:11 -05:00
estrogen elf cc14d4ee04 fix: trim titles of Hextech Docs posts 2026-05-25 13:31:04 -05:00
estrogen elf f3cf177409 Merge pull request #10 from DarkIntaqt/ft/hextechdocs-fix
Fix: Object reference not set to an instance of an object
2026-05-19 16:27:45 -05:00
DarkIntaqt 226eb6ccb3 fix: change hextechdocs html parsing to adapt for new format
It works on my machine
2026-05-19 23:21:42 +02:00
estrogen elf 94902c635e Increment version 2025-12-18 08:35:12 -06:00
estrogen elf d005e5ac56 Update thingies in about page 2025-12-18 08:34:32 -06:00
estrogen elf 221f21caa9 Merge pull request #9 from AoshiW/fix-Oopsies-Dialog-Spam 2025-12-18 08:26:01 -06:00
AoshiW b9f1b5a51e fix Oopsies Dialog Spam 2025-12-18 00:09:48 +01:00
estrogen elf 019d70df53 Merge branch 'main' of https://github.com/BlossomiShymae/Needlework.Net 2025-08-09 08:53:42 -05:00
estrogen elf c0eeef8658 chore: increment version 2025-08-09 08:53:37 -05:00
estrogen elf 347dd8da68 feat: add bug report button 2025-08-09 08:53:05 -05:00
estrogen elf 7a7c8e479c chore: increment version 2025-08-09 08:49:00 -05:00
estrogen elf 0ea0538873 fix: broken game client document URL 2025-08-09 08:48:42 -05:00
estrogen elf 09cc3320e5 refactor: logging for exceptions 2025-08-09 08:47:17 -05:00
estrogen elf 47d02a61fb feat: update README.md 2025-07-05 14:25:43 -05:00
estrogen elf fd57aad4dd chore: increment version 2025-07-05 14:16:43 -05:00
estrogen elf 560a9622ef fix: missing params bug 2025-07-05 14:16:31 -05:00
estrogen elf 2cc0f829b5 chore: fix package downgrade 2025-07-03 15:12:57 -05:00
estrogen elf cdd66aff6c chore: remove unused dependency 2025-07-03 14:58:14 -05:00
estrogen elf 159c30a491 chore: update Briar 2025-07-03 10:18:42 -05:00
estrogen elf 77673d70e9 feat: use %appdata% for kv store 2025-06-23 11:46:33 -05:00
estrogen elf 471559d987 refactor: page factory and less mvvm-breaking 2025-06-23 09:37:30 -05:00
estrogen elf c78f75a332 refactor: datamodels and service name 2025-06-22 19:10:36 -05:00
estrogen elf 73787608c4 feat: add settings page 2025-06-22 16:14:04 -05:00
estrogen elf 8845431126 fix: home page scroll 2025-06-19 18:14:11 -05:00
estrogen elf 352de3cdea feat: update home layout 2025-06-19 11:09:53 -05:00
estrogen elf be81fc7d57 refactor: constants 2025-06-19 07:35:19 -05:00
estrogen elf d526354fea feat: use file kv-store for updates 2025-06-19 07:12:52 -05:00
estrogen elf 4dc2d74ccf feat: use file kv-store for posts 2025-06-18 16:57:25 -05:00
estrogen elf 116c798db3 fix: reduce memory usage 2025-06-18 02:12:49 -05:00
estrogen elf e193eb990a fix: wrong binding for IsVisible 2025-06-18 01:46:20 -05:00
estrogen elf f7882392fd feat: add HextechDocs posts carousel 2025-06-18 01:40:07 -05:00
estrogen elf f9285a2bef feat: display clicked schema to pane 2025-06-17 22:52:14 -05:00
estrogen elf b56c18a552 fix: use virtualization for endpoints list 2025-06-17 22:20:27 -05:00
estrogen elf b35099e5ab fix: case of empty content schema 2025-06-17 22:16:13 -05:00
estrogen elf 876f50607f fix: change schemas list to prevent layout cycle 2025-06-17 21:50:53 -05:00
estrogen elf 57334535cf feat: save offsets in viewmodels 2025-06-17 13:37:03 -05:00
estrogen elf 910d26c00d refactor: remove control cache to fix bugs 2025-06-17 11:51:08 -05:00
estrogen elf 50cc15cafb feat: remove expander from schema item 2025-06-17 11:44:06 -05:00
estrogen elf 748a620bff feat: display all schemas on load and empty search 2025-06-16 23:41:32 -05:00
estrogen elf 3802a6f8fa fix: schema type bugs 2025-06-16 17:28:20 -05:00
estrogen elf 83a73b2746 fix: schema nav position 2025-06-16 03:27:59 -05:00
estrogen elf 5a4a2f05f3 refactor: remove magic from view locator 2025-06-16 02:50:05 -05:00
estrogen elf e195665ab1 fix: schema panel scroll issue 2025-06-16 01:05:34 -05:00
estrogen elf 8c8befe9ca refactor: use LRU for control cache 2025-06-16 01:00:27 -05:00
estrogen elf a74c18ac39 feat: add schemas page 2025-06-15 21:40:48 -05:00
estrogen elf cbc3c42116 feat: add schema search pane 2025-06-15 14:00:58 -05:00
estrogen elf b74437d05e fix: consistent card styles 2025-06-14 01:56:14 -05:00
estrogen elf d527226c7a feat: update styling for libraries 2025-06-14 01:51:38 -05:00
estrogen elf a7a4992907 refactor: use HyperlinkButton 2025-06-14 01:33:28 -05:00
estrogen elf 22ad838362 fix: broken links 2025-06-13 23:59:06 -05:00
estrogen elf ac6632b4c3 fix: styling for endpoint list 2025-06-13 23:57:11 -05:00
estrogen elf c571f5a1de fix: crash when encountering GetLolVanguardV1Notification 2025-06-13 23:49:13 -05:00
estrogen elf 53a393ee1a refactor: use icons from Icons.Avalonia 2025-06-13 23:31:17 -05:00
estrogen elf 79776ab848 refactor: use provided Windows 11 check 2025-06-13 22:57:22 -05:00
estrogen elf a9aaa426d3 refactor: code/folder structure, remove hacks 2025-06-13 22:52:58 -05:00
estrogen elf 06dcadd94f build: update dependencies, add cache package 2025-06-12 21:14:26 -05:00
estrogen elf e95aa987a1 feat: add MacOS platform options 2025-06-01 16:40:33 -05:00
estrogen elf 7997cf222c feat: update Briar 2025-06-01 16:34:23 -05:00
estrogen elf a321d84757 feat: increment version 2025-05-31 21:28:36 -05:00
estrogen elf 4bef9a20dd refactor: change dependency from GrrrLCU to Briar 2025-05-31 21:28:11 -05:00
estrogen elf fb5fbe1fea fix: encode Swagger URL, add Markdown copy 2025-05-31 17:24:43 -05:00
estrogen elf adc8b0c0f1 feat: update README.md 2025-05-30 13:46:22 -05:00
estrogen elf be7d575b48 fix: insecure SSL for game client api 2025-05-30 13:10:57 -05:00
estrogen elf f9dd654b6a fix: lcu schema update 2025-05-30 12:43:55 -05:00
estrogen elf 57d3eb4172 feat: add game client channel link 2025-05-30 12:15:46 -05:00
estrogen elf ce2336ab4d fix: adjust margin for console 2025-05-30 12:13:50 -05:00
estrogen elf 9a76e1af4a feat: add AoshiW to about page 2025-05-30 12:08:39 -05:00
estrogen elf 6f0126863b feat: increment version 2025-05-29 23:10:40 -05:00
estrogen elf 826134888e feat: update about page 2025-05-29 23:07:39 -05:00
estrogen elf ef16642c04 fix: prevent closing only tab 2025-05-29 15:32:24 -05:00
estrogen elf a5f49c48b8 refactor: remove unused code 2025-05-29 15:13:50 -05:00
estrogen elf 1364cdc38c feat: Add Game Client to Endpoints 2025-05-29 15:10:00 -05:00
estrogen elf c51f20a324 refactor: use data source, remove data loading in ctor of view models 2025-05-25 12:56:40 -05:00
estrogen elf 6d1acee8df refactor: logging 2025-05-24 13:26:38 -05:00
estrogen elf 375d5a2ff8 Update app preview 2025-05-05 03:21:52 -05:00
estrogen elf 2aa77f3e02 Update picture 2025-05-05 03:05:32 -05:00
estrogen elf 576863bd72 Update year 2025-05-05 03:02:02 -05:00
estrogen elf 68e5abd1d1 Add copy Swagger URL context flyout 2025-05-05 02:55:36 -05:00
estrogen elf b18f425257 Match sorting with that of https://swagger.dysolix.dev 2025-05-05 00:19:29 -05:00
estrogen elf 5ebed22ae3 Add LCU Schema build check 2025-05-04 23:56:13 -05:00
estrogen elf dc44cf72df Change plugin filtering 2025-05-04 20:52:28 -05:00
estrogen elf 01cb8886c6 Increment version 2025-05-03 20:48:44 -05:00
estrogen elf 38e4a64bb8 Add Mica theme for Windows 11 and newer 2025-05-03 19:19:59 -05:00
BlossomiShymae b63713f054 Bump version 2024-12-18 01:07:25 -06:00
BlossomiShymae 6a776dfd5f Add #lcu-api channel link 2024-12-17 23:49:13 -06:00
BlossomiShymae 9270c6d1f1 Add logging 2024-12-17 23:36:28 -06:00
BlossomiShymae f65c6f1b09 Update and add packages 2024-12-17 21:09:40 -06:00
BlossomiShymae bd6589c310 Bump version 2024-12-15 23:22:52 -06:00
BlossomiShymae cf947f3af4 Update event types URL 2024-12-15 23:22:30 -06:00
BlossomiShymae 2e4637f533 Bump version 2024-12-06 22:18:26 -06:00
BlossomiShymae 7aaa79956c Add event type selection for event viewer 2024-12-06 22:17:48 -06:00
BlossomiShymae e9d4615ecf Add libraries list 2024-12-06 18:59:43 -06:00
BlossomiShymae fb63adc1b7 Bump version 2024-12-06 16:25:33 -06:00
BlossomiShymae b41be19cd9 Fix bug where endpoints search breaks, resolves #6 2024-12-06 16:19:38 -06:00
BlossomiShymae 38e1ea2301 Fix workflows, lol 2024-12-06 01:46:13 -06:00
BlossomiShymae 30451b8c8c Bump version 2024-12-06 01:25:25 -06:00
BlossomiShymae 05927030eb Slow down ProcessEvents 2024-12-06 01:23:36 -06:00
BlossomiShymae e9f99a9e28 Update README.md 2024-12-05 23:06:06 -06:00
BlossomiShymae 1e838abdbf Merge branch 'main' of github.com:BlossomiShymae/Needlework.Net 2024-12-05 19:21:04 -06:00
BlossomiShymae dede2e909c Add tabs view to endpoints 2024-12-05 12:31:52 -06:00
BlossomiShymae 58556283f0 Update dependencies 2024-12-04 17:47:34 -06:00
Blossomi Shymae 16781a4df4 Update README.md 2024-11-25 19:37:58 -06:00
BlossomiShymae 569f49d484 Collapse pane by default and reduce width 2024-10-28 22:24:29 -05:00
BlossomiShymae 8eabd64911 Store response body instead of updating once 2024-10-28 20:10:35 -05:00
BlossomiShymae 02e739e1a3 Refactor endpoints views 2024-10-28 19:54:54 -05:00
BlossomiShymae c253d00ff1 Refactor request viewmodels 2024-10-28 19:50:19 -05:00
BlossomiShymae 4edd71a04a Update workflows 2024-10-28 15:44:54 -05:00
BlossomiShymae a4fe10157f Refactor folder structure 2024-10-28 15:33:21 -05:00
BlossomiShymae bc4ed78767 Add gears icon 2024-10-28 13:00:45 -05:00
BlossomiShymae 375285067d Bump version 2024-08-23 22:53:05 -05:00
BlossomiShymae 3ec277bdd3 Update GrrrLCU 2024-08-23 22:50:58 -05:00
BlossomiShymae c097890588 Fix issues where messages failed to register 2024-08-23 21:21:01 -05:00
BlossomiShymae 3352740733 Add busy area for sending request in endpoint 2024-08-23 20:26:08 -05:00
BlossomiShymae 48751efc28 Fix endpoints not retaining state 2024-08-23 20:03:18 -05:00
BlossomiShymae b6f713c675 Update GrrrLCU 2024-08-23 19:39:49 -05:00
BlossomiShymae 59619764c2 Improve endpoint loading times by using lazy loading 2024-08-22 20:15:13 -05:00
BlossomiShymae de6f9f64dd Fix use of GrrrLCU 2024-08-22 19:39:27 -05:00
BlossomiShymae 4eae0bd913 Update GrrrLCU 2024-08-22 19:26:11 -05:00
BlossomiShymae 7288c471a4 Bump version, update GrrrLCU 2024-08-20 06:09:19 -05:00
BlossomiShymae 7faedcf039 Update app preview 2024-08-19 06:45:37 -05:00
BlossomiShymae 641d230647 Bump version 2024-08-19 05:15:40 -05:00
BlossomiShymae d53c24c57f Update event viewer to have colored text 2024-08-19 05:15:14 -05:00
BlossomiShymae a24a72b3b2 Update about page 2024-08-19 04:40:36 -05:00
BlossomiShymae 2c88ae44a2 Add unhandled exception logging 2024-08-19 02:58:36 -05:00
BlossomiShymae f0294b3042 Use dialog instead of window for oopsies 2024-08-18 22:10:04 -05:00
BlossomiShymae d26180dce5 Refactor folder stucture 2024-08-18 20:08:25 -05:00
BlossomiShymae baf189e6a9 Refactor workspace name 2024-08-18 19:03:47 -05:00
BlossomiShymae 88149d1458 Bump version 2024-08-17 16:26:52 -05:00
BlossomiShymae 79fd79c01d Fix bug where event viewer may crash from a race condition, resolves #4 2024-08-17 16:25:43 -05:00
BlossomiShymae 7550102406 Fix bug where body template can be incorrect, resolves #5 2024-08-17 16:15:06 -05:00
BlossomiShymae 98996609a3 Bump version, fix error, complete TODOs 2024-08-16 15:04:42 -05:00
Blossomi Shymae 65464d22e3 Merge pull request #2 from AoshiW/perf
bug fix in WebsocketView(Model), optimization and others
2024-08-16 14:49:34 -05:00
AoshiW 0ca7f7869d add missing changes/feedback 2024-08-16 21:14:57 +02:00
AoshiW af47e7c763 Merge branch 'main' into perf 2024-08-16 08:49:47 +02:00
BlossomiShymae 04058f12c1 Bump version, fix bug where whitespace in request body is removed 2024-08-16 01:30:39 -05:00
AoshiW 3a7d39971a bug fix in WebsocketView, optimization and others 2024-08-16 08:17:43 +02:00
BlossomiShymae b0b5476c48 Bump version, fix social links not working 2024-08-15 17:32:35 -05:00
BlossomiShymae b3158a81b8 Update app preview 2024-08-15 17:17:11 -05:00
BlossomiShymae 83400bceed Bump version, migrate to FluentAvalonia with bug fixes 2024-08-15 17:06:17 -05:00
BlossomiShymae 1133f2d785 Update dependencies 2024-08-14 04:43:58 -05:00
BlossomiShymae 14dde760b0 Fix bug where /lol-account-verification failed to open 2024-08-14 03:46:11 -05:00
BlossomiShymae e0a2685dcf Increment version for fix 2024-08-13 02:44:49 -05:00
BlossomiShymae ca2f8c4852 Fix events processing too quickly 2024-08-13 02:43:47 -05:00
BlossomiShymae 8f81aa526e Add update checker 2024-08-13 02:41:02 -05:00
BlossomiShymae 360a0f28c7 Fix responses not being set to empty on oopsies 2024-08-10 06:09:48 -05:00
181 changed files with 6988 additions and 2459 deletions
+14 -26
View File
@@ -1,9 +1,12 @@
# .github/workflows/release.yml
on:
push:
branches:
- release
release:
types: [created]
env:
PROJECT_NAME: Needlework.Net
ASSEMBLY_NAME: NeedleworkDotNet
jobs:
build:
@@ -15,30 +18,15 @@ jobs:
with:
dotnet-version: 8.0.x
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: release
uses: actions/checkout@v4
- name: Build
run: dotnet build Needlework.Net.Desktop -c Release
run: dotnet build ${{env.PROJECT_NAME}} -c Release
- name: Publish
run: dotnet publish Needlework.Net.Desktop -c Release -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -o publish -r win-x64 --self-contained=false
- name: Get Version
id: version
shell: powershell
run: |
$xml=[xml](Get-Content .\Needlework.Net.Desktop\Needlework.Net.Desktop.csproj)
$ver=($xml.Project.PropertyGroup).AssemblyVersion
$ver="VERSION=$ver"
$ver=$ver -replace '\s',''
echo $ver >> $env:GITHUB_OUTPUT
- name: Zip Files
run: 7z a -tzip NeedleworkDotNet-win-x64.zip ./Publish/* README.md LICENSE
- name: Release
run: dotnet publish ${{env.PROJECT_NAME}} -c Release -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false -o publish -r win-x64 --self-contained=false
- name: Zip files
run: 7z a -tzip ${{env.ASSEMBLY_NAME}}-win-x64.zip ./Publish/* README.md LICENSE
- name: Upload to release
uses: softprops/action-gh-release@v1
if: ${{startsWith(github.ref, 'refs/tags/') }}
with:
name: "Needlework.Net v${{ steps.version.outputs.VERSION }}"
prerelease: false
tag_name: "${{ steps.version.outputs.VERSION }}"
files: |
NeedleworkDotNet-win-x64.zip
files: ${{env.ASSEMBLY_NAME}}-win-x64.zip
+5
View File
@@ -34,6 +34,7 @@ bld/
[Oo]bj/
[Ll]og/
[Ll]ogs/
[Dd]ata/
# Visual Studio 2015/2017 cache/options directory
.vs/
@@ -482,3 +483,7 @@ $RECYCLE.BIN/
# Vim temporary swap files
*.swp
*.sqlite
*.sqlite-shm
*.sqlite-wal
@@ -1,27 +0,0 @@
using Xunit.Abstractions;
namespace Needlework.Net.Core.Tests;
public class LcuSchemaHandlerTest
{
private readonly ITestOutputHelper _output;
internal HttpClient HttpClient { get; } = new();
public LcuSchemaHandlerTest(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public async Task PluginsTestAsync()
{
var reader = new LcuSchemaHandler(await Resources.GetOpenApiDocumentAsync(HttpClient));
var plugins = reader.Plugins.Keys.ToList();
foreach (var plugin in plugins)
_output.WriteLine($"Plugin: {plugin}");
Assert.True(plugins.Count > 0);
}
}
@@ -1,27 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Needlework.Net.Core\Needlework.Net.Core.csproj" />
</ItemGroup>
</Project>
@@ -1,23 +0,0 @@
using Xunit.Abstractions;
namespace Needlework.Net.Core.Tests;
public class ResourcesTest
{
private readonly ITestOutputHelper _output;
internal HttpClient HttpClient { get; } = new();
public ResourcesTest(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public async Task DocumentTestAsync()
{
var document = await Resources.GetOpenApiDocumentAsync(HttpClient);
Assert.True(document.Info.Title == "LCU SCHEMA");
}
}
@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OpenApi" Version="1.6.16" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.16" />
</ItemGroup>
</Project>
-21
View File
@@ -1,21 +0,0 @@
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
namespace Needlework.Net.Core;
public static class Resources
{
/// <summary>
/// Get the OpenApi document of the LCU schema. Provided by dysolix.
/// </summary>
/// <param name="httpClient"></param>
/// <returns></returns>
public static async Task<OpenApiDocument> GetOpenApiDocumentAsync(HttpClient httpClient)
{
var stream = await httpClient.GetStreamAsync("https://raw.githubusercontent.com/dysolix/hasagi-types/main/swagger.json");
var document = new OpenApiStreamReader().Read(stream, out var _);
return document;
}
}
-26
View File
@@ -1,26 +0,0 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Needlework.Net.Desktop.App"
RequestedThemeVariant="Dark"
xmlns:local="using:Needlework.Net.Desktop"
xmlns:converters="using:Needlework.Net.Desktop.Converters"
xmlns:sukiUi="clr-namespace:SukiUI;assembly=SukiUI"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme></FluentTheme>
<sukiUi:SukiTheme ThemeColor="Blue" />
<materialIcons:MaterialIconStyles />
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
</Application.Styles>
<Application.Resources>
<converters:EnumerableBoolConverter x:Key="EnumerableBoolConverter"/>
<converters:NullBoolConverter x:Key="NullBoolConverter"/>
</Application.Resources>
</Application>
-47
View File
@@ -1,47 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Needlework.Net.Desktop.ViewModels;
using Needlework.Net.Desktop.Views;
using System;
using System.Text.Json;
namespace Needlework.Net.Desktop;
public partial class App(IServiceProvider serviceProvider) : Application
{
private readonly IServiceProvider _serviceProvider = serviceProvider;
public static JsonSerializerOptions JsonSerializerOptions { get; } = new()
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
public static readonly int MaxCharacters = 10_000;
public static Window? MainWindow;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow()
{
DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
};
MainWindow = desktop.MainWindow;
}
base.OnFrameworkInitializationCompleted();
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

@@ -1,8 +0,0 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Desktop.Messages
{
public class ContentRequestMessage : RequestMessage<string>
{
}
}
@@ -1,9 +0,0 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using Needlework.Net.Core;
namespace Needlework.Net.Desktop.Messages
{
public class DataReadyMessage(LcuSchemaHandler handler) : ValueChangedMessage<LcuSchemaHandler>(handler)
{
}
}
@@ -1,9 +0,0 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using Needlework.Net.Core;
namespace Needlework.Net.Desktop.Messages
{
public class DataRequestMessage : RequestMessage<LcuSchemaHandler>
{
}
}
@@ -1,20 +0,0 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Desktop.Messages
{
public class EditorUpdateMessage(EditorUpdate editorUpdate) : ValueChangedMessage<EditorUpdate>(editorUpdate)
{
}
public class EditorUpdate
{
public string Text { get; }
public string Key { get; }
public EditorUpdate(string text, string key)
{
Text = text;
Key = key;
}
}
}
@@ -1,9 +0,0 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.OpenApi.Models;
namespace Needlework.Net.Desktop.Messages
{
public class HostDocumentRequestMessage : RequestMessage<OpenApiDocument>
{
}
}
@@ -1,8 +0,0 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Desktop.Messages
{
public class OopsiesWindowCanceledMessage(object? data) : ValueChangedMessage<object?>(data)
{
}
}
@@ -1,8 +0,0 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Desktop.Messages
{
public class OopsiesWindowRequestedMessage(string text) : ValueChangedMessage<string>(text)
{
}
}
@@ -1,65 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<PropertyGroup Label="Avalonia">
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>NeedleworkDotNet</AssemblyName>
<AssemblyVersion>0.2.2.0</AssemblyVersion>
<FileVersion>0.2.2.0</FileVersion>
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.Desktop" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.0-beta2" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.0-beta2" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.6" />
<PackageReference Include="BlossomiShymae.GrrrLCU" Version="0.9.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<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="Projektanker.Icons.Avalonia" Version="9.4.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
<PackageReference Include="SukiUI" Version="6.0.0-beta7" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.60" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Needlework.Net.Core\Needlework.Net.Core.csproj" />
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Views\AboutView.axaml" />
</ItemGroup>
<ItemGroup>
<Compile Update="Views\EndpointView.axaml.cs">
<DependentUpon>EndpointView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\OopsiesWindow.axaml.cs">
<DependentUpon>OopsiesWindow.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="Utilities\" />
</ItemGroup>
</Project>
-51
View File
@@ -1,51 +0,0 @@
using Avalonia;
using Microsoft.Extensions.DependencyInjection;
using Needlework.Net.Desktop.Services;
using Needlework.Net.Desktop.ViewModels;
using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome;
using System;
using System.Linq;
namespace Needlework.Net.Desktop;
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
IconProvider.Current
.Register<FontAwesomeIconProvider>();
return AppBuilder.Configure(() => new App(BuildServices()))
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
private static IServiceProvider BuildServices()
{
var builder = new ServiceCollection();
builder.AddSingleton<MainWindowViewModel>();
builder.AddSingleton<WindowService>();
// Dynamically add ViewModels
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => !p.IsAbstract && typeof(PageBase).IsAssignableFrom(p));
foreach (var type in types)
builder.AddSingleton(typeof(PageBase), type);
builder.AddHttpClient();
var services = builder.BuildServiceProvider();
return services;
}
}
@@ -1,53 +0,0 @@
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Desktop.Messages;
using Needlework.Net.Desktop.ViewModels;
using Needlework.Net.Desktop.Views;
using SukiUI.Controls;
using System;
using System.Collections.Generic;
namespace Needlework.Net.Desktop.Services
{
public class WindowService : IRecipient<OopsiesWindowCanceledMessage>
{
public IServiceProvider ServiceProvider { get; }
public Dictionary<string, SukiWindow> EndpointWindows { get; } = []; // Workaround memory leak by storing and reusing windows.
// Figure out why creating and closing windows leaks memory.
public OopsiesWindow? OopsiesWindow { get; set; }
public WindowService(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
WeakReferenceMessenger.Default.Register<OopsiesWindowCanceledMessage>(this);
}
public void ShowOopsiesWindow(string text)
{
if (OopsiesWindow != null) OopsiesWindow!.Close();
var window = new OopsiesWindow();
window.DataContext = new OopsiesWindowViewModel(text);
window.Show(App.MainWindow!);
window.Closed += OnOopsiesWindowClosed;
OopsiesWindow = window;
}
public void OnOopsiesWindowClosed(object? sender, EventArgs e)
{
if (sender == null) return;
var window = (OopsiesWindow)sender;
window.DataContext = null;
window.Closed -= OnOopsiesWindowClosed;
OopsiesWindow = null;
}
public void Receive(OopsiesWindowCanceledMessage message)
{
if (OopsiesWindow is OopsiesWindow window) window.Close();
}
}
}
-28
View File
@@ -1,28 +0,0 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using System;
using System.ComponentModel;
namespace Needlework.Net.Desktop
{
public class ViewLocator : IDataTemplate
{
public Control? Build(object? param)
{
if (param is null) return new TextBlock { Text = "data was null" };
var name = param.GetType().FullName!
.Replace("ViewModels", "Views")
.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null) return (Control)Activator.CreateInstance(type)!;
else return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object? data)
{
return data is INotifyPropertyChanged;
}
}
}
@@ -1,9 +0,0 @@
namespace Needlework.Net.Desktop.ViewModels
{
public class AboutViewModel : PageBase
{
public AboutViewModel() : base("About", Material.Icons.MaterialIconKind.InfoCircle)
{
}
}
}
@@ -1,99 +0,0 @@
using Avalonia.Collections;
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Desktop.Messages;
using Needlework.Net.Desktop.Services;
using SukiUI.Controls;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Needlework.Net.Desktop.ViewModels
{
public partial class ConsoleViewModel : PageBase, IRecipient<DataReadyMessage>
{
public IAvaloniaReadOnlyList<string> RequestMethods { get; } = new AvaloniaList<string>(["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS", "TRACE"]);
[ObservableProperty] private bool _isBusy = true;
[ObservableProperty] private bool _isRequestBusy = false;
[ObservableProperty] private IAvaloniaReadOnlyList<string> _requestPaths = new AvaloniaList<string>();
[ObservableProperty] private string? _requestMethodSelected = "GET";
[ObservableProperty] private string? _requestPath = null;
[ObservableProperty] private string? _requestBody = null;
[ObservableProperty] private string? _responsePath = null;
[ObservableProperty] private string? _responseStatus = null;
[ObservableProperty] private string? _responseAuthorization = null;
public WindowService WindowService { get; }
public ConsoleViewModel(WindowService windowService) : base("Console", Material.Icons.MaterialIconKind.Console, -200)
{
WindowService = windowService;
WeakReferenceMessenger.Default.Register<DataReadyMessage>(this);
}
[RelayCommand]
private async Task SendRequest()
{
try
{
IsRequestBusy = true;
if (string.IsNullOrEmpty(RequestPath)) throw new Exception("Path is empty.");
var method = RequestMethodSelected switch
{
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
"DELETE" => HttpMethod.Delete,
"HEAD" => HttpMethod.Head,
"PATCH" => HttpMethod.Patch,
"OPTIONS" => HttpMethod.Options,
"TRACE" => HttpMethod.Trace,
_ => throw new Exception("Method is not selected."),
};
var processInfo = Connector.GetProcessInfo();
var requestBody = WeakReferenceMessenger.Default.Send(new ContentRequestMessage(), "ConsoleRequestEditor").Response;
var content = new StringContent(Regex.Replace(requestBody, @"\s+", ""), new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var response = await Connector.SendAsync(method, RequestPath, content) ?? throw new Exception("Response is null.");
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
var body = await response.Content.ReadAsStringAsync();
body = !string.IsNullOrEmpty(body) ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(body), App.JsonSerializerOptions) : string.Empty;
if (body.Length >= App.MaxCharacters) WindowService.ShowOopsiesWindow(body);
else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(body), nameof(ConsoleViewModel));
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode.ToString()}";
ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{RequestPath}";
ResponseAuthorization = $"Basic {riotAuthentication.Value}";
}
catch (Exception ex)
{
await SukiHost.ShowToast("Request Failed", ex.Message, SukiUI.Enums.NotificationType.Error);
ResponseStatus = null;
ResponsePath = null;
ResponseAuthorization = null;
WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(string.Empty), nameof(ConsoleViewModel));
}
finally
{
IsRequestBusy = false;
}
}
public void Receive(DataReadyMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(() =>
{
RequestPaths = new AvaloniaList<string>([.. message.Value.Paths]);
IsBusy = false;
});
}
}
}
@@ -1,42 +0,0 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Desktop.Messages;
using SukiUI.Controls;
using System.Linq;
namespace Needlework.Net.Desktop.ViewModels
{
public partial class EndpointViewModel : ObservableObject, ISukiStackPageTitleProvider
{
public string Endpoint { get; }
public string Title => Endpoint;
[ObservableProperty] private IAvaloniaReadOnlyList<PathOperationViewModel> _pathOperations;
[ObservableProperty] private PathOperationViewModel? _selectedPathOperation;
[ObservableProperty] private string? _search;
[ObservableProperty] private IAvaloniaReadOnlyList<PathOperationViewModel> _filteredPathOperations;
public EndpointViewModel(string endpoint)
{
Endpoint = endpoint;
var handler = WeakReferenceMessenger.Default.Send<DataRequestMessage>().Response;
PathOperations = new AvaloniaList<PathOperationViewModel>(handler.Plugins[endpoint].Select(x => new PathOperationViewModel(x)));
FilteredPathOperations = new AvaloniaList<PathOperationViewModel>(PathOperations);
}
partial void OnSearchChanged(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
FilteredPathOperations = new AvaloniaList<PathOperationViewModel>(PathOperations);
return;
}
FilteredPathOperations = new AvaloniaList<PathOperationViewModel>(PathOperations.Where(o => o.Path.ToLower().Contains(value.ToLower())));
}
}
}
@@ -1,21 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using SukiUI.Controls;
using System.Net.Http;
namespace Needlework.Net.Desktop.ViewModels
{
public partial class EndpointsContainerViewModel : PageBase
{
[ObservableProperty] private ISukiStackPageTitleProvider _activeViewModel;
public EndpointsContainerViewModel(HttpClient httpClient) : base("Endpoints", Material.Icons.MaterialIconKind.Hub, -500)
{
_activeViewModel = new EndpointsViewModel(httpClient, OnClicked);
}
private void OnClicked(ISukiStackPageTitleProvider viewModel)
{
ActiveViewModel = viewModel;
}
}
}
@@ -1,55 +0,0 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Desktop.Messages;
using SukiUI.Controls;
using System;
using System.Linq;
using System.Net.Http;
namespace Needlework.Net.Desktop.ViewModels
{
public partial class EndpointsViewModel : ObservableObject, IRecipient<DataReadyMessage>, ISukiStackPageTitleProvider
{
public HttpClient HttpClient { get; }
public string Title => "Endpoints";
public Action<ISukiStackPageTitleProvider> OnClicked;
[ObservableProperty] private IAvaloniaReadOnlyList<string> _plugins = new AvaloniaList<string>();
[ObservableProperty] private bool _isBusy = true;
[ObservableProperty] private string _search = string.Empty;
[ObservableProperty] private IAvaloniaReadOnlyList<string> _query = new AvaloniaList<string>();
[ObservableProperty] private string? _selectedQuery = string.Empty;
public EndpointsViewModel(HttpClient httpClient, Action<ISukiStackPageTitleProvider> onClicked)
{
HttpClient = httpClient;
OnClicked = onClicked;
WeakReferenceMessenger.Default.Register(this);
}
public void Receive(DataReadyMessage message)
{
IsBusy = false;
Plugins = new AvaloniaList<string>([.. message.Value.Plugins.Keys]);
Query = new AvaloniaList<string>([.. Plugins]);
}
partial void OnSearchChanged(string value)
{
if (!string.IsNullOrEmpty(Search))
Query = new AvaloniaList<string>(Plugins.Where(x => x.Contains(value)));
else
Query = Plugins;
}
partial void OnSelectedQueryChanged(string? value)
{
if (string.IsNullOrEmpty(value)) return;
OnClicked.Invoke(new EndpointViewModel(value));
}
}
}
@@ -1,64 +0,0 @@
using Avalonia.Media;
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Diagnostics;
using System.Threading;
namespace Needlework.Net.Desktop.ViewModels
{
public partial class HomeViewModel : PageBase
{
[ObservableProperty] private string _statusText = string.Empty;
[ObservableProperty] private IBrush? _statusForeground;
[ObservableProperty] private string _statusAddress = string.Empty;
public HomeViewModel() : base("Home", Material.Icons.MaterialIconKind.Home, int.MinValue)
{
var thread = new Thread(StartProcessing) { IsBackground = true };
thread.Start();
}
private void StartProcessing()
{
while (true)
{
void Set(string text, Color color, string address)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(() =>
{
StatusText = text;
StatusForeground = new SolidColorBrush(color.ToUInt32());
StatusAddress = address;
});
}
try
{
var processInfo = Connector.GetProcessInfo();
Set("Online", Colors.Green, $"https://127.0.0.1:{processInfo.AppPort}/");
}
catch (InvalidOperationException)
{
Set("Offline", Colors.Red, "N/A");
}
Thread.Sleep(TimeSpan.FromSeconds(5));
}
}
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo(url)
{
UseShellExecute = true
}
};
process.Start();
}
}
}
@@ -1,88 +0,0 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.OpenApi.Models;
using Needlework.Net.Core;
using Needlework.Net.Desktop.Messages;
using Needlework.Net.Desktop.Services;
using SukiUI.Controls;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
namespace Needlework.Net.Desktop.ViewModels
{
public partial class MainWindowViewModel : ObservableObject, IRecipient<DataRequestMessage>, IRecipient<HostDocumentRequestMessage>, IRecipient<OopsiesWindowRequestedMessage>
{
public IAvaloniaReadOnlyList<PageBase> Pages { get; }
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
public HttpClient HttpClient { get; }
public WindowService WindowService { get; }
public LcuSchemaHandler? LcuSchemaHandler { get; set; }
public OpenApiDocument? HostDocument { get; set; }
[ObservableProperty] private bool _isBusy = true;
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, WindowService windowService)
{
Pages = new AvaloniaList<PageBase>(pages.OrderBy(x => x.Index).ThenBy(x => x.DisplayName));
HttpClient = httpClient;
WindowService = windowService;
WeakReferenceMessenger.Default.RegisterAll(this);
Task.Run(FetchDataAsync);
}
private async Task FetchDataAsync()
{
var document = await Resources.GetOpenApiDocumentAsync(HttpClient);
HostDocument = document;
var handler = new LcuSchemaHandler(document);
LcuSchemaHandler = handler;
WeakReferenceMessenger.Default.Send(new DataReadyMessage(handler));
await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(async () => await SukiHost.ShowToast("OpenAPI Data Processed", "Some pages can now be used.", SukiUI.Enums.NotificationType.Success, TimeSpan.FromSeconds(5)));
IsBusy = false;
}
public void Receive(DataRequestMessage message)
{
message.Reply(LcuSchemaHandler!);
}
public void Receive(HostDocumentRequestMessage message)
{
message.Reply(HostDocument!);
}
[RelayCommand]
private void OpenUrl(string url)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo(url)
{
UseShellExecute = true
}
};
process.Start();
}
[RelayCommand]
private void OpenConsole()
{
}
public void Receive(OopsiesWindowRequestedMessage message)
{
WindowService.ShowOopsiesWindow(message.Value);
}
}
}
@@ -1,29 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Desktop.Messages;
using System.Diagnostics;
using System.IO;
namespace Needlework.Net.Desktop.ViewModels
{
public partial class OopsiesWindowViewModel(string text) : ObservableObject
{
public string Text { get; } = text;
[RelayCommand]
private void OpenDefaultEditor()
{
var temp = Path.GetTempFileName().Replace(".tmp", ".json");
File.WriteAllText(temp, Text);
Process.Start("explorer", "\"" + temp + "\"");
CloseDialog();
}
[RelayCommand]
private void CloseDialog()
{
WeakReferenceMessenger.Default.Send(new OopsiesWindowCanceledMessage(null));
}
}
}
@@ -1,153 +0,0 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.OpenApi.Models;
using Needlework.Net.Desktop.Messages;
using System.Collections.Generic;
namespace Needlework.Net.Desktop.ViewModels
{
public partial class OperationViewModel : ObservableObject
{
public string Summary { get; }
public string Description { get; }
public string ReturnType { get; }
public bool IsRequestBody { get; }
public string? RequestBodyType { get; }
public IAvaloniaReadOnlyList<PropertyClassViewModel> RequestClasses { get; }
public IAvaloniaReadOnlyList<PropertyClassViewModel> ResponseClasses { get; }
public IAvaloniaReadOnlyList<ParameterViewModel> PathParameters { get; }
public IAvaloniaReadOnlyList<ParameterViewModel> QueryParameters { get; }
public OperationViewModel(OpenApiOperation operation)
{
Summary = operation.Summary ?? string.Empty;
Description = operation.Description ?? string.Empty;
IsRequestBody = operation.RequestBody != null;
ReturnType = GetReturnType(operation.Responses);
RequestClasses = GetRequestClasses(operation.RequestBody);
ResponseClasses = GetResponseClasses(operation.Responses);
PathParameters = GetParameters(operation.Parameters, ParameterLocation.Path);
QueryParameters = GetParameters(operation.Parameters, ParameterLocation.Query);
RequestBodyType = GetRequestBodyType(operation.RequestBody);
}
private string? GetRequestBodyType(OpenApiRequestBody? requestBody)
{
if (requestBody == null) return null;
if (requestBody.Content.TryGetValue("application/json", out var media))
{
var schema = media.Schema;
return GetSchemaType(schema);
}
return null;
}
private AvaloniaList<ParameterViewModel> GetParameters(IList<OpenApiParameter> parameters, ParameterLocation location)
{
var pathParameters = new AvaloniaList<ParameterViewModel>();
foreach (var parameter in parameters)
{
if (parameter.In != location) continue;
pathParameters.Add(new ParameterViewModel(parameter.Name, GetSchemaType(parameter.Schema), parameter.Required));
}
return pathParameters;
}
private AvaloniaList<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses)
{
if (responses.TryGetValue("2XX", out var response)
&& response.Content.TryGetValue("application/json", out var media))
{
var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response;
var schema = media.Schema;
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
WalkSchema(schema, propertyClasses, document);
return propertyClasses;
}
return [];
}
private void WalkSchema(OpenApiSchema schema, AvaloniaList<PropertyClassViewModel> propertyClasses, OpenApiDocument document)
{
var type = GetSchemaType(schema);
if (IsComponent(type))
{
string componentId = GetComponentId(schema);
var componentSchema = document.Components.Schemas[componentId];
var responseClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum);
propertyClasses.Add(responseClass);
foreach ((var _, var property) in componentSchema.Properties)
// Check for self-references like "LolLootLootOddsResponse"
// I blame dubble
if (IsComponent(GetSchemaType(property)) && componentId != GetComponentId(property))
WalkSchema(property, propertyClasses, document);
}
}
private static string GetComponentId(OpenApiSchema schema)
{
string componentId;
if (schema.Reference != null) componentId = schema.Reference.Id;
else if (schema.Items != null) componentId = schema.Items.Reference.Id;
else componentId = schema.AdditionalProperties.Reference.Id;
return componentId;
}
private static bool IsComponent(string type)
{
return !(type.Contains("object")
|| type.Contains("array")
|| type.Contains("bool")
|| type.Contains("string")
|| type.Contains("integer")
|| type.Contains("number"));
}
private AvaloniaList<PropertyClassViewModel> GetRequestClasses(OpenApiRequestBody? requestBody)
{
if (requestBody == null) return [];
if (requestBody.Content.TryGetValue("application/json", out var media))
{
var document = WeakReferenceMessenger.Default.Send(new HostDocumentRequestMessage()).Response;
var schema = media.Schema;
if (schema == null) return [];
var type = GetSchemaType(media.Schema);
if (IsComponent(type))
{
var componentId = GetComponentId(schema);
var componentSchema = document.Components.Schemas[componentId];
AvaloniaList<PropertyClassViewModel> propertyClasses = [];
WalkSchema(componentSchema, propertyClasses, document);
return propertyClasses;
}
}
return [];
}
private string GetReturnType(OpenApiResponses responses)
{
if (responses.TryGetValue("2XX", out var response)
&& response.Content.TryGetValue("application/json", out var media))
{
var schema = media.Schema;
return GetSchemaType(schema);
}
return "none";
}
public static string GetSchemaType(OpenApiSchema schema)
{
if (schema.Reference != null) return schema.Reference.Id;
if (schema.Type == "object" && schema.AdditionalProperties?.Reference != null) return schema.AdditionalProperties.Reference.Id;
if (schema.Type == "integer" || schema.Type == "number") return $"{schema.Type}:{schema.Format}";
if (schema.Type == "array" && schema.Items.Reference != null) return $"{schema.Items.Reference.Id}[]";
if (schema.Type == "array" && (schema.Items.Type == "integer" || schema.Items.Type == "number")) return $"{schema.Items.Type}:{schema.Items.Format}[]";
if (schema.Type == "array") return $"{schema.Items.Type}[]";
return schema.Type;
}
}
}
@@ -1,13 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Material.Icons;
namespace Needlework.Net.Desktop.ViewModels
{
public abstract partial class PageBase(string displayName, MaterialIconKind icon, int index = 0) : ObservableValidator
{
[ObservableProperty] private string _displayName = displayName;
[ObservableProperty] private MaterialIconKind _icon = icon;
[ObservableProperty] private int _index = index;
}
}
@@ -1,20 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.Desktop.ViewModels
{
public partial class ParameterViewModel : ObservableObject
{
public string Name { get; }
public string Type { get; }
public bool IsRequired { get; }
[ObservableProperty] private string? _value = null;
public ParameterViewModel(string name, string type, bool isRequired, string? value = null)
{
Name = name;
Type = type;
IsRequired = isRequired;
Value = value;
}
}
}
@@ -1,133 +0,0 @@
using Avalonia.Media;
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Core;
using Needlework.Net.Desktop.Messages;
using SukiUI.Controls;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Needlework.Net.Desktop.ViewModels
{
public partial class PathOperationViewModel : ObservableObject
{
public string Method { get; }
public SolidColorBrush Color { get; }
public string Path { get; }
public OperationViewModel Operation { get; }
public ProcessInfo? ProcessInfo { get; }
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private string? _responsePath;
[ObservableProperty] private string? _responseStatus;
[ObservableProperty] private string? _responseAuthentication;
[ObservableProperty] private string? _responseUsername;
[ObservableProperty] private string? _responsePassword;
[ObservableProperty] private string? _responseAuthorization;
public PathOperationViewModel(PathOperation pathOperation)
{
Method = pathOperation.Method.ToUpper();
Color = new SolidColorBrush(GetColor(pathOperation.Method.ToUpper()));
Path = pathOperation.Path;
Operation = new OperationViewModel(pathOperation.Operation);
ProcessInfo = GetProcessInfo();
ResponsePath = ProcessInfo != null ? $"https://127.0.0.1:{ProcessInfo.AppPort}{Path}" : null;
ResponseUsername = ProcessInfo != null ? new RiotAuthentication(ProcessInfo.RemotingAuthToken).Username : null;
ResponsePassword = ProcessInfo != null ? new RiotAuthentication(ProcessInfo.RemotingAuthToken).Password : null;
ResponseAuthorization = ProcessInfo != null ? $"Basic {new RiotAuthentication(ProcessInfo.RemotingAuthToken).Value}" : null;
}
private ProcessInfo? GetProcessInfo()
{
try
{
var processInfo = Connector.GetProcessInfo();
return processInfo;
}
catch (Exception) { }
return null;
}
[RelayCommand]
public async Task SendRequest()
{
try
{
IsBusy = true;
var method = Method.ToUpper() switch
{
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
"DELETE" => HttpMethod.Delete,
"HEAD" => HttpMethod.Head,
"PATCH" => HttpMethod.Patch,
"OPTIONS" => HttpMethod.Options,
"TRACE" => HttpMethod.Trace,
_ => throw new Exception("Method is missing.")
};
var processInfo = Connector.GetProcessInfo();
var path = Path;
foreach (var pathParameter in Operation.PathParameters)
{
path = path.Replace($"{{{pathParameter.Name}}}", pathParameter.Value);
}
var query = "";
foreach (var queryParameter in Operation.QueryParameters)
{
if (query.Length != 0 && !string.IsNullOrWhiteSpace(queryParameter.Value))
query += $"&{queryParameter.Name}={Uri.EscapeDataString(queryParameter.Value)}";
else if (query.Length == 0 && !string.IsNullOrWhiteSpace(queryParameter.Value))
query += $"?{queryParameter.Name}={Uri.EscapeDataString(queryParameter.Value)}";
}
var uri = $"{path}{query}";
var requestBody = WeakReferenceMessenger.Default.Send(new ContentRequestMessage(), "EndpointRequestEditor").Response;
var content = new StringContent(Regex.Replace(requestBody, @"\s+", ""), new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
var response = await Connector.SendAsync(method, $"{uri}", content) ?? throw new Exception("Response is null.");
var riotAuthentication = new RiotAuthentication(processInfo.RemotingAuthToken);
var responseBody = await response.Content.ReadAsStringAsync();
responseBody = !string.IsNullOrEmpty(responseBody) ? JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(responseBody), App.JsonSerializerOptions) : string.Empty;
if (responseBody.Length >= App.MaxCharacters) WeakReferenceMessenger.Default.Send(new OopsiesWindowRequestedMessage(responseBody));
else WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(responseBody, "EndpointResponseEditor")));
ResponseStatus = $"{(int)response.StatusCode} {response.StatusCode}";
ResponsePath = $"https://127.0.0.1:{processInfo.AppPort}{uri}";
ResponseAuthentication = $"Basic {riotAuthentication.Value}";
ResponseUsername = riotAuthentication.Username;
ResponsePassword = riotAuthentication.Password;
}
catch (Exception ex)
{
await SukiHost.ShowToast("Request Failed", ex.Message, SukiUI.Enums.NotificationType.Error);
WeakReferenceMessenger.Default.Send(new EditorUpdateMessage(new(string.Empty, "EndpointResponseEditor")));
}
finally
{
IsBusy = false;
}
}
public static Color GetColor(string method) => method switch
{
"GET" => Avalonia.Media.Color.FromRgb(95, 99, 186),
"POST" => Avalonia.Media.Color.FromRgb(103, 186, 95),
"PUT" => Avalonia.Media.Color.FromRgb(186, 139, 95),
"DELETE" => Avalonia.Media.Color.FromRgb(186, 95, 95),
"HEAD" => Avalonia.Media.Color.FromRgb(136, 95, 186),
"PATCH" => Avalonia.Media.Color.FromRgb(95, 186, 139),
_ => throw new InvalidOperationException("Method does not have assigned color.")
};
}
}
@@ -1,36 +0,0 @@
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using System.Collections.Generic;
using System.Linq;
namespace Needlework.Net.Desktop.ViewModels
{
public class PropertyClassViewModel : ObservableObject
{
public string Id { get; }
public IAvaloniaReadOnlyList<PropertyFieldViewModel> PropertyFields { get; } = new AvaloniaList<PropertyFieldViewModel>();
public IAvaloniaReadOnlyList<PropertyEnumViewModel> PropertyEnums { get; } = new AvaloniaList<PropertyEnumViewModel>();
public PropertyClassViewModel(string id, IDictionary<string, OpenApiSchema> properties, IList<IOpenApiAny> enumValue)
{
AvaloniaList<PropertyFieldViewModel> propertyFields = [];
AvaloniaList<PropertyEnumViewModel> propertyEnums = [];
foreach ((var propertyName, var propertySchema) in properties)
{
var type = OperationViewModel.GetSchemaType(propertySchema);
var field = new PropertyFieldViewModel(propertyName, type);
propertyFields.Add(field);
}
if (enumValue != null && enumValue.Any())
{
var propertyEnum = new PropertyEnumViewModel(enumValue);
propertyEnums.Add(propertyEnum);
}
PropertyFields = propertyFields;
PropertyEnums = propertyEnums;
Id = id;
}
}
}
@@ -1,17 +0,0 @@
using Microsoft.OpenApi.Any;
using System.Collections.Generic;
using System.Linq;
namespace Needlework.Net.Desktop.ViewModels
{
public class PropertyEnumViewModel
{
public string Type { get; } = "Enum";
public string Values { get; }
public PropertyEnumViewModel(IList<IOpenApiAny> enumValue)
{
Values = $"[{string.Join(", ", enumValue.Select(x => ((OpenApiString)x).Value).ToList())}]";
}
}
}
@@ -1,14 +0,0 @@
namespace Needlework.Net.Desktop.ViewModels
{
public class PropertyFieldViewModel
{
public string Name { get; }
public string Type { get; }
public PropertyFieldViewModel(string name, string type)
{
Name = name;
Type = type;
}
}
}
@@ -1,125 +0,0 @@
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Material.Icons;
using Needlework.Net.Desktop.Messages;
using Needlework.Net.Desktop.Services;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading;
using Websocket.Client;
namespace Needlework.Net.Desktop.ViewModels
{
public partial class WebsocketViewModel : PageBase
{
[NotifyPropertyChangedFor(nameof(FilteredEventLog))]
[ObservableProperty] private ObservableCollection<string> _eventLog = [];
[NotifyPropertyChangedFor(nameof(FilteredEventLog))]
[ObservableProperty] private string _search = string.Empty;
[ObservableProperty] private bool _isAttach = true;
[ObservableProperty] private bool _isTail = false;
[ObservableProperty] private string? _selectedEventLog = null;
private Dictionary<string, EventMessage> _events = [];
public WebsocketClient? Client { get; set; }
public WindowService WindowService { get; }
public List<string> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? [.. EventLog] : [.. EventLog.Where(x => x.ToLower().Contains(Search.ToLower()))];
public WebsocketViewModel(WindowService windowService) : base("Event Viewer", MaterialIconKind.Connection, -100)
{
WindowService = windowService;
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
thread.Start();
}
private void InitializeWebsocket()
{
while (true)
{
try
{
var client = Connector.CreateLcuWebsocketClient();
client.EventReceived.Subscribe(OnMessage);
client.DisconnectionHappened.Subscribe(OnDisconnection);
client.ReconnectionHappened.Subscribe(OnReconnection);
client.Start();
client.Send(new EventMessage(RequestType.Subscribe, EventMessage.Kinds.OnJsonApiEvent));
Client = client;
return;
}
catch (Exception) { }
Thread.Sleep(TimeSpan.FromSeconds(5));
}
}
[RelayCommand]
private void Clear()
{
EventLog = [];
}
partial void OnSelectedEventLogChanged(string? value)
{
if (value == null) return;
if (_events.TryGetValue(value, out var message))
{
var text = JsonSerializer.Serialize(message, App.JsonSerializerOptions);
if (text.Length >= App.MaxCharacters) WindowService.ShowOopsiesWindow(text);
else WeakReferenceMessenger.Default.Send(new ResponseUpdatedMessage(text), nameof(WebsocketViewModel));
}
}
private void OnReconnection(ReconnectionInfo info)
{
Trace.WriteLine($"-- Reconnection --\nType{info.Type}");
}
private void OnDisconnection(DisconnectionInfo info)
{
Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}");
Client?.Dispose();
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
thread.Start();
}
private void OnMessage(EventMessage message)
{
Avalonia.Threading.Dispatcher.UIThread.Invoke(() =>
{
if (!IsAttach) return;
var line = $"{DateTime.Now:HH:mm:ss.fff} {message.Data?.EventType.ToUpper()} {message.Data?.Uri}";
var log = EventLog.ToList();
Trace.WriteLine($"Message: {line}");
if (log.Count < 1000)
{
log.Add(line);
_events[line] = message;
}
else
{
var key = $"{log[0]}";
log.RemoveAt(0);
_events.Remove(key);
log.Add(line);
_events[line] = message;
}
EventLog = []; // This is a hack needed to update for ListBox
EventLog = new ObservableCollection<string>(log);
});
}
}
}
@@ -1,41 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
xmlns:theme="clr-namespace:SukiUI.Theme;assembly=SukiUI"
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Desktop.Views.AboutView"
x:DataType="vm:AboutViewModel">
<Grid Margin="8"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<WrapPanel
theme:WrapPanelExtensions.AnimatedScroll="true"
Orientation="Horizontal">
<suki:GlassCard Margin="8">
<Image Source="/Assets/about.png"
RenderOptions.BitmapInterpolationMode="MediumQuality"
Width="200"
Height="200"/>
</suki:GlassCard>
<StackPanel>
<suki:GlassCard Width="400" Margin="8">
<StackPanel>
<TextBlock Classes="h3">Blossomi Shymae</TextBlock>
</StackPanel>
</suki:GlassCard>
<suki:GlassCard Width="400" Margin="8">
<suki:GroupBox Header="About">
<TextBlock TextWrapping="Wrap">
Needlework.Net is .NET rewrite of Needlework. Like Needlework, this project is inspired by
LCU Explorer. This tool was made to help others with LCU development. Feel free to ask any questions
or help contribute to the project! 💜
</TextBlock>
</suki:GroupBox>
</suki:GlassCard>
</StackPanel>
</WrapPanel>
</Grid>
</UserControl>
@@ -1,86 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:theme="clr-namespace:SukiUI.Theme;assembly=SukiUI"
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Desktop.Views.ConsoleView"
x:DataType="vm:ConsoleViewModel">
<suki:BusyArea IsBusy="{Binding IsBusy}" BusyText="Loading...">
<Grid Margin="16" RowDefinitions="auto,*" ColumnDefinitions="*,*">
<Grid Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2">
<suki:GlassCard Margin="0 0 0 16">
<suki:GroupBox Header="Console">
<Grid RowDefinitions="auto,auto" ColumnDefinitions="auto,*">
<ComboBox ItemsSource="{Binding RequestMethods}" SelectedItem="{Binding RequestMethodSelected}"
Grid.Row="0" Grid.Column="0"/>
<AutoCompleteBox
ItemsSource="{Binding RequestPaths}"
Text="{Binding RequestPath}"
MaxDropDownHeight="400"
FilterMode="StartsWith"
Grid.Row="0" Grid.Column="1"/>
<avaloniaEdit:TextEditor
Name="RequestEditor"
Text=""
ShowLineNumbers="True"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible"
FontSize="12"
Height="100"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"/>
</Grid>
</suki:GroupBox>
</suki:GlassCard>
<Button Classes="Flat Rounded"
Margin="0 0 0 0"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
FontWeight="DemiBold"
Command="{Binding SendRequestCommand}"
theme:ButtonExtensions.ShowProgress="{Binding IsRequestBusy}">
Send
</Button>
</Grid>
<StackPanel
Margin="0 0 8 0"
Grid.Row="1"
Grid.Column="0">
<suki:GlassCard Margin="0 4">
<suki:GroupBox Header="Path">
<TextBlock Text="{Binding ResponsePath}"/>
</suki:GroupBox>
</suki:GlassCard>
<suki:GlassCard Margin="0 4">
<suki:GroupBox Header="Status">
<TextBlock Text="{Binding ResponseStatus}"/>
</suki:GroupBox>
</suki:GlassCard>
<suki:GlassCard Margin="0 4">
<suki:GroupBox Header="Authorization">
<TextBlock Text="{Binding ResponseAuthorization}" />
</suki:GroupBox>
</suki:GlassCard>
</StackPanel>
<suki:GlassCard
Margin="0 8"
Grid.Row="1"
Grid.Column="1">
<avaloniaEdit:TextEditor
Name="ResponseEditor"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible"
ShowLineNumbers="True"
Text=""
FontSize="12"/>
</suki:GlassCard>
</Grid>
</suki:BusyArea>
</UserControl>
@@ -1,72 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using AvaloniaEdit;
using AvaloniaEdit.TextMate;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Desktop.Extensions;
using Needlework.Net.Desktop.Messages;
using Needlework.Net.Desktop.ViewModels;
using SukiUI;
using TextMateSharp.Grammars;
namespace Needlework.Net.Desktop.Views;
public partial class ConsoleView : UserControl, IRecipient<ResponseUpdatedMessage>, IRecipient<ContentRequestMessage>
{
private TextEditor? _responseEditor;
private TextEditor? _requestEditor;
public ConsoleView()
{
InitializeComponent();
}
public void Receive(ResponseUpdatedMessage message)
{
_responseEditor!.Text = message.Value;
}
public void Receive(ContentRequestMessage message)
{
message.Reply(_requestEditor!.Text);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_responseEditor = this.FindControl<TextEditor>("ResponseEditor");
_requestEditor = this.FindControl<TextEditor>("RequestEditor");
_responseEditor?.ApplyJsonEditorSettings();
_requestEditor?.ApplyJsonEditorSettings();
WeakReferenceMessenger.Default.Register<ResponseUpdatedMessage, string>(this, nameof(ConsoleViewModel));
WeakReferenceMessenger.Default.Register<ContentRequestMessage, string>(this, "ConsoleRequestEditor");
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
SukiTheme.GetInstance().OnBaseThemeChanged += OnBaseThemeChanged;
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
WeakReferenceMessenger.Default.UnregisterAll(this);
SukiTheme.GetInstance().OnBaseThemeChanged -= OnBaseThemeChanged;
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)
{
var registryOptions = new RegistryOptions(
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
var responseTmi = _responseEditor.InstallTextMate(registryOptions);
responseTmi.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions
.GetLanguageByExtension(".json").Id));
var requestTmi = _requestEditor.InstallTextMate(registryOptions);
requestTmi.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions
.GetLanguageByExtension(".json").Id));
}
}
@@ -1,321 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:theme="clr-namespace:SukiUI.Theme;assembly=SukiUI"
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
xmlns:avalonEdit="https://github.com/avaloniaui/avaloniaedit"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Desktop.Views.EndpointView"
x:DataType="vm:EndpointViewModel">
<Grid RowDefinitions="auto,*" ColumnDefinitions="3*,2,4*,2,4*">
<Grid Grid.Row="0"
Grid.Column="0"
RowDefinitions="*"
ColumnDefinitions="auto,*">
<TextBox Text="{Binding Search}"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"/>
</Grid>
<Grid Grid.Row="1"
Grid.Column="0"
RowDefinitions="*"
ColumnDefinitions="*">
<ListBox ItemsSource="{Binding FilteredPathOperations}"
SelectedItem="{Binding SelectedPathOperation}"
ScrollViewer.HorizontalScrollBarVisibility="Visible"
Margin="0 0 0 0"
Grid.Row="1"
Grid.Column="0">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid
RowDefinitions="*"
ColumnDefinitions="auto,*">
<Button
VerticalAlignment="Center"
Classes="Flat"
Margin="0 0 8 0"
Content="{Binding Method}"
Background="{Binding Color}"
FontSize="8"
Width="45"
Padding="10 2 10 2"
Grid.Row="0"
Grid.Column="0"/>
<TextBlock
VerticalAlignment="Center"
Text="{Binding Path}"
FontSize="11"
Grid.Row="0"
Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<GridSplitter Background="Gray"
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"/>
<Grid Grid.Row="0"
Grid.Column="2"
RowDefinitions="*"
ColumnDefinitions="auto,*,auto">
<TextBox Grid.Row="0"
Grid.Column="0"
Text="{Binding SelectedPathOperation.Method}"
FontSize="12"
IsReadOnly="True"/>
<TextBox Grid.Row="0"
Grid.Column="1"
FontSize="12"
Text="{Binding SelectedPathOperation.ResponsePath}"
IsReadOnly="True"/>
<StackPanel Grid.Row="0"
Grid.Column="2"
Orientation="Horizontal">
<Button Classes="Flat"
Margin="4"
FontSize="12"
HorizontalAlignment="Right"
Padding="12 4 12 4"
VerticalAlignment="Center"
Command="{Binding SelectedPathOperation.SendRequestCommand}">Send</Button>
</StackPanel>
</Grid>
<Grid Grid.Row="1" Grid.Column="2">
<TabControl>
<TabItem Header="Params">
<ScrollViewer>
<StackPanel IsVisible="{Binding SelectedPathOperation, Converter={StaticResource NullBoolConverter}}">
<suki:GroupBox Header="Path Parameters"
Margin="0 4"
IsVisible="{Binding SelectedPathOperation.Operation.PathParameters, Converter={StaticResource EnumerableBoolConverter}}">
<DataGrid
ItemsSource="{Binding SelectedPathOperation.Operation.PathParameters}"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
<DataGrid.Styles>
<Style Selector="DataGridRow DataGridCell">
<Setter Property="FontSize" Value="12"></Setter>
</Style>
</DataGrid.Styles>
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridCheckBoxColumn Header="Required" Binding="{Binding IsRequired}"/>
<DataGridTemplateColumn Header="Value">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="vm:ParameterViewModel">
<TextBox Text="{Binding Value}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Type" Binding="{Binding Type}"/>
</DataGrid.Columns>
</DataGrid>
</suki:GroupBox>
<suki:GroupBox Header="Query Parameters"
Margin="0 4"
IsVisible="{Binding SelectedPathOperation.Operation.QueryParameters, Converter={StaticResource EnumerableBoolConverter}}">
<DataGrid
ItemsSource="{Binding SelectedPathOperation.Operation.QueryParameters}"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
<DataGrid.Styles>
<Style Selector="DataGridRow DataGridCell">
<Setter Property="FontSize" Value="12"></Setter>
</Style>
</DataGrid.Styles>
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridCheckBoxColumn Header="Required" Binding="{Binding IsRequired}"/>
<DataGridTemplateColumn Header="Value">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="vm:ParameterViewModel">
<TextBox Text="{Binding Value}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Type" Binding="{Binding Type}"/>
</DataGrid.Columns>
</DataGrid>
</suki:GroupBox>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="Body">
<avalonEdit:TextEditor
Name="EndpointRequestEditor"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible"
Text=""
ShowLineNumbers="True"
FontSize="12"/>
</TabItem>
<TabItem Header="Auth">
<Grid RowDefinitions="auto,auto,auto,*" ColumnDefinitions="*,4*">
<TextBlock FontSize="12"
Grid.Row="0"
Grid.Column="0"
VerticalAlignment="Center">
Username
</TextBlock>
<TextBox FontSize="12"
Grid.Row="0"
Grid.Column="1"
IsReadOnly="True"
Text="{Binding SelectedPathOperation.ResponseUsername}" />
<TextBlock FontSize="12"
Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Center">
Password
</TextBlock>
<TextBox FontSize="12"
Grid.Row="1"
Grid.Column="1"
IsReadOnly="True"
Text="{Binding SelectedPathOperation.ResponsePassword}"/>
<TextBlock FontSize="12"
Grid.Row="2"
Grid.Column="0"
VerticalAlignment="Center">
Authorization
</TextBlock>
<TextBox FontSize="12"
Grid.Row="2"
Grid.Column="1"
IsReadOnly="True"
Text="{Binding SelectedPathOperation.ResponseAuthorization}"/>
</Grid>
</TabItem>
<TabItem Header="Schemas">
<ScrollViewer>
<StackPanel>
<suki:GlassCard Margin="0 4" IsVisible="{Binding SelectedPathOperation.Operation.RequestBodyType, Converter={StaticResource NullBoolConverter}}">
<TextBlock>
<Run Text="Request body: " FontWeight="DemiBold" FontSize="12"/>
<Run Text="{Binding SelectedPathOperation.Operation.RequestBodyType}" FontSize="12"/>
</TextBlock>
</suki:GlassCard>
<Border Margin="0 4" IsVisible="{Binding SelectedPathOperation.Operation.RequestClasses, Converter={StaticResource EnumerableBoolConverter}}">
<ItemsRepeater ItemsSource="{Binding SelectedPathOperation.Operation.RequestClasses}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0 4 0 8">
<suki:GlassCard IsVisible="{Binding PropertyFields, Converter={StaticResource EnumerableBoolConverter}}">
<suki:GroupBox Header="{Binding Id}">
<DataGrid
ItemsSource="{Binding PropertyFields}"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
<DataGrid.Styles>
<Style Selector="DataGridRow DataGridCell">
<Setter Property="FontSize" Value="12"></Setter>
</Style>
</DataGrid.Styles>
</DataGrid>
</suki:GroupBox>
</suki:GlassCard>
<suki:GlassCard Margin="0 0 0 8" IsVisible="{Binding PropertyEnums, Converter={StaticResource EnumerableBoolConverter}}">
<suki:GroupBox Header="{Binding Id}">
<DataGrid
ItemsSource="{Binding PropertyEnums}"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
<DataGrid.Styles>
<Style Selector="DataGridRow DataGridCell">
<Setter Property="FontSize" Value="12"></Setter>
</Style>
</DataGrid.Styles>
</DataGrid>
</suki:GroupBox>
</suki:GlassCard>
</StackPanel>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Border>
<suki:GlassCard Margin="0 4">
<TextBlock>
<Run Text="Return value: " FontWeight="DemiBold" FontSize="12"/>
<Run Text="{Binding SelectedPathOperation.Operation.ReturnType}" FontSize="12"/>
</TextBlock>
</suki:GlassCard>
<Border Margin="0 4" IsVisible="{Binding SelectedPathOperation.Operation.ResponseClasses, Converter={StaticResource EnumerableBoolConverter}}">
<ItemsRepeater ItemsSource="{Binding SelectedPathOperation.Operation.ResponseClasses}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0 4 0 8">
<suki:GlassCard IsVisible="{Binding PropertyFields, Converter={StaticResource EnumerableBoolConverter}}">
<suki:GroupBox Header="{Binding Id}">
<DataGrid
ItemsSource="{Binding PropertyFields}"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
<DataGrid.Styles>
<Style Selector="DataGridRow DataGridCell">
<Setter Property="FontSize" Value="12"></Setter>
</Style>
</DataGrid.Styles>
</DataGrid>
</suki:GroupBox>
</suki:GlassCard>
<suki:GlassCard Margin="0 0 0 8" IsVisible="{Binding PropertyEnums, Converter={StaticResource EnumerableBoolConverter}}">
<suki:GroupBox Header="{Binding Id}">
<DataGrid
ItemsSource="{Binding PropertyEnums}"
AutoGenerateColumns="True"
IsReadOnly="True"
GridLinesVisibility="Horizontal">
<DataGrid.Styles>
<Style Selector="DataGridRow DataGridCell">
<Setter Property="FontSize" Value="12"></Setter>
</Style>
</DataGrid.Styles>
</DataGrid>
</suki:GroupBox>
</suki:GlassCard>
</StackPanel>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
<GridSplitter Grid.Row="0" Grid.Column="3" Grid.RowSpan="2" Background="Gray"/>
<StackPanel Grid.Row="0" Grid.Column="4" Orientation="Horizontal">
<Button HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="4"
FontSize="10"
Padding="12 4 12 4"
Classes="Flat"
Content="{Binding SelectedPathOperation.ResponseStatus}"/>
</StackPanel>
<Grid Grid.Row="1" Grid.Column="4">
<TabControl>
<TabItem Header="Preview">
<avalonEdit:TextEditor
Name="EndpointResponseEditor"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible"
ShowLineNumbers="True"
Text=""
FontSize="12"/>
</TabItem>
</TabControl>
</Grid>
</Grid>
</UserControl>
@@ -1,83 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using AvaloniaEdit;
using AvaloniaEdit.TextMate;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Desktop.Extensions;
using Needlework.Net.Desktop.Messages;
using Needlework.Net.Desktop.ViewModels;
using SukiUI;
using TextMateSharp.Grammars;
namespace Needlework.Net.Desktop.Views;
public partial class EndpointView : UserControl, IRecipient<EditorUpdateMessage>, IRecipient<ContentRequestMessage>
{
private TextEditor? _requestEditor;
private TextEditor? _responseEditor;
public EndpointView()
{
InitializeComponent();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var vm = (EndpointViewModel)DataContext!;
_requestEditor = this.FindControl<TextEditor>("EndpointRequestEditor");
_responseEditor = this.FindControl<TextEditor>("EndpointResponseEditor");
_requestEditor?.ApplyJsonEditorSettings();
_responseEditor?.ApplyJsonEditorSettings();
WeakReferenceMessenger.Default.Register<EditorUpdateMessage>(this);
WeakReferenceMessenger.Default.Register<ContentRequestMessage, string>(this, "EndpointRequestEditor");
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
SukiTheme.GetInstance().OnBaseThemeChanged += OnBaseThemeChanged;
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
WeakReferenceMessenger.Default.UnregisterAll(this);
SukiTheme.GetInstance().OnBaseThemeChanged -= OnBaseThemeChanged;
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)
{
var registryOptions = new RegistryOptions(
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
var requestTmi = _requestEditor.InstallTextMate(registryOptions);
requestTmi.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions
.GetLanguageByExtension(".json").Id));
var responseTmi = _requestEditor.InstallTextMate(registryOptions);
responseTmi.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions
.GetLanguageByExtension(".json").Id));
}
public void Receive(EditorUpdateMessage message)
{
switch (message.Value.Key)
{
case "EndpointRequestEditor":
_requestEditor!.Text = message.Value.Text;
break;
case "EndpointResponseEditor":
_responseEditor!.Text = message.Value.Text;
break;
default:
break;
}
}
public void Receive(ContentRequestMessage message)
{
message.Reply(_requestEditor!.Text);
}
}
@@ -1,14 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
xmlns:theme="clr-namespace:SukiUI.Theme;assembly=SukiUI"
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
xmlns:avalonEdit="https://github.com/avaloniaui/avaloniaedit"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Desktop.Views.EndpointsContainerView"
x:DataType="vm:EndpointsContainerViewModel">
<suki:SukiStackPage Content="{Binding ActiveViewModel}"
Margin="-24 -4 0 0"/>
</UserControl>
@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace Needlework.Net.Desktop.Views;
public partial class EndpointsContainerView : UserControl
{
public EndpointsContainerView()
{
InitializeComponent();
}
}
@@ -1,25 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
xmlns:theme="clr-namespace:SukiUI.Theme;assembly=SukiUI"
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Desktop.Views.EndpointsView"
x:DataType="vm:EndpointsViewModel">
<suki:BusyArea IsBusy="{Binding IsBusy}" BusyText="Loading...">
<Grid Margin="16" RowDefinitions="auto,auto,*" ColumnDefinitions="*">
<TextBox Watermark="Search" Margin="0 4" Text="{Binding Search}" Grid.Row="1" Grid.Column="0"/>
<ScrollViewer Grid.Row="2" Grid.Column="0">
<ListBox ItemsSource="{Binding Query}" SelectedItem="{Binding SelectedQuery}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Foreground="White" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</Grid>
</suki:BusyArea>
</UserControl>
@@ -1,12 +0,0 @@
using Avalonia.Controls;
namespace Needlework.Net.Desktop.Views
{
public partial class EndpointsView : UserControl
{
public EndpointsView()
{
InitializeComponent();
}
}
}
@@ -1,75 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
xmlns:theme="clr-namespace:SukiUI.Theme;assembly=SukiUI"
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Desktop.Views.HomeView"
x:DataType="vm:HomeViewModel">
<!-- TOP LEVEL -->
<ScrollViewer>
<WrapPanel Margin="8"
theme:WrapPanelExtensions.AnimatedScroll="true"
Orientation="Horizontal">
<!-- WELCOME -->
<StackPanel>
<suki:GlassCard Margin="8">
<StackPanel>
<TextBlock Classes="h3">Welcome to Needlework.Net</TextBlock>
<TextBlock>Get started with LCU development by clicking on the endpoints tab in the left panel.</TextBlock>
</StackPanel>
</suki:GlassCard>
<suki:GlassCard Margin="8" Classes="Accent">
<TextBlock TextWrapping="Wrap">THE PROGRAM IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF MERCHANTABILITY, NONINFRINGMENT, OR OF FITNESS FOR A PARTICULAR PURPOSE. LICENSOR DOES NOT WARRANT THAT THE FUNCTIONS CONTAINED IN THE PROGRAM WILL MEET YOUR REQUIREMENTS OR THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. LICENSOR MAKES NO WARRANTIES RESPECTING ANY HARM THAT MAY BE CAUSED BY MALICIOUS USE OF THIS SOFTWARE. LICENSOR FURTHER EXPRESSLY DISCLAIMS ANY WARRANTY OR REPRESENTATION TO AUTHORIZED USERS OR TO ANY THIRD PARTY.</TextBlock>
</suki:GlassCard>
</StackPanel>
<!-- STATUS -->
<StackPanel>
<suki:GlassCard Margin="8" Width="250">
<suki:GroupBox Header="Status">
<TextBlock FontSize="24" FontWeight="Bold" Margin="0 4" Foreground="{Binding StatusForeground}" Text="{Binding StatusText}" />
</suki:GroupBox>
</suki:GlassCard>
<suki:GlassCard Margin="8" Width="250">
<suki:GroupBox Header="Address">
<TextBlock Text="{Binding StatusAddress}"/>
</suki:GroupBox>
</suki:GlassCard>
</StackPanel>
<!-- LEGAL -->
<suki:GlassCard Margin="8" Width="300">
<suki:GroupBox Header="Disclaimer">
<TextBlock TextWrapping="Wrap">Needlework.Net isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties. Riot Games, and all associated properties are trademarks or registered trademarks of Riot Games, Inc.</TextBlock>
</suki:GroupBox>
</suki:GlassCard>
<!-- FOOTER -->
<StackPanel>
<suki:GlassCard Margin="8" Width="400">
<StackPanel>
<TextBlock>© 2024 - Blossomi Shymae</TextBlock>
<TextBlock>MIT License</TextBlock>
</StackPanel>
</suki:GlassCard>
<suki:GlassCard Margin="8" Width="400">
<suki:GroupBox Header="Resources">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
</Style>
</StackPanel.Styles>
<Button CommandParameter="https://hextechdocs.dev/tag/lcu/" Margin="0 0 16 0">
Hextech Docs
</Button>
<Button CommandParameter="https://hextechdocs.dev/getting-started-with-the-lcu-api/">
Getting Started
</Button>
</StackPanel>
</suki:GroupBox>
</suki:GlassCard>
</StackPanel>
</WrapPanel>
</ScrollViewer>
</UserControl>
@@ -1,12 +0,0 @@
using Avalonia.Controls;
namespace Needlework.Net.Desktop.Views
{
public partial class HomeView : UserControl
{
public HomeView()
{
InitializeComponent();
}
}
}
@@ -1,77 +0,0 @@
<suki:SukiWindow
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:i="https://github.com/projektanker/icons.avalonia"
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Desktop.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Title="Needlework.Net"
Icon="/Assets/app.ico"
Width="1280"
Height="720">
<suki:SukiWindow.LogoContent>
<Image Source="/Assets/app.png"
Width="20"
Height="20"
VerticalAlignment="Center"/>
</suki:SukiWindow.LogoContent>
<!-- TOP LEVEL -->
<suki:SukiSideMenu ItemsSource="{Binding Pages}">
<!-- ITEMS -->
<suki:SukiSideMenu.ItemTemplate>
<DataTemplate>
<suki:SukiSideMenuItem Header="{Binding DisplayName}">
<suki:SukiSideMenuItem.Icon>
<materialIcons:MaterialIcon Kind="{Binding Icon}" />
</suki:SukiSideMenuItem.Icon>
</suki:SukiSideMenuItem>
</DataTemplate>
</suki:SukiSideMenu.ItemTemplate>
<!-- FOOTER -->
<suki:SukiSideMenu.FooterContent>
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<StackPanel.Styles>
<Style Selector="Button.Basic">
<Setter Property="Command" Value="{Binding OpenUrlCommand}" />
</Style>
<Style Selector="materialIcons|MaterialIcon">
<Setter Property="Width" Value="25" />
<Setter Property="Height" Value="25" />
</Style>
<Style Selector="i|Icon">
<Setter Property="FontSize" Value="25" />
</Style>
</StackPanel.Styles>
<Button Classes="Flat"
Content="{Binding Version}"
FontSize="12"
Margin="0 0 4 0"
Padding="12 4 12 4"
VerticalAlignment="Center"/>
<Button Classes="Basic"
VerticalAlignment="Center"
CommandParameter="https://github.com/BlossomiShymae/Needlework.Net"
ToolTip.Tip="Open on GitHub."
Margin="0 0 4 0">
<StackPanel Orientation="Horizontal">
<materialIcons:MaterialIcon Kind="Github" Margin="0 0 4 0" />
<TextBlock FontSize="12"
VerticalAlignment="Center"
Foreground="White">Star</TextBlock>
</StackPanel>
</Button>
<Button Classes="Basic"
VerticalAlignment="Center"
CommandParameter="https://discord.gg/chEvEX5J4E"
ToolTip.Tip="Open Discord server.">
<i:Icon Value="fa-brand fa-discord" />
</Button>
</StackPanel>
</suki:SukiSideMenu.FooterContent>
</suki:SukiSideMenu>
</suki:SukiWindow>
@@ -1,11 +0,0 @@
using SukiUI.Controls;
namespace Needlework.Net.Desktop.Views;
public partial class MainWindow : SukiWindow
{
public MainWindow()
{
InitializeComponent();
}
}
@@ -1,45 +0,0 @@
<suki:SukiWindow
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Desktop.Views.OopsiesWindow"
x:DataType="vm:OopsiesWindowViewModel"
Title="Needlework.Net - Oopsies"
WindowStartupLocation="CenterOwner"
Width="560"
Height="200">
<Grid RowDefinitions="auto,auto,auto" ColumnDefinitions="auto,auto"
Margin="8"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2">
This response is too large for Needlework.Net to handle for performance reasons.
</TextBlock>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="0 0 0 12">
It can be viewed in an external editor or viewer.
</TextBlock>
<Button Command="{Binding OpenDefaultEditorCommand}"
Grid.Row="2"
Grid.Column="0"
Margin="0 0 8 0">
Open
</Button>
<Button Command="{Binding CloseDialogCommand}"
Grid.Row="2"
Grid.Column="1"
Margin="8 0 0 0">
Cancel
</Button>
</Grid>
</suki:SukiWindow>
@@ -1,11 +0,0 @@
using SukiUI.Controls;
namespace Needlework.Net.Desktop.Views;
public partial class OopsiesWindow : SukiWindow
{
public OopsiesWindow()
{
InitializeComponent();
}
}
@@ -1,69 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI"
xmlns:vm="using:Needlework.Net.Desktop.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Desktop.Views.WebsocketView"
x:DataType="vm:WebsocketViewModel">
<Grid RowDefinitions="*,2,*" Margin="16">
<Border Grid.Row="0"
Padding="0 0 0 8">
<suki:GlassCard>
<Grid RowDefinitions="auto,*" ColumnDefinitions="*">
<Grid
Grid.Row="0"
Grid.Column="0"
RowDefinitions="*"
ColumnDefinitions="auto,*,auto,auto">
<Button Grid.Row="0"
Grid.Column="0"
Command="{Binding ClearCommand}"
Margin="0 0 8 0">Clear</Button>
<TextBox Grid.Row="0"
Grid.Column="1"
Margin="0 0 8 0"
Text="{Binding Search, Mode=TwoWay}"/>
<StackPanel Orientation="Horizontal"
Grid.Row="0"
Grid.Column="2"
Margin="0 0 8 0">
<ToggleSwitch Margin="0 0 0 8"
IsChecked="{Binding IsAttach}"/>
<TextBlock Margin="0 6 0 0"
FontSize="18">Attach</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal"
Grid.Row="0"
Grid.Column="3">
<ToggleSwitch Margin="0 0 0 8"
IsChecked="{Binding IsTail}"/>
<TextBlock Margin="0 6 0 0"
FontSize="18">Tail</TextBlock>
</StackPanel>
</Grid>
<ListBox Grid.Row="1"
Grid.Column="0"
Name="EventViewer"
ItemsSource="{Binding FilteredEventLog}"
SelectedItem="{Binding SelectedEventLog}"/>
</Grid>
</suki:GlassCard>
</Border>
<GridSplitter Grid.Row="1" ResizeDirection="Rows" Background="Gray"/>
<Border Grid.Row="2"
Padding="0 8 0 0">
<suki:GlassCard>
<avaloniaEdit:TextEditor
Name="ResponseEditor"
ShowLineNumbers="True"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible"
Text=""
FontSize="12"/>
</suki:GlassCard>
</Border>
</Grid>
</UserControl>
@@ -1,57 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using AvaloniaEdit;
using AvaloniaEdit.TextMate;
using CommunityToolkit.Mvvm.Messaging;
using Needlework.Net.Desktop.Extensions;
using Needlework.Net.Desktop.Messages;
using Needlework.Net.Desktop.ViewModels;
using SukiUI;
using TextMateSharp.Grammars;
namespace Needlework.Net.Desktop.Views;
public partial class WebsocketView : UserControl, IRecipient<ResponseUpdatedMessage>
{
private TextEditor? _responseEditor;
public WebsocketView()
{
InitializeComponent();
}
public void Receive(ResponseUpdatedMessage message)
{
_responseEditor!.Text = message.Value;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var vm = (WebsocketViewModel)DataContext!;
var viewer = this.FindControl<ListBox>("EventViewer");
viewer!.PropertyChanged += (s, e) => { if (vm.IsTail) viewer.ScrollIntoView(vm.EventLog.Count - 1); };
_responseEditor = this.FindControl<TextEditor>("ResponseEditor");
_responseEditor?.ApplyJsonEditorSettings();
WeakReferenceMessenger.Default.Register(this, nameof(WebsocketViewModel));
OnBaseThemeChanged(Application.Current!.ActualThemeVariant);
SukiTheme.GetInstance().OnBaseThemeChanged += OnBaseThemeChanged;
}
private void OnBaseThemeChanged(ThemeVariant currentTheme)
{
var registryOptions = new RegistryOptions(
currentTheme == ThemeVariant.Dark ? ThemeName.DarkPlus : ThemeName.LightPlus);
var responseTmi = _responseEditor.InstallTextMate(registryOptions);
responseTmi.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions
.GetLanguageByExtension(".json").Id));
}
}
+1 -13
View File
@@ -3,11 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Core", "Needlework.Net.Core\Needlework.Net.Core.csproj", "{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Desktop", "Needlework.Net.Desktop\Needlework.Net.Desktop.csproj", "{7388B579-2DC0-46D6-957A-6683D0FCF5D3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net.Core.Tests", "Needlework.Net.Core.Tests\Needlework.Net.Core.Tests.csproj", "{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Needlework.Net", "Needlework.Net\Needlework.Net.csproj", "{7388B579-2DC0-46D6-957A-6683D0FCF5D3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -18,17 +14,9 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B14E1B39-3C5A-400F-8148-CC3A4833CBC4}.Release|Any CPU.Build.0 = Release|Any CPU
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7388B579-2DC0-46D6-957A-6683D0FCF5D3}.Release|Any CPU.Build.0 = Release|Any CPU
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0E08542E-6E3F-4825-9F9C-7D6275D6AEC5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+23
View File
@@ -0,0 +1,23 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Needlework.Net.App"
xmlns:local="using:Needlework.Net"
xmlns:converters="using:Needlework.Net.Converters"
xmlns:sty="using:FluentAvalonia.Styling"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<sty:FluentAvaloniaTheme PreferSystemTheme="False" PreferUserAccentColor="False" />
<materialIcons:MaterialIconStyles />
<StyleInclude Source="Controls/Card.axaml"/>
<StyleInclude Source="Controls/UserCard.axaml"/>
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
</Application.Styles>
<Application.Resources>
<converters:EnumerableToVisibility x:Key="EnumerableToVisibilityConverter"/>
<converters:NullableToVisibility x:Key="NullableToVisibilityConverter"/>
</Application.Resources>
</Application>
+73
View File
@@ -0,0 +1,73 @@
using Akavache;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Templates;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Needlework.Net.Constants;
using Needlework.Net.Extensions;
using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages;
using Needlework.Net.Views.MainWindow;
using System;
using System.Reactive.Linq;
using System.Text.Json;
namespace Needlework.Net;
public partial class App : Application, IEnableLogger
{
private readonly IDataTemplate _viewLocator;
private readonly IBlobCache _blobCache;
private readonly PageFactory _pageFactory;
private readonly MainWindowViewModel _mainWindowViewModel;
public App(IServiceProvider serviceProvider)
{
_viewLocator = serviceProvider.GetRequiredService<IDataTemplate>();
_blobCache = serviceProvider.GetRequiredService<IBlobCache>();
_pageFactory = serviceProvider.GetRequiredService<PageFactory>();
_mainWindowViewModel = serviceProvider.GetRequiredService<MainWindowViewModel>();
this.Log()
.Debug("NeedleworkDotNet version: {Version}", AppInfo.Version);
this.Log()
.Debug("OS description: {Description}", System.Runtime.InteropServices.RuntimeInformation.OSDescription);
}
public static JsonSerializerOptions JsonSerializerOptions { get; } = new()
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
public static readonly int MaxCharacters = 10_000;
public static Window? MainWindow;
public override void Initialize()
{
DataTemplates.Add(_viewLocator);
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindowView(_mainWindowViewModel, _pageFactory);
MainWindow = desktop.MainWindow;
desktop.ShutdownRequested += (_, _) =>
{
_blobCache.Flush().Wait();
_blobCache.Dispose();
};
}
base.OnFrameworkInitializationCompleted();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
using System.Reflection;
namespace Needlework.Net.Constants
{
public static class AppInfo
{
public static readonly string Name = "Needlework.Net";
public static readonly string Version = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
}
}
@@ -0,0 +1,9 @@
namespace Needlework.Net.Constants
{
public static class BlobCacheKeys
{
public static readonly string GithubLatestRelease = nameof(GithubLatestRelease);
public static readonly string AppSettings = nameof(AppSettings);
}
}
@@ -0,0 +1,11 @@
namespace Needlework.Net.Constants
{
public static class FlurlClientKeys
{
public static readonly string GithubClient = nameof(GithubClient);
public static readonly string GithubUserContentClient = nameof(GithubUserContentClient);
public static readonly string Client = nameof(Client);
}
}
+9
View File
@@ -0,0 +1,9 @@
using System;
namespace Needlework.Net.Constants
{
public static class Intervals
{
public static readonly TimeSpan CheckForUpdates = TimeSpan.FromMinutes(60);
}
}
+66
View File
@@ -0,0 +1,66 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:Needlework.Net.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Needlework.Net.Controls.BusyArea">
<UserControl.Styles>
<Style Selector="controls|BusyArea">
<Setter Property="Template">
<ControlTemplate>
<Panel>
<ContentControl Content="{TemplateBinding Content}"/>
<DockPanel Name="LoadingBusyArea"
HorizontalAlignment="Center"
VerticalAlignment="Center"
LastChildFill="True">
<TextBlock Margin="16"
DockPanel.Dock="Bottom"
HorizontalAlignment="Center"
FontWeight="DemiBold"
Text="{TemplateBinding BusyText}"/>
<ProgressBar
Width="100"
IsIndeterminate="True"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</DockPanel>
</Panel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="controls|BusyArea DockPanel#LoadingBusyArea">
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.3" />
</Transitions>
</Setter>
</Style>
<Style Selector="controls|BusyArea[IsBusy=True] DockPanel#LoadingBusyArea">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="controls|BusyArea[IsBusy=False] DockPanel#LoadingBusyArea">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="controls|BusyArea ContentControl">
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.3"/>
</Transitions>
</Setter>
</Style>
<Style Selector="controls|BusyArea[IsBusy=True] ContentControl">
<Setter Property="Opacity" Value="0.1"/>
</Style>
<Style Selector="controls|BusyArea[IsBusy=False] ContentControl">
<Setter Property="Opacity" Value="1"/>
</Style>
</UserControl.Styles>
</UserControl>
+30
View File
@@ -0,0 +1,30 @@
using Avalonia;
using Avalonia.Controls;
namespace Needlework.Net.Controls;
public partial class BusyArea : UserControl
{
public BusyArea()
{
InitializeComponent();
}
public static readonly StyledProperty<bool> IsBusyProperty =
AvaloniaProperty.Register<BusyArea, bool>(nameof(IsBusy), defaultValue: false);
public bool IsBusy
{
get { return GetValue(IsBusyProperty); }
set { SetValue(IsBusyProperty, value); }
}
public static readonly StyledProperty<string?> BusyTextProperty =
AvaloniaProperty.Register<BusyArea, string?>(nameof(BusyText), defaultValue: null);
public string? BusyText
{
get => GetValue(BusyTextProperty);
set => SetValue(BusyTextProperty, value);
}
}
+20
View File
@@ -0,0 +1,20 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Needlework.Net.Controls">
<Design.PreviewWith>
<controls:Card />
</Design.PreviewWith>
<Style Selector="controls|Card">
<!-- Set Defaults -->
<Setter Property="Template">
<ControlTemplate>
<Border Padding="12"
CornerRadius="4"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}">
<ContentPresenter Content="{TemplateBinding Content}"/>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Styles>
+10
View File
@@ -0,0 +1,10 @@
using Avalonia.Controls;
namespace Needlework.Net.Controls;
public class Card : ContentControl
{
public Card()
{
}
}
+71
View File
@@ -0,0 +1,71 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="https://github.com/projektanker/icons.avalonia"
xmlns:controls="using:Needlework.Net.Controls">
<Design.PreviewWith>
<WrapPanel>
<controls:UserCard
Width="300"
Height="400"
UserImage="/Assets/Users/blossomishymae.png"
UserName="estrogen elf"
UserGithub="BlossomiShymae">
Needlework.Net is the .NET rewrite of Needlework. This tool was made to help others with LCU and Game Client development. Feel free to ask any questions
or help contribute to the project! Made with love. 💜
</controls:UserCard>
</WrapPanel>
</Design.PreviewWith>
<Style Selector="controls|UserCard">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<Border CornerRadius="4"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
Margin="0 50 0 0"
Padding="16 66 16 16">
<Grid RowDefinitions="auto,auto,auto"
ColumnDefinitions="*">
<TextBlock Grid.Row="0"
Grid.Column="0"
Theme="{StaticResource SubtitleTextBlockStyle}"
Margin="0 0 0 4"
Text="{TemplateBinding UserName}"/>
<Grid Grid.Row="1"
Grid.Column="0"
RowDefinitions="*"
ColumnDefinitions="auto,auto"
Margin="0 0 0 16">
<Button Grid.Row="0"
Grid.Column="0"
Theme="{StaticResource TransparentButton}"
FontSize="20"
Name="PART_GithubButton">
<i:Icon Value="fa-github"/>
</Button>
<TextBlock Grid.Row="0"
Grid.Column="1"
Margin="8 0 0 0"
VerticalAlignment="Center"
Text="{TemplateBinding UserGithub}"/>
</Grid>
<TextBlock Grid.Row="2"
Grid.Column="0"
TextWrapping="WrapWithOverflow"
Text="{TemplateBinding Content}"/>
</Grid>
</Border>
<Border CornerRadius="100"
Width="100"
Height="100"
Margin="{TemplateBinding UserImageMargin}"
ClipToBounds="True">
<Image Source="{TemplateBinding UserImage}"
RenderOptions.BitmapInterpolationMode="HighQuality"/>
</Border>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>
+91
View File
@@ -0,0 +1,91 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Media;
using System.Diagnostics;
namespace Needlework.Net.Controls;
[TemplatePart("PART_GithubButton", typeof(Button))]
public partial class UserCard : ContentControl
{
private Button? _githubButton;
public UserCard()
{
UserImageMargin = new(0, !double.IsNaN(Height) ? 100 - Height : 0, 0, 0);
}
public static readonly StyledProperty<IImage?> UserImageProperty =
AvaloniaProperty.Register<UserCard, IImage?>(nameof(UserImage), defaultValue: null);
public IImage? UserImage
{
get { return GetValue(UserImageProperty); }
set { SetValue(UserImageProperty, value); }
}
public static readonly StyledProperty<string?> UserNameProperty =
AvaloniaProperty.Register<UserCard, string?>(nameof(UserName), defaultValue: null);
public string? UserName
{
get { return GetValue(UserNameProperty); }
set { SetValue(UserNameProperty, value); }
}
public static readonly StyledProperty<string?> UserGithubProperty =
AvaloniaProperty.Register<UserCard, string?>(nameof(UserGithub), defaultValue: null);
public string? UserGithub
{
get { return GetValue(UserGithubProperty); }
set { SetValue(UserGithubProperty, value); }
}
public static readonly DirectProperty<UserCard, Thickness> UserImageMarginProperty =
AvaloniaProperty.RegisterDirect<UserCard, Thickness>(nameof(UserImageMargin), o => o.UserImageMargin);
private Thickness _userImageMargin = new(0, 0, 0, 0);
public Thickness UserImageMargin
{
get { return _userImageMargin; }
private set { SetAndRaise(UserImageMarginProperty, ref _userImageMargin, value); }
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
SizeChanged += UserCard_SizeChanged;
if (_githubButton != null)
{
_githubButton.Click -= GithubButton_Click;
}
_githubButton = e.NameScope.Find("PART_GithubButton") as Button;
if (_githubButton != null)
{
_githubButton.Click += GithubButton_Click;
}
}
private void UserCard_SizeChanged(object? sender, SizeChangedEventArgs e)
{
UserImageMargin = new(0, !double.IsNaN(e.NewSize.Height) ? 100 - e.NewSize.Height : 0, 0, 0);
}
private void GithubButton_Click(object? sender, RoutedEventArgs e)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo($"https://github.com/{UserGithub}") { UseShellExecute = true }
};
process.Start();
}
}
@@ -4,9 +4,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Needlework.Net.Desktop.Converters
namespace Needlework.Net.Converters
{
public class EnumerableBoolConverter : IValueConverter
public class EnumerableToVisibility : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
@@ -2,9 +2,9 @@
using System;
using System.Globalization;
namespace Needlework.Net.Desktop.Converters
namespace Needlework.Net.Converters
{
public class NullBoolConverter : IValueConverter
public class NullableToVisibility : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
+13
View File
@@ -0,0 +1,13 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Needlework.Net.DataModels
{
public partial class AppSettings : ObservableObject
{
[ObservableProperty]
private bool _isCheckForUpdates = true;
[ObservableProperty]
private bool _isCheckForSchema = true;
}
}
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace Needlework.Net.DataModels
{
public class GithubRelease
{
[JsonPropertyName("tag_name")]
public string TagName { get; set; } = string.Empty;
public bool IsLatest(string assemblyVersion) => int.Parse(TagName.Replace(".", "")) > int.Parse(assemblyVersion.ToString().Replace(".", ""));
}
}
@@ -0,0 +1,13 @@
namespace Needlework.Net.DataModels
{
public class HextechDocsPost
{
public required string Path { get; init; }
public required string Title { get; init; }
public required string Excerpt { get; init; }
public string Url => $"https://hextechdocs.dev{Path}";
}
}
@@ -0,0 +1,16 @@
using Serilog;
namespace Needlework.Net.Extensions
{
public static class EnableLoggerExtensions
{
private static readonly ILogger _logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}", path: "Logs/debug-.log", rollingInterval: RollingInterval.Day, shared: true)
.CreateLogger();
public static ILogger Log(this IEnableLogger? context) => _logger.ForContext(context?.GetType() ?? typeof(Program));
}
public interface IEnableLogger;
}
@@ -3,7 +3,7 @@ using AvaloniaEdit;
using AvaloniaEdit.Highlighting;
using AvaloniaEdit.Indentation.CSharp;
namespace Needlework.Net.Desktop.Extensions
namespace Needlework.Net.Extensions
{
public static class TextEditorExtensions
{
+288
View File
@@ -0,0 +1,288 @@
using Microsoft.OpenApi.Models;
using Needlework.Net.Models;
using Needlework.Net.ViewModels.Pages.Endpoints;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
namespace Needlework.Net.Helpers
{
public static class OpenApiHelpers
{
public static string GetReturnType(OpenApiResponses responses)
{
if (!TryGetResponse(responses, out var response))
return "none";
if (TryGetApplicationJsonMedia(response, out var media))
{
var schema = media.Schema;
return GetSchemaType(schema);
}
return "none";
}
public static bool TryGetApplicationJsonMedia(OpenApiResponse response, [NotNullWhen(true)] out OpenApiMediaType? media) // Because GetLolGameflowV1SpectateDelayedLaunch has an empty schema with no type...
{
var flag = false;
if (response.Content.TryGetValue("application/json", out var _media))
{
if (_media?.Schema?.Type != null)
{
media = _media;
flag = true;
}
else
{
media = null;
}
}
else
{
media = null;
}
return flag;
}
public static bool TryGetApplicationJsonMedia(OpenApiRequestBody requestBody, [NotNullWhen(true)] out OpenApiMediaType? media)
{
var flag = false;
if (requestBody.Content.TryGetValue("application/json", out var _media))
{
if (_media?.Schema?.Type != null)
{
media = _media;
flag = true;
}
else
{
media = null;
}
}
else
{
media = null;
}
return flag;
}
public static string GetSchemaType(OpenApiSchema? schema)
{
if (schema == null) return "object"; // Because GetLolVanguardV1Notification exists where it has a required parameter without a type...
if (schema.Reference != null) return schema.Reference.Id;
if (schema.Type == "object" && schema.AdditionalProperties?.Reference != null) return schema.AdditionalProperties.Reference.Id;
if (schema.Type == "integer" || schema.Type == "number") return $"{schema.Type}:{schema.Format}";
if (schema.Type == "array" && schema.AdditionalProperties?.Reference != null) return $"{schema.AdditionalProperties.Reference.Id}[]";
if (schema.Type == "array" && schema.AdditionalProperties?.Type != null) return $"{schema.AdditionalProperties.Type}[]";
if (schema.Type == "array" && schema.Items.Reference != null) return $"{schema.Items.Reference.Id}[]";
if (schema.Type == "array" && (schema.Items.Type == "integer" || schema.Items.Type == "number")) return $"{schema.Items.Type}:{schema.Items.Format}[]";
if (schema.Type == "array") return $"{schema.Items.Type}[]";
return schema.Type;
}
public static List<string> CreateTemplate(List<PropertyClassViewModel> requestClasses)
{
if (requestClasses.Count == 0) return [];
List<string> template = [];
template.Add("{");
var rootClass = requestClasses.First();
if (rootClass.PropertyEnums.Any()) return [rootClass.PropertyEnums.First().Values];
var propertyFields = rootClass.PropertyFields;
for (int i = 0; i < propertyFields.Count; i++)
{
template.Add($"\"{propertyFields[i].Name}\"");
template.Add(":");
template.Add($"#{propertyFields[i].Type}");
if (i == propertyFields.Count - 1) template.Add("}");
else template.Add(",");
}
for (int i = 0; i < template.Count; i++)
{
var type = template[i];
if (!type.Contains("#")) continue;
var foundClass = requestClasses.Where(c => c.Id == type.Replace("#", string.Empty));
if (foundClass.Any())
{
if (foundClass.First().PropertyEnums.Any())
{
template[i] = string.Join(string.Empty, CreateTemplate([.. foundClass]));
}
else
{
List<PropertyClassViewModel> classes = [.. requestClasses];
classes.Remove(rootClass);
template[i] = string.Join(string.Empty, CreateTemplate(classes));
}
}
else
{
template[i] = GetRequestDefaultValue(type);
}
}
return template;
}
public static string GetComponentId(OpenApiSchema schema)
{
string componentId;
if (schema.Reference != null) componentId = schema.Reference.Id;
else if (schema.Items != null) componentId = schema.Items.Reference.Id;
else componentId = schema.AdditionalProperties.Reference.Id;
return componentId;
}
public static List<ParameterViewModel> GetParameters(List<OpenApiParameter> parameters, ParameterLocation location)
{
var pathParameters = new List<ParameterViewModel>();
foreach (var parameter in parameters)
{
if (parameter.In != location) continue;
pathParameters.Add(new ParameterViewModel(parameter.Name, GetSchemaType(parameter.Schema), parameter.Required));
}
return pathParameters;
}
public static string? GetRequestBodyType(OpenApiRequestBody? requestBody)
{
if (requestBody == null) return null;
if (requestBody.Content.TryGetValue("application/json", out var media))
{
var schema = media.Schema;
if (schema == null) return null; // Because "PostLolAccountVerificationV1SendDeactivationPin" exists where the media body is empty...
return GetSchemaType(schema);
}
return null;
}
public static List<PropertyClassViewModel> GetRequestClasses(OpenApiRequestBody? requestBody, Document document)
{
if (requestBody == null) return [];
if (TryGetApplicationJsonMedia(requestBody, out var media))
{
var rawDocument = document.OpenApiDocument;
var schema = media.Schema;
if (schema == null) return [];
var type = GetSchemaType(media.Schema);
if (IsComponent(type))
{
var componentId = GetComponentId(schema);
var componentSchema = rawDocument.Components.Schemas[componentId];
List<PropertyClassViewModel> propertyClasses = [];
WalkSchema(componentSchema, propertyClasses, rawDocument);
return propertyClasses;
}
}
return [];
}
public static string GetRequestDefaultValue(string type)
{
var defaultValue = string.Empty;
if (type.Contains("[]")) defaultValue = "[]";
else if (type.Contains("string")) defaultValue = "\"\"";
else if (type.Contains("boolean")) defaultValue = "false";
else if (type.Contains("integer")) defaultValue = "0";
else if (type.Contains("double") || type.Contains("float")) defaultValue = "0.0";
else if (type.Contains("object")) defaultValue = "{}";
return defaultValue;
}
public static string? GetRequestTemplate(OpenApiRequestBody? requestBody, Document document)
{
var requestClasses = GetRequestClasses(requestBody, document);
if (requestClasses.Count == 0)
{
var type = GetRequestBodyType(requestBody);
if (type == null) return null;
return GetRequestDefaultValue(type);
}
var template = CreateTemplate(requestClasses);
return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(string.Join(string.Empty, template)), App.JsonSerializerOptions);
}
public static List<PropertyClassViewModel> GetResponseClasses(OpenApiResponses responses, Document document)
{
if (!TryGetResponse(responses, out var response))
return [];
if (TryGetApplicationJsonMedia(response, out var media))
{
var rawDocument = document.OpenApiDocument;
var schema = media.Schema;
if (schema == null) return [];
List<PropertyClassViewModel> propertyClasses = [];
WalkSchema(schema, propertyClasses, rawDocument);
return propertyClasses;
}
return [];
}
public static bool IsComponent(string type)
{
return !(type.Contains("object")
|| type.Contains("array")
|| type.Contains("bool")
|| type.Contains("string")
|| type.Contains("integer")
|| type.Contains("number"));
}
public static bool TryGetResponse(OpenApiResponses responses, [NotNullWhen(true)] out OpenApiResponse? response)
{
response = null;
var flag = false;
if (responses.TryGetValue("2XX", out var x))
{
response = x;
flag = true;
}
else if (responses.TryGetValue("200", out var y))
{
response = y;
flag = true;
}
return flag;
}
public static void WalkSchema(OpenApiSchema schema, List<PropertyClassViewModel> propertyClasses, OpenApiDocument document)
{
var type = GetSchemaType(schema);
if (IsComponent(type))
{
string componentId = GetComponentId(schema);
var componentSchema = document.Components.Schemas[componentId];
var responseClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum);
if (propertyClasses.Where(c => c.Id == componentId).Any()) return; // Avoid adding duplicate schemas in classes
propertyClasses.Add(responseClass);
foreach ((var _, var property) in componentSchema.Properties)
// Check for self-references like "LolLootLootOddsResponse"
// I blame dubble
if (IsComponent(GetSchemaType(property)) && componentId != GetComponentId(property))
WalkSchema(property, propertyClasses, document);
}
}
public static PropertyClassViewModel WalkSchema(OpenApiSchema schema, OpenApiDocument document)
{
string componentId = GetComponentId(schema);
var componentSchema = document.Components.Schemas[componentId];
var propertyClass = new PropertyClassViewModel(componentId, componentSchema.Properties, componentSchema.Enum);
return propertyClass;
}
}
}
@@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Messages
{
public class OopsiesDialogRequestedMessage(string text) : ValueChangedMessage<string>(text)
{
}
}
@@ -1,6 +1,6 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace Needlework.Net.Desktop.Messages
namespace Needlework.Net.Messages
{
public class ResponseUpdatedMessage(string data) : ValueChangedMessage<string>(data)
{
@@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Models;
namespace Needlework.Net.Core;
namespace Needlework.Net.Models;
public class LcuSchemaHandler
public class Document
{
internal OpenApiDocument OpenApiDocument { get; }
@@ -12,7 +14,7 @@ public class LcuSchemaHandler
public List<string> Paths => [.. OpenApiDocument.Paths.Keys];
public LcuSchemaHandler(OpenApiDocument openApiDocument)
public Document(OpenApiDocument openApiDocument)
{
OpenApiDocument = openApiDocument;
var plugins = new SortedDictionary<string, List<PathOperation>>();
@@ -34,10 +36,10 @@ public class LcuSchemaHandler
{
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;
}
}
@@ -45,19 +47,16 @@ public class LcuSchemaHandler
{
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;
}
}
@@ -65,8 +64,10 @@ public class LcuSchemaHandler
}
}
plugins = new(plugins.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.OrderBy(x => x.Path).ToList()));
Plugins = plugins;
}
}
public record PathOperation(string Method, string Path, OpenApiOperation Operation);
+31
View File
@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Needlework.Net.Models;
public class Library
{
[JsonPropertyName("repo")]
public required string Repo { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("language")]
public required string Language { get; init; }
[JsonPropertyName("owner")]
public required string Owner { get; init; }
[JsonPropertyName("tags")]
public List<string> Tags { get; init; } = [];
public string Link
{
get
{
if (Owner.Equals("jellies")) return $"https://github.com/elliejs/{Repo}";
return $"https://github.com/{Owner}/{Repo}";
}
}
}
+9
View File
@@ -0,0 +1,9 @@
using FluentAvalonia.UI.Controls;
using System;
namespace Needlework.Net.Models
{
public record Notification(string Title, string Message, InfoBarSeverity InfoBarSeverity, TimeSpan? Duration = null, string? Url = null)
{
}
}
+5
View File
@@ -0,0 +1,5 @@
using Microsoft.OpenApi.Models;
namespace Needlework.Net.Models;
public record PathOperation(string Method, string Path, string Tag, OpenApiOperation Operation);
+8
View File
@@ -0,0 +1,8 @@
using Needlework.Net.ViewModels.Pages.Endpoints;
namespace Needlework.Net.Models
{
public record SchemaPaneItem(string Key, Tab Tab)
{
}
}
+10
View 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;
}
}
+100
View File
@@ -0,0 +1,100 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<PropertyGroup Label="Avalonia">
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>NeedleworkDotNet</AssemblyName>
<AssemblyVersion>0.14.3.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="akavache" Version="10.2.41" />
<PackageReference Include="AngleSharp" Version="1.3.0" />
<PackageReference Include="Avalonia" Version="11.2.8" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.8" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.8" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.8" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.8" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.8" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.3.0" />
<PackageReference Include="BlossomiShymae.Briar" Version="0.2.3" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="DebounceThrottle" Version="3.0.1" />
<PackageReference Include="FastCache.Cached" Version="1.8.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.3.0" />
<PackageReference Include="Flurl" Version="4.0.0" />
<PackageReference Include="Flurl.Http" Version="4.0.2" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.4.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="9.0.3" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.24" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.6.24" />
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.6.2" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.6.2" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.69" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Remove="Utilities\**" />
<Compile Remove="Utilities\**" />
<EmbeddedResource Remove="Utilities\**" />
<None Remove="Utilities\**" />
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Views\AboutView.axaml" />
</ItemGroup>
<ItemGroup>
<Compile Update="Controls\BusyArea.axaml.cs">
<DependentUpon>BusyArea.axaml</DependentUpon>
</Compile>
<Compile Update="Views\MainWindow\MainWindowView.axaml.cs">
<DependentUpon>MainWindowView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\EndpointListView.axaml.cs">
<DependentUpon>EndpointListView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\EndpointsView.axaml.cs">
<DependentUpon>EndpointsView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Endpoints\PluginView.axaml.cs">
<DependentUpon>PluginView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\Schemas\SchemaSearchDetailsView.axaml.cs">
<DependentUpon>SchemaSearchDetailsView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\Pages\WebSocket\WebsocketView.axaml.cs">
<DependentUpon>WebSocketView.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\Users\" />
</ItemGroup>
<ItemGroup>
<None Include="ViewModels\Pages\Schemas\SchemaSearchDetailsViewModel.cs" />
</ItemGroup>
</Project>
+152
View File
@@ -0,0 +1,152 @@
using Akavache;
using Akavache.Sqlite3;
using Avalonia;
using Avalonia.Controls.Templates;
using Flurl.Http.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Needlework.Net.Constants;
using Needlework.Net.Extensions;
using Needlework.Net.Services;
using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages;
using Needlework.Net.ViewModels.Pages.About;
using Needlework.Net.ViewModels.Pages.Console;
using Needlework.Net.ViewModels.Pages.Endpoints;
using Needlework.Net.ViewModels.Pages.Home;
using Needlework.Net.ViewModels.Pages.Schemas;
using Needlework.Net.ViewModels.Pages.Settings;
using Needlework.Net.ViewModels.Pages.WebSocket;
using Needlework.Net.Views.MainWindow;
using Needlework.Net.Views.Pages.About;
using Needlework.Net.Views.Pages.Console;
using Needlework.Net.Views.Pages.Endpoints;
using Needlework.Net.Views.Pages.Home;
using Needlework.Net.Views.Pages.Schemas;
using Needlework.Net.Views.Pages.Settings;
using Needlework.Net.Views.Pages.WebSocket;
using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome;
using Serilog;
using System;
using System.IO;
namespace Needlework.Net;
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += Program_UnhandledException;
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
IconProvider.Current.Register<FontAwesomeIconProvider>();
return AppBuilder.Configure(() => new App(BuildServices()))
.UsePlatformDetect()
.WithInterFont()
.With(new Win32PlatformOptions { CompositionMode = [Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition] })
.With(new MacOSPlatformOptions { ShowInDock = true, })
.LogToTrace();
}
private static IServiceProvider BuildServices()
{
var builder = new ServiceCollection();
AddViews(builder);
AddViewModels(builder);
AddServices(builder);
return builder.BuildServiceProvider();
}
private static void AddViews(ServiceCollection builder)
{
var locator = new ViewLocator();
// MAIN WINDOW
locator.Register<NotificationViewModel>(() => new NotificationView());
locator.Register<ViewModels.MainWindow.SchemaSearchDetailsViewModel>(() => new Views.MainWindow.SchemaSearchDetailsView());
locator.Register<SchemaViewModel>(() => new SchemaView());
// ABOUT
locator.Register<AboutViewModel>(() => new AboutView());
// CONSOLE
locator.Register<ConsoleViewModel>(() => new ConsoleView());
// ENDPOINTS
locator.Register<EndpointListViewModel>(() => new EndpointListView());
locator.Register<EndpointSearchDetailsViewModel>(() => new EndpointSearchDetailsView());
locator.Register<EndpointsViewModel>(() => new EndpointsView());
locator.Register<EndpointTabItemContentViewModel>(() => new EndpointTabItemContentView());
locator.Register<PathOperationViewModel>(() => new PathOperationView());
locator.Register<PluginViewModel>(() => new PluginView());
locator.Register<PropertyClassViewModel>(() => new PropertyClassView());
// HOME
locator.Register<HomeViewModel>(() => new HomeView());
locator.Register<LibraryViewModel>(() => new LibraryView());
locator.Register<HextechDocsPostViewModel>(() => new HextechDocsPostView());
// SCHEMAS
locator.Register<SchemasViewModel>(() => new SchemasView());
locator.Register<ViewModels.Pages.Schemas.SchemaSearchDetailsViewModel>(() => new Views.Pages.Schemas.SchemaSearchDetailsView());
// WEBSOCKET
locator.Register<WebSocketViewModel>(() => new WebSocketView());
locator.Register<EventViewModel>(() => new EventView());
// SETTINGS
locator.Register<SettingsViewModel>(() => new SettingsView());
builder.AddSingleton<IDataTemplate>(locator);
}
private static void AddServices(ServiceCollection builder)
{
builder.AddSingleton<DialogService>();
builder.AddSingleton<DocumentService>();
builder.AddSingleton<NotificationService>();
builder.AddSingleton<SchemaPaneService>();
builder.AddSingleton<HextechDocsService>();
builder.AddSingleton<GithubService>();
builder.AddSingleton<IBlobCache>((_) =>
{
var appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
appDataFolder = string.IsNullOrEmpty(appDataFolder) ? "AppData" : appDataFolder;
var appFolder = Path.Join(appDataFolder, AppInfo.Name);
Directory.CreateDirectory(appFolder);
var filePath = Path.Join(appFolder, "cache.sqlite");
return new SqlRawPersistentBlobCache(filePath);
});
builder.AddSingleton<IFlurlClientCache>(new FlurlClientCache()
.Add(FlurlClientKeys.GithubClient, "https://api.github.com")
.Add(FlurlClientKeys.GithubUserContentClient, "https://raw.githubusercontent.com")
.Add(FlurlClientKeys.Client));
builder.AddLogging((builder) => builder.AddSerilog(EnableLoggerExtensions.Log(null)));
}
private static void AddViewModels(ServiceCollection builder)
{
builder.AddSingleton<MainWindowViewModel>();
builder.AddSingleton<PageBase, HomeViewModel>();
builder.AddSingleton<PageBase, ConsoleViewModel>();
builder.AddSingleton<PageBase, EndpointsViewModel>();
builder.AddSingleton<PageBase, WebSocketViewModel>();
builder.AddSingleton<PageBase, SchemasViewModel>();
builder.AddSingleton<PageBase, AboutViewModel>();
builder.AddSingleton<PageBase, SettingsViewModel>();
builder.AddSingleton<PageFactory>();
}
private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
File.WriteAllText($"Logs/fatal-{DateTime.Now:yyyyMMdd}.log", e.ExceptionObject.ToString());
}
}
+20
View File
@@ -0,0 +1,20 @@
using FluentAvalonia.UI.Controls;
using System;
using System.Threading.Tasks;
namespace Needlework.Net.Services
{
public class DialogService
{
public async Task<ContentDialogResult> ShowAsync<T>(object data)
where T : IDialog, IDisposable
{
T dialog = Activator.CreateInstance<T>();
var result = await dialog.ShowAsync(data);
dialog.Dispose();
return result;
}
}
}
@@ -0,0 +1,65 @@
using FastCache;
using Flurl.Http;
using Flurl.Http.Configuration;
using Microsoft.OpenApi.Readers;
using Needlework.Net.Constants;
using Needlework.Net.Extensions;
using Needlework.Net.Models;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Needlework.Net
{
public class DocumentService : IEnableLogger
{
private readonly OpenApiStreamReader _reader = new();
private readonly IFlurlClient _githubUserContentClient;
public DocumentService(IFlurlClientCache clients)
{
_githubUserContentClient = clients.Get(FlurlClientKeys.GithubUserContentClient);
}
public async Task<Document> GetLcuSchemaDocumentAsync(CancellationToken cancellationToken = default)
{
if (Cached<Document>.TryGet(nameof(GetLcuSchemaDocumentAsync), out var cached))
{
return cached;
}
var lcuSchemaStream = await _githubUserContentClient.Request("/dysolix/hasagi-types/main/swagger.json")
.GetStreamAsync(cancellationToken: cancellationToken);
var lcuSchemaRaw = _reader.Read(lcuSchemaStream, out var diagnostic);
foreach (var error in diagnostic.Errors)
{
this.Log()
.Warning("Diagnostic error: {Message}", error);
}
var document = new Document(lcuSchemaRaw);
return cached.Save(document, TimeSpan.FromMinutes(60));
}
public async Task<Document> GetLolClientDocumentAsync(CancellationToken cancellationToken = default)
{
if (Cached<Document>.TryGet(nameof(GetLolClientDocumentAsync), out var cached))
{
return cached;
}
var lolClientStream = await _githubUserContentClient.Request("/AlsoSylv/Irelia/refs/heads/master/schemas/game.json")
.GetStreamAsync(cancellationToken: cancellationToken);
var lolClientRaw = _reader.Read(lolClientStream, out var diagnostic);
foreach (var error in diagnostic.Errors)
{
this.Log()
.Warning("Diagnostic error: {Message}", error);
}
var document = new Document(lolClientRaw);
return cached.Save(document, TimeSpan.FromMinutes(60));
}
}
}
+42
View File
@@ -0,0 +1,42 @@
using Akavache;
using Flurl.Http;
using Flurl.Http.Configuration;
using Needlework.Net.Constants;
using Needlework.Net.DataModels;
using Needlework.Net.Extensions;
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.Services
{
public class GithubService : IEnableLogger
{
private readonly IFlurlClient _githubClient;
private readonly IFlurlClient _githubUserContentClient;
private readonly IBlobCache _blobCache;
public GithubService(IBlobCache blobCache, IFlurlClientCache clients)
{
_githubClient = clients.Get(FlurlClientKeys.GithubClient);
_githubUserContentClient = clients.Get(FlurlClientKeys.GithubUserContentClient);
_blobCache = blobCache;
}
public async Task<GithubRelease> GetLatestReleaseAsync()
{
return await _blobCache.GetOrFetchObject(BlobCacheKeys.GithubLatestRelease, async () =>
{
this.Log()
.Debug("Downloading latest release info from GitHub...");
var release = await _githubClient
.Request("/repos/BlossomiShymae/Needlework.Net/releases/latest")
.WithHeader("User-Agent", $"{AppInfo.Name}/{AppInfo.Version}")
.GetJsonAsync<GithubRelease>();
return release;
}, DateTimeOffset.Now + Intervals.CheckForUpdates);
}
}
}
@@ -0,0 +1,49 @@
using Akavache;
using AngleSharp;
using Needlework.Net.DataModels;
using Needlework.Net.Extensions;
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.Services
{
public class HextechDocsService : IEnableLogger
{
private readonly IBrowsingContext _context = BrowsingContext.New(Configuration.Default.WithDefaultLoader());
private readonly IBlobCache _blobCache;
public HextechDocsService(IBlobCache blobCache)
{
_blobCache = blobCache;
}
public async Task<List<HextechDocsPost>> GetPostsAsync()
{
return await _blobCache.GetOrFetchObject("HextechDocsPosts", async () =>
{
this.Log()
.Debug("Downloading HextechDocs posts...");
var document = await _context.OpenAsync("https://hextechdocs.dev/tag/lcu/");
var elements = document.QuerySelectorAll("article.post-card");
var posts = new List<HextechDocsPost>();
foreach (var element in elements)
{
var path = element.QuerySelector("a.post-card-content-link")!.GetAttribute("href")!;
var title = element.QuerySelector(".post-card-title")!.TextContent.Trim();
var excerpt = element.QuerySelector(".post-card-excerpt")!.TextContent;
var post = new HextechDocsPost()
{
Path = path,
Title = title,
Excerpt = excerpt,
};
posts.Add(post);
}
return posts;
}, DateTimeOffset.Now + TimeSpan.FromHours(12));
}
}
}

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