mirror of
https://github.com/BlossomiShymae/Needlework.Net.git
synced 2025-12-06 10:10:48 +01:00
Compare commits
9 Commits
b63713f054
...
0.11.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
375d5a2ff8 | ||
|
|
2aa77f3e02 | ||
|
|
576863bd72 | ||
|
|
68e5abd1d1 | ||
|
|
b18f425257 | ||
|
|
5ebed22ae3 | ||
|
|
dc44cf72df | ||
|
|
01cb8886c6 | ||
|
|
38e4a64bb8 |
Binary file not shown.
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 5.1 KiB |
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
10
Needlework.Net/Models/SystemBuild.cs
Normal file
10
Needlework.Net/Models/SystemBuild.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Needlework.Net.Models
|
||||||
|
{
|
||||||
|
public class SystemBuild
|
||||||
|
{
|
||||||
|
public string Branch { get; set; } = string.Empty;
|
||||||
|
public string Patchline { get; set; } = string.Empty;
|
||||||
|
public string PatchlineVisibleName { get; set; } = string.Empty;
|
||||||
|
public string Version { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
|
<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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
BIN
app-preview.png
BIN
app-preview.png
Binary file not shown.
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 397 KiB |
Reference in New Issue
Block a user