mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
172 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aaeb327b6 | ||
|
|
75e771c75e | ||
|
|
d4515936bd | ||
|
|
d9f2261129 | ||
|
|
f0d4ea9a10 | ||
|
|
bdcbba3237 | ||
|
|
a931a661cd | ||
|
|
0fb2991085 | ||
|
|
cda04c6aa7 | ||
|
|
b23659bf80 | ||
|
|
2a3f119fb0 | ||
|
|
30e888ffb2 | ||
|
|
27b79659a4 | ||
|
|
4cef096cb5 | ||
|
|
e065ba0964 | ||
|
|
4c0d86fd13 | ||
|
|
70ab81bb0f | ||
|
|
72be37834d | ||
|
|
f1a2a828c8 | ||
|
|
3eaaf0e4e2 | ||
|
|
41d7441eb3 | ||
|
|
173a1343c1 | ||
|
|
0fc0413612 | ||
|
|
1fc1f9b08d | ||
|
|
133a1a532e | ||
|
|
f4fa08a939 | ||
|
|
5020621c46 | ||
|
|
4d526bb363 | ||
|
|
b37d5331a2 | ||
|
|
451486e3fc | ||
|
|
bbacb038cb | ||
|
|
7aba05d9c8 | ||
|
|
fbcd43c0af | ||
|
|
51314da123 | ||
|
|
8ed2e8cc72 | ||
|
|
7ab10d7824 | ||
|
|
ca5f84911b | ||
|
|
500efa60ed | ||
|
|
650e5d8a6e | ||
|
|
79a9128434 | ||
|
|
c7c883bb11 | ||
|
|
3ee98810f1 | ||
|
|
f09c924a61 | ||
|
|
86d9a381c4 | ||
|
|
b2d141af11 | ||
|
|
15865eb746 | ||
|
|
242b9aa036 | ||
|
|
7b81cc7a9c | ||
|
|
2060f20707 | ||
|
|
27a6c4f1fb | ||
|
|
6427fe23ef | ||
|
|
6cf516f883 | ||
|
|
080674f648 | ||
|
|
09834ada6b | ||
|
|
efbc282a14 | ||
|
|
e59a1c5444 | ||
|
|
356c157ab2 | ||
|
|
be83ebfae1 | ||
|
|
f94cbed525 | ||
|
|
1199d2c737 | ||
|
|
126e866a02 | ||
|
|
fbc0c02af0 | ||
|
|
e646e26908 | ||
|
|
4a8bf8182c | ||
|
|
86423a86c5 | ||
|
|
0766361441 | ||
|
|
ded93dda75 | ||
|
|
f437c6615c | ||
|
|
e663c2f353 | ||
|
|
90b396703a | ||
|
|
be4fa571cd | ||
|
|
13e43a1c23 | ||
|
|
2d9165599b | ||
|
|
5b35167908 | ||
|
|
099c5d3f6d | ||
|
|
5f15c05e3d | ||
|
|
de95fd6419 | ||
|
|
bd0901a8ab | ||
|
|
84fa2dadcc | ||
|
|
e204b65afc | ||
|
|
9c71c4ffcc | ||
|
|
62694be22b | ||
|
|
20f16ceb3e | ||
|
|
a1e8f90710 | ||
|
|
0d9baa2cac | ||
|
|
96b3e6d092 | ||
|
|
4178f1b89b | ||
|
|
e44a20f202 | ||
|
|
a12874d114 | ||
|
|
c7e3b18d62 | ||
|
|
b607bbf819 | ||
|
|
5b16313b65 | ||
|
|
577aed0f80 | ||
|
|
39a3e6eb37 | ||
|
|
96f503d30c | ||
|
|
f104554661 | ||
|
|
451823ebf9 | ||
|
|
7346c5eb4e | ||
|
|
73f0e356c4 | ||
|
|
168bed4b2c | ||
|
|
d634eb6818 | ||
|
|
0cb9e59b8a | ||
|
|
ca10afa25a | ||
|
|
5fe38f305b | ||
|
|
46f58730dc | ||
|
|
d144859d94 | ||
|
|
40439aea8a | ||
|
|
d54828ff55 | ||
|
|
7e0c0fa5bf | ||
|
|
8312c7c88b | ||
|
|
456d87003f | ||
|
|
7a63013dfd | ||
|
|
15c3019576 | ||
|
|
c15e948659 | ||
|
|
c5a4226662 | ||
|
|
0f9b906b7b | ||
|
|
f55dc038e2 | ||
|
|
8540c1eebc | ||
|
|
8f71994574 | ||
|
|
fe3e835211 | ||
|
|
6b5477ba44 | ||
|
|
e147b8773f | ||
|
|
b386a35f07 | ||
|
|
42a19239e6 | ||
|
|
404cd9d705 | ||
|
|
33935dabc2 | ||
|
|
5e04c51456 | ||
|
|
50c5bb44ff | ||
|
|
60641c3f57 | ||
|
|
40753177cb | ||
|
|
ba2f2d5708 | ||
|
|
6f5a65c0e3 | ||
|
|
20f353d9eb | ||
|
|
1a1a865b8d | ||
|
|
7b3b1f1317 | ||
|
|
0891c3f521 | ||
|
|
9657df36c2 | ||
|
|
9a54df4163 | ||
|
|
8ac410fb4d | ||
|
|
538a076b69 | ||
|
|
7db14a9c2a | ||
|
|
0e710f92d2 | ||
|
|
fec2dc6c99 | ||
|
|
bdb40aca6a | ||
|
|
c90f147564 | ||
|
|
2b36b01145 | ||
|
|
d876f269fd | ||
|
|
729f6717e1 | ||
|
|
b5371a9656 | ||
|
|
04dae7ccd8 | ||
|
|
a5fcfca609 | ||
|
|
aee5271a2d | ||
|
|
89fe93b3eb | ||
|
|
dc38def97c | ||
|
|
2432b1ec5d | ||
|
|
97c7671595 | ||
|
|
b8895d8250 | ||
|
|
cb882ab062 | ||
|
|
27c7875c26 | ||
|
|
3cf505d9b2 | ||
|
|
4407166005 | ||
|
|
7b6e63a932 | ||
|
|
4a452c205e | ||
|
|
4dd2ba3c6d | ||
|
|
1e7155a902 | ||
|
|
129a1a7fa8 | ||
|
|
f37f4fe37d | ||
|
|
0df746e4c6 | ||
|
|
285f788015 | ||
|
|
d0525dd814 | ||
|
|
4372eb0ded | ||
|
|
15e6964340 |
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -5,7 +5,7 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "nuget" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
- package-ecosystem: "nuget"
|
||||
directory: "/src/Snap.Hutao" # Snap.Hutao.csproj
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "weekly"
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,8 +1,12 @@
|
||||
desktop.ini
|
||||
|
||||
*.csproj.user
|
||||
*.pubxml
|
||||
*.DotSettings.user
|
||||
|
||||
.vs/
|
||||
.idea/
|
||||
src/Snap.Hutao/_ReSharper.Caches
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao/bin/
|
||||
src/Snap.Hutao/Snap.Hutao/obj/
|
||||
|
||||
@@ -44,7 +44,6 @@ Snap Hutao is an open-source Genshin Impact toolbox on Windows platform, aim to
|
||||
|
||||
* [Snap.Hutao.Server](https://github.com/DGP-Studio/Snap.Hutao.Server)
|
||||
* [Snap.Metadata](https://github.com/DGP-Studio/Snap.Metadata)
|
||||
* [Snap.Data.Mapper](https://github.com/DGP-Studio/Snap.Data.Mapper)
|
||||
|
||||
## 近期活跃数据 / Active Stat
|
||||

|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| >=1.4.11 | :white_check_mark: |
|
||||
| <1.4.11 | :x: |
|
||||
| >=1.6.0 | :white_check_mark: |
|
||||
| <1.6.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ pr:
|
||||
|
||||
|
||||
pool:
|
||||
vmImage: 'windows-2022'
|
||||
name: Default
|
||||
demands: agent.name -equals Hutao-Server
|
||||
|
||||
variables:
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
@@ -125,7 +126,7 @@ steps:
|
||||
- task: CmdLine@2
|
||||
displayName: Build MSIX
|
||||
inputs:
|
||||
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
|
||||
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
|
||||
|
||||
- task: MsixSigning@1
|
||||
name: signMsix
|
||||
@@ -173,16 +174,17 @@ steps:
|
||||
changeLogCompareToRelease: 'lastFullRelease'
|
||||
changeLogType: 'commitBased'
|
||||
|
||||
- task: DownloadSecureFile@1
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
||||
name: RcloneConfigFile
|
||||
displayName: Download Rclone Config
|
||||
inputs:
|
||||
secureFile: 'rclone.conf'
|
||||
|
||||
- task: rclone@1
|
||||
displayName: Upload CI via Rclone
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
||||
inputs:
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix downloadDGPCN:/releases/Alpha/'
|
||||
configPath: '$(RcloneConfigFile.secureFilePath)'
|
||||
configPath: 'C:\agent\_work\_tasks\rclone.conf'
|
||||
|
||||
- task: rclone@1
|
||||
displayName: Upload PR CI via Rclone
|
||||
condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
|
||||
inputs:
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix downloadDGPCN:/releases/PR/'
|
||||
configPath: 'C:\agent\_work\_tasks\rclone.conf'
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[.ShellClassInfo]
|
||||
IconResource=D:\Develop\Projects\Snap.Hutao\src\Snap.Hutao\Snap.Hutao\Assets\Logo.ico,0
|
||||
[ViewState]
|
||||
Mode=
|
||||
Vid=
|
||||
FolderType=Generic
|
||||
BIN
res/HutaoIconSourceTransparentBackgroundGradient1.png
Normal file
BIN
res/HutaoIconSourceTransparentBackgroundGradient1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -1,3 +1,3 @@
|
||||
本文件夹中的所有图片,均由 [DGP Studio](https://github.com/DGP-Studio) 委托 [Bilibili 画画的芦苇](https://space.bilibili.com/274422134) 绘制
|
||||
|
||||
Copyright ©2023 DGP Studio, All Rights Reserved.
|
||||
Copyright © 2023 DGP Studio, All Rights Reserved.
|
||||
1
src/Snap.Hutao.Bookmark/SpiralAbyssUpload.js
Normal file
1
src/Snap.Hutao.Bookmark/SpiralAbyssUpload.js
Normal file
File diff suppressed because one or more lines are too long
@@ -167,6 +167,9 @@ csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_prefer_readonly_struct = true:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
|
||||
# SA1600: Elements should be documented
|
||||
dotnet_diagnostic.SA1600.severity = none
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Snap.Hutao.Installer;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock";
|
||||
private const string ValueName = "AllowDevelopmentWithoutDevLicense";
|
||||
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
_ = args;
|
||||
string ps1File = Path.Combine(AppContext.BaseDirectory, "Install.ps1");
|
||||
|
||||
if (!File.Exists(ps1File))
|
||||
{
|
||||
Console.WriteLine("未检测到 Install.ps1 文件");
|
||||
Console.WriteLine("请勿移动该安装程序,按下任意键退出...");
|
||||
Console.ReadKey();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//以管理策略打开开发者模式
|
||||
Registry.SetValue(AppxKey, ValueName, 1, RegistryValueKind.DWord);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine("开发者模式未开启,请手动开启,参阅下方链接");
|
||||
Console.WriteLine("https://learn.microsoft.com/zh-CN/windows/apps/get-started/developer-mode-features-and-debugging");
|
||||
}
|
||||
|
||||
await InstallAsync(ps1File).ConfigureAwait(false);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("官方文档与使用教程");
|
||||
Console.WriteLine("https://hut.ao");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("在开始菜单中启动 Snap.Hutao ,按下任意键退出...");
|
||||
Console.ReadKey();
|
||||
}
|
||||
|
||||
private static async Task InstallAsync(string ps1File)
|
||||
{
|
||||
Console.WriteLine("请注意 PowerShell 中的提示");
|
||||
Process ps = new()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo()
|
||||
{
|
||||
FileName = "powershell.exe",
|
||||
Arguments = $"-ExecutionPolicy Unrestricted \"{ps1File}\"",
|
||||
UseShellExecute = true,
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
ps.Start();
|
||||
await ps.WaitForExitAsync();
|
||||
Console.WriteLine("安装脚本运行完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<DebugType>embedded</DebugType>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,79 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!-- UAC 清单选项
|
||||
如果想要更改 Windows 用户帐户控制级别,请使用
|
||||
以下节点之一替换 requestedExecutionLevel 节点。
|
||||
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||
|
||||
指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
|
||||
如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
|
||||
元素。
|
||||
-->
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
|
||||
Windows 版本的列表。取消评论适当的元素,
|
||||
Windows 将自动选择最兼容的环境。 -->
|
||||
|
||||
<!-- Windows Vista -->
|
||||
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
|
||||
|
||||
<!-- Windows 7 -->
|
||||
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
|
||||
自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI,无需
|
||||
选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
|
||||
在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
|
||||
|
||||
将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
|
||||
<!--
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
-->
|
||||
|
||||
<!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
|
||||
<!--
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
-->
|
||||
|
||||
</assembly>
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class CommandGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.CommandAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2<IMethodSymbol>>> commands =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedMethods, CommandMethod)
|
||||
.Where(GeneratorSyntaxContext2<IMethodSymbol>.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(commands, GenerateCommandImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedMethods(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is MethodDeclarationSyntax methodDeclarationSyntax
|
||||
&& methodDeclarationSyntax.Parent is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& methodDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2<IMethodSymbol> CommandMethod(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out IMethodSymbol? methodSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, methodSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateCommandImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2<IMethodSymbol>> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2<IMethodSymbol> context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateCommandImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateCommandImplementation(SourceProductionContext production, GeneratorSyntaxContext2<IMethodSymbol> context2)
|
||||
{
|
||||
INamedTypeSymbol classSymbol = context2.Symbol.ContainingType;
|
||||
|
||||
AttributeData commandInfo = context2.SingleAttribute(AttributeName);
|
||||
string commandName = (string)commandInfo.ConstructorArguments[0].Value!;
|
||||
|
||||
string commandType = context2.Symbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task")
|
||||
? "AsyncRelayCommand"
|
||||
: "RelayCommand";
|
||||
|
||||
string genericParameter = context2.Symbol.Parameters.ElementAtOrDefault(0) is IParameterSymbol parameter
|
||||
? $"<{parameter.Type.ToDisplayString(SymbolDisplayFormats.FullyQualifiedNonNullableFormat)}>"
|
||||
: string.Empty;
|
||||
|
||||
string concurrentExecution = commandInfo.HasNamedArgumentWith<bool>("AllowConcurrentExecutions", value => value)
|
||||
? ", AsyncRelayCommandOptions.AllowConcurrentExecutions"
|
||||
: string.Empty;
|
||||
|
||||
string className = classSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
|
||||
// TODO: 支持嵌套类
|
||||
string code = $$"""
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace {{classSymbol.ContainingNamespace}};
|
||||
|
||||
partial class {{className}}
|
||||
{
|
||||
private ICommand _{{commandName}};
|
||||
|
||||
public ICommand {{commandName}}
|
||||
{
|
||||
get => _{{commandName}} ??= new {{commandType}}{{genericParameter}}({{context2.Symbol.Name}}{{concurrentExecution}});
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
string normalizedClassName = classSymbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.{commandName}.g.cs", code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class ConstructorGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.ConstructorGeneratedAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, ConstructorGeneratedClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(injectionClasses, GenerateConstructorImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 ConstructorGeneratedClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateConstructorImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2 context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateConstructorImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateConstructorImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
AttributeData constructorInfo = context2.SingleAttribute(AttributeName);
|
||||
|
||||
bool resolveHttpClient = constructorInfo.HasNamedArgumentWith<bool>("ResolveHttpClient", value => value);
|
||||
string httpclient = resolveHttpClient ? ", System.Net.Http.HttpClient httpClient" : string.Empty;
|
||||
|
||||
FieldValueAssignmentOptions options = new(resolveHttpClient);
|
||||
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(ConstructorGenerator)}}", "1.0.0.0")]
|
||||
partial class {{context2.Symbol.Name}}
|
||||
{
|
||||
public {{context2.Symbol.Name}}(System.IServiceProvider serviceProvider{{httpclient}})
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithFieldValueAssignment(sourceBuilder, context2, options);
|
||||
|
||||
sourceBuilder.Append("""
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
production.AddSource($"{context2.Symbol.ToDisplayString()}.ctor.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithFieldValueAssignment(StringBuilder builder, GeneratorSyntaxContext2 context2, FieldValueAssignmentOptions options)
|
||||
{
|
||||
IEnumerable<IFieldSymbol> fields = context2.Symbol.GetMembers()
|
||||
.Where(m => m.Kind == SymbolKind.Field)
|
||||
.OfType<IFieldSymbol>();
|
||||
|
||||
foreach (IFieldSymbol fieldSymbol in fields)
|
||||
{
|
||||
if (fieldSymbol.IsReadOnly && !fieldSymbol.IsStatic)
|
||||
{
|
||||
switch (fieldSymbol.Type.ToDisplayString())
|
||||
{
|
||||
case "System.IServiceProvider":
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.AppendLine(" = serviceProvider;");
|
||||
break;
|
||||
|
||||
case "System.Net.Http.HttpClient":
|
||||
if (options.ResolveHttpClient)
|
||||
{
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.AppendLine(" = httpClient;");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.Append(" = serviceProvider.GetRequiredService<System.Net.Http.IHttpClientFactory>().CreateClient(nameof(")
|
||||
.Append(context2.Symbol.Name)
|
||||
.AppendLine("));");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.Append(" = serviceProvider.GetRequiredService<")
|
||||
.Append(fieldSymbol.Type)
|
||||
.AppendLine(">();");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach(INamedTypeSymbol interfaceSymbol in context2.Symbol.Interfaces)
|
||||
{
|
||||
if (interfaceSymbol.Name == "IRecipient")
|
||||
{
|
||||
builder
|
||||
.Append(" CommunityToolkit.Mvvm.Messaging.IMessengerExtensions.Register<")
|
||||
.Append(interfaceSymbol.TypeArguments[0])
|
||||
.AppendLine(">(serviceProvider.GetRequiredService<CommunityToolkit.Mvvm.Messaging.IMessenger>(), this);");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct FieldValueAssignmentOptions
|
||||
{
|
||||
public readonly bool ResolveHttpClient;
|
||||
|
||||
public FieldValueAssignmentOptions(bool resolveHttpClient)
|
||||
{
|
||||
ResolveHttpClient = resolveHttpClient;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class DependencyPropertyGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.DependencyPropertyAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> commands =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, CommandMethod)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(commands, GenerateDependencyPropertyImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 CommandMethod(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? methodSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, methodSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateDependencyPropertyImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2 context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateDependencyPropertyImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateDependencyPropertyImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
foreach (AttributeData propertyInfo in context2.Attributes.Where(attr => attr.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
ImmutableArray<TypedConstant> arguments = propertyInfo.ConstructorArguments;
|
||||
|
||||
string propertyName = (string)arguments[0].Value!;
|
||||
string propertyType = arguments[0].Type!.ToDisplayString();
|
||||
string type = arguments[1].Value!.ToString();
|
||||
string defaultValue = arguments.Length > 2
|
||||
? GetLiteralString(arguments[2])
|
||||
: "default";
|
||||
string className = context2.Symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
|
||||
string code = $$"""
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
partial class {{className}}
|
||||
{
|
||||
private DependencyProperty {{propertyName}}Property =
|
||||
DependencyProperty.Register(nameof({{propertyName}}), typeof({{type}}), typeof({{className}}), new PropertyMetadata(({{type}}){{defaultValue}}));
|
||||
|
||||
public {{type}} {{propertyName}}
|
||||
{
|
||||
get => ({{type}})GetValue({{propertyName}}Property);
|
||||
set => SetValue({{propertyName}}Property, value);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.{propertyName}.g.cs", code);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetLiteralString(TypedConstant typedConstant)
|
||||
{
|
||||
if (typedConstant.Value is bool boolValue)
|
||||
{
|
||||
return boolValue ? "true" : "false";
|
||||
}
|
||||
|
||||
return typedConstant.Value!.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace System.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
internal sealed class NotNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
@@ -4,50 +4,59 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 注入HttpClient代码生成器
|
||||
/// 旨在使用源生成器提高注入效率
|
||||
/// 防止在运行时动态查找注入类型
|
||||
/// </summary>
|
||||
[Generator]
|
||||
internal sealed class HttpClientGenerator : ISourceGenerator
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class HttpClientGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string DefaultName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.Default";
|
||||
private const string XRpcName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc";
|
||||
private const string XRpc2Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc2";
|
||||
private const string XRpc3Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc3";
|
||||
private const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
|
||||
|
||||
private const string HttpClientConfiguration = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.";
|
||||
private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute";
|
||||
private const string DynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
|
||||
private const string UseDynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
|
||||
private const string CRLF = "\r\n";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// Register a syntax receiver that will be created for each generation pass
|
||||
context.RegisterForSyntaxNotifications(() => new HttpClientSyntaxContextReceiver());
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddHttpClientsImplementation);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
// retrieve the populated receiver
|
||||
if (context.SyntaxContextReceiver is not HttpClientSyntaxContextReceiver receiver)
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 HttpClientClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
return;
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder sourceCodeBuilder = new();
|
||||
return default;
|
||||
}
|
||||
|
||||
sourceCodeBuilder.Append($$"""
|
||||
private static void GenerateAddHttpClientsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
@@ -58,75 +67,47 @@ internal sealed class HttpClientGenerator : ISourceGenerator
|
||||
|
||||
internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(HttpClientGenerator)}}","1.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(HttpClientGenerator)}}", "1.0.0.0")]
|
||||
public static partial IServiceCollection AddHttpClients(this IServiceCollection services)
|
||||
{
|
||||
""");
|
||||
|
||||
FillWithHttpClients(receiver, sourceCodeBuilder);
|
||||
FillUpWithAddHttpClient(sourceBuilder, context, context2s);
|
||||
|
||||
sourceCodeBuilder.Append("""
|
||||
sourceBuilder.Append("""
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource("IocHttpClientConfiguration.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
|
||||
context.AddSource("IocHttpClientConfiguration.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillWithHttpClients(HttpClientSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
|
||||
private static void FillUpWithAddHttpClient(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
|
||||
{
|
||||
List<string> lines = new();
|
||||
StringBuilder lineBuilder = new();
|
||||
|
||||
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
lineBuilder.Clear().Append("\r\n");
|
||||
lineBuilder.Clear().Append(CRLF);
|
||||
lineBuilder.Append(@" services.AddHttpClient<");
|
||||
|
||||
AttributeData httpClientInfo = classSymbol
|
||||
.GetAttributes()
|
||||
.Single(attr => attr.AttributeClass!.ToDisplayString() == HttpClientSyntaxContextReceiver.AttributeName);
|
||||
ImmutableArray<TypedConstant> arguments = httpClientInfo.ConstructorArguments;
|
||||
AttributeData httpClientData = context.SingleAttribute(AttributeName);
|
||||
ImmutableArray<TypedConstant> arguments = httpClientData.ConstructorArguments;
|
||||
|
||||
if (arguments.Length == 2)
|
||||
{
|
||||
TypedConstant interfaceType = arguments[1];
|
||||
lineBuilder.Append($"{interfaceType.Value}, ");
|
||||
lineBuilder.Append($"{arguments[1].Value}, ");
|
||||
}
|
||||
|
||||
TypedConstant configuration = arguments[0];
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>(");
|
||||
lineBuilder.Append(arguments[0].ToCSharpString().Substring(HttpClientConfiguration.Length)).Append("Configuration)");
|
||||
|
||||
lineBuilder.Append($"{classSymbol.ToDisplayString()}>(");
|
||||
|
||||
string injectAsName = configuration.ToCSharpString();
|
||||
switch (injectAsName)
|
||||
if (context.SingleOrDefaultAttribute(PrimaryHttpMessageHandlerAttributeName) is AttributeData handlerData)
|
||||
{
|
||||
case DefaultName:
|
||||
lineBuilder.Append("DefaultConfiguration)");
|
||||
break;
|
||||
case XRpcName:
|
||||
lineBuilder.Append("XRpcConfiguration)");
|
||||
break;
|
||||
case XRpc2Name:
|
||||
lineBuilder.Append("XRpc2Configuration)");
|
||||
break;
|
||||
case XRpc3Name:
|
||||
lineBuilder.Append("XRpc3Configuration)");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"非法的 HttpClientConfiguration 值: [{injectAsName}]");
|
||||
}
|
||||
|
||||
AttributeData? handlerInfo = classSymbol
|
||||
.GetAttributes()
|
||||
.SingleOrDefault(attr => attr.AttributeClass!.ToDisplayString() == PrimaryHttpMessageHandlerAttributeName);
|
||||
|
||||
if (handlerInfo != null)
|
||||
{
|
||||
ImmutableArray<KeyValuePair<string, TypedConstant>> properties = handlerInfo.NamedArguments;
|
||||
ImmutableArray<KeyValuePair<string, TypedConstant>> properties = handlerData.NamedArguments;
|
||||
lineBuilder.Append(@".ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() {");
|
||||
|
||||
foreach (KeyValuePair<string, TypedConstant> property in properties)
|
||||
@@ -141,7 +122,7 @@ internal sealed class HttpClientGenerator : ISourceGenerator
|
||||
lineBuilder.Append(" })");
|
||||
}
|
||||
|
||||
if (classSymbol.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == DynamicSecretAttributeName))
|
||||
if (context.HasAttributeWithName(UseDynamicSecretAttributeName))
|
||||
{
|
||||
lineBuilder.Append(".AddHttpMessageHandler<DynamicSecretHandler>()");
|
||||
}
|
||||
@@ -153,37 +134,7 @@ internal sealed class HttpClientGenerator : ISourceGenerator
|
||||
|
||||
foreach (string line in lines.OrderBy(x => x))
|
||||
{
|
||||
sourceCodeBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
|
||||
private class HttpClientSyntaxContextReceiver : ISyntaxContextReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// 注入特性的名称
|
||||
/// </summary>
|
||||
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
|
||||
|
||||
/// <summary>
|
||||
/// 所有需要注入的类型符号
|
||||
/// </summary>
|
||||
public List<INamedTypeSymbol> Classes { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
|
||||
{
|
||||
// any class with at least one attribute is a candidate for injection generation
|
||||
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
|
||||
{
|
||||
// get as named type symbol
|
||||
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
|
||||
{
|
||||
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
Classes.Add(classSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,46 +4,59 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using System;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 注入代码生成器
|
||||
/// 旨在使用源生成器提高注入效率
|
||||
/// 防止在运行时动态查找注入类型
|
||||
/// </summary>
|
||||
[Generator]
|
||||
internal sealed class InjectionGenerator : ISourceGenerator
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class InjectionGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
|
||||
private const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
|
||||
private const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
|
||||
private const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
|
||||
private const string CRLF = "\r\n";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
private static readonly DiagnosticDescriptor invalidInjectionDescriptor = new("SH101", "无效的 InjectAs 枚举值", "尚未支持生成 {0} 配置", "Quality", DiagnosticSeverity.Error, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// Register a syntax receiver that will be created for each generation pass
|
||||
context.RegisterForSyntaxNotifications(() => new InjectionSyntaxContextReceiver());
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddInjectionsImplementation);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
// retrieve the populated receiver
|
||||
if (context.SyntaxContextReceiver is not InjectionSyntaxContextReceiver receiver)
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 HttpClientClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
return;
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder sourceCodeBuilder = new();
|
||||
sourceCodeBuilder.Append($$"""
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateAddInjectionsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
@@ -51,101 +64,64 @@ internal sealed class InjectionGenerator : ISourceGenerator
|
||||
|
||||
internal static partial class ServiceCollectionExtension
|
||||
{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(InjectionGenerator)}}","1.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(InjectionGenerator)}}", "1.0.0.0")]
|
||||
public static partial IServiceCollection AddInjections(this IServiceCollection services)
|
||||
{
|
||||
""");
|
||||
|
||||
FillWithInjectionServices(receiver, sourceCodeBuilder);
|
||||
sourceCodeBuilder.Append("""
|
||||
FillUpWithAddServices(sourceBuilder, context, context2s);
|
||||
sourceBuilder.Append("""
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource("ServiceCollectionExtension.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
|
||||
context.AddSource("ServiceCollectionExtension.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillWithInjectionServices(InjectionSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
|
||||
private static void FillUpWithAddServices(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
|
||||
{
|
||||
List<string> lines = new();
|
||||
StringBuilder lineBuilder = new();
|
||||
|
||||
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
AttributeData injectionInfo = classSymbol
|
||||
.GetAttributes()
|
||||
.Single(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
|
||||
|
||||
lineBuilder
|
||||
.Clear()
|
||||
.Append(CRLF);
|
||||
lineBuilder.Clear().Append(CRLF);
|
||||
|
||||
AttributeData injectionInfo = context.SingleAttribute(AttributeName);
|
||||
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
|
||||
TypedConstant injectAs = arguments[0];
|
||||
|
||||
string injectAsName = injectAs.ToCSharpString();
|
||||
string injectAsName = arguments[0].ToCSharpString();
|
||||
switch (injectAsName)
|
||||
{
|
||||
case InjectAsSingletonName:
|
||||
lineBuilder.Append(@" services.AddSingleton<");
|
||||
lineBuilder.Append(" services.AddSingleton<");
|
||||
break;
|
||||
case InjectAsTransientName:
|
||||
lineBuilder.Append(@" services.AddTransient<");
|
||||
lineBuilder.Append(" services.AddTransient<");
|
||||
break;
|
||||
case InjectAsScopedName:
|
||||
lineBuilder.Append(@" services.AddScoped<");
|
||||
lineBuilder.Append(" services.AddScoped<");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"非法的 InjectAs 值: [{injectAsName}]");
|
||||
production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, null, injectAsName));
|
||||
break;
|
||||
}
|
||||
|
||||
if (arguments.Length == 2)
|
||||
{
|
||||
TypedConstant interfaceType = arguments[1];
|
||||
lineBuilder.Append($"{interfaceType.Value}, ");
|
||||
lineBuilder.Append($"{arguments[1].Value}, ");
|
||||
}
|
||||
|
||||
lineBuilder.Append($"{classSymbol.ToDisplayString()}>();");
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>();");
|
||||
|
||||
lines.Add(lineBuilder.ToString());
|
||||
}
|
||||
|
||||
foreach (string line in lines.OrderBy(x => x))
|
||||
{
|
||||
sourceCodeBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
|
||||
private class InjectionSyntaxContextReceiver : ISyntaxContextReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// 注入特性的名称
|
||||
/// </summary>
|
||||
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
|
||||
|
||||
/// <summary>
|
||||
/// 所有需要注入的类型符号
|
||||
/// </summary>
|
||||
public List<INamedTypeSymbol> Classes { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
|
||||
{
|
||||
// any class with at least one attribute is a candidate for injection generation
|
||||
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
|
||||
{
|
||||
// get as named type symbol
|
||||
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
|
||||
{
|
||||
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
Classes.Add(classSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Enum;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Resource.Localization.LocalizationAttribute";
|
||||
private const string LocalizationKeyName = "Snap.Hutao.Resource.Localization.LocalizationKeyAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValuesProvider<GeneratorSyntaxContext2> localizationEnums = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedEnums, LocalizationEnum)
|
||||
.Where(GeneratorSyntaxContext2.NotNull);
|
||||
|
||||
context.RegisterSourceOutput(localizationEnums, GenerateGetLocalizedDescriptionImplementation);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedEnums(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is EnumDeclarationSyntax enumDeclarationSyntax
|
||||
&& enumDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 LocalizationEnum(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(context.Node, token) is INamedTypeSymbol enumSymbol)
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = enumSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, enumSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateGetLocalizedDescriptionImplementation(SourceProductionContext context, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Resource.Localization;
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(LocalizedEnumGenerator)}}", "1.0.0.0")]
|
||||
internal static class {{context2.Symbol.Name}}Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <param name="value">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
public static string GetLocalizedDescription(this {{context2.Symbol}} value)
|
||||
{
|
||||
string key = value switch
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithSwitchBranches(sourceBuilder, context2);
|
||||
|
||||
sourceBuilder.Append($$"""
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return Enum.GetName(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return SH.ResourceManager.GetString(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <param name="value">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
[return:MaybeNull]
|
||||
public static string GetLocalizedDescriptionOrDefault(this {{context2.Symbol}} value)
|
||||
{
|
||||
string key = value switch
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithSwitchBranches(sourceBuilder, context2);
|
||||
|
||||
sourceBuilder.Append($$"""
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
return SH.ResourceManager.GetString(key);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource($"{context2.Symbol.Name}Extension.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithSwitchBranches(StringBuilder sourceBuilder, GeneratorSyntaxContext2 context)
|
||||
{
|
||||
IEnumerable<IFieldSymbol> fields = context.Symbol.GetMembers()
|
||||
.Where(m => m.Kind == SymbolKind.Field)
|
||||
.Cast<IFieldSymbol>();
|
||||
|
||||
foreach(IFieldSymbol fieldSymbol in fields)
|
||||
{
|
||||
AttributeData? localizationKeyInfo = fieldSymbol.GetAttributes()
|
||||
.SingleOrDefault(data => data.AttributeClass!.ToDisplayString() == LocalizationKeyName);
|
||||
if (localizationKeyInfo != null)
|
||||
{
|
||||
sourceBuilder
|
||||
.Append(" ")
|
||||
.Append(fieldSymbol)
|
||||
.Append(" => \"")
|
||||
.Append(localizationKeyInfo.ConstructorArguments[0].Value)
|
||||
.AppendLine("\",");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,4 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:启用分析器发布跟踪", Justification = "<挂起>", Scope = "member", Target = "~F:Snap.Hutao.SourceGeneration.TypeInternalAnalyzer.typeInternalDescriptor")]
|
||||
[assembly: SuppressMessage("", "RS2008")]
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Identity;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class IdentityGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string FileName = "IdentityStructs.json";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<AdditionalText>> provider = context.AdditionalTextsProvider.Where(MatchFileName).Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(provider, GenerateIdentityStructs);
|
||||
}
|
||||
|
||||
private static bool MatchFileName(AdditionalText text)
|
||||
{
|
||||
return Path.GetFileName(text.Path) == FileName;
|
||||
}
|
||||
|
||||
private static void GenerateIdentityStructs(SourceProductionContext context, ImmutableArray<AdditionalText> texts)
|
||||
{
|
||||
AdditionalText jsonFile = texts.Single();
|
||||
|
||||
string identityJson = jsonFile.GetText(context.CancellationToken)!.ToString();
|
||||
List<IdentityStructMetadata> identities = identityJson.FromJson<List<IdentityStructMetadata>>()!;
|
||||
|
||||
if (identities.Any())
|
||||
{
|
||||
foreach (IdentityStructMetadata identityStruct in identities)
|
||||
{
|
||||
GenerateIdentityStruct(context, identityStruct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateIdentityStruct(SourceProductionContext context, IdentityStructMetadata metadata)
|
||||
{
|
||||
string name = metadata.Name;
|
||||
|
||||
StringBuilder sourceBuilder = new StringBuilder().AppendLine($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive.Converter;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Snap.Hutao.Model.Primitive;
|
||||
|
||||
/// <summary>
|
||||
/// {{metadata.Documentation}}
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(IdentityConverter<{{name}}>))]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(IdentityGenerator)}}","1.0.0.0")]
|
||||
internal readonly partial struct {{name}}
|
||||
{
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public readonly uint Value;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="{{name}}"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="value">value</param>
|
||||
public {{name}}(uint value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator uint({{name}} value)
|
||||
{
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
public static implicit operator {{name}}(uint value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
if (metadata.Equatable)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IEquatable<{{name}}>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is {{name}} other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals({{name}} other)
|
||||
{
|
||||
return Value == other.Value;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.EqualityOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IEqualityOperators<{{name}}, {{name}}, bool>, IEqualityOperators<{{name}}, uint, bool>
|
||||
{
|
||||
public static bool operator ==({{name}} left, {{name}} right)
|
||||
{
|
||||
return left.Value == right.Value;
|
||||
}
|
||||
|
||||
public static bool operator ==({{name}} left, uint right)
|
||||
{
|
||||
return left.Value == right;
|
||||
}
|
||||
|
||||
public static bool operator !=({{name}} left, {{name}} right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public static bool operator !=({{name}} left, uint right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.AdditionOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IAdditionOperators<{{name}}, {{name}}, {{name}}>, IAdditionOperators<{{name}}, uint, {{name}}>
|
||||
{
|
||||
public static {{name}} operator +({{name}} left, {{name}} right)
|
||||
{
|
||||
return left.Value + right.Value;
|
||||
}
|
||||
|
||||
public static {{name}} operator +({{name}} left, uint right)
|
||||
{
|
||||
return left.Value + right;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.IncrementOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IIncrementOperators<{{name}}>
|
||||
{
|
||||
public static unsafe {{name}} operator ++({{name}} value)
|
||||
{
|
||||
++*(uint*)&value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
context.AddSource($"{name}.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private sealed class IdentityStructMetadata
|
||||
{
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
public string? Documentation { get; set; }
|
||||
|
||||
public bool Equatable { get; set; }
|
||||
|
||||
public bool EqualityOperators { get; set; }
|
||||
|
||||
public bool AdditionOperators { get; set; }
|
||||
|
||||
public bool IncrementOperators { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public static class JsonParser
|
||||
stringBuilder ??= new StringBuilder();
|
||||
splitArrayPool ??= new Stack<List<string>>();
|
||||
|
||||
//Remove all whitespace not within strings to make parsing simpler
|
||||
// Remove all whitespace not within strings to make parsing simpler
|
||||
stringBuilder.Length = 0;
|
||||
for (int i = 0; i < json.Length; i++)
|
||||
{
|
||||
@@ -64,7 +64,7 @@ public static class JsonParser
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
//Parse the thing!
|
||||
// Parse the thing!
|
||||
return (T?)ParseValue(typeof(T), stringBuilder.ToString());
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ public static class JsonParser
|
||||
return json.Length - 1;
|
||||
}
|
||||
|
||||
//Splits { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
|
||||
// Splits { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
|
||||
private static List<string> Split(string json)
|
||||
{
|
||||
List<string> splitArray = splitArrayPool!.Count > 0 ? splitArrayPool.Pop() : new List<string>();
|
||||
@@ -205,7 +205,7 @@ public static class JsonParser
|
||||
|
||||
try
|
||||
{
|
||||
return Enum.Parse(type, json, false);
|
||||
return System.Enum.Parse(type, json, false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -315,7 +315,7 @@ public static class JsonParser
|
||||
return null;
|
||||
}
|
||||
|
||||
Dictionary<string, object?> dict = new Dictionary<string, object?>(elems.Count / 2);
|
||||
Dictionary<string, object?> dict = new(elems.Count / 2);
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]);
|
||||
@@ -396,7 +396,7 @@ public static class JsonParser
|
||||
{
|
||||
object instance = FormatterServices.GetUninitializedObject(type);
|
||||
|
||||
//The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
|
||||
// The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class AttributeDataExtension
|
||||
{
|
||||
public static bool HasNamedArgumentWith<TValue>(this AttributeData data, string key, Func<TValue, bool> predicate)
|
||||
{
|
||||
return data.NamedArguments.Any(a => a.Key == key && predicate((TValue)a.Value.Value!));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class EnumerableExtension
|
||||
{
|
||||
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
{
|
||||
return DistinctByIterator(source, keySelector);
|
||||
}
|
||||
|
||||
private static IEnumerable<TSource> DistinctByIterator<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
{
|
||||
using IEnumerator<TSource> enumerator = source.GetEnumerator();
|
||||
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
HashSet<TKey> set = new();
|
||||
|
||||
do
|
||||
{
|
||||
TSource element = enumerator.Current;
|
||||
if (set.Add(keySelector(element)))
|
||||
{
|
||||
yield return element;
|
||||
}
|
||||
}
|
||||
while (enumerator.MoveNext());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal readonly struct GeneratorSyntaxContext2
|
||||
{
|
||||
public readonly GeneratorSyntaxContext Context;
|
||||
public readonly INamedTypeSymbol Symbol;
|
||||
public readonly ImmutableArray<AttributeData> Attributes;
|
||||
public readonly bool HasValue = false;
|
||||
|
||||
public GeneratorSyntaxContext2(GeneratorSyntaxContext context, INamedTypeSymbol symbol, ImmutableArray<AttributeData> attributes)
|
||||
{
|
||||
Context = context;
|
||||
Symbol = symbol;
|
||||
Attributes = attributes;
|
||||
HasValue = true;
|
||||
}
|
||||
|
||||
public static bool NotNull(GeneratorSyntaxContext2 context)
|
||||
{
|
||||
return context.HasValue;
|
||||
}
|
||||
|
||||
public bool HasAttributeWithName(string name)
|
||||
{
|
||||
return Attributes.Any(attr => attr.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public AttributeData SingleAttribute(string name)
|
||||
{
|
||||
return Attributes.Single(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public AttributeData? SingleOrDefaultAttribute(string name)
|
||||
{
|
||||
return Attributes.SingleOrDefault(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public TSyntaxNode Node<TSyntaxNode>()
|
||||
where TSyntaxNode : SyntaxNode
|
||||
{
|
||||
return (TSyntaxNode)Context.Node;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct GeneratorSyntaxContext2<TSymbol>
|
||||
where TSymbol : ISymbol
|
||||
{
|
||||
public readonly GeneratorSyntaxContext Context;
|
||||
public readonly TSymbol Symbol;
|
||||
public readonly ImmutableArray<AttributeData> Attributes;
|
||||
public readonly bool HasValue = false;
|
||||
|
||||
public GeneratorSyntaxContext2(GeneratorSyntaxContext context, TSymbol symbol, ImmutableArray<AttributeData> attributes)
|
||||
{
|
||||
Context = context;
|
||||
Symbol = symbol;
|
||||
Attributes = attributes;
|
||||
HasValue = true;
|
||||
}
|
||||
|
||||
public static bool NotNull(GeneratorSyntaxContext2<TSymbol> context)
|
||||
{
|
||||
return context.HasValue;
|
||||
}
|
||||
|
||||
public AttributeData SingleAttribute(string name)
|
||||
{
|
||||
return Attributes.Single(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class GeneratorSyntaxContextExtension
|
||||
{
|
||||
public static bool TryGetDeclaredSymbol<TSymbol>(this GeneratorSyntaxContext context, System.Threading.CancellationToken token,[NotNullWhen(true)] out TSymbol? symbol)
|
||||
where TSymbol : class, ISymbol
|
||||
{
|
||||
symbol = context.SemanticModel.GetDeclaredSymbol(context.Node, token) as TSymbol;
|
||||
return symbol != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class SymbolDisplayFormats
|
||||
{
|
||||
public static SymbolDisplayFormat FullyQualifiedNonNullableFormat { get; } = new(
|
||||
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
||||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class SyntaxExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether a given <see cref="MemberDeclarationSyntax"/> has or could potentially have any attribute lists.
|
||||
/// </summary>
|
||||
/// <param name="declaration">The input <see cref="MemberDeclarationSyntax"/> to check.</param>
|
||||
/// <returns>Whether <paramref name="declaration"/> has or potentially has any attribute lists.</returns>
|
||||
public static bool HasAttributeLists<TSyntax>(this TSyntax declaration)
|
||||
where TSyntax : MemberDeclarationSyntax
|
||||
{
|
||||
return declaration.AttributeLists.Count > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class TypeSymbolExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether or not a given <see cref="ITypeSymbol"/> has or inherits from a specified type.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
|
||||
/// <param name="name">The full name of the type to check for inheritance.</param>
|
||||
/// <returns>Whether or not <paramref name="typeSymbol"/> is or inherits from <paramref name="name"/>.</returns>
|
||||
public static bool IsOrInheritsFrom(this ITypeSymbol typeSymbol, string name)
|
||||
{
|
||||
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
|
||||
{
|
||||
if (currentType.ToDisplayString() == name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration;
|
||||
|
||||
[Generator]
|
||||
internal sealed class ReliquaryWeightConfigurationGenerator : ISourceGenerator
|
||||
{
|
||||
private const string ReliquaryWeightConfigurationFileName = "ReliquaryWeightConfiguration.json";
|
||||
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
AdditionalText configurationJsonFile = context.AdditionalFiles
|
||||
.First(af => Path.GetFileName(af.Path) == ReliquaryWeightConfigurationFileName);
|
||||
|
||||
string configurationJson = configurationJsonFile.GetText(context.CancellationToken)!.ToString();
|
||||
Dictionary<string, ReliquaryWeightConfigurationMetadata> metadataMap =
|
||||
JsonParser.FromJson<Dictionary<string, ReliquaryWeightConfigurationMetadata>>(configurationJson)!;
|
||||
|
||||
StringBuilder sourceCodeBuilder = new();
|
||||
sourceCodeBuilder.Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物评分权重配置
|
||||
/// </summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(ReliquaryWeightConfigurationGenerator)}}","1.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
internal static class ReliquaryWeightConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认
|
||||
/// </summary>
|
||||
public static readonly AffixWeight Default = new(0, 100, 75, 0, 100, 100, 0, 55, 0);
|
||||
|
||||
/// <summary>
|
||||
/// 词条权重
|
||||
/// </summary>
|
||||
public static readonly List<AffixWeight> AffixWeights = new()
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
foreach (KeyValuePair<string, ReliquaryWeightConfigurationMetadata> kvp in metadataMap.OrderBy(kvp => kvp.Key))
|
||||
{
|
||||
AppendAffixWeight(sourceCodeBuilder, kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
sourceCodeBuilder.Append($$"""
|
||||
};
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource("ReliquaryWeightConfiguration.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.AddSource("ReliquaryWeightConfiguration.g.cs", ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendAffixWeight(StringBuilder builder, string id, ReliquaryWeightConfigurationMetadata metadata)
|
||||
{
|
||||
StringBuilder lineBuilder = new StringBuilder()
|
||||
.Append(" new AffixWeight(").Append(id).Append(',')
|
||||
.Append(' ').Append(metadata.Hp).Append(',')
|
||||
.Append(' ').Append(metadata.Attack).Append(',')
|
||||
.Append(' ').Append(metadata.Defense).Append(',')
|
||||
.Append(' ').Append(metadata.CritRate).Append(',')
|
||||
.Append(' ').Append(metadata.CritHurt).Append(',')
|
||||
.Append(' ').Append(metadata.Mastery).Append(',')
|
||||
.Append(' ').Append(metadata.Recharge).Append(',')
|
||||
.Append(' ').Append(metadata.Healing).Append(')')
|
||||
.Append('.').Append(metadata.ElementType).Append("(").Append(metadata.ElementHurt).Append(')');
|
||||
|
||||
if (metadata.PhysicialHurt != 0)
|
||||
{
|
||||
lineBuilder.Append(".Physical(").Append(metadata.PhysicialHurt).Append(')');
|
||||
}
|
||||
|
||||
lineBuilder.Append(',');
|
||||
|
||||
|
||||
builder.AppendLine(lineBuilder.ToString());
|
||||
}
|
||||
|
||||
private sealed class ReliquaryWeightConfigurationMetadata
|
||||
{
|
||||
[DataMember(Name = "hp")]
|
||||
public int Hp { get; set; }
|
||||
|
||||
[DataMember(Name = "atk")]
|
||||
public int Attack { get; set; }
|
||||
|
||||
[DataMember(Name = "def")]
|
||||
public int Defense { get; set; }
|
||||
|
||||
[DataMember(Name = "cpct")]
|
||||
public int CritRate { get; set; }
|
||||
|
||||
[DataMember(Name = "cdmg")]
|
||||
public int CritHurt { get; set; }
|
||||
|
||||
[DataMember(Name = "mastery")]
|
||||
public int Mastery { get; set; }
|
||||
|
||||
[DataMember(Name = "recharge")]
|
||||
public int Recharge { get; set; }
|
||||
|
||||
[DataMember(Name = "heal")]
|
||||
public int Healing { get; set; }
|
||||
|
||||
[DataMember(Name = "element")]
|
||||
public string ElementType { get; set; } = default!;
|
||||
|
||||
[DataMember(Name = "dmg")]
|
||||
public int ElementHurt { get; set; }
|
||||
|
||||
[DataMember(Name = "phy")]
|
||||
public int PhysicialHurt { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
@@ -7,6 +7,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -19,7 +20,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" Version="3.3.4" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,62 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// 类型应为内部分析器
|
||||
/// </summary>
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
internal sealed class TypeInternalAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor typeInternalDescriptor = new("SH001", "Type should be internal", "Type should be internal", "Quality", DiagnosticSeverity.Info, true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(typeInternalDescriptor);
|
||||
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterCompilationStartAction(CompilationStart);
|
||||
}
|
||||
|
||||
private void CompilationStart(CompilationStartAnalysisContext context)
|
||||
{
|
||||
context.RegisterSyntaxNodeAction(HandleSyntax<ClassDeclarationSyntax>, SyntaxKind.ClassDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleSyntax<InterfaceDeclarationSyntax>, SyntaxKind.InterfaceDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleSyntax<StructDeclarationSyntax>, SyntaxKind.StructDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleSyntax<EnumDeclarationSyntax>, SyntaxKind.EnumDeclaration);
|
||||
}
|
||||
|
||||
private void HandleSyntax<TSyntax>(SyntaxNodeAnalysisContext classSyntax)
|
||||
where TSyntax : BaseTypeDeclarationSyntax
|
||||
{
|
||||
TSyntax syntax = (TSyntax)classSyntax.Node;
|
||||
|
||||
bool privateExists = false;
|
||||
bool internalExists = false;
|
||||
foreach(SyntaxToken token in syntax.Modifiers)
|
||||
{
|
||||
if (token.IsKind(SyntaxKind.PrivateKeyword))
|
||||
{
|
||||
privateExists = true;
|
||||
}
|
||||
if (token.IsKind(SyntaxKind.InternalKeyword))
|
||||
{
|
||||
internalExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!privateExists && !internalExists)
|
||||
{
|
||||
Location location = syntax.Identifier.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(typeInternalDescriptor, location);
|
||||
classSyntax.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
188
src/Snap.Hutao/Snap.Hutao.SourceGeneration/UniversalAnalyzer.cs
Normal file
188
src/Snap.Hutao/Snap.Hutao.SourceGeneration/UniversalAnalyzer.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// 通用分析器
|
||||
/// </summary>
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor typeInternalDescriptor = new("SH001", "Type should be internal", "Type [{0}] should be internal", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor readOnlyStructRefDescriptor = new("SH002", "ReadOnly struct should be passed with ref-like key word", "ReadOnly Struct [{0}] should be passed with ref-like key word", "Quality", DiagnosticSeverity.Info, true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
|
||||
{
|
||||
get
|
||||
{
|
||||
return new DiagnosticDescriptor[]
|
||||
{
|
||||
typeInternalDescriptor,
|
||||
readOnlyStructRefDescriptor,
|
||||
}.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterCompilationStartAction(CompilationStart);
|
||||
}
|
||||
|
||||
private void CompilationStart(CompilationStartAnalysisContext context)
|
||||
{
|
||||
SyntaxKind[] types = new SyntaxKind[]
|
||||
{
|
||||
SyntaxKind.ClassDeclaration,
|
||||
SyntaxKind.InterfaceDeclaration,
|
||||
SyntaxKind.StructDeclaration,
|
||||
SyntaxKind.EnumDeclaration
|
||||
};
|
||||
|
||||
context.RegisterSyntaxNodeAction(HandleTypeDeclaration, types);
|
||||
|
||||
context.RegisterSyntaxNodeAction(HandleMethodDeclaration, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleConstructorDeclaration, SyntaxKind.ConstructorDeclaration);
|
||||
}
|
||||
|
||||
private void HandleTypeDeclaration(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
BaseTypeDeclarationSyntax syntax = (BaseTypeDeclarationSyntax)context.Node;
|
||||
|
||||
bool privateExists = false;
|
||||
bool internalExists = false;
|
||||
bool fileExists = false;
|
||||
|
||||
foreach (SyntaxToken token in syntax.Modifiers)
|
||||
{
|
||||
if (token.IsKind(SyntaxKind.PrivateKeyword))
|
||||
{
|
||||
privateExists = true;
|
||||
}
|
||||
|
||||
if (token.IsKind(SyntaxKind.InternalKeyword))
|
||||
{
|
||||
internalExists = true;
|
||||
}
|
||||
|
||||
if (token.IsKind(SyntaxKind.FileKeyword))
|
||||
{
|
||||
fileExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!privateExists && !internalExists && !fileExists)
|
||||
{
|
||||
Location location = syntax.Identifier.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(typeInternalDescriptor, location, syntax.Identifier);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleMethodDeclaration(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
|
||||
|
||||
// 跳过异步方法,因为异步方法无法使用 ref in out
|
||||
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.AsyncKeyword)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳过重载方法
|
||||
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.OverrideKeyword)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳过方法定义 如 接口
|
||||
if (methodSyntax.Body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ParameterSyntax parameter in methodSyntax.ParameterList.Parameters)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
|
||||
{
|
||||
if (IsBuiltInType(symbol.Type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过 CancellationToken
|
||||
if (symbol.Type.ToDisplayString() == "System.Threading.CancellationToken")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (symbol.Type.IsReadOnly && symbol.RefKind == RefKind.None)
|
||||
{
|
||||
Location location = parameter.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(readOnlyStructRefDescriptor, location, symbol.Type);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConstructorDeclaration(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
ConstructorDeclarationSyntax constructorSyntax = (ConstructorDeclarationSyntax)context.Node;
|
||||
|
||||
foreach (ParameterSyntax parameter in constructorSyntax.ParameterList.Parameters)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
|
||||
{
|
||||
if (IsBuiltInType(symbol.Type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过 CancellationToken
|
||||
if (symbol.Type.ToDisplayString() == "System.Threading.CancellationToken")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (symbol.Type.IsReadOnly && symbol.RefKind == RefKind.None)
|
||||
{
|
||||
Location location = parameter.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(readOnlyStructRefDescriptor, location, symbol.Type);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsBuiltInType(ITypeSymbol symbol)
|
||||
{
|
||||
return symbol.SpecialType switch
|
||||
{
|
||||
SpecialType.System_Boolean => true,
|
||||
SpecialType.System_Char => true,
|
||||
SpecialType.System_SByte => true,
|
||||
SpecialType.System_Byte => true,
|
||||
SpecialType.System_Int16 => true,
|
||||
SpecialType.System_UInt16 => true,
|
||||
SpecialType.System_Int32 => true,
|
||||
SpecialType.System_UInt32 => true,
|
||||
SpecialType.System_Int64 => true,
|
||||
SpecialType.System_UInt64 => true,
|
||||
SpecialType.System_Decimal => true,
|
||||
SpecialType.System_Single => true,
|
||||
SpecialType.System_Double => true,
|
||||
SpecialType.System_IntPtr => true,
|
||||
SpecialType.System_UIntPtr => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
namespace Snap.Hutao.Test;
|
||||
|
||||
[TestClass]
|
||||
public class CSharpLanguageFeatureTest
|
||||
{
|
||||
[TestMethod]
|
||||
public unsafe void NullStringFixedAlsoNullPointer()
|
||||
{
|
||||
string testStr = null!;
|
||||
fixed(char* pStr = testStr)
|
||||
{
|
||||
Assert.IsTrue(pStr == null);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void EmptyStringFixedIsNullTerminator()
|
||||
{
|
||||
string testStr = string.Empty;
|
||||
fixed (char* pStr = testStr)
|
||||
{
|
||||
Assert.IsTrue(*pStr == '\0');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,32 +4,39 @@ using System;
|
||||
namespace Snap.Hutao.Test;
|
||||
|
||||
[TestClass]
|
||||
public class DependencyInjectionTest
|
||||
public sealed class DependencyInjectionTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void OriginalTypeNotDiscoverable()
|
||||
{
|
||||
IServiceProvider services = new ServiceCollection()
|
||||
.AddSingleton<IService, ServiceA>()
|
||||
.AddSingleton<IService, ServiceB>()
|
||||
.BuildServiceProvider();
|
||||
private readonly IServiceProvider services = new ServiceCollection()
|
||||
.AddSingleton<IService, ServiceA>()
|
||||
.AddSingleton<IService, ServiceB>()
|
||||
.AddScoped<IScopedService, ServiceA>()
|
||||
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
|
||||
.BuildServiceProvider();
|
||||
|
||||
[TestMethod]
|
||||
public void OriginalTypeCannotResolved()
|
||||
{
|
||||
Assert.IsNull(services.GetService<ServiceA>());
|
||||
Assert.IsNull(services.GetService<ServiceB>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ScopedServiceInitializeMultipleTimesInScope()
|
||||
public void GenericServicesCanBeResolved()
|
||||
{
|
||||
IServiceProvider services = new ServiceCollection()
|
||||
.AddScoped<IService, ServiceA>()
|
||||
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
|
||||
.BuildServiceProvider();
|
||||
|
||||
IServiceScopeFactory scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
Assert.IsNotNull(services.GetService<IGenericService<int>>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ScopedServiceInitializeMultipleTimesInScope()
|
||||
{
|
||||
using (IServiceScope scope = services.CreateScope())
|
||||
{
|
||||
IService service1 = scope.ServiceProvider.GetRequiredService<IService>();
|
||||
IService service2 = scope.ServiceProvider.GetRequiredService<IService>();
|
||||
IScopedService service1 = scope.ServiceProvider.GetRequiredService<IScopedService>();
|
||||
IScopedService service2 = scope.ServiceProvider.GetRequiredService<IScopedService>();
|
||||
Assert.AreNotEqual(service1.Id, service2.Id);
|
||||
}
|
||||
}
|
||||
@@ -39,7 +46,12 @@ public class DependencyInjectionTest
|
||||
Guid Id { get; }
|
||||
}
|
||||
|
||||
private sealed class ServiceA : IService
|
||||
private interface IScopedService
|
||||
{
|
||||
Guid Id { get; }
|
||||
}
|
||||
|
||||
private sealed class ServiceA : IService, IScopedService
|
||||
{
|
||||
public Guid Id
|
||||
{
|
||||
@@ -54,4 +66,24 @@ public class DependencyInjectionTest
|
||||
get => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private interface IGenericService<T>
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class GenericService<T> : IGenericService<T>
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class NonInjectedServiceA
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class NonInjectedServiceB
|
||||
{
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public NonInjectedServiceB(NonInjectedServiceA? serviceA)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Snap.Hutao.Test;
|
||||
|
||||
@@ -11,6 +13,19 @@ public class JsonSerializeTest
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SmapleEmptyStringObjectJson = """
|
||||
{
|
||||
"A" : ""
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SmapleNumberKeyDictionaryJson = """
|
||||
{
|
||||
"111" : "12",
|
||||
"222" : "34"
|
||||
}
|
||||
""";
|
||||
|
||||
[TestMethod]
|
||||
public void DelegatePropertyCanSerialize()
|
||||
{
|
||||
@@ -18,9 +33,35 @@ public class JsonSerializeTest
|
||||
Assert.AreEqual(sample.B, 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(JsonException))]
|
||||
public void EmptyStringCannotSerializeAsNumber()
|
||||
{
|
||||
StringNumberSample sample = JsonSerializer.Deserialize<StringNumberSample>(SmapleEmptyStringObjectJson)!;
|
||||
Assert.AreEqual(sample.A, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NumberStringKeyCanSerializeAsKey()
|
||||
{
|
||||
JsonSerializerOptions options = new()
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
Dictionary<int,string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, options)!;
|
||||
Assert.AreEqual(sample[111], "12");
|
||||
}
|
||||
|
||||
private class Sample
|
||||
{
|
||||
public int A { get => B; set => B = value; }
|
||||
public int B { get; set; }
|
||||
}
|
||||
|
||||
private class StringNumberSample
|
||||
{
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||
public int A { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
public sealed class EnumRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void EnumParseCanNotHandleEmptyString()
|
||||
{
|
||||
Enum.Parse<EnumA>(string.Empty);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EnumParseCanHandleNumberString()
|
||||
{
|
||||
EnumA a = Enum.Parse<EnumA>("2");
|
||||
Assert.AreEqual(a, EnumA.ValueB);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EnumToStringDecimal()
|
||||
{
|
||||
Assert.AreEqual("2", EnumA.ValueB.ToString("D"));
|
||||
}
|
||||
|
||||
private enum EnumA
|
||||
{
|
||||
None = 0,
|
||||
ValueA = 1,
|
||||
ValueB = 2,
|
||||
ValueC = 3,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
public sealed class ForEachRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void ListOfStringCanEnumerateAsReadOnlySpanOfChar()
|
||||
{
|
||||
List<string> strings = new()
|
||||
{
|
||||
"a", "b", "c"
|
||||
};
|
||||
|
||||
int count = 0;
|
||||
foreach (ReadOnlySpan<char> chars in strings)
|
||||
{
|
||||
Assert.IsTrue(chars.Length == 1);
|
||||
++count;
|
||||
}
|
||||
|
||||
Assert.AreEqual(3, count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
public sealed class PropertyRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void GetTwiceOnPropertyResultsNotSame()
|
||||
{
|
||||
Assert.AreNotEqual(UUID, UUID);
|
||||
}
|
||||
|
||||
public static Guid UUID { get => Guid.NewGuid(); }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
public sealed class RangeRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void RangeTrimLastOne()
|
||||
{
|
||||
int[] array = { 1, 2, 3, 4 };
|
||||
int[] test = { 1, 2, 3 };
|
||||
int[] result = array[..^1];
|
||||
Assert.AreEqual(3, result.Length);
|
||||
Assert.IsTrue(MemoryExtensions.SequenceEqual<int>(test, result));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
public sealed class StringRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public unsafe void NullStringFixedIsNullPointer()
|
||||
{
|
||||
string testStr = null!;
|
||||
fixed (char* pStr = testStr)
|
||||
{
|
||||
Assert.IsTrue(pStr == null);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void EmptyStringFixedIsNullTerminator()
|
||||
{
|
||||
string testStr = string.Empty;
|
||||
fixed (char* pStr = testStr)
|
||||
{
|
||||
Assert.IsTrue(*pStr == '\0');
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void EmptyStringAsSpanIsZeroLength()
|
||||
{
|
||||
string testStr = string.Empty;
|
||||
ReadOnlySpan<char> testSpan = testStr;
|
||||
Assert.IsTrue(testSpan.Length == 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
public sealed class UnsafeRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public unsafe void UInt32AllSetIs()
|
||||
{
|
||||
byte[] bytes = { 0xFF, 0xFF, 0xFF, 0xFF, };
|
||||
|
||||
fixed (byte* pBytes = bytes)
|
||||
{
|
||||
Assert.AreEqual(uint.MaxValue, *(uint*)pBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,15 @@
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.0.4" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.0.4" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -12,10 +12,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug As Fake Elevated|Any CPU = Debug As Fake Elevated|Any CPU
|
||||
Debug As Fake Elevated|arm64 = Debug As Fake Elevated|arm64
|
||||
Debug As Fake Elevated|x64 = Debug As Fake Elevated|x64
|
||||
Debug As Fake Elevated|x86 = Debug As Fake Elevated|x86
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|arm64 = Debug|arm64
|
||||
Debug|x64 = Debug|x64
|
||||
@@ -26,6 +30,18 @@ Global
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|Any CPU.ActiveCfg = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|Any CPU.Build.0 = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|Any CPU.Deploy.0 = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|arm64.ActiveCfg = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|arm64.Build.0 = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|arm64.Deploy.0 = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x64.ActiveCfg = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x64.Build.0 = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x64.Deploy.0 = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x86.ActiveCfg = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x86.Build.0 = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x86.Deploy.0 = Debug As Fake Elevated|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|Any CPU.Deploy.0 = Debug|x64
|
||||
@@ -50,6 +66,14 @@ Global
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.ActiveCfg = Release|x86
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Build.0 = Release|x86
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Deploy.0 = Release|x86
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|Any CPU.ActiveCfg = Debug As Fake Elevated|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|Any CPU.Build.0 = Debug As Fake Elevated|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|arm64.ActiveCfg = Debug As Fake Elevated|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|arm64.Build.0 = Debug As Fake Elevated|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|x64.ActiveCfg = Debug As Fake Elevated|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|x64.Build.0 = Debug As Fake Elevated|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|x86.ActiveCfg = Debug As Fake Elevated|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|x86.Build.0 = Debug As Fake Elevated|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
@@ -66,6 +90,14 @@ Global
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|Any CPU.ActiveCfg = Debug As Fake Elevated|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|Any CPU.Build.0 = Debug As Fake Elevated|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|arm64.ActiveCfg = Debug As Fake Elevated|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|arm64.Build.0 = Debug As Fake Elevated|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|x64.ActiveCfg = Debug As Fake Elevated|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|x64.Build.0 = Debug As Fake Elevated|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|x86.ActiveCfg = Debug As Fake Elevated|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|x86.Build.0 = Debug As Fake Elevated|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
|
||||
@@ -44,13 +44,19 @@
|
||||
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
|
||||
<!-- OpenPaneLength -->
|
||||
<x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double>
|
||||
<x:Double x:Key="CompatSplitViewOpenPaneLength2">268</x:Double>
|
||||
<GridLength x:Key="CompatGridLength2">268</GridLength>
|
||||
|
||||
<x:Double x:Key="CompatSplitViewOpenPaneLength2">304</x:Double>
|
||||
<GridLength x:Key="CompatGridLength2">288</GridLength>
|
||||
|
||||
<x:Double x:Key="HomeAdaptiveCardHeight">180</x:Double>
|
||||
|
||||
<!-- Brushes -->
|
||||
<SolidColorBrush x:Key="AvatarPropertyAddValueBrush" Color="{ThemeResource AvatarPropertyAddValueColor}"/>
|
||||
<!-- Settings -->
|
||||
<x:Double x:Key="SettingsCardSpacing">3</x:Double>
|
||||
|
||||
<x:Double x:Key="SettingsCardMinHeight">0</x:Double>
|
||||
<x:Double x:Key="SettingsCardWrapThreshold">0</x:Double>
|
||||
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
|
||||
<Style
|
||||
x:Key="SettingsSectionHeaderTextBlockStyle"
|
||||
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
|
||||
@@ -84,7 +90,7 @@
|
||||
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
|
||||
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
|
||||
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
|
||||
<shmmc:ParameterDescriptor x:Key="DescParamDescriptor"/>
|
||||
<shmmc:DescriptionsParametersDescriptor x:Key="DescParamDescriptor"/>
|
||||
<shmmc:ElementNameIconConverter x:Key="ElementNameIconConverter"/>
|
||||
<shmmc:EmotionIconConverter x:Key="EmotionIconConverter"/>
|
||||
<shmmc:EquipIconConverter x:Key="EquipIconConverter"/>
|
||||
@@ -93,7 +99,7 @@
|
||||
<shmmc:GachaEquipIconConverter x:Key="GachaEquipIconConverter"/>
|
||||
<shmmc:ItemIconConverter x:Key="ItemIconConverter"/>
|
||||
<shmmc:MonsterIconConverter x:Key="MonsterIconConverter"/>
|
||||
<shmmc:PropertyDescriptor x:Key="PropertyDescriptor"/>
|
||||
<shmmc:PropertiesParametersDescriptor x:Key="PropertyDescriptor"/>
|
||||
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
|
||||
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
|
||||
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Notifications;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.Shell;
|
||||
using System.Diagnostics;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -21,19 +20,25 @@ namespace Snap.Hutao;
|
||||
[SuppressMessage("", "SH001")]
|
||||
public sealed partial class App : Application
|
||||
{
|
||||
private const string AppInstanceKey = "main";
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IActivation activation;
|
||||
private readonly ILogger<App> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object.
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
public App(ILogger<App> logger)
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public App(IServiceProvider serviceProvider)
|
||||
{
|
||||
// load app resource
|
||||
// Load app resource
|
||||
InitializeComponent();
|
||||
this.logger = logger;
|
||||
|
||||
_ = new ExceptionRecorder(this, logger);
|
||||
activation = serviceProvider.GetRequiredService<IActivation>();
|
||||
logger = serviceProvider.GetRequiredService<ILogger<App>>();
|
||||
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -42,19 +47,16 @@ public sealed partial class App : Application
|
||||
try
|
||||
{
|
||||
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
AppInstance firstInstance = AppInstance.FindOrRegisterForKey("main");
|
||||
AppInstance firstInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey);
|
||||
|
||||
if (firstInstance.IsCurrent)
|
||||
{
|
||||
// manually invoke
|
||||
Activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
|
||||
firstInstance.Activated += Activation.Activate;
|
||||
ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate;
|
||||
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
|
||||
activation.InitializeWith(firstInstance);
|
||||
|
||||
logger.LogInformation("Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version);
|
||||
logger.LogInformation("Cache folder : {folder}", ApplicationData.Current.LocalCacheFolder.Path);
|
||||
|
||||
JumpListHelper.ConfigureAsync().SafeForget(logger);
|
||||
LogDiagnosticInformation();
|
||||
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -63,10 +65,19 @@ public sealed partial class App : Application
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch
|
||||
{
|
||||
// AppInstance.GetCurrent() calls failed
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
}
|
||||
|
||||
private void LogDiagnosticInformation()
|
||||
{
|
||||
HutaoOptions hutaoOptions = serviceProvider.GetRequiredService<HutaoOptions>();
|
||||
|
||||
logger.LogInformation("FamilyName: {name}", hutaoOptions.FamilyName);
|
||||
logger.LogInformation("Version: {version}", hutaoOptions.Version);
|
||||
logger.LogInformation("LocalCache: {folder}", hutaoOptions.LocalCache);
|
||||
}
|
||||
}
|
||||
28
src/Snap.Hutao/Snap.Hutao/AppResourceProvider.cs
Normal file
28
src/Snap.Hutao/Snap.Hutao/AppResourceProvider.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序资源提供器
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IAppResourceProvider))]
|
||||
internal sealed class AppResourceProvider : IAppResourceProvider
|
||||
{
|
||||
private readonly App app;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的应用程序资源提供器
|
||||
/// </summary>
|
||||
/// <param name="app">应用</param>
|
||||
public AppResourceProvider(App app)
|
||||
{
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T GetResource<T>(string name)
|
||||
{
|
||||
return (T)app.Resources[name];
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,14 @@ internal static class AnimationDurations
|
||||
/// 图片缩放动画
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageZoom = TimeSpan.FromSeconds(0.5);
|
||||
|
||||
/// <summary>
|
||||
/// 图像淡入
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageFadeIn = TimeSpan.FromSeconds(0.3);
|
||||
|
||||
/// <summary>
|
||||
/// 图像淡出
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageFadeOut = TimeSpan.FromSeconds(0.2);
|
||||
}
|
||||
@@ -10,29 +10,10 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<AutoHeightBehavior>.DependBoxed<double>(nameof(TargetWidth), BoxedValues.DoubleOne);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<AutoHeightBehavior>.DependBoxed<double>(nameof(TargetHeight), BoxedValues.DoubleOne);
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
/// </summary>
|
||||
public double TargetWidth
|
||||
{
|
||||
get => (double)GetValue(TargetWidthProperty);
|
||||
set => SetValue(TargetWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标高度
|
||||
/// </summary>
|
||||
public double TargetHeight
|
||||
{
|
||||
get => (double)GetValue(TargetHeightProperty);
|
||||
set => SetValue(TargetHeightProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
{
|
||||
@@ -54,6 +35,6 @@ internal sealed class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Height = (double)AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
|
||||
AssociatedObject.Height = AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
|
||||
}
|
||||
}
|
||||
@@ -10,29 +10,10 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetWidth), 320D);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetHeight), 1024D);
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
/// </summary>
|
||||
public double TargetWidth
|
||||
{
|
||||
get => (double)GetValue(TargetWidthProperty);
|
||||
set => SetValue(TargetWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标高度
|
||||
/// </summary>
|
||||
public double TargetHeight
|
||||
{
|
||||
get => (double)GetValue(TargetHeightProperty);
|
||||
set => SetValue(TargetHeightProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
{
|
||||
@@ -54,6 +35,6 @@ internal sealed class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Width = (double)AssociatedObject.Height * (TargetWidth / TargetHeight);
|
||||
AssociatedObject.Width = AssociatedObject.Height * (TargetWidth / TargetHeight);
|
||||
}
|
||||
}
|
||||
@@ -10,30 +10,10 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
/// 在元素加载完成后执行命令的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class InvokeCommandOnLoadedBehavior : BehaviorBase<UIElement>
|
||||
[DependencyProperty("Command", typeof(ICommand))]
|
||||
[DependencyProperty("CommandParameter", typeof(object))]
|
||||
internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIElement>
|
||||
{
|
||||
private static readonly DependencyProperty CommandProperty = Property<InvokeCommandOnLoadedBehavior>.Depend<ICommand>(nameof(Command));
|
||||
private static readonly DependencyProperty CommandParameterProperty = Property<InvokeCommandOnLoadedBehavior>.Depend<object>(nameof(CommandParameter));
|
||||
|
||||
/// <summary>
|
||||
/// 待执行的命令
|
||||
/// </summary>
|
||||
public ICommand Command
|
||||
{
|
||||
get => (ICommand)GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 命令参数
|
||||
/// </summary>
|
||||
[MaybeNull]
|
||||
public object CommandParameter
|
||||
{
|
||||
get => GetValue(CommandParameterProperty);
|
||||
set => SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
{
|
||||
|
||||
@@ -12,16 +12,7 @@ namespace Snap.Hutao.Control;
|
||||
/// when object is not used anymore.
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class BindingProxy : DependencyObject
|
||||
[DependencyProperty("DataContext", typeof(object))]
|
||||
internal sealed partial class BindingProxy : DependencyObject
|
||||
{
|
||||
private static readonly DependencyProperty DataContextProperty = Property<BindingProxy>.Depend<object>(nameof(DataContext));
|
||||
|
||||
/// <summary>
|
||||
/// 数据上下文
|
||||
/// </summary>
|
||||
public object? DataContext
|
||||
{
|
||||
get => GetValue(DataContextProperty);
|
||||
set => SetValue(DataContextProperty, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
/// <summary>
|
||||
/// 依赖对象转换器
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">源类型</typeparam>
|
||||
/// <typeparam name="TTo">目标类型</typeparam>
|
||||
internal abstract class DependencyValueConverter<TFrom, TTo> : DependencyObject, IValueConverter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
#if DEBUG
|
||||
try
|
||||
{
|
||||
return Convert((TFrom)value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
|
||||
.LogError(ex, "值转换器异常");
|
||||
}
|
||||
|
||||
return null;
|
||||
#else
|
||||
return Convert((TFrom)value);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object? ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return ConvertBack((TTo)value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从源类型转换到目标类型
|
||||
/// </summary>
|
||||
/// <param name="from">源</param>
|
||||
/// <returns>目标</returns>
|
||||
public abstract TTo Convert(TFrom from);
|
||||
|
||||
/// <summary>
|
||||
/// 从目标类型转换到源类型
|
||||
/// 重写时请勿调用基类方法
|
||||
/// </summary>
|
||||
/// <param name="to">目标</param>
|
||||
/// <returns>源</returns>
|
||||
public virtual TFrom ConvertBack(TTo to)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
@@ -15,30 +15,15 @@ internal static class ContentDialogExtension
|
||||
/// 阻止用户交互
|
||||
/// </summary>
|
||||
/// <param name="contentDialog">对话框</param>
|
||||
/// <param name="taskContext">任务上下文</param>
|
||||
/// <returns>用于恢复用户交互</returns>
|
||||
public static async ValueTask<IDisposable> BlockAsync(this ContentDialog contentDialog)
|
||||
public static async ValueTask<ContentDialogHideToken> BlockAsync(this ContentDialog contentDialog, ITaskContext taskContext)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
contentDialog.ShowAsync().AsTask().SafeForget();
|
||||
|
||||
// E_ASYNC_OPERATION_NOT_STARTED 0x80000019
|
||||
// Only a single ContentDialog can be open at any time.
|
||||
return new ContentDialogHider(contentDialog);
|
||||
}
|
||||
|
||||
private class ContentDialogHider : IDisposable
|
||||
{
|
||||
private readonly ContentDialog contentDialog;
|
||||
|
||||
public ContentDialogHider(ContentDialog contentDialog)
|
||||
{
|
||||
this.contentDialog = contentDialog;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Hide() must be called on main thread.
|
||||
ThreadHelper.InvokeOnMainThread(contentDialog.Hide);
|
||||
}
|
||||
return new ContentDialogHideToken(contentDialog, taskContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Extension;
|
||||
|
||||
internal readonly struct ContentDialogHideToken : IDisposable
|
||||
{
|
||||
private readonly ContentDialog contentDialog;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
public ContentDialogHideToken(ContentDialog contentDialog, ITaskContext taskContext)
|
||||
{
|
||||
this.contentDialog = contentDialog;
|
||||
this.taskContext = taskContext;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Hide() must be called on main thread.
|
||||
taskContext.InvokeOnMainThread(contentDialog.Hide);
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,7 @@ internal sealed class CachedImage : ImageEx
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
// We can only use Ioc to retrive IImageCache,
|
||||
// no IServiceProvider is available.
|
||||
// We can only use Ioc to retrieve IImageCache, no IServiceProvider is available.
|
||||
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
|
||||
|
||||
try
|
||||
@@ -42,12 +41,12 @@ internal sealed class CachedImage : ImageEx
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// BitmapImage initialize with a uri will increase image quality and loading speed.
|
||||
return new BitmapImage(new(file));
|
||||
return new BitmapImage(file.ToUri());
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// The image is corrupted, remove it.
|
||||
imageCache.Remove(imageUri.Enumerate());
|
||||
imageCache.Remove(imageUri);
|
||||
return null;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -55,9 +54,5 @@ internal sealed class CachedImage : ImageEx
|
||||
// task was explicitly canceled
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Control.Animation;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -19,10 +20,10 @@ namespace Snap.Hutao.Control.Image;
|
||||
/// 为其他图像类控件提供基类
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
[DependencyProperty("EnableLazyLoading", typeof(bool), true)]
|
||||
internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
{
|
||||
private static readonly DependencyProperty SourceProperty = Property<CompositionImage>.Depend(nameof(Source), default(Uri), OnSourceChanged);
|
||||
private static readonly DependencyProperty EnableLazyLoadingProperty = Property<CompositionImage>.DependBoxed<bool>(nameof(EnableLazyLoading), BoxedValues.True);
|
||||
private static readonly ConcurrentCancellationTokenSource<CompositionImage> LoadingTokenSource = new();
|
||||
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
@@ -33,9 +34,9 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
/// <summary>
|
||||
/// 构造一个新的单色图像
|
||||
/// </summary>
|
||||
public CompositionImage()
|
||||
protected CompositionImage()
|
||||
{
|
||||
serviceProvider = Ioc.Default.GetRequiredService<IServiceProvider>();
|
||||
serviceProvider = Ioc.Default;
|
||||
|
||||
AllowFocusOnInteraction = false;
|
||||
IsDoubleTapEnabled = false;
|
||||
@@ -56,15 +57,6 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用延迟加载
|
||||
/// </summary>
|
||||
public bool EnableLazyLoading
|
||||
{
|
||||
get => (bool)GetValue(EnableLazyLoadingProperty);
|
||||
set => SetValue(EnableLazyLoadingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合成组合视觉
|
||||
/// </summary>
|
||||
@@ -82,7 +74,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
|
||||
{
|
||||
TaskCompletionSource loadCompleteTaskSource = new();
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file));
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(file.ToUri());
|
||||
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
|
||||
await loadCompleteTaskSource.Task.ConfigureAwait(true);
|
||||
return surface;
|
||||
@@ -123,7 +115,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
private static void OnApplyImageFailed(IServiceProvider serviceProvider, Uri? uri, Exception exception)
|
||||
{
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
|
||||
|
||||
if (exception is HttpRequestException httpRequestException)
|
||||
{
|
||||
@@ -157,11 +149,11 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
imageCache.Remove(uri.Enumerate());
|
||||
imageCache.Remove(uri);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
imageCache.Remove(uri.Enumerate());
|
||||
imageCache.Remove(uri);
|
||||
}
|
||||
|
||||
if (imageSurface != null)
|
||||
@@ -183,7 +175,11 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
if (EnableLazyLoading)
|
||||
{
|
||||
await AnimationBuilder.Create().Opacity(1d, 0d).StartAsync(this, token).ConfigureAwait(true);
|
||||
await AnimationBuilder
|
||||
.Create()
|
||||
.Opacity(from: 0D, to: 1D, duration: AnimationDurations.ImageFadeIn)
|
||||
.StartAsync(this, token)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -200,7 +196,11 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
if (EnableLazyLoading)
|
||||
{
|
||||
await AnimationBuilder.Create().Opacity(0d, 1d).StartAsync(this, token).ConfigureAwait(true);
|
||||
await AnimationBuilder
|
||||
.Create()
|
||||
.Opacity(from: 1D, to: 0D, duration: AnimationDurations.ImageFadeOut)
|
||||
.StartAsync(this, token)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -13,47 +13,30 @@ namespace Snap.Hutao.Control.Image;
|
||||
/// 渐变图像
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class Gradient : CompositionImage
|
||||
[DependencyProperty("BackgroundDirection", typeof(GradientDirection), GradientDirection.TopToBottom)]
|
||||
[DependencyProperty("ForegroundDirection", typeof(GradientDirection), GradientDirection.TopToBottom)]
|
||||
internal sealed partial class Gradient : CompositionImage
|
||||
{
|
||||
private static readonly DependencyProperty BackgroundDirectionProperty = Property<Gradient>.Depend(nameof(BackgroundDirection), GradientDirection.TopToBottom);
|
||||
private static readonly DependencyProperty ForegroundDirectionProperty = Property<Gradient>.Depend(nameof(ForegroundDirection), GradientDirection.TopToBottom);
|
||||
|
||||
private double imageAspectRatio;
|
||||
|
||||
/// <summary>
|
||||
/// 背景方向
|
||||
/// </summary>
|
||||
public GradientDirection BackgroundDirection
|
||||
{
|
||||
get => (GradientDirection)GetValue(BackgroundDirectionProperty);
|
||||
set => SetValue(BackgroundDirectionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 前景方向
|
||||
/// </summary>
|
||||
public GradientDirection ForegroundDirection
|
||||
{
|
||||
get => (GradientDirection)GetValue(ForegroundDirectionProperty);
|
||||
set => SetValue(ForegroundDirectionProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnUpdateVisual(SpriteVisual spriteVisual)
|
||||
protected override void OnUpdateVisual(SpriteVisual? spriteVisual)
|
||||
{
|
||||
if (spriteVisual is not null)
|
||||
if (spriteVisual is null)
|
||||
{
|
||||
Height = Math.Clamp(ActualWidth / imageAspectRatio, 0D, MaxHeight);
|
||||
spriteVisual.Size = ActualSize;
|
||||
return;
|
||||
}
|
||||
|
||||
Height = Math.Clamp(ActualWidth / imageAspectRatio, 0D, MaxHeight);
|
||||
spriteVisual.Size = ActualSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
|
||||
{
|
||||
TaskCompletionSource loadCompleteTaskSource = new();
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file));
|
||||
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(file.ToUri());
|
||||
surface.LoadCompleted += (_, _) => loadCompleteTaskSource.TrySetResult();
|
||||
await loadCompleteTaskSource.Task.ConfigureAwait(true);
|
||||
imageAspectRatio = surface.NaturalSize.AspectRatio();
|
||||
return surface;
|
||||
|
||||
@@ -6,20 +6,18 @@ namespace Snap.Hutao.Control.Image;
|
||||
/// <summary>
|
||||
/// 渐变锚点
|
||||
/// </summary>
|
||||
/// <param name="Offset">便宜</param>
|
||||
/// <param name="Color">颜色</param>
|
||||
[HighQuality]
|
||||
internal struct GradientStop
|
||||
internal readonly struct GradientStop
|
||||
{
|
||||
/// <summary>
|
||||
/// 便宜
|
||||
/// 偏移
|
||||
/// </summary>
|
||||
public float Offset;
|
||||
public readonly float Offset;
|
||||
|
||||
/// <summary>
|
||||
/// 颜色
|
||||
/// </summary>
|
||||
public Windows.UI.Color Color;
|
||||
public readonly Windows.UI.Color Color;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的渐变锚点
|
||||
|
||||
@@ -29,9 +29,9 @@ internal sealed class MonoChrome : CompositionImage
|
||||
/// <inheritdoc/>
|
||||
protected override SpriteVisual CompositeSpriteVisual(Compositor compositor, LoadedImageSurface imageSurface)
|
||||
{
|
||||
CompositionColorBrush blackLayerBursh = compositor.CreateColorBrush(Colors.Black);
|
||||
CompositionColorBrush blackLayerBrush = compositor.CreateColorBrush(Colors.Black);
|
||||
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.Uniform, vRatio: 0f);
|
||||
CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBursh, imageSurfaceBrush, BlendEffectMode.Overlay);
|
||||
CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBrush, imageSurfaceBrush, BlendEffectMode.Overlay);
|
||||
CompositionEffectBrush opacityBrush = compositor.CompositeLuminanceToAlphaEffectBrush(overlayBrush);
|
||||
|
||||
backgroundBrush = compositor.CreateColorBrush();
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
|
||||
namespace Snap.Hutao.Control.Markup;
|
||||
|
||||
/// <summary>
|
||||
/// Xaml 服务提供器扩展
|
||||
/// </summary>
|
||||
internal static class XamlServiceProviderExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Get IProvideValueTarget from serviceProvider
|
||||
/// </summary>
|
||||
/// <param name="provider">serviceProvider</param>
|
||||
/// <returns>IProvideValueTarget</returns>
|
||||
public static IProvideValueTarget GetProvideValueTarget(this IXamlServiceProvider provider)
|
||||
{
|
||||
return (IProvideValueTarget)provider.GetService(typeof(IProvideValueTarget));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get IRootObjectProvider from serviceProvider
|
||||
/// </summary>
|
||||
/// <param name="provider">serviceProvider</param>
|
||||
/// <returns>IRootObjectProvider</returns>
|
||||
public static IRootObjectProvider GetRootObjectProvider(this IXamlServiceProvider provider)
|
||||
{
|
||||
return (IRootObjectProvider)provider.GetService(typeof(IRootObjectProvider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get IUriContext from serviceProvider
|
||||
/// </summary>
|
||||
/// <param name="provider">serviceProvider</param>
|
||||
/// <returns>IUriContext</returns>
|
||||
public static IUriContext GetUriContext(this IXamlServiceProvider provider)
|
||||
{
|
||||
return (IUriContext)provider.GetService(typeof(IUriContext));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get IXamlTypeResolver from serviceProvider
|
||||
/// </summary>
|
||||
/// <param name="provider">serviceProvider</param>
|
||||
/// <returns>IXamlTypeResolver</returns>
|
||||
public static IXamlTypeResolver GetXamlTypeResolver(this IXamlServiceProvider provider)
|
||||
{
|
||||
return (IXamlTypeResolver)provider.GetService(typeof(IXamlTypeResolver));
|
||||
}
|
||||
}
|
||||
47
src/Snap.Hutao/Snap.Hutao/Control/Media/Bgra32.cs
Normal file
47
src/Snap.Hutao/Snap.Hutao/Control/Media/Bgra32.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
|
||||
/// <summary>
|
||||
/// BGRA 结构
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal struct Bgra32
|
||||
{
|
||||
/// <summary>
|
||||
/// B
|
||||
/// </summary>
|
||||
public byte B;
|
||||
|
||||
/// <summary>
|
||||
/// G
|
||||
/// </summary>
|
||||
public byte G;
|
||||
|
||||
/// <summary>
|
||||
/// R
|
||||
/// </summary>
|
||||
public byte R;
|
||||
|
||||
/// <summary>
|
||||
/// A
|
||||
/// </summary>
|
||||
public byte A;
|
||||
|
||||
/// <summary>
|
||||
/// 从 Color 转换
|
||||
/// </summary>
|
||||
/// <param name="color">颜色</param>
|
||||
/// <returns>新的 BGRA8 结构</returns>
|
||||
public static unsafe implicit operator Bgra32(Color color)
|
||||
{
|
||||
Unsafe.SkipInit(out Bgra32 bgra8);
|
||||
*(uint*)&bgra8 = BinaryPrimitives.ReverseEndianness(*(uint*)&color);
|
||||
return bgra8;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
|
||||
/// <summary>
|
||||
/// BGRA8 结构
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
internal struct Bgra8
|
||||
{
|
||||
/// <summary>
|
||||
/// B
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
public byte B;
|
||||
|
||||
/// <summary>
|
||||
/// G
|
||||
/// </summary>
|
||||
[FieldOffset(1)]
|
||||
public byte G;
|
||||
|
||||
/// <summary>
|
||||
/// R
|
||||
/// </summary>
|
||||
[FieldOffset(2)]
|
||||
public byte R;
|
||||
|
||||
/// <summary>
|
||||
/// A
|
||||
/// </summary>
|
||||
[FieldOffset(3)]
|
||||
public byte A;
|
||||
|
||||
[FieldOffset(0)]
|
||||
private readonly uint data;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的 BGRA8 结构
|
||||
/// </summary>
|
||||
/// <param name="b">B</param>
|
||||
/// <param name="g">G</param>
|
||||
/// <param name="r">R</param>
|
||||
/// <param name="a">A</param>
|
||||
public Bgra8(byte b, byte g, byte r, byte a)
|
||||
{
|
||||
B = b;
|
||||
G = g;
|
||||
R = r;
|
||||
A = a;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从Color值转换
|
||||
/// </summary>
|
||||
/// <param name="color">颜色</param>
|
||||
/// <returns>新的 BGRA8 结构</returns>
|
||||
public static Bgra8 FromColor(Color color)
|
||||
{
|
||||
return new(color.B, color.G, color.R, color.A);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从RGB值转换
|
||||
/// </summary>
|
||||
/// <param name="r">R</param>
|
||||
/// <param name="g">G</param>
|
||||
/// <param name="b">B</param>
|
||||
/// <returns>新的 BGRA8 结构</returns>
|
||||
public static Bgra8 FromRgb(byte r, byte g, byte b)
|
||||
{
|
||||
return new(b, g, r, 0xFF);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.UI;
|
||||
|
||||
@@ -13,61 +14,71 @@ namespace Snap.Hutao.Control.Media;
|
||||
/// RGBA 颜色
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
internal struct Rgba8
|
||||
internal struct Rgba32
|
||||
{
|
||||
/// <summary>
|
||||
/// R
|
||||
/// </summary>
|
||||
[FieldOffset(3)]
|
||||
public byte R;
|
||||
|
||||
/// <summary>
|
||||
/// G
|
||||
/// </summary>
|
||||
[FieldOffset(2)]
|
||||
public byte G;
|
||||
|
||||
/// <summary>
|
||||
/// B
|
||||
/// </summary>
|
||||
[FieldOffset(1)]
|
||||
public byte B;
|
||||
|
||||
/// <summary>
|
||||
/// A
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
public byte A;
|
||||
|
||||
[FieldOffset(0)]
|
||||
private readonly uint data;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的 RGBA8 颜色
|
||||
/// </summary>
|
||||
/// <param name="hex">色值字符串</param>
|
||||
public Rgba8(ReadOnlySpan<char> hex)
|
||||
public Rgba32(string hex)
|
||||
: this(Convert.ToUInt32(hex, 16))
|
||||
{
|
||||
R = 0;
|
||||
G = 0;
|
||||
B = 0;
|
||||
A = 0;
|
||||
data = Convert.ToUInt32(hex.ToString(), 16);
|
||||
}
|
||||
|
||||
private Rgba8(byte r, byte g, byte b, byte a)
|
||||
/// <summary>
|
||||
/// 使用 RGBA 代码初始化新的结构
|
||||
/// </summary>
|
||||
/// <param name="code">RGBA 代码</param>
|
||||
public unsafe Rgba32(uint code)
|
||||
{
|
||||
// RRGGBBAA -> AABBGGRR
|
||||
fixed (Rgba32* pSelf = &this)
|
||||
{
|
||||
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(code);
|
||||
}
|
||||
}
|
||||
|
||||
private Rgba32(byte r, byte g, byte b, byte a)
|
||||
{
|
||||
data = 0;
|
||||
R = r;
|
||||
G = g;
|
||||
B = b;
|
||||
A = a;
|
||||
}
|
||||
|
||||
public static implicit operator Color(Rgba8 hexColor)
|
||||
public static unsafe implicit operator Color(Rgba32 hexColor)
|
||||
{
|
||||
return Color.FromArgb(hexColor.A, hexColor.R, hexColor.G, hexColor.B);
|
||||
// AABBGGRR -> BBGGRRAA
|
||||
// AABBGGRR -> 000000AA
|
||||
uint a = (*(uint*)&hexColor) >> 24;
|
||||
|
||||
// AABBGGRR -> BBGGRR00
|
||||
uint rgb = (*(uint*)&hexColor) << 8;
|
||||
|
||||
// BBGGRR00 + 000000AA
|
||||
uint rgba = rgb + a;
|
||||
|
||||
return *(Color*)&rgba;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -75,7 +86,7 @@ internal struct Rgba8
|
||||
/// </summary>
|
||||
/// <param name="hsl">HSL 颜色</param>
|
||||
/// <returns>RGBA8颜色</returns>
|
||||
public static Rgba8 FromHsl(HslColor hsl)
|
||||
public static Rgba32 FromHsl(HslColor hsl)
|
||||
{
|
||||
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
|
||||
double h1 = hsl.H / 60;
|
||||
@@ -20,22 +20,22 @@ internal static class SoftwareBitmapExtension
|
||||
/// </summary>
|
||||
/// <param name="softwareBitmap">软件位图</param>
|
||||
/// <param name="tint">底色</param>
|
||||
public static unsafe void NormalBlend(this SoftwareBitmap softwareBitmap, Bgra8 tint)
|
||||
public static unsafe void NormalBlend(this SoftwareBitmap softwareBitmap, Bgra32 tint)
|
||||
{
|
||||
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.ReadWrite))
|
||||
{
|
||||
using (IMemoryBufferReference reference = buffer.CreateReference())
|
||||
{
|
||||
reference.As<IMemoryBufferByteAccess>().GetBuffer(out byte* data, out uint length);
|
||||
|
||||
for (int i = 0; i < length; i += 4)
|
||||
Span<Bgra32> bytes = new(data, unchecked((int)length / sizeof(Bgra32)));
|
||||
foreach (ref Bgra32 pixel in bytes)
|
||||
{
|
||||
Bgra8* pixel = (Bgra8*)(data + i);
|
||||
byte baseAlpha = pixel->A;
|
||||
pixel->B = (byte)(((pixel->B * baseAlpha) + (tint.B * (0xFF - baseAlpha))) / 0xFF);
|
||||
pixel->G = (byte)(((pixel->G * baseAlpha) + (tint.G * (0xFF - baseAlpha))) / 0xFF);
|
||||
pixel->R = (byte)(((pixel->R * baseAlpha) + (tint.R * (0xFF - baseAlpha))) / 0xFF);
|
||||
pixel->A = 0xFF;
|
||||
byte baseAlpha = pixel.A;
|
||||
int opposite = 0xFF - baseAlpha;
|
||||
pixel.B = (byte)(((pixel.B * baseAlpha) + (tint.B * opposite)) / 0xFF);
|
||||
pixel.G = (byte)(((pixel.G * baseAlpha) + (tint.G * opposite)) / 0xFF);
|
||||
pixel.R = (byte)(((pixel.R * baseAlpha) + (tint.R * opposite)) / 0xFF);
|
||||
pixel.A = 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
@@ -10,31 +9,12 @@ namespace Snap.Hutao.Control.Panel;
|
||||
/// 纵横比控件
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal class AspectRatio : Microsoft.UI.Xaml.Controls.Control
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AspectRatio : Microsoft.UI.Xaml.Controls.Control
|
||||
{
|
||||
private const double Epsilon = 2.2204460492503131e-016;
|
||||
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<AspectRatio>.DependBoxed<double>(nameof(TargetWidth), BoxedValues.DoubleOne);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<AspectRatio>.DependBoxed<double>(nameof(TargetHeight), BoxedValues.DoubleOne);
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
/// </summary>
|
||||
public double TargetWidth
|
||||
{
|
||||
get => (double)GetValue(TargetWidthProperty);
|
||||
set => SetValue(TargetWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标高度
|
||||
/// </summary>
|
||||
public double TargetHeight
|
||||
{
|
||||
get => (double)GetValue(TargetHeightProperty);
|
||||
set => SetValue(TargetHeightProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
@@ -54,7 +34,7 @@ internal class AspectRatio : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
|
||||
// 更高
|
||||
else if (ratioAvailable < ratio)
|
||||
if (ratioAvailable < ratio)
|
||||
{
|
||||
double newHeight = availableSize.Width / ratio;
|
||||
return new Size(availableSize.Width, newHeight);
|
||||
|
||||
@@ -33,6 +33,11 @@ internal sealed partial class PanelSelector : SplitButton
|
||||
set => SetValue(CurrentProperty, value);
|
||||
}
|
||||
|
||||
private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
OnCurrentChanged((PanelSelector)obj, (string)args.NewValue);
|
||||
}
|
||||
|
||||
private static void OnCurrentChanged(PanelSelector sender, string current)
|
||||
{
|
||||
MenuFlyout menuFlyout = (MenuFlyout)sender.RootSplitButton.Flyout;
|
||||
@@ -43,21 +48,16 @@ internal sealed partial class PanelSelector : SplitButton
|
||||
sender.IconPresenter.Glyph = ((FontIcon)targetItem.Icon).Glyph;
|
||||
}
|
||||
|
||||
private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
OnCurrentChanged((PanelSelector)obj, (string)args.NewValue);
|
||||
}
|
||||
|
||||
private void OnRootControlLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// because the GroupName shares in global
|
||||
// we have to impl a control scoped GroupName.
|
||||
// we have to implement a control scoped GroupName.
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
MenuFlyout menuFlyout = (MenuFlyout)selector.RootSplitButton.Flyout;
|
||||
int hash = GetHashCode();
|
||||
foreach (RadioMenuFlyoutItem item in menuFlyout.Items.Cast<RadioMenuFlyoutItem>())
|
||||
{
|
||||
item.GroupName = $"PanelSelector{hash}Group";
|
||||
item.GroupName = $"{nameof(PanelSelector)}GroupOf@{hash}";
|
||||
}
|
||||
|
||||
OnCurrentChanged(selector, Current);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
@@ -25,7 +26,7 @@ internal class ScopedPage : Page
|
||||
/// <summary>
|
||||
/// 构造一个新的页面
|
||||
/// </summary>
|
||||
public ScopedPage()
|
||||
protected ScopedPage()
|
||||
{
|
||||
Unloaded += OnScopedPageUnloaded;
|
||||
currentScope = Ioc.Default.CreateScope();
|
||||
@@ -51,7 +52,7 @@ internal class ScopedPage : Page
|
||||
/// 应当在 InitializeComponent() 前调用
|
||||
/// </summary>
|
||||
/// <typeparam name="TViewModel">视图模型类型</typeparam>
|
||||
public void InitializeWith<TViewModel>()
|
||||
protected void InitializeWith<TViewModel>()
|
||||
where TViewModel : class, IViewModel
|
||||
{
|
||||
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
@@ -64,7 +65,7 @@ internal class ScopedPage : Page
|
||||
/// </summary>
|
||||
/// <param name="extra">额外内容</param>
|
||||
/// <returns>任务</returns>
|
||||
public async Task NotifyRecipentAsync(INavigationData extra)
|
||||
public async Task NotifyRecipientAsync(INavigationData extra)
|
||||
{
|
||||
if (extra.Data != null && DataContext is INavigationRecipient recipient)
|
||||
{
|
||||
@@ -100,7 +101,7 @@ internal class ScopedPage : Page
|
||||
{
|
||||
if (e.Parameter is INavigationData extra)
|
||||
{
|
||||
NotifyRecipentAsync(extra).SafeForget();
|
||||
NotifyRecipientAsync(extra).SafeForget();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Documents;
|
||||
@@ -57,10 +58,10 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
TextBlock text = (TextBlock)((DescriptionTextBlock)d).Content;
|
||||
ReadOnlySpan<char> description = (string)e.NewValue;
|
||||
|
||||
ApplyDescription(text, description);
|
||||
UpdateDescription(text, description);
|
||||
}
|
||||
|
||||
private static void ApplyDescription(TextBlock text, ReadOnlySpan<char> description)
|
||||
private static void UpdateDescription(TextBlock text, in ReadOnlySpan<char> description)
|
||||
{
|
||||
text.Inlines.Clear();
|
||||
|
||||
@@ -68,7 +69,7 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
for (int i = 0; i < description.Length;)
|
||||
{
|
||||
// newline
|
||||
if (description[i] == '\\' && description[i + 1] == 'n')
|
||||
if (description[i..].IndexOf(@"\n") is 0)
|
||||
{
|
||||
AppendText(text, description[last..i]);
|
||||
AppendLineBreak(text);
|
||||
@@ -77,10 +78,10 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
}
|
||||
|
||||
// color tag
|
||||
else if (description[i] == '<' && description[i + 1] == 'c')
|
||||
else if (description[i..].IndexOf("<c") is 0)
|
||||
{
|
||||
AppendText(text, description[last..i]);
|
||||
Rgba8 color = new(description.Slice(i + 8, 8));
|
||||
Rgba32 color = new(description.Slice(i + 8, 8).ToString());
|
||||
int length = description[(i + ColorTagLeftLength)..].IndexOf('<');
|
||||
AppendColorText(text, description.Slice(i + ColorTagLeftLength, length), color);
|
||||
|
||||
@@ -89,7 +90,7 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
}
|
||||
|
||||
// italic
|
||||
else if (description[i] == '<' && description[i + 1] == 'i')
|
||||
else if (description[i..].IndexOf("<i") is 0)
|
||||
{
|
||||
AppendText(text, description[last..i]);
|
||||
|
||||
@@ -111,12 +112,12 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendText(TextBlock text, ReadOnlySpan<char> slice)
|
||||
private static void AppendText(TextBlock text, in ReadOnlySpan<char> slice)
|
||||
{
|
||||
text.Inlines.Add(new Run { Text = slice.ToString() });
|
||||
}
|
||||
|
||||
private static void AppendColorText(TextBlock text, ReadOnlySpan<char> slice, Rgba8 color)
|
||||
private static void AppendColorText(TextBlock text, in ReadOnlySpan<char> slice, Rgba32 color)
|
||||
{
|
||||
Color targetColor;
|
||||
if (ThemeHelper.IsDarkMode(text.ActualTheme))
|
||||
@@ -127,7 +128,7 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
{
|
||||
HslColor hsl = color.ToHsl();
|
||||
hsl.L *= 0.3;
|
||||
targetColor = Rgba8.FromHsl(hsl);
|
||||
targetColor = Rgba32.FromHsl(hsl);
|
||||
}
|
||||
|
||||
text.Inlines.Add(new Run
|
||||
@@ -137,7 +138,7 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
});
|
||||
}
|
||||
|
||||
private static void AppendItalicText(TextBlock text, ReadOnlySpan<char> slice)
|
||||
private static void AppendItalicText(TextBlock text, in ReadOnlySpan<char> slice)
|
||||
{
|
||||
text.Inlines.Add(new Run
|
||||
{
|
||||
@@ -153,6 +154,7 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
|
||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
ApplyDescription((TextBlock)Content, Description);
|
||||
// Simply re-apply texts
|
||||
UpdateDescription((TextBlock)Content, Description);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
|
||||
<!-- Licensed under the MIT License. -->
|
||||
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<FontFamily x:Key="MiSans">ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
|
||||
<FontFamily x:Key="CascadiaMonoAndMiSans">ms-appx:///Resource/Font/CascadiaMono.ttf#Cascadia Mono, ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
|
||||
|
||||
<StaticResource x:Key="PivotHeaderItemFontFamily" ResourceKey="MiSans"/>
|
||||
<StaticResource x:Key="ContentControlThemeFontFamily" ResourceKey="MiSans"/>
|
||||
|
||||
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock"/>
|
||||
<Style x:Key="BaseTextBlockStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Theme;
|
||||
@@ -12,37 +11,6 @@ namespace Snap.Hutao.Control.Theme;
|
||||
[HighQuality]
|
||||
internal static class ThemeHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断主题是否相等
|
||||
/// </summary>
|
||||
/// <param name="applicationTheme">应用主题</param>
|
||||
/// <param name="elementTheme">元素主题</param>
|
||||
/// <returns>主题是否相等</returns>
|
||||
public static bool Equals(ApplicationTheme applicationTheme, ElementTheme elementTheme)
|
||||
{
|
||||
return (applicationTheme, elementTheme) switch
|
||||
{
|
||||
(ApplicationTheme.Light, ElementTheme.Light) => true,
|
||||
(ApplicationTheme.Dark, ElementTheme.Dark) => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 <see cref="ApplicationTheme"/> 转换到 <see cref="ElementTheme"/>
|
||||
/// </summary>
|
||||
/// <param name="applicationTheme">应用主题</param>
|
||||
/// <returns>元素主题</returns>
|
||||
public static ElementTheme ApplicationToElement(ApplicationTheme applicationTheme)
|
||||
{
|
||||
return applicationTheme switch
|
||||
{
|
||||
ApplicationTheme.Light => ElementTheme.Light,
|
||||
ApplicationTheme.Dark => ElementTheme.Dark,
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 <see cref="ElementTheme"/> 转换到 <see cref="ApplicationTheme"/>
|
||||
/// </summary>
|
||||
@@ -58,22 +26,6 @@ internal static class ThemeHelper
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 <see cref="ElementTheme"/> 转换到 <see cref="SystemBackdropTheme"/>
|
||||
/// </summary>
|
||||
/// <param name="elementTheme">元素主题</param>
|
||||
/// <returns>背景主题</returns>
|
||||
public static SystemBackdropTheme ElementToSystemBackdrop(ElementTheme elementTheme)
|
||||
{
|
||||
return elementTheme switch
|
||||
{
|
||||
ElementTheme.Default => SystemBackdropTheme.Default,
|
||||
ElementTheme.Light => SystemBackdropTheme.Light,
|
||||
ElementTheme.Dark => SystemBackdropTheme.Dark,
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否为暗黑模式
|
||||
/// </summary>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
[HighQuality]
|
||||
[SuppressMessage("", "SA1600")]
|
||||
internal abstract class DisposableObject : IDisposable
|
||||
{
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
Dispose(isDisposing: true);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool isDisposing)
|
||||
{
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
protected void VerifyNotDisposed()
|
||||
{
|
||||
if (IsDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(GetType().FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Abstraction;
|
||||
/// </summary>
|
||||
/// <typeparam name="TSelf">自身类型</typeparam>
|
||||
[HighQuality]
|
||||
internal interface ICloneable<TSelf>
|
||||
internal interface ICloneable<out TSelf>
|
||||
{
|
||||
/// <summary>
|
||||
/// 克隆
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Abstraction;
|
||||
/// <typeparam name="T1">元组的第一个类型</typeparam>
|
||||
/// <typeparam name="T2">元组的第二个类型</typeparam>
|
||||
[HighQuality]
|
||||
internal interface IDeconstructable<T1, T2>
|
||||
internal interface IDeconstruct<T1, T2>
|
||||
{
|
||||
/// <summary>
|
||||
/// 解构
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Annotation;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此方法为命令的调用方法
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
|
||||
internal sealed class CommandAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 指示此方法为命令的调用方法
|
||||
/// </summary>
|
||||
/// <param name="name">命令名称</param>
|
||||
public CommandAttribute(string name)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许并行执行
|
||||
/// </summary>
|
||||
public bool AllowConcurrentExecutions { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Annotation;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此类自动生成构造器
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class ConstructorGeneratedAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 指示此类自动生成构造器
|
||||
/// </summary>
|
||||
public ConstructorGeneratedAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在构造函数中插入 HttpClient
|
||||
/// </summary>
|
||||
public bool ResolveHttpClient { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Annotation;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
internal sealed class DependencyPropertyAttribute : Attribute
|
||||
{
|
||||
public DependencyPropertyAttribute(string name, Type type)
|
||||
{
|
||||
}
|
||||
|
||||
public DependencyPropertyAttribute(string name, Type type, object defaultValue)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// 为图像缓存提供抽象
|
||||
/// </summary>
|
||||
/// <typeparam name="T">缓存类型</typeparam>
|
||||
[HighQuality]
|
||||
internal interface IImageCache : ICastableService
|
||||
internal interface IImageCache : ICastService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the file path containing cached item for given Uri
|
||||
@@ -21,7 +22,13 @@ internal interface IImageCache : ICastableService
|
||||
/// Removed items based on uri list passed
|
||||
/// </summary>
|
||||
/// <param name="uriForCachedItems">Enumerable uri list</param>
|
||||
void Remove(IEnumerable<Uri> uriForCachedItems);
|
||||
void Remove(in ReadOnlySpan<Uri> uriForCachedItems);
|
||||
|
||||
/// <summary>
|
||||
/// Removed item based on uri passed
|
||||
/// </summary>
|
||||
/// <param name="uriForCachedItem">uri</param>
|
||||
void Remove(Uri uriForCachedItem);
|
||||
|
||||
/// <summary>
|
||||
/// Removes invalid cached files
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
|
||||
@@ -24,6 +23,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
{
|
||||
private const string CacheFolderName = nameof(ImageCache);
|
||||
|
||||
// TODO: use FrozenDictionary
|
||||
private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new()
|
||||
{
|
||||
[0] = TimeSpan.FromSeconds(4),
|
||||
@@ -36,6 +36,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
|
||||
private readonly ILogger logger;
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
|
||||
@@ -45,12 +46,13 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageCache"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="httpClientFactory">http客户端工厂</param>
|
||||
public ImageCache(ILogger<ImageCache> logger, IHttpClientFactory httpClientFactory)
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public ImageCache(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.logger = logger;
|
||||
httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
|
||||
logger = serviceProvider.GetRequiredService<ILogger<ImageCache>>();
|
||||
httpClient = serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ImageCache));
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -73,9 +75,15 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Remove(IEnumerable<Uri> uriForCachedItems)
|
||||
public void Remove(Uri uriForCachedItem)
|
||||
{
|
||||
if (uriForCachedItems == null || !uriForCachedItems.Any())
|
||||
Remove(new ReadOnlySpan<Uri>(uriForCachedItem));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Remove(in ReadOnlySpan<Uri> uriForCachedItems)
|
||||
{
|
||||
if (uriForCachedItems.Length <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -103,26 +111,28 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
string fileName = GetCacheFileName(uri);
|
||||
string filePath = Path.Combine(GetCacheFolder(), fileName);
|
||||
|
||||
if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0)
|
||||
if (File.Exists(filePath) && new FileInfo(filePath).Length != 0)
|
||||
{
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
try
|
||||
{
|
||||
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
|
||||
{
|
||||
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
|
||||
}
|
||||
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
concurrentTasks.TryRemove(fileName, out _);
|
||||
}
|
||||
finally
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
try
|
||||
{
|
||||
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
|
||||
{
|
||||
taskCompletionSource.TrySetResult();
|
||||
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
|
||||
}
|
||||
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
concurrentTasks.TryRemove(fileName, out _);
|
||||
}
|
||||
finally
|
||||
{
|
||||
taskCompletionSource.TrySetResult();
|
||||
}
|
||||
|
||||
return filePath;
|
||||
@@ -131,24 +141,10 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// <inheritdoc/>
|
||||
public string GetFilePathFromCategoryAndFileName(string category, string fileName)
|
||||
{
|
||||
Uri dummyUri = new(Web.HutaoEndpoints.StaticFile(category, fileName));
|
||||
Uri dummyUri = Web.HutaoEndpoints.StaticFile(category, fileName).ToUri();
|
||||
return Path.Combine(GetCacheFolder(), GetCacheFileName(dummyUri));
|
||||
}
|
||||
|
||||
private static void RemoveInternal(IEnumerable<string> filePaths)
|
||||
{
|
||||
foreach (string filePath in filePaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetCacheFileName(Uri uri)
|
||||
{
|
||||
string url = uri.ToString();
|
||||
@@ -164,14 +160,28 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
return treatNullFileAsInvalid;
|
||||
}
|
||||
|
||||
// Get extended properties.
|
||||
FileInfo fileInfo = new(file);
|
||||
return fileInfo.Length == 0;
|
||||
}
|
||||
|
||||
private void RemoveInternal(IEnumerable<string> filePaths)
|
||||
{
|
||||
foreach (string filePath in filePaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Remove Cache Image Failed:{File}", filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadFileAsync(Uri uri, string baseFile)
|
||||
{
|
||||
logger.LogInformation("Begin downloading for {uri}", uri);
|
||||
logger.LogInformation("Begin downloading for {Uri}", uri);
|
||||
|
||||
int retryCount = 0;
|
||||
while (retryCount < 6)
|
||||
@@ -189,21 +199,24 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (message.StatusCode == HttpStatusCode.NotFound)
|
||||
|
||||
switch (message.StatusCode)
|
||||
{
|
||||
// directly goto https://static.hut.ao
|
||||
retryCount += 3;
|
||||
}
|
||||
else if (message.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry {uri} after {delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
case HttpStatusCode.NotFound:
|
||||
// directly goto https://static.hut.ao
|
||||
retryCount += 3;
|
||||
break;
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry {Uri} after {Delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,13 +229,15 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
|
||||
private string GetCacheFolder()
|
||||
{
|
||||
if (cacheFolder == null)
|
||||
if (cacheFolder is not null)
|
||||
{
|
||||
baseFolder ??= ApplicationData.Current.LocalCacheFolder.Path;
|
||||
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
|
||||
cacheFolder = info.FullName;
|
||||
return cacheFolder!;
|
||||
}
|
||||
|
||||
baseFolder ??= serviceProvider.GetRequiredService<HutaoOptions>().LocalCache;
|
||||
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
|
||||
cacheFolder = info.FullName;
|
||||
|
||||
return cacheFolder!;
|
||||
}
|
||||
}
|
||||
@@ -58,11 +58,13 @@ internal sealed class CommandLineBuilder
|
||||
s.Append(WhiteSpace);
|
||||
s.Append(key);
|
||||
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
s.Append(WhiteSpace);
|
||||
s.Append(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
s.Append(WhiteSpace);
|
||||
s.Append(value);
|
||||
}
|
||||
|
||||
return s.ToString();
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Win32;
|
||||
using Snap.Hutao.Core.Json;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 核心环境参数
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class CoreEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// 米游社请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{HoyolabXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabOsUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBSOversea/{HoyolabOsXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社移动端请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/{HoyolabXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab 移动端请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabOsMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBSOversea/{HoyolabOsXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社 Rpc 版本
|
||||
/// </summary>
|
||||
public const string HoyolabXrpcVersion = "2.44.1";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab Rpc 版本
|
||||
/// </summary>
|
||||
public const string HoyolabOsXrpcVersion = "2.28.0";
|
||||
|
||||
/// <summary>
|
||||
/// 盐
|
||||
/// </summary>
|
||||
// https://github.com/UIGF-org/Hoyolab.Salt
|
||||
public static readonly ImmutableDictionary<SaltType, string> DynamicSecretSalts = new Dictionary<SaltType, string>()
|
||||
{
|
||||
[SaltType.K2] = "dZAwGk4e9aC0MXXItkwnHamjA1x30IYw",
|
||||
[SaltType.LK2] = "IEIZiKYaput2OCKQprNuGsog1NZc1FkS",
|
||||
[SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
|
||||
[SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
[SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
|
||||
|
||||
// This SALT is not reliable
|
||||
[SaltType.OSK2] = "6cqshh5dhw73bzxn20oexa9k516chk7s",
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
/// <summary>
|
||||
/// 默认的Json序列化选项
|
||||
/// </summary>
|
||||
public static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true,
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
|
||||
{
|
||||
Modifiers =
|
||||
{
|
||||
JsonTypeInfoResolvers.ResolveEnumType,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 当前版本
|
||||
/// </summary>
|
||||
public static readonly Version Version;
|
||||
|
||||
/// <summary>
|
||||
/// 标准UA
|
||||
/// </summary>
|
||||
public static readonly string CommonUA;
|
||||
|
||||
/// <summary>
|
||||
/// 数据文件夹
|
||||
/// </summary>
|
||||
public static readonly string DataFolder;
|
||||
|
||||
/// <summary>
|
||||
/// 包家族名称
|
||||
/// </summary>
|
||||
public static readonly string FamilyName;
|
||||
|
||||
/// <summary>
|
||||
/// 米游社设备Id
|
||||
/// </summary>
|
||||
public static readonly string HoyolabDeviceId;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃设备Id
|
||||
/// </summary>
|
||||
public static readonly string HutaoDeviceId;
|
||||
|
||||
/// <summary>
|
||||
/// 安装位置
|
||||
/// </summary>
|
||||
public static readonly string InstalledLocation;
|
||||
|
||||
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
|
||||
private const string MachineGuidValue = "MachineGuid";
|
||||
|
||||
static CoreEnvironment()
|
||||
{
|
||||
DataFolder = GetDatafolderPath();
|
||||
Version = Package.Current.Id.Version.ToVersion();
|
||||
FamilyName = Package.Current.Id.FamilyName;
|
||||
InstalledLocation = Package.Current.InstalledLocation.Path;
|
||||
CommonUA = $"Snap Hutao/{Version}";
|
||||
|
||||
// simply assign a random guid
|
||||
HoyolabDeviceId = Guid.NewGuid().ToString();
|
||||
HutaoDeviceId = GetUniqueUserID();
|
||||
}
|
||||
|
||||
private static string GetUniqueUserID()
|
||||
{
|
||||
string userName = Environment.UserName;
|
||||
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
|
||||
return Convert.ToMd5HexString($"{userName}{machineGuid}");
|
||||
}
|
||||
|
||||
private static string GetDatafolderPath()
|
||||
{
|
||||
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
|
||||
|
||||
if (string.IsNullOrEmpty(preferredPath))
|
||||
{
|
||||
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
#if RELEASE
|
||||
// 将测试版与正式版的文件目录分离
|
||||
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
|
||||
#else
|
||||
// 使得迁移能正常生成
|
||||
string folderName = "Hutao";
|
||||
#endif
|
||||
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
else
|
||||
{
|
||||
return preferredPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库当前项
|
||||
/// 简化对数据库中选中项的管理
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体的类型</typeparam>
|
||||
/// <typeparam name="TMessage">消息的类型</typeparam>
|
||||
[HighQuality]
|
||||
internal sealed class DbCurrent<TEntity, TMessage>
|
||||
where TEntity : class, ISelectable
|
||||
where TMessage : Message.ValueChangedMessage<TEntity>, new()
|
||||
{
|
||||
private readonly DbSet<TEntity> dbSet;
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
private TEntity? current;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的数据库当前项
|
||||
/// </summary>
|
||||
/// <param name="dbSet">数据集</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
public DbCurrent(DbSet<TEntity> dbSet, IMessenger messenger)
|
||||
{
|
||||
this.dbSet = dbSet;
|
||||
this.messenger = messenger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的项
|
||||
/// </summary>
|
||||
public TEntity? Current
|
||||
{
|
||||
get => current;
|
||||
set
|
||||
{
|
||||
// prevent useless sets
|
||||
if (current?.InnerId == value?.InnerId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// only update when not processing a deletion
|
||||
if (value != null)
|
||||
{
|
||||
if (current != null)
|
||||
{
|
||||
current.IsSelected = false;
|
||||
dbSet.UpdateAndSave(current);
|
||||
}
|
||||
}
|
||||
|
||||
TMessage message = new() { OldValue = current, NewValue = value };
|
||||
|
||||
current = value;
|
||||
|
||||
if (current != null)
|
||||
{
|
||||
current.IsSelected = true;
|
||||
dbSet.UpdateAndSave(current);
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,19 +13,6 @@ namespace Snap.Hutao.Core.Database;
|
||||
[HighQuality]
|
||||
internal static class DbSetExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取对应的数据库上下文
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <returns>对应的数据库上下文</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static DbContext Context<TEntity>(this DbSet<TEntity> dbSet)
|
||||
where TEntity : class
|
||||
{
|
||||
return dbSet.GetService<ICurrentDbContext>().Context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加并保存
|
||||
/// </summary>
|
||||
@@ -37,7 +24,7 @@ internal static class DbSetExtension
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Add(entity);
|
||||
return dbSet.Context().SaveChanges();
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -47,11 +34,11 @@ internal static class DbSetExtension
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static async ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Add(entity);
|
||||
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,7 +52,7 @@ internal static class DbSetExtension
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.AddRange(entities);
|
||||
return dbSet.Context().SaveChanges();
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -75,11 +62,11 @@ internal static class DbSetExtension
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entities">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static async ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
|
||||
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.AddRange(entities);
|
||||
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -93,7 +80,7 @@ internal static class DbSetExtension
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Remove(entity);
|
||||
return dbSet.Context().SaveChanges();
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -103,11 +90,11 @@ internal static class DbSetExtension
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static async ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Remove(entity);
|
||||
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -121,7 +108,7 @@ internal static class DbSetExtension
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Update(entity);
|
||||
return dbSet.Context().SaveChanges();
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -131,10 +118,37 @@ internal static class DbSetExtension
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static async ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Update(entity);
|
||||
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int SaveChangesAndClearChangeTracker<TEntity>(this DbSet<TEntity> dbSet)
|
||||
where TEntity : class
|
||||
{
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = dbContext.SaveChanges();
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet)
|
||||
where TEntity : class
|
||||
{
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static DbContext Context<TEntity>(this DbSet<TEntity> dbSet)
|
||||
where TEntity : class
|
||||
{
|
||||
return dbSet.GetService<ICurrentDbContext>().Context;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 可选择的项
|
||||
/// 若要使用 <see cref="DbCurrent{TEntity, TMessage}"/>
|
||||
/// 或 <see cref="ScopedDbCurrent{TEntity, TMessage}"/>
|
||||
/// 若要使用 <see cref="ScopedDbCurrent{TEntity, TMessage}"/>
|
||||
/// 必须实现该接口
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
@@ -16,8 +17,7 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
|
||||
where TEntity : class, ISelectable
|
||||
where TMessage : Message.ValueChangedMessage<TEntity>, new()
|
||||
{
|
||||
private readonly IServiceScopeFactory scopeFactory;
|
||||
private readonly Func<IServiceProvider, DbSet<TEntity>> dbSetSelector;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
private TEntity? current;
|
||||
@@ -25,14 +25,11 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
|
||||
/// <summary>
|
||||
/// 构造一个新的数据库当前项
|
||||
/// </summary>
|
||||
/// <param name="scopeFactory">范围工厂</param>
|
||||
/// <param name="dbSetSelector">数据集选择器</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
public ScopedDbCurrent(IServiceScopeFactory scopeFactory, Func<IServiceProvider, DbSet<TEntity>> dbSetSelector, IMessenger messenger)
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public ScopedDbCurrent(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.scopeFactory = scopeFactory;
|
||||
this.dbSetSelector = dbSetSelector;
|
||||
this.messenger = messenger;
|
||||
messenger = serviceProvider.GetRequiredService<IMessenger>();
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,9 +46,10 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
DbSet<TEntity> dbSet = dbSetSelector(scope.ServiceProvider);
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||
|
||||
// only update when not processing a deletion
|
||||
if (value != null)
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Database;
|
||||
/// 可枚举扩展
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class EnumerableExtension
|
||||
internal static class SelectableExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取选中的值或默认值
|
||||
@@ -1,11 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 可转换类型服务
|
||||
/// </summary>
|
||||
internal interface ICastableService
|
||||
internal interface ICastService
|
||||
{
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 有名称的对象
|
||||
@@ -1,15 +1,15 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 海外服/Hoyolab 可区分
|
||||
/// 海外服/HoYoLAB 可区分
|
||||
/// </summary>
|
||||
internal interface IOverseaSupport
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否为 海外服/Hoyolab
|
||||
/// 是否为 海外服/HoYoLAB
|
||||
/// </summary>
|
||||
public bool IsOversea { get; }
|
||||
}
|
||||
@@ -8,23 +8,23 @@ namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
/// 由源生成器生成注入代码
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class HttpClientAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的特性
|
||||
/// </summary>
|
||||
/// <param name="configration">配置</param>
|
||||
public HttpClientAttribute(HttpClientConfiguration configration)
|
||||
/// <param name="configuration">配置</param>
|
||||
public HttpClientAttribute(HttpClientConfiguration configuration)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的特性
|
||||
/// </summary>
|
||||
/// <param name="configration">配置</param>
|
||||
/// <param name="configuration">配置</param>
|
||||
/// <param name="interfaceType">实现的接口类型</param>
|
||||
public HttpClientAttribute(HttpClientConfiguration configration, Type interfaceType)
|
||||
public HttpClientAttribute(HttpClientConfiguration configuration, Type interfaceType)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ internal enum HttpClientConfiguration
|
||||
XRpc2,
|
||||
|
||||
/// <summary>
|
||||
/// 国际服Hoyolab请求配置
|
||||
/// Hoyolab app
|
||||
/// </summary>
|
||||
XRpc3,
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
/// 配置首选Http消息处理器特性
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class PrimaryHttpMessageHandlerAttribute : Attribute
|
||||
{
|
||||
/// <inheritdoc cref="System.Net.Http.HttpClientHandler.MaxConnectionsPerServer"/>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 对象扩展
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class CastableServiceExtension
|
||||
internal static class CastServiceExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// <see langword="as"/> 的链式调用扩展
|
||||
@@ -15,7 +17,7 @@ internal static class CastableServiceExtension
|
||||
/// <typeparam name="T">目标转换类型</typeparam>
|
||||
/// <param name="service">对象</param>
|
||||
/// <returns>转换类型后的对象</returns>
|
||||
public static T? As<T>(this ICastableService service)
|
||||
public static T? As<T>(this ICastService service)
|
||||
where T : class
|
||||
{
|
||||
return service as T;
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Service;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.Globalization;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 依赖注入
|
||||
/// </summary>
|
||||
internal static class DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化依赖注入
|
||||
/// </summary>
|
||||
/// <returns>服务提供器</returns>
|
||||
public static ServiceProvider Initialize()
|
||||
{
|
||||
ServiceProvider serviceProvider = new ServiceCollection()
|
||||
|
||||
// Microsoft extension
|
||||
.AddLogging(builder => builder.AddUnconditionalDebug())
|
||||
.AddMemoryCache()
|
||||
|
||||
// Hutao extensions
|
||||
.AddJsonOptions()
|
||||
.AddDatabase()
|
||||
.AddInjections()
|
||||
.AddHttpClients()
|
||||
|
||||
// Discrete services
|
||||
.AddSingleton<IMessenger, WeakReferenceMessenger>()
|
||||
.AddTransient(typeof(TaskCompletionSource))
|
||||
.AddTransient(typeof(TaskCompletionSource<>))
|
||||
|
||||
.BuildServiceProvider(true);
|
||||
|
||||
Ioc.Default.ConfigureServices(serviceProvider);
|
||||
|
||||
serviceProvider.InitializeCulture();
|
||||
|
||||
return serviceProvider;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void InitializeCulture(this IServiceProvider serviceProvider)
|
||||
{
|
||||
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
|
||||
appOptions.PreviousCulture = CultureInfo.CurrentCulture;
|
||||
|
||||
CultureInfo cultureInfo = appOptions.CurrentCulture;
|
||||
|
||||
CultureInfo.CurrentCulture = cultureInfo;
|
||||
CultureInfo.CurrentUICulture = cultureInfo;
|
||||
ApplicationLanguages.PrimaryLanguageOverride = cultureInfo.Name;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
@@ -17,6 +18,7 @@ internal static class EnumerableServiceExtension
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="name">名称</param>
|
||||
/// <returns>对应的服务</returns>
|
||||
[Obsolete("该方法会导致不必要的服务实例化")]
|
||||
public static TService Pick<TService>(this IEnumerable<TService> services, string name)
|
||||
where TService : INamedService
|
||||
{
|
||||
@@ -30,6 +32,7 @@ internal static class EnumerableServiceExtension
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="isOversea">是否为海外服/Hoyolab</param>
|
||||
/// <returns>对应的服务</returns>
|
||||
[Obsolete("该方法会导致不必要的服务实例化")]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static TService Pick<TService>(this IEnumerable<TService> services, bool isOversea)
|
||||
where TService : IOverseaSupport
|
||||
@@ -44,6 +47,7 @@ internal static class EnumerableServiceExtension
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="isOversea">是否为海外服/Hoyolab</param>
|
||||
/// <returns>对应的服务</returns>
|
||||
[Obsolete("该方法会导致不必要的服务实例化")]
|
||||
public static TService PickRequiredService<TService>(this IServiceProvider serviceProvider, bool isOversea)
|
||||
where TService : IOverseaSupport
|
||||
{
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Json;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
@@ -20,7 +20,7 @@ internal static class IocConfiguration
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
public static IServiceCollection AddJsonOptions(this IServiceCollection services)
|
||||
{
|
||||
return services.AddSingleton(CoreEnvironment.JsonOptions);
|
||||
return services.AddSingleton(JsonOptions.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -30,28 +30,34 @@ internal static class IocConfiguration
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
public static IServiceCollection AddDatabase(this IServiceCollection services)
|
||||
{
|
||||
string dbFile = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Userdata.db");
|
||||
return services
|
||||
.AddTransient(typeof(Database.ScopedDbCurrent<,>))
|
||||
.AddDbContext<AppDbContext>(AddDbContextCore);
|
||||
}
|
||||
|
||||
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
|
||||
{
|
||||
HutaoOptions hutaoOptions = provider.GetRequiredService<HutaoOptions>();
|
||||
string dbFile = System.IO.Path.Combine(hutaoOptions.DataFolder, "Userdata.db");
|
||||
string sqlConnectionString = $"Data Source={dbFile}";
|
||||
|
||||
// temporarily create a context
|
||||
// Temporarily create a context
|
||||
using (AppDbContext context = AppDbContext.Create(sqlConnectionString))
|
||||
{
|
||||
if (context.Database.GetPendingMigrations().Any())
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[Debug] Performing AppDbContext Migrations");
|
||||
System.Diagnostics.Debug.WriteLine("[Database] Performing AppDbContext Migrations");
|
||||
#endif
|
||||
context.Database.Migrate();
|
||||
}
|
||||
}
|
||||
|
||||
return services.AddDbContext<AppDbContext>(builder =>
|
||||
{
|
||||
builder
|
||||
builder
|
||||
#if DEBUG
|
||||
.EnableSensitiveDataLogging()
|
||||
.EnableSensitiveDataLogging()
|
||||
#endif
|
||||
.UseSqlite(sqlConnectionString);
|
||||
});
|
||||
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
|
||||
.UseSqlite(sqlConnectionString);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
@@ -15,6 +16,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
|
||||
/// <summary>
|
||||
/// 添加 <see cref="HttpClient"/>
|
||||
/// 此方法将会自动生成
|
||||
/// </summary>
|
||||
/// <param name="services">集合</param>
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
@@ -23,11 +25,14 @@ internal static partial class IocHttpClientConfiguration
|
||||
/// <summary>
|
||||
/// 默认配置
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void DefaultConfiguration(HttpClient client)
|
||||
private static void DefaultConfiguration(IServiceProvider serviceProvider, HttpClient client)
|
||||
{
|
||||
HutaoOptions hutaoOptions = serviceProvider.GetRequiredService<HutaoOptions>();
|
||||
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.CommonUA);
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -37,11 +42,11 @@ internal static partial class IocHttpClientConfiguration
|
||||
private static void XRpcConfiguration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(HoyolabOptions.UserAgent);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", HoyolabOptions.XrpcVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,26 +56,42 @@ internal static partial class IocHttpClientConfiguration
|
||||
private static void XRpc2Configuration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(HoyolabOptions.UserAgent);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-aigis", string.Empty);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_id", "bll8iq97cem8");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", HoyolabOptions.XrpcVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// 国际服 API 测试
|
||||
/// HoYoLAB app
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void XRpc3Configuration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabOsUA);
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(HoyolabOptions.UserAgentOversea);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", HoyolabOptions.XrpcVersionOversea);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-language", "zh-cn");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// HoYoLAB web
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void XRpc4Configuration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(HoyolabOptions.UserAgentOversea);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", "1.5.0");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "4");
|
||||
|
||||
@@ -12,6 +12,7 @@ internal static partial class ServiceCollectionExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 向容器注册服务
|
||||
/// 此方法将会自动生成
|
||||
/// </summary>
|
||||
/// <param name="services">容器</param>
|
||||
/// <returns>可继续操作的服务集合</returns>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供器扩展
|
||||
/// </summary>
|
||||
internal static class ServiceProviderExtension
|
||||
{
|
||||
/// <inheritdoc cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T CreateInstance<T>(this IServiceProvider serviceProvider, params object[] parameters)
|
||||
{
|
||||
return ActivatorUtilities.CreateInstance<T>(serviceProvider, parameters);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user