Compare commits

...

172 Commits
1.6.2 ... 1.6.6

Author SHA1 Message Date
DismissedLight
9aaeb327b6 Merge pull request #799 from DGP-Studio/l10n_main
New Crowdin updates
2023-07-13 19:22:44 +08:00
Masterain
75e771c75e New translations SH.resx (English) 2023-07-13 04:11:51 -07:00
Masterain
d4515936bd New translations SH.resx (Chinese Traditional) 2023-07-13 04:11:49 -07:00
Masterain
d9f2261129 New translations SH.resx (Korean) 2023-07-13 04:11:48 -07:00
Masterain
f0d4ea9a10 New translations SH.resx (Japanese) 2023-07-13 04:11:47 -07:00
Lightczx
bdcbba3237 update text 2023-07-13 19:09:58 +08:00
Masterain
a931a661cd New translations SH.resx (English) 2023-07-13 04:09:52 -07:00
Masterain
0fb2991085 New translations SH.resx (Chinese Traditional) 2023-07-13 04:09:50 -07:00
Masterain
cda04c6aa7 New translations SH.resx (Korean) 2023-07-13 04:09:49 -07:00
Masterain
b23659bf80 New translations SH.resx (Japanese) 2023-07-13 04:09:48 -07:00
DismissedLight
2a3f119fb0 update translation tips 2023-07-13 16:48:09 +08:00
DismissedLight
30e888ffb2 Add Gacha pull prediction 2023-07-13 15:39:10 +08:00
Lightczx
27b79659a4 Add animation to hutao cloud gacha statistics view 2023-07-12 22:18:37 +08:00
DismissedLight
4cef096cb5 refine code 2023-07-12 17:27:17 +08:00
DismissedLight
e065ba0964 Add GI 4.0 materials for Cultivation 2023-07-12 16:41:21 +08:00
DismissedLight
4c0d86fd13 Add InventoryItems localization support 2023-07-12 16:20:17 +08:00
DismissedLight
70ab81bb0f fix #796 2023-07-12 13:21:48 +08:00
DismissedLight
72be37834d Add compatibility with GI 4.0 2023-07-12 10:33:29 +08:00
DismissedLight
f1a2a828c8 Update HutaoStatisticsCard.xaml 2023-07-10 16:36:56 +08:00
DismissedLight
3eaaf0e4e2 Add Hutao GachaEvent Statistics 2023-07-10 15:13:41 +08:00
Lightczx
41d7441eb3 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-07-10 09:31:53 +08:00
Lightczx
173a1343c1 Hutao GachaEvent Statistics 1 2023-07-10 09:31:16 +08:00
DismissedLight
0fc0413612 remove unnecessary DS handler in UserClient 2023-07-10 09:30:32 +08:00
DismissedLight
1fc1f9b08d Update .gitignore 2023-07-10 09:25:23 +08:00
Lightczx
133a1a532e code style 2023-07-07 22:50:07 +08:00
Lightczx
f4fa08a939 fix developer hint visibility 2023-07-07 22:22:43 +08:00
Lightczx
5020621c46 add gachalogserviceexpiredat in hutaocloudservice 2023-07-07 11:13:47 +08:00
Lightczx
4d526bb363 DependencyProperty generation 2023-07-07 10:11:54 +08:00
DismissedLight
b37d5331a2 Merge pull request #784 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/Microsoft.NET.Test.Sdk-17.6.3
Bump Microsoft.NET.Test.Sdk from 17.6.2 to 17.6.3 in /src/Snap.Hutao
2023-07-03 15:41:55 +08:00
dependabot[bot]
451486e3fc Bump Microsoft.NET.Test.Sdk from 17.6.2 to 17.6.3 in /src/Snap.Hutao
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.6.2 to 17.6.3.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md)
- [Commits](https://github.com/microsoft/vstest/compare/v17.6.2...v17.6.3)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 07:18:55 +00:00
Lightczx
bbacb038cb fix CHS cannot load correct metadata 2023-06-27 20:43:22 +08:00
DismissedLight
7aba05d9c8 Merge pull request #781 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/Microsoft.EntityFrameworkCore.Tools-7.0.8
Bump Microsoft.EntityFrameworkCore.Tools from 7.0.7 to 7.0.8 in /src/Snap.Hutao
2023-06-26 17:12:58 +08:00
dependabot[bot]
fbcd43c0af Bump Microsoft.EntityFrameworkCore.Tools in /src/Snap.Hutao
Bumps [Microsoft.EntityFrameworkCore.Tools](https://github.com/dotnet/efcore) from 7.0.7 to 7.0.8.
- [Release notes](https://github.com/dotnet/efcore/releases)
- [Commits](https://github.com/dotnet/efcore/compare/v7.0.7...v7.0.8)

---
updated-dependencies:
- dependency-name: Microsoft.EntityFrameworkCore.Tools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 09:12:40 +00:00
DismissedLight
51314da123 Merge pull request #782 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/Microsoft.EntityFrameworkCore.Sqlite-7.0.8
Bump Microsoft.EntityFrameworkCore.Sqlite from 7.0.7 to 7.0.8 in /src/Snap.Hutao
2023-06-26 17:12:20 +08:00
DismissedLight
8ed2e8cc72 Merge pull request #783 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/StyleCop.Analyzers.Unstable-1.2.0.507
Bump StyleCop.Analyzers.Unstable from 1.2.0.435 to 1.2.0.507 in /src/Snap.Hutao
2023-06-26 17:12:03 +08:00
dependabot[bot]
7ab10d7824 Bump StyleCop.Analyzers.Unstable in /src/Snap.Hutao
Bumps [StyleCop.Analyzers.Unstable](https://github.com/DotNetAnalyzers/StyleCopAnalyzers) from 1.2.0.435 to 1.2.0.507.
- [Release notes](https://github.com/DotNetAnalyzers/StyleCopAnalyzers/releases)
- [Changelog](https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/KnownChanges.md)
- [Commits](https://github.com/DotNetAnalyzers/StyleCopAnalyzers/commits)

---
updated-dependencies:
- dependency-name: StyleCop.Analyzers.Unstable
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 08:05:16 +00:00
dependabot[bot]
ca5f84911b Bump Microsoft.EntityFrameworkCore.Sqlite in /src/Snap.Hutao
Bumps [Microsoft.EntityFrameworkCore.Sqlite](https://github.com/dotnet/efcore) from 7.0.7 to 7.0.8.
- [Release notes](https://github.com/dotnet/efcore/releases)
- [Commits](https://github.com/dotnet/efcore/compare/v7.0.7...v7.0.8)

---
updated-dependencies:
- dependency-name: Microsoft.EntityFrameworkCore.Sqlite
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 08:04:49 +00:00
DismissedLight
500efa60ed Merge pull request #779 from DGP-Studio/l10n_main
New Crowdin updates
2023-06-21 22:01:21 +08:00
Masterain
650e5d8a6e New translations SH.resx (Japanese) 2023-06-21 07:00:22 -07:00
Masterain
79a9128434 New translations SH.resx (English) 2023-06-21 07:00:21 -07:00
Masterain
c7c883bb11 New translations SH.resx (Chinese Traditional) 2023-06-21 07:00:20 -07:00
Masterain
3ee98810f1 New translations SH.resx (Korean) 2023-06-21 07:00:18 -07:00
Lightczx
f09c924a61 reduce NoModuleFound chance 2023-06-18 20:02:09 +08:00
DismissedLight
86d9a381c4 Merge pull request #772 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/Microsoft.NET.Test.Sdk-17.6.2
Bump Microsoft.NET.Test.Sdk from 17.5.0 to 17.6.2 in /src/Snap.Hutao
2023-06-16 16:30:38 +08:00
dependabot[bot]
b2d141af11 Bump Microsoft.NET.Test.Sdk from 17.5.0 to 17.6.2 in /src/Snap.Hutao
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.5.0 to 17.6.2.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md)
- [Commits](https://github.com/microsoft/vstest/compare/v17.5.0...v17.6.2)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-16 08:30:07 +00:00
DismissedLight
15865eb746 Merge pull request #771 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/MSTest.TestFramework-3.0.4
Bump MSTest.TestFramework from 3.0.2 to 3.0.4 in /src/Snap.Hutao
2023-06-16 16:29:58 +08:00
dependabot[bot]
242b9aa036 Bump MSTest.TestFramework from 3.0.2 to 3.0.4 in /src/Snap.Hutao
Bumps [MSTest.TestFramework](https://github.com/microsoft/testfx) from 3.0.2 to 3.0.4.
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.0.2...v3.0.4)

---
updated-dependencies:
- dependency-name: MSTest.TestFramework
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-16 08:29:51 +00:00
DismissedLight
7b81cc7a9c Merge pull request #770 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/Microsoft.CodeAnalysis.CSharp-4.6.0
Bump Microsoft.CodeAnalysis.CSharp from 4.5.0 to 4.6.0 in /src/Snap.Hutao
2023-06-16 16:29:41 +08:00
DismissedLight
2060f20707 Merge pull request #769 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/coverlet.collector-6.0.0
Bump coverlet.collector from 3.2.0 to 6.0.0 in /src/Snap.Hutao
2023-06-16 16:29:27 +08:00
DismissedLight
27a6c4f1fb Merge pull request #768 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/MSTest.TestAdapter-3.0.4
Bump MSTest.TestAdapter from 3.0.2 to 3.0.4 in /src/Snap.Hutao
2023-06-16 16:29:14 +08:00
dependabot[bot]
6427fe23ef Bump Microsoft.CodeAnalysis.CSharp in /src/Snap.Hutao
Bumps [Microsoft.CodeAnalysis.CSharp](https://github.com/dotnet/roslyn) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/dotnet/roslyn/releases)
- [Changelog](https://github.com/dotnet/roslyn/blob/main/docs/Breaking%20API%20Changes.md)
- [Commits](https://github.com/dotnet/roslyn/commits)

---
updated-dependencies:
- dependency-name: Microsoft.CodeAnalysis.CSharp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-16 08:22:18 +00:00
dependabot[bot]
6cf516f883 Bump coverlet.collector from 3.2.0 to 6.0.0 in /src/Snap.Hutao
Bumps [coverlet.collector](https://github.com/coverlet-coverage/coverlet) from 3.2.0 to 6.0.0.
- [Release notes](https://github.com/coverlet-coverage/coverlet/releases)
- [Commits](https://github.com/coverlet-coverage/coverlet/compare/v3.2.0...v6.0.0)

---
updated-dependencies:
- dependency-name: coverlet.collector
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-16 08:22:06 +00:00
dependabot[bot]
080674f648 Bump MSTest.TestAdapter from 3.0.2 to 3.0.4 in /src/Snap.Hutao
Bumps [MSTest.TestAdapter](https://github.com/microsoft/testfx) from 3.0.2 to 3.0.4.
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.0.2...v3.0.4)

---
updated-dependencies:
- dependency-name: MSTest.TestAdapter
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-16 08:22:01 +00:00
Lightczx
09834ada6b fix dependabot 2023-06-16 16:20:43 +08:00
Lightczx
efbc282a14 separate tests 2023-06-11 14:17:41 +08:00
Lightczx
e59a1c5444 1.6.5 rc1 2023-06-10 19:01:15 +08:00
Lightczx
356c157ab2 Support new language: Japanese 2023-06-08 22:14:37 +08:00
DismissedLight
be83ebfae1 Merge pull request #757 from DGP-Studio/l10n_main
New Crowdin updates
2023-06-08 21:56:03 +08:00
Lightczx
f94cbed525 improve #749 2023-06-08 21:53:04 +08:00
Lightczx
1199d2c737 fix #745 2023-06-08 21:44:55 +08:00
Lightczx
126e866a02 use metadata reliquary affix weight 2023-06-08 20:37:01 +08:00
Masterain
fbc0c02af0 New translations SH.resx (Japanese) 2023-06-08 02:28:01 -07:00
Masterain
e646e26908 New translations SH.resx (Japanese) 2023-06-08 01:46:30 -07:00
Masterain
4a8bf8182c New translations SH.resx (Chinese Traditional) 2023-06-08 01:46:28 -07:00
Masterain
86423a86c5 New translations SH.resx (Japanese) 2023-06-07 11:07:24 -07:00
Masterain
0766361441 New translations SH.resx (English) 2023-06-07 11:07:23 -07:00
Masterain
ded93dda75 New translations SH.resx (Chinese Traditional) 2023-06-07 11:07:22 -07:00
Masterain
f437c6615c New translations SH.resx (Korean) 2023-06-07 11:07:21 -07:00
Lightczx
e663c2f353 refine UI 2023-06-07 22:21:55 +08:00
Lightczx
90b396703a Update GameFpsUnlocker.cs 2023-06-07 15:54:08 +08:00
Lightczx
be4fa571cd fix unlock fps 2023-06-07 15:52:51 +08:00
DismissedLight
13e43a1c23 Merge pull request #751 from DGP-Studio/feat/metadata2
Migrating to metadata2
2023-06-05 11:19:58 +08:00
Lightczx
2d9165599b migrate to metadata2 2023-06-05 11:17:47 +08:00
Lightczx
5b35167908 optimize Win32 calls 2023-05-29 20:47:57 +08:00
Masterain
099c5d3f6d Update SECURITY.md 2023-05-29 00:28:50 -07:00
DismissedLight
5f15c05e3d Merge pull request #736 from DGP-Studio/l10n_main
New Crowdin updates
2023-05-27 14:33:54 +08:00
Masterain
de95fd6419 New translations SH.resx (English) 2023-05-26 23:17:16 -07:00
Masterain
bd0901a8ab New translations SH.resx (Chinese Traditional) 2023-05-26 23:17:15 -07:00
Masterain
84fa2dadcc New translations SH.resx (Korean) 2023-05-26 23:17:14 -07:00
DismissedLight
e204b65afc Merge pull request #738 from Masterain98/main
fix #737 Localization Update
2023-05-27 09:47:55 +08:00
Masterain98
9c71c4ffcc Automatic Config File Push 2023-05-27 00:13:32 +00:00
Masterain
62694be22b Localization update 2023-05-26 14:20:00 -07:00
Masterain
20f16ceb3e New translations SH.resx (English) 2023-05-26 06:04:04 -07:00
Masterain
a1e8f90710 New translations SH.resx (Chinese Traditional) 2023-05-26 06:04:03 -07:00
Masterain
0d9baa2cac New translations SH.resx (Korean) 2023-05-26 06:04:02 -07:00
DismissedLight
96b3e6d092 Merge pull request #734 from DGP-Studio/fix-unlock-fps
Fix unlock fps
2023-05-26 16:22:14 +08:00
Lightczx
4178f1b89b fix unlock fps 2023-05-26 16:20:12 +08:00
Lightczx
e44a20f202 Try to fix unlock fps 2023-05-25 20:50:37 +08:00
Masterain98
a12874d114 Automatic Config File Push 2023-05-25 00:12:55 +00:00
Lightczx
c7e3b18d62 fix 3.7 metadata crash 2023-05-23 13:13:41 +08:00
Lightczx
b607bbf819 Update .gitignore 2023-05-21 21:59:49 +08:00
Lightczx
5b16313b65 Delete desktop.ini 2023-05-21 21:58:40 +08:00
Lightczx
577aed0f80 Typo 2023-05-21 21:27:10 +08:00
Masterain98
39a3e6eb37 Automatic Config File Push 2023-05-20 04:14:35 +00:00
Lightczx
96f503d30c Introducing Elevation Badge 2023-05-17 20:58:40 +08:00
Lightczx
f104554661 remove installer 2023-05-16 22:45:09 +08:00
DismissedLight
451823ebf9 Merge pull request #723 from Xhichn/Dev
Improve HoYoLAB API request
2023-05-16 16:21:39 +08:00
Lightczx
7346c5eb4e typo 2023-05-16 16:20:41 +08:00
Lightczx
73f0e356c4 move InitializeCulture 2023-05-16 16:14:49 +08:00
Lightczx
168bed4b2c shell interop 2023-05-16 15:32:44 +08:00
Lightczx
d634eb6818 Unify Encoding to UIT-8 with BOM 2023-05-15 15:43:03 +08:00
DismissedLight
0cb9e59b8a Merge pull request #725 from DGP-Studio/feat/uigf_v2.3
fully support UIGF v2.3
2023-05-15 12:38:57 +08:00
Lightczx
ca10afa25a fully support UIGF v2.3 2023-05-15 12:37:46 +08:00
Lightczx
5fe38f305b refine game service 2023-05-14 22:43:50 +08:00
Lightczx
46f58730dc fix # 722 2023-05-14 22:20:45 +08:00
Xhichn
d144859d94 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-05-14 21:55:18 +08:00
Xhichn
40439aea8a Removed some unused endpoints 2023-05-14 21:52:46 +08:00
Lightczx
d54828ff55 fix #720 2023-05-14 21:15:22 +08:00
Xhichn
7e0c0fa5bf Change hoyolab api host 2023-05-14 17:49:03 +08:00
Xhichn
8312c7c88b Update hoyolab requests 2023-05-14 12:24:27 +08:00
Lightczx
456d87003f fix #709 2023-05-13 22:37:11 +08:00
Lightczx
7a63013dfd fix #719 2023-05-13 20:47:49 +08:00
Lightczx
15c3019576 fix jumplist launch 2023-05-11 22:13:06 +08:00
Lightczx
c15e948659 增加米奇妙妙工具 2023-05-11 18:17:10 +08:00
Lightczx
c5a4226662 fix scheme displayname 2023-05-10 22:12:02 +08:00
DismissedLight
0f9b906b7b Merge pull request #712 from DGP-Studio/l10n_main
New Crowdin updates
2023-05-10 21:26:39 +08:00
Masterain
f55dc038e2 New translations SH.resx (English) 2023-05-10 06:26:16 -07:00
Masterain
8540c1eebc New translations SH.resx (English) 2023-05-10 06:24:15 -07:00
Masterain
8f71994574 New translations SH.resx (Chinese Traditional) 2023-05-10 06:24:14 -07:00
Masterain
fe3e835211 New translations SH.resx (Korean) 2023-05-10 06:24:13 -07:00
Lightczx
6b5477ba44 1.16.3 release candidate 1 2023-05-10 21:19:26 +08:00
Lightczx
e147b8773f support dynamically change game fps 2023-05-10 19:52:22 +08:00
Masterain98
b386a35f07 Automatic Config File Push 2023-05-10 05:47:11 +00:00
Lightczx
42a19239e6 fix launch scheme detection 2023-05-10 13:44:09 +08:00
Masterain98
404cd9d705 Automatic Config File Push 2023-05-10 00:17:39 +00:00
Lightczx
33935dabc2 code style 2023-05-06 22:43:09 +08:00
Lightczx
5e04c51456 Address several db sync issues 2023-05-06 15:34:40 +08:00
Lightczx
50c5bb44ff auto dependency injection 2023-05-06 13:05:19 +08:00
Lightczx
60641c3f57 fix GetCharacterAsync 2023-05-01 15:47:58 +08:00
Lightczx
40753177cb Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-05-01 10:29:14 +08:00
Lightczx
ba2f2d5708 fix gacha fetch 2023-05-01 10:29:11 +08:00
Masterain
6f5a65c0e3 Update azure-pipelines.yml 2023-04-30 13:05:13 -07:00
Lightczx
20f353d9eb fix avatar property 2023-04-30 21:05:07 +08:00
DismissedLight
1a1a865b8d Merge pull request #700 from DGP-Studio/refactor/application-model
Refactor Application Model
2023-04-30 20:28:10 +08:00
Lightczx
7b3b1f1317 remove all ThreadHelper usage 2023-04-30 20:26:07 +08:00
Lightczx
0891c3f521 fixup all services 2023-04-29 20:54:17 +08:00
Lightczx
9657df36c2 Generate Enum Localization 2023-04-26 21:43:21 +08:00
Lightczx
9a54df4163 remove CoreEnvironment 2023-04-24 19:34:58 +08:00
Lightczx
8ac410fb4d miyoushe 2.49.1 2023-04-23 13:44:09 +08:00
Lightczx
538a076b69 increase boot speed 2023-04-20 19:11:40 +08:00
Lightczx
7db14a9c2a config utf-8 bom 2023-04-20 17:32:33 +08:00
Masterain
0e710f92d2 fix pipelines env 2023-04-20 02:24:08 -07:00
Masterain
fec2dc6c99 Use dedicated build sever 2023-04-20 02:19:24 -07:00
Lightczx
bdb40aca6a daily note card 2023-04-20 15:54:57 +08:00
Masterain
c90f147564 i10n refinement 2023-04-19 20:04:43 -07:00
DismissedLight
2b36b01145 Merge pull request #690 from Xhichn/SignIn
Feat: Daily check-in support for hoyolab user
2023-04-19 21:28:07 +08:00
Lightczx
d876f269fd typo 2023-04-19 21:27:35 +08:00
Lightczx
729f6717e1 Update GachaStatisticsCard.xaml 2023-04-19 19:06:27 +08:00
Xhichn
b5371a9656 Use seperated js interface for hoyolab check-in 2023-04-19 18:23:45 +08:00
Lightczx
04dae7ccd8 achievement card 2023-04-19 18:22:30 +08:00
Xhichn
a5fcfca609 Feat: Daily check-in support for hoyolab user 2023-04-19 13:18:01 +08:00
Lightczx
aee5271a2d more ref-like parameters 2023-04-18 21:57:13 +08:00
Lightczx
89fe93b3eb use ref-like keyword to accelerate runtime speed 2023-04-18 18:54:43 +08:00
Lightczx
dc38def97c fixup cookie creation key collision 2023-04-18 11:24:56 +08:00
Lightczx
2432b1ec5d fix #675 2023-04-17 12:41:06 +08:00
Lightczx
97c7671595 use hoyoverse instead of hoyolab to login 2023-04-17 12:00:48 +08:00
Lightczx
b8895d8250 update to was 1.3 and fix upload spiralabyss 2023-04-14 15:48:00 +08:00
Lightczx
cb882ab062 Update GachaLogViewModelSlim.cs 2023-04-12 22:36:01 +08:00
Lightczx
27c7875c26 refactor gachalog service 2023-04-12 22:34:49 +08:00
Lightczx
3cf505d9b2 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-04-11 18:42:47 +08:00
Lightczx
4407166005 temp state 2023-04-11 18:42:44 +08:00
DismissedLight
7b6e63a932 Merge pull request #671 from DGP-Studio/l10n_main
New Crowdin updates
2023-04-11 18:41:48 +08:00
Masterain
4a452c205e New translations SH.resx (English) 2023-04-11 01:37:14 -07:00
Masterain
4dd2ba3c6d New translations SH.resx (English) 2023-04-11 01:28:25 -07:00
Masterain
1e7155a902 New translations SH.resx (Chinese Traditional) 2023-04-11 01:28:24 -07:00
Masterain
129a1a7fa8 New translations SH.resx (Korean) 2023-04-11 01:28:24 -07:00
Masterain
f37f4fe37d Update SH.resx 2023-04-11 01:27:38 -07:00
DismissedLight
0df746e4c6 Merge pull request #670 from Masterain98/main
WIKI Pages Localization
2023-04-11 15:39:26 +08:00
Masterain
285f788015 fix typo 2023-04-11 00:37:54 -07:00
Masterain
d0525dd814 WIKI pages localization 2023-04-11 00:25:38 -07:00
Masterain
4372eb0ded =w= 2023-04-10 18:09:04 -07:00
Masterain98
15e6964340 Automatic Config File Push 2023-04-11 00:58:30 +00:00
675 changed files with 20861 additions and 12376 deletions

View File

@@ -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
View File

@@ -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/

View File

@@ -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
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)

View File

@@ -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

View File

@@ -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'

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -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.

File diff suppressed because one or more lines are too long

View File

@@ -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]
#### 命名样式 ####

View File

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

View File

@@ -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>

View File

@@ -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>

View File

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

View File

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

View File

@@ -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();
}
}

View File

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

View File

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

View File

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

View File

@@ -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("\",");
}
}
}
}

View File

@@ -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")]

View File

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

View File

@@ -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)
{

View File

@@ -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!));
}
}

View File

@@ -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());
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>

View File

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

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

View File

@@ -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');
}
}
}

View File

@@ -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)
{
}
}
}

View File

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

View File

@@ -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,
}
}

View File

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

View File

@@ -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(); }
}

View File

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

View File

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

View File

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

View File

@@ -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>

View File

@@ -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

View File

@@ -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"/>

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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()
{

View File

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

View File

@@ -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();
}
}

View File

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

View File

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

View File

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

View File

@@ -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
{

View File

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

View File

@@ -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>
/// 构造一个新的渐变锚点

View File

@@ -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();

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();
}
}

View File

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

View File

@@ -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}"/>

View File

@@ -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>

View File

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

View File

@@ -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>
/// 克隆

View File

@@ -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>
/// 解构

View File

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

View File

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

View File

@@ -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)
{
}
}

View File

@@ -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

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -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]

View File

@@ -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)

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Database;
/// 可枚举扩展
/// </summary>
[HighQuality]
internal static class EnumerableExtension
internal static class SelectableExtension
{
/// <summary>
/// 获取选中的值或默认值

View File

@@ -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
{
}

View File

@@ -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>
/// 有名称的对象

View File

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

View File

@@ -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)
{
}
}

View File

@@ -25,7 +25,7 @@ internal enum HttpClientConfiguration
XRpc2,
/// <summary>
/// 国际服Hoyolab请求配置
/// Hoyolab app
/// </summary>
XRpc3,
}

View File

@@ -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"/>

View File

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

View File

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

View File

@@ -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
{

View File

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

View File

@@ -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");

View File

@@ -12,6 +12,7 @@ internal static partial class ServiceCollectionExtension
{
/// <summary>
/// 向容器注册服务
/// 此方法将会自动生成
/// </summary>
/// <param name="services">容器</param>
/// <returns>可继续操作的服务集合</returns>

View File

@@ -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