9 Commits

Author SHA1 Message Date
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
13 changed files with 170 additions and 40 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
namespace Needlework.Net.Models; namespace Needlework.Net.Models;
@@ -35,10 +36,10 @@ public class OpenApiDocumentWrapper
{ {
pluginsKey = "default"; pluginsKey = "default";
if (plugins.TryGetValue(pluginsKey, out var p)) if (plugins.TryGetValue(pluginsKey, out var p))
p.Add(new(method.ToString(), path, operation)); p.Add(new(method.ToString(), path, pluginsKey, operation));
else else
{ {
operations.Add(new(method.ToString(), path, operation)); operations.Add(new(method.ToString(), path, pluginsKey, operation));
plugins[pluginsKey] = operations; plugins[pluginsKey] = operations;
} }
} }
@@ -46,19 +47,16 @@ public class OpenApiDocumentWrapper
{ {
foreach (var tag in operation.Tags) foreach (var tag in operation.Tags)
{ {
var lowercaseTag = tag.Name.ToLower(); if (tag.Name == "plugins")
if (lowercaseTag == "plugins")
continue; continue;
else if (lowercaseTag.Contains("plugin "))
pluginsKey = lowercaseTag.Replace("plugin ", "");
else else
pluginsKey = lowercaseTag; pluginsKey = tag.Name;
if (plugins.TryGetValue(pluginsKey, out var p)) if (plugins.TryGetValue(pluginsKey, out var p))
p.Add(new(method.ToString(), path, operation)); p.Add(new(method.ToString(), path, tag.Name, operation));
else else
{ {
operations.Add(new(method.ToString(), path, operation)); operations.Add(new(method.ToString(), path, tag.Name, operation));
plugins[pluginsKey] = operations; plugins[pluginsKey] = operations;
} }
} }
@@ -66,6 +64,10 @@ public class OpenApiDocumentWrapper
} }
} }
plugins = new(plugins.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.OrderBy(x => x.Path).ToList()));
Plugins = plugins; Plugins = plugins;
} }
} }

View File

@@ -2,4 +2,4 @@ using Microsoft.OpenApi.Models;
namespace Needlework.Net.Models; namespace Needlework.Net.Models;
public record PathOperation(string Method, string Path, OpenApiOperation Operation); public record PathOperation(string Method, string Path, string Tag, OpenApiOperation Operation);

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;
}
}

View File

@@ -11,7 +11,7 @@
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch> <AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
<ApplicationIcon>app.ico</ApplicationIcon> <ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>NeedleworkDotNet</AssemblyName> <AssemblyName>NeedleworkDotNet</AssemblyName>
<AssemblyVersion>0.10.0.0</AssemblyVersion> <AssemblyVersion>0.11.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion> <FileVersion>$(AssemblyVersion)</FileVersion>
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions> <AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
</PropertyGroup> </PropertyGroup>

View File

@@ -36,7 +36,11 @@ class Program
return AppBuilder.Configure(() => new App(BuildServices())) return AppBuilder.Configure(() => new App(BuildServices()))
.UsePlatformDetect() .UsePlatformDetect()
.WithInterFont() .WithInterFont()
.LogToTrace(); .LogToTrace()
.With(new Win32PlatformOptions
{
CompositionMode = [ Win32CompositionMode.WinUIComposition, Win32CompositionMode.DirectComposition ]
});
} }
private static IServiceProvider BuildServices() private static IServiceProvider BuildServices()

View File

@@ -1,4 +1,5 @@
using Avalonia.Collections; using Avalonia.Collections;
using BlossomiShymae.GrrrLCU;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
@@ -18,8 +19,10 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Reflection; using System.Reflection;
using System.Text.Json.Nodes;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers;
namespace Needlework.Net.ViewModels.MainWindow; namespace Needlework.Net.ViewModels.MainWindow;
@@ -34,6 +37,9 @@ public partial class MainWindowViewModel
public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0"; public string Version { get; } = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0";
[ObservableProperty] private bool _isUpdateShown = false; [ObservableProperty] private bool _isUpdateShown = false;
[ObservableProperty] private string _schemaVersion = "N/A";
[ObservableProperty] private string _schemaVersionLatest = "N/A";
public HttpClient HttpClient { get; } public HttpClient HttpClient { get; }
public DialogService DialogService { get; } public DialogService DialogService { get; }
public OpenApiDocumentWrapper? OpenApiDocumentWrapper { get; set; } public OpenApiDocumentWrapper? OpenApiDocumentWrapper { get; set; }
@@ -45,6 +51,19 @@ public partial class MainWindowViewModel
private readonly ILogger<MainWindowViewModel> _logger; private readonly ILogger<MainWindowViewModel> _logger;
private readonly System.Timers.Timer _latestUpdateTimer = new()
{
Interval = TimeSpan.FromMinutes(10).TotalMilliseconds,
Enabled = true
};
private readonly System.Timers.Timer _schemaVersionTimer = new()
{
Interval = TimeSpan.FromSeconds(5).TotalMilliseconds,
Enabled = true
};
private bool _isSchemaVersionChecked = false;
public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, DialogService dialogService, ILogger<MainWindowViewModel> logger) public MainWindowViewModel(IEnumerable<PageBase> pages, HttpClient httpClient, DialogService dialogService, ILogger<MainWindowViewModel> logger)
{ {
_logger = logger; _logger = logger;
@@ -66,20 +85,60 @@ public partial class MainWindowViewModel
WeakReferenceMessenger.Default.RegisterAll(this); WeakReferenceMessenger.Default.RegisterAll(this);
Task.Run(FetchDataAsync); Task.Run(FetchDataAsync);
new Thread(ProcessEvents) { IsBackground = true }.Start();
_latestUpdateTimer.Elapsed += OnLatestUpdateTimerElapsed;
_schemaVersionTimer.Elapsed += OnSchemaVersionTimerElapsed;
_latestUpdateTimer.Start();
_schemaVersionTimer.Start();
OnLatestUpdateTimerElapsed(null, null);
OnSchemaVersionTimerElapsed(null, null);
} }
private void ProcessEvents(object? obj) private async void OnSchemaVersionTimerElapsed(object? sender, ElapsedEventArgs? e)
{ {
while (!IsUpdateShown) if (OpenApiDocumentWrapper == null) return;
{ if (!ProcessFinder.IsPortOpen()) return;
Task.Run(CheckLatestVersionAsync);
Thread.Sleep(TimeSpan.FromMinutes(10)); // Avoid tripping unauthenticated rate limits try
{
var client = Connector.GetLcuHttpClientInstance();
var currentSemVer = OpenApiDocumentWrapper.Info.Version.Split('.');
var systemBuild = await client.GetFromJsonAsync<SystemBuild>("/system/v1/builds") ?? throw new NullReferenceException();
var latestSemVer = systemBuild.Version.Split('.');
if (!_isSchemaVersionChecked)
{
_logger.LogInformation("LCU Schema (current): {Version}", OpenApiDocumentWrapper.Info.Version);
_logger.LogInformation("LCU Schema (latest): {Version}", systemBuild.Version);
_isSchemaVersionChecked = true;
}
bool isVersionMatching = currentSemVer[0] == latestSemVer[0] && currentSemVer[1] == latestSemVer[1]; // Compare major and minor versions
if (!isVersionMatching)
{
Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
{
await ShowInfoBarAsync(new("Newer System Build", true, $"LCU Schema is possibly outdated compared to latest system build. Consider submitting a pull request on dysolix/hasagi-types.\nCurrent: {string.Join(".", currentSemVer)}\nLatest: {string.Join(".", latestSemVer)}", InfoBarSeverity.Warning, TimeSpan.FromSeconds(60), new Avalonia.Controls.Button()
{
Command = OpenUrlCommand,
CommandParameter = "https://github.com/dysolix/hasagi-types#updating-the-types",
Content = "Submit PR"
}));
});
_schemaVersionTimer.Elapsed -= OnSchemaVersionTimerElapsed;
_schemaVersionTimer.Stop();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Schema version check failed");
} }
} }
private async Task CheckLatestVersionAsync() private async void OnLatestUpdateTimerElapsed(object? sender, ElapsedEventArgs? e)
{ {
try try
{ {
@@ -100,14 +159,16 @@ public partial class MainWindowViewModel
{ {
Avalonia.Threading.Dispatcher.UIThread.Post(async () => Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
{ {
await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(10), new Avalonia.Controls.Button() await ShowInfoBarAsync(new("Needlework.Net Update", true, $"There is a new version available: {release.TagName}.", InfoBarSeverity.Informational, TimeSpan.FromSeconds(30), new Avalonia.Controls.Button()
{ {
Command = OpenUrlCommand, Command = OpenUrlCommand,
CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases", CommandParameter = "https://github.com/BlossomiShymae/Needlework.Net/releases",
Content = "Download" Content = "Download"
})); }));
IsUpdateShown = true;
}); });
_latestUpdateTimer.Elapsed -= OnLatestUpdateTimerElapsed;
_latestUpdateTimer.Stop();
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel; using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Needlework.Net.Models; using Needlework.Net.Models;
@@ -14,6 +15,8 @@ public partial class PathOperationViewModel : ObservableObject
public string Path { get; } public string Path { get; }
public OperationViewModel Operation { get; } public OperationViewModel Operation { get; }
public string Url { get; }
[ObservableProperty] private bool _isBusy; [ObservableProperty] private bool _isBusy;
[ObservableProperty] private Lazy<LcuRequestViewModel> _lcuRequest; [ObservableProperty] private Lazy<LcuRequestViewModel> _lcuRequest;
@@ -25,6 +28,7 @@ public partial class PathOperationViewModel : ObservableObject
{ {
Method = pathOperation.Method.ToUpper() Method = pathOperation.Method.ToUpper()
}); });
Url = $"https://swagger.dysolix.dev/lcu/#/{pathOperation.Tag}/{pathOperation.Operation.OperationId}";
} }
[RelayCommand] [RelayCommand]
@@ -50,4 +54,10 @@ public partial class PathOperationViewModel : ObservableObject
LcuRequest.Value.RequestPath = sb.ToString(); LcuRequest.Value.RequestPath = sb.ToString();
await LcuRequest.Value.ExecuteAsync(); await LcuRequest.Value.ExecuteAsync();
} }
[RelayCommand]
private void CopyUrl()
{
App.MainWindow?.Clipboard?.SetTextAsync(Url);
}
} }

View File

@@ -1,3 +1,6 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using FluentAvalonia.UI.Windowing; using FluentAvalonia.UI.Windowing;
namespace Needlework.Net.Views.MainWindow; namespace Needlework.Net.Views.MainWindow;
@@ -9,5 +12,19 @@ public partial class MainWindowView : AppWindow
InitializeComponent(); InitializeComponent();
TitleBar.ExtendsContentIntoTitleBar = true; TitleBar.ExtendsContentIntoTitleBar = true;
TransparencyLevelHint = [WindowTransparencyLevel.Mica, WindowTransparencyLevel.None];
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (IsWindows11OrNewer())
{
Background = null;
}
}
}
private static bool IsWindows11OrNewer()
{
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build >= 22000;
} }
} }

View File

@@ -55,23 +55,28 @@
<Grid <Grid
RowDefinitions="*" RowDefinitions="*"
ColumnDefinitions="auto,*"> ColumnDefinitions="auto,*">
<TextBlock <Grid.ContextFlyout>
VerticalAlignment="Center" <MenuFlyout>
TextAlignment="Center" <MenuItem Header="Copy Swagger URL" Command="{Binding CopyUrlCommand}"/>
Margin="0 0 8 0" </MenuFlyout>
Text="{Binding LcuRequest.Value.Method}" </Grid.ContextFlyout>
Background="{Binding LcuRequest.Value.Color}" <TextBlock
FontSize="8" VerticalAlignment="Center"
Width="50" TextAlignment="Center"
Padding="10 2 10 2" Margin="0 0 8 0"
Grid.Row="0" Text="{Binding LcuRequest.Value.Method}"
Grid.Column="0"/> Background="{Binding LcuRequest.Value.Color}"
<TextBlock FontSize="8"
VerticalAlignment="Center" Width="50"
Text="{Binding Path}" Padding="10 2 10 2"
FontSize="11" Grid.Row="0"
Grid.Row="0" Grid.Column="0"/>
Grid.Column="1"/> <TextBlock
VerticalAlignment="Center"
Text="{Binding Path}"
FontSize="11"
Grid.Row="0"
Grid.Column="1"/>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>

View File

@@ -15,6 +15,27 @@
<ui:TabView TabItems="{Binding Endpoints}" <ui:TabView TabItems="{Binding Endpoints}"
AddTabButtonCommand="{Binding AddEndpointCommand}" AddTabButtonCommand="{Binding AddEndpointCommand}"
TabCloseRequested="TabView_TabCloseRequested"> TabCloseRequested="TabView_TabCloseRequested">
<!--Need to override Tab header for Mica theme...-->
<ui:TabView.Resources>
<ResourceDictionary>
<SolidColorBrush x:Key="TabViewItemHeaderBackgroundSelected" Color="{DynamicResource ControlFillColorTransparent}"/>
</ResourceDictionary>
</ui:TabView.Resources>
<!--We need to hack this style for Mica theme since there is no way to explicity set style priority in Avalonia...-->
<ui:TabView.Styles>
<Style Selector="Grid > ContentPresenter#TabContentPresenter">
<Style.Animations>
<Animation IterationCount="1" Duration="0:0:1" FillMode="Both">
<KeyFrame Cue="0%">
<Setter Property="Background" Value="{DynamicResource ControlFillColorTransparent}"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Background" Value="{DynamicResource ControlFillColorTransparent}"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</ui:TabView.Styles>
<ui:TabView.TabItemTemplate> <ui:TabView.TabItemTemplate>
<DataTemplate DataType="vm:EndpointItem"> <DataTemplate DataType="vm:EndpointItem">
<ui:TabViewItem Header="{Binding Header}" <ui:TabViewItem Header="{Binding Header}"

View File

@@ -71,7 +71,7 @@
</controls:Card> </controls:Card>
<controls:Card Margin="12" Width="300"> <controls:Card Margin="12" Width="300">
<StackPanel> <StackPanel>
<TextBlock>© 2024 - Blossomi Shymae</TextBlock> <TextBlock>© 2025 - Blossomi Shymae</TextBlock>
<TextBlock>MIT License</TextBlock> <TextBlock>MIT License</TextBlock>
</StackPanel> </StackPanel>
</controls:Card> </controls:Card>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

After

Width:  |  Height:  |  Size: 397 KiB