Compare commits

..

255 Commits

Author SHA1 Message Date
Masterain
db0d6d4bc6 New translations sh.resx (English) 2023-12-20 23:00:38 -08:00
Masterain
c0c2dd535e New translations sh.resx (English) 2023-12-20 22:55:48 -08:00
Masterain
deb49fe26a New translations sh.resx (English) 2023-12-20 18:38:42 -08:00
Masterain
c5191b0a1c New translations sh.resx (Chinese Traditional) 2023-12-20 18:38:41 -08:00
Masterain
9efc24dadd New translations sh.resx (Russian) 2023-12-20 18:38:40 -08:00
Masterain
b68e696fc3 New translations sh.resx (Korean) 2023-12-20 18:38:39 -08:00
Masterain
a68c461197 New translations sh.resx (Japanese) 2023-12-20 18:38:38 -08:00
Masterain
4ec727ab6b New translations sh.resx (Indonesian) 2023-12-20 18:38:37 -08:00
Masterain
b407edb979 New translations sh.resx (Indonesian) 2023-12-19 18:24:19 -08:00
Masterain
0d83c4bb6d New translations sh.resx (Indonesian) 2023-12-18 18:09:58 -08:00
Masterain
14e0d59e56 New translations sh.resx (Indonesian) 2023-12-17 18:11:49 -08:00
DismissedLight
655d8a74af remove box 2023-12-17 19:30:49 +08:00
DismissedLight
4cf76ebbc4 fix type issue 2023-12-17 16:50:53 +08:00
DismissedLight
10b282a88a unify response behavior 2023-12-17 16:48:16 +08:00
DismissedLight
e60956c5c8 temp fix #1160 2023-12-16 19:17:09 +08:00
DismissedLight
aa4b544500 1.8.5 package 2023-12-16 15:06:25 +08:00
DismissedLight
3fc35cc3a5 Merge pull request #1183 from DGP-Studio/develop 2023-12-16 15:02:03 +08:00
Masterain
3233be6f25 New Crowdin updates (#1157) 2023-12-16 15:01:13 +08:00
DismissedLight
03f6778ec3 Merge pull request #1182 from DGP-Studio/develop 2023-12-16 15:00:09 +08:00
DismissedLight
0310afd77d correct game record requests 2023-12-15 21:34:04 +08:00
DismissedLight
e94f68d87b add console banner 2023-12-15 20:58:30 +08:00
DismissedLight
73dc103d11 Merge pull request #1179 from qhy040404/fix/pwsh 2023-12-15 19:27:59 +08:00
qhy040404
c947c759b8 fix pwsh argument 2023-12-15 19:26:17 +08:00
DismissedLight
4581bd79f9 fix gamepath reselect issue 2023-12-15 18:58:41 +08:00
Masterain
1b4fd995ce Merge pull request #1178 from DGP-Studio/Masterain98-patch-2
Update README.md
2023-12-15 01:54:21 -08:00
DismissedLight
72ebd1067b attempt to fix code 5001 2023-12-15 17:33:06 +08:00
Lightczx
e66819de55 fix #1060 2023-12-15 11:59:40 +08:00
Masterain
4d3bd6f438 Update README.md 2023-12-14 15:35:12 -08:00
DismissedLight
9f793670fe failed attempt: fight with device_fp 2023-12-14 22:47:17 +08:00
DismissedLight
414e0715a5 Merge pull request #1175 from DGP-Studio/feature/multi-gamepath 2023-12-14 15:25:03 +08:00
Lightczx
c8bea36540 code style 2023-12-14 15:22:20 +08:00
Lightczx
9e5b5e24d9 impl #1173 2023-12-14 15:15:29 +08:00
qhy040404
2968017663 Merge pull request #1176 from DGP-Studio/main
Sync action to develop
2023-12-14 15:06:55 +08:00
Lightczx
ac78df369c impl #526 2023-12-14 14:48:56 +08:00
qhy040404
2d7b3732e7 Update alpha.yml 2023-12-13 22:29:39 +08:00
DismissedLight
176baeb5c6 shadow improvement 2023-12-13 22:06:43 +08:00
DismissedLight
8fe1b48fd4 fix qrcode dialog 2023-12-13 20:22:29 +08:00
Lightczx
de46d5f9bf Update KnownReturnCode.cs 2023-12-13 17:24:44 +08:00
Lightczx
289b3219c9 fix some image blank 2023-12-13 13:32:42 +08:00
Masterain
af6a1208c6 Merge pull request #1172 from qhy040404/ci/action
Use GitHub Actions to generate Alpha
2023-12-12 19:47:02 -08:00
qhy040404
be6ad70ad6 misc 2023-12-12 22:02:54 +08:00
DismissedLight
d740632c27 code style 2023-12-12 21:40:18 +08:00
qhy040404
fd2e9980c7 fix 2023-12-12 19:24:52 +08:00
qhy040404
0b7b259d2f migrate to GitHub actions 2023-12-12 18:14:23 +08:00
Lightczx
c67dfea819 Update README.md 2023-12-12 17:13:47 +08:00
DismissedLight
b84cd98484 Merge pull request #1170 from DGP-Studio/develop 2023-12-12 17:08:56 +08:00
Lightczx
1c991aa120 user service refactor 2023-12-12 17:07:28 +08:00
Masterain
d92da924ff Update README.md 2023-12-11 23:58:02 -08:00
qhy040404
57f7ac944c fix signing 2023-12-12 15:31:37 +08:00
qhy040404
5ad4c0a5be Update appveyor signing 2023-12-12 15:23:59 +08:00
DismissedLight
6768d7b8f4 Merge pull request #1169 from qhy040404/feat/qr 2023-12-12 14:25:32 +08:00
Lightczx
ad20b83b4e minor fix 2023-12-12 14:25:05 +08:00
Lightczx
f4547b60de completing 2023-12-12 14:22:15 +08:00
qhy040404
dcf1b01566 Update azure-pipelines.yml 2023-12-12 10:42:08 +08:00
DismissedLight
217586fece Device needs rework 2023-12-11 22:55:47 +08:00
qhy040404
2fb6cd3441 code style (?) 2023-12-11 18:47:41 +08:00
qhy040404
a8d4dc84a1 impl #870 2023-12-11 14:31:34 +08:00
Masterain
c39a198c57 Update azure-pipelines.yml for Azure Pipelines 2023-12-10 21:07:17 -08:00
Masterain
9c106b24fb Update azure-pipelines.yml for Azure Pipelines 2023-12-10 21:02:19 -08:00
Masterain
73c62a63ea Update azure-pipelines.yml for Azure Pipelines 2023-12-10 20:44:05 -08:00
Lightczx
e8762d658f add console window 2023-12-11 11:44:03 +08:00
DismissedLight
824fba89a8 minor code style 2023-12-10 21:37:27 +08:00
DismissedLight
ecd17de279 text hint improvement 2023-12-09 18:26:02 +08:00
DismissedLight
46c683c570 Merge pull request #1164 from qhy040404/ci/cake 2023-12-09 15:04:50 +08:00
qhy040404
364d0ed0be Update azure-pipelines.yml 2023-12-09 14:28:43 +08:00
qhy040404
46a90be95c prepare release 2023-12-09 12:06:21 +08:00
qhy040404
d7863ab5e0 code style 2023-12-09 11:12:25 +08:00
qhy040404
e7e6467ea8 release version 2023-12-09 11:09:19 +08:00
qhy040404
5fa6bc03c8 更新 appveyor.yml 2023-12-09 10:17:03 +08:00
qhy040404
4d5115e11b add appveyor 2023-12-09 09:51:30 +08:00
DismissedLight
bc9b167c5b disable image lazy loading 2023-12-08 22:39:51 +08:00
qhy040404
f5c3e55b3e sign outside 2023-12-08 17:44:26 +08:00
qhy040404
abb559d35f prepare for veyor 2023-12-08 17:02:40 +08:00
qhy040404
f4d23d6174 better abstract 2023-12-08 16:39:36 +08:00
Lightczx
3cc17375f0 Settings folder size display 2023-12-08 16:19:38 +08:00
qhy040404
50c0fa2061 abstract 2023-12-08 11:41:13 +08:00
Lightczx
859492e580 infobarservice refactor 2023-12-08 11:16:55 +08:00
qhy040404
1ab1d182af Update PublishDistribution.yml 2023-12-08 10:30:07 +08:00
qhy040404
bde5122060 change target repo and avoid abs path 2023-12-08 10:01:34 +08:00
qhy040404
e090d7e04b wrong repo 2023-12-08 09:50:56 +08:00
qhy040404
7ef2834b42 Hello cake 2023-12-08 09:50:56 +08:00
qhy040404
c68fbe9d96 Auto sync appxmanifest 2023-12-08 09:50:48 +08:00
DismissedLight
f16769969e fix #1163 2023-12-07 23:00:40 +08:00
DismissedLight
24b66de082 code style 2023-12-07 22:55:32 +08:00
DismissedLight
a5bfdbaa4b impl #1016 2023-12-07 22:38:21 +08:00
Lightczx
559ae250bd cultivation wip [skip ci] 2023-12-07 17:25:48 +08:00
Lightczx
bd344e50ab minor game process optimization 2023-12-07 10:57:16 +08:00
Lightczx
e5d67a80dd move files 2023-12-07 10:37:15 +08:00
Lightczx
8d8ec8b05d code style 2023-12-07 09:34:05 +08:00
Lightczx
82ccd59451 sign in website url 2023-12-07 09:16:00 +08:00
Lightczx
3ba3ba55cb adjust propertynames 2023-12-06 17:16:23 +08:00
Lightczx
e6e6e22b9c apply hutao api changes 2023-12-06 16:39:48 +08:00
Lightczx
97842559d7 apply api changes 2023-12-06 15:45:30 +08:00
Lightczx
a97aa26d79 refactor options 2023-12-06 15:41:13 +08:00
DismissedLight
8d7373c6cb Merge pull request #1161 from qhy040404/feat/ip 2023-12-06 13:53:34 +08:00
Lightczx
045c127fb2 code style 2023-12-06 13:53:16 +08:00
qhy040404
4dd6765e35 show ip 2023-12-06 12:47:30 +08:00
Masterain
d374519685 Update issue templates 2023-12-05 01:56:35 -08:00
Lightczx
9993082b86 1.8.4 package 2023-12-05 14:30:04 +08:00
Lightczx
f835178b10 always extract sdk for bili 2023-12-05 14:09:41 +08:00
DismissedLight
0b8b10e2f7 Merge pull request #1156 from DGP-Studio/develop 2023-12-05 13:52:10 +08:00
Masterain
97130156f0 New Crowdin updates (#1142) 2023-12-05 13:38:33 +08:00
Lightczx
06def00e2c ignore non exist files 2023-12-05 13:33:25 +08:00
Lightczx
2679a68785 pre-check filesystem permission before convert 2023-12-05 10:58:41 +08:00
DismissedLight
57e8bc8bdf resource file sharding for client converting 2023-12-04 23:14:41 +08:00
DismissedLight
b6ad96c0cb fix stream copy work totalbytes 2023-12-04 21:35:43 +08:00
Lightczx
b6769b63e3 Add progress report for HttpShardCopyWorker 2023-12-04 17:10:42 +08:00
Lightczx
1c67da607c Add http sharding 2023-12-04 16:09:28 +08:00
DismissedLight
70cb4b8285 Merge pull request #1153 from qhy040404/fix/pwsh
fix #1145 (Part 2)
2023-12-03 23:34:21 +08:00
DismissedLight
56fc4dcbcd code style 2023-12-03 23:34:00 +08:00
DismissedLight
626418680a Merge pull request #1154 from qhy040404/fix/link
fix #1151
2023-12-03 23:16:26 +08:00
qhy040404
8f3e166773 fix #1151 2023-12-03 22:26:55 +08:00
qhy040404
013639f57e fix #1145 (Part 2) 2023-12-03 22:02:48 +08:00
DismissedLight
21ad6be9da metadata service refactor 2023-12-03 21:20:49 +08:00
DismissedLight
ed556c8539 remove wiki avatar page resize blinking 2023-12-03 17:37:03 +08:00
DismissedLight
42e11ec94a add tests 2023-12-02 21:22:26 +08:00
qhy040404
6cdfac6e09 fix #1147 2023-12-02 15:51:15 +08:00
Masterain
b0c23e329b Update README.md 2023-12-02 15:51:15 +08:00
DismissedLight
3ca46d3836 Merge pull request #1149 from qhy040404/fix/culture 2023-12-02 15:39:34 +08:00
qhy040404
5df2d7210b fix #1147 2023-12-02 15:26:52 +08:00
Lightczx
7dece546a5 game running tracker async scope 2023-12-01 16:25:21 +08:00
Lightczx
2b851a5459 activation optimization 2023-12-01 14:58:10 +08:00
Lightczx
98a711da70 fix #1145 2023-12-01 10:50:07 +08:00
Lightczx
176c26df51 sign-in endpoints 2023-12-01 10:43:06 +08:00
Lightczx
af87891a5f add server strings 2023-11-29 17:13:06 +08:00
DismissedLight
b523a2bb2a 1.8.3 package 2023-11-28 22:05:02 +08:00
Masterain
b3f8093c09 Update README.md 2023-11-28 02:39:41 -08:00
Lightczx
b4eb97a6ea fix incorrect resource name 2023-11-28 17:00:26 +08:00
Lightczx
77217d2fc3 fix #1138 2023-11-28 15:44:37 +08:00
Lightczx
8e386c1457 1.8.2 hotfix package 2023-11-28 13:32:09 +08:00
DismissedLight
330154c9ec Merge pull request #1136 from DGP-Studio/develop 2023-11-28 13:18:42 +08:00
Lightczx
f97385089a fix #1134 2023-11-28 13:16:16 +08:00
DismissedLight
8982fcd427 1.8.1 package 2023-11-27 19:57:27 +08:00
DismissedLight
aaa2e09dde Merge pull request #1131 from DGP-Studio/develop 2023-11-27 19:34:55 +08:00
Masterain
58fdcc7804 Update MGMT-publish.yml 2023-11-27 03:23:39 -08:00
DismissedLight
ca352a5262 revoke some static resources version 2023-11-27 19:22:57 +08:00
DismissedLight
0f9e34ffb8 Merge pull request #1118 from DGP-Studio/l10n_develop 2023-11-27 19:17:34 +08:00
DismissedLight
76a60e30fc #1123 repeat notification regression 2023-11-27 19:15:55 +08:00
Lightczx
693566812b add new server localization keys 2023-11-27 17:30:14 +08:00
DismissedLight
8fbe2ee831 Merge pull request #1129 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-0fa0185f23 2023-11-27 17:28:50 +08:00
dependabot[bot]
639a86d11b Bump the packages group in /src/Snap.Hutao with 1 update
Bumps the packages group in /src/Snap.Hutao with 1 update: [Microsoft.CodeAnalysis.CSharp](https://github.com/dotnet/roslyn).

- [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
  dependency-group: packages
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-27 07:12:58 +00:00
Masterain
3f21a5d8fc New translations sh.resx (English) 2023-11-26 08:34:00 -08:00
Masterain
8a62327e30 New translations sh.resx (Chinese Traditional) 2023-11-26 08:33:58 -08:00
Masterain
69b9a0c9dc New translations sh.resx (Korean) 2023-11-26 08:33:57 -08:00
Masterain
af56f3ac30 New translations sh.resx (Japanese) 2023-11-26 08:33:56 -08:00
DismissedLight
ffc899efe5 fix #1123 2023-11-26 20:15:22 +08:00
DismissedLight
80b958d9d3 impl #1121 2023-11-26 19:59:02 +08:00
DismissedLight
b60f7c215e minor ui fix 2023-11-26 19:14:49 +08:00
DismissedLight
7128dddb57 remove skill bottom padding 2023-11-26 16:42:00 +08:00
DismissedLight
9960b6f6b1 fix #1126 2023-11-26 15:55:37 +08:00
DismissedLight
7099ca43b6 impl #1127 2023-11-26 15:04:41 +08:00
DismissedLight
c9627e19e7 fix #1124 2023-11-26 14:41:53 +08:00
DismissedLight
6999103aaa refine wiki avatar page ui 2023-11-26 14:35:33 +08:00
Masterain
e54eef3aa7 New translations sh.resx (English) 2023-11-25 08:02:24 -08:00
DismissedLight
f2ef6ff8ec fix #1099 again 2023-11-25 10:10:16 +08:00
Masterain
35ddaaeb35 New translations sh.resx (English) 2023-11-24 07:47:53 -08:00
Masterain
c833655231 New translations sh.resx (Chinese Traditional) 2023-11-24 07:47:52 -08:00
Masterain
8724784803 New translations sh.resx (Korean) 2023-11-24 07:47:51 -08:00
Masterain
d1ceac0fe9 New translations sh.resx (Japanese) 2023-11-24 07:47:49 -08:00
Lightczx
43415ebd0d IPinnable 2023-11-24 17:31:10 +08:00
Lightczx
a8b697e782 fix #1119 2023-11-24 13:41:58 +08:00
Lightczx
602b31c52d restrict combobox size 2023-11-24 11:51:46 +08:00
Masterain
3b43389049 New translations sh.resx (Chinese Traditional) 2023-11-23 07:49:51 -08:00
Masterain
3a1fc839eb New translations sh.resx (Japanese) 2023-11-23 07:49:50 -08:00
DismissedLight
e0f967341e TCG decrypt [skip ci] 2023-11-23 23:34:31 +08:00
Lightczx
74ac738236 bump static resource version 2023-11-23 13:08:32 +08:00
Masterain
0189c4824b New translations sh.resx (English) 2023-11-22 07:43:58 -08:00
Masterain
1a43841833 New translations sh.resx (Chinese Traditional) 2023-11-22 07:43:56 -08:00
Masterain
29ad939498 New translations sh.resx (Korean) 2023-11-22 07:43:55 -08:00
Masterain
60c8e948e8 New translations sh.resx (Japanese) 2023-11-22 07:43:54 -08:00
DismissedLight
a17f9ca543 optimize discord activity 2023-11-22 22:28:06 +08:00
Lightczx
ee86f12168 impl #1082 2023-11-22 16:59:01 +08:00
DismissedLight
9fb79a9fbd Merge pull request #1117 from qhy040404/develop 2023-11-22 13:51:56 +08:00
Lightczx
467eb13c87 file nesting 2023-11-22 13:48:05 +08:00
Lightczx
40b055d310 lock on get winrt obj disposed 2023-11-22 13:43:35 +08:00
qhy040404
126d19e96a Apply suggestions 2023-11-22 13:39:07 +08:00
Lightczx
485ac1e682 trim unused xml 2023-11-22 13:28:40 +08:00
qhy040404
1117e322a6 Support the coexistence of Snap Hutao and Snap Hutao Dev 2023-11-22 13:10:05 +08:00
Masterain
85b40b71e5 Update PublishDistribution.yml 2023-11-21 17:33:53 -08:00
Masterain
66bbf4335a Update PublishDistribution.yml 2023-11-21 17:30:52 -08:00
DismissedLight
cb20bd0df4 Merge pull request #1116 from qhy040404/develop 2023-11-22 09:05:47 +08:00
qhy040404
cb9c9a0af2 Sync tab when navigate from GachaLog's HutaoCloudView to SpiralAbyssRecordPage 2023-11-21 22:14:27 +08:00
Lightczx
28ea71cf77 Add CollectionsMarshal Test 2023-11-21 16:17:53 +08:00
Lightczx
94744c4bc1 1.8.0 package 2023-11-21 15:03:43 +08:00
DismissedLight
58af255485 Merge pull request #1114 from DGP-Studio/develop 2023-11-21 14:10:59 +08:00
Masterain
c45e6560b8 New Crowdin updates (#1091)
Co-authored-by: DismissedLight <1686188646@qq.com>
2023-11-21 13:58:15 +08:00
Lightczx
29d7d36b66 Frozen Collections 2023-11-21 13:53:32 +08:00
Lightczx
2bbf6f192e fix conditional ci 2023-11-21 10:57:23 +08:00
Lightczx
cc8565428b replace hutao endpoints 2023-11-21 10:47:33 +08:00
DismissedLight
d84e0bf199 fix typo 2023-11-20 21:56:53 +08:00
DismissedLight
9d8c981f5a add profile picture basic support 2023-11-20 21:34:37 +08:00
Lightczx
1f412b289a move localization annotations 2023-11-20 17:25:03 +08:00
Lightczx
07b3e98ac0 add more specifically http message 2023-11-20 16:58:48 +08:00
DismissedLight
8a9f4ced34 Merge pull request #1111 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-dfb7e59321 2023-11-20 15:23:19 +08:00
dependabot[bot]
0a0aa3e404 Bump the packages group in /src/Snap.Hutao with 1 update
Bumps the packages group in /src/Snap.Hutao with 1 update: [Microsoft.Extensions.DependencyInjection](https://github.com/dotnet/runtime).

- [Release notes](https://github.com/dotnet/runtime/releases)
- [Commits](https://github.com/dotnet/runtime/compare/v7.0.0...v8.0.0)

---
updated-dependencies:
- dependency-name: Microsoft.Extensions.DependencyInjection
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: packages
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 07:19:21 +00:00
Lightczx
84861c5e6b update nuget package version 2023-11-20 14:44:57 +08:00
DismissedLight
a23e6f55d4 add basic discord support 2023-11-19 20:51:58 +08:00
DismissedLight
c1cf5aa499 prevent binding failure when achievement page disposed 2023-11-19 16:59:26 +08:00
DismissedLight
91b6e6793c simplify the startup statement 2023-11-19 16:48:43 +08:00
DismissedLight
ac1ef44801 fix startup user role change caused dead lock 2023-11-19 16:16:10 +08:00
DismissedLight
e7b23c8a5a minor bug fixes 2023-11-19 13:16:22 +08:00
DismissedLight
68a3851595 refine #1096 2023-11-18 22:47:09 +08:00
DismissedLight
7861ebf998 #impl 1096 2023-11-18 22:41:25 +08:00
DismissedLight
0cc75ab245 fix #1106 2023-11-17 22:29:17 +08:00
Lightczx
ef09c87835 fix service register type for geetest 2023-11-17 13:06:49 +08:00
Lightczx
b13ec8c12a win32 dialog 2023-11-16 16:58:41 +08:00
Lightczx
29e14111f0 fix azure pipeline 2023-11-16 14:40:52 +08:00
Lightczx
0dd66288cd fix azure pipeline 2023-11-16 14:30:59 +08:00
DismissedLight
bb835ee20e Merge pull request #1103 from DGP-Studio/dotnet8 2023-11-16 14:24:57 +08:00
Lightczx
a1037e1827 fix #1099 2023-11-16 14:13:32 +08:00
Lightczx
ba6ee9bb85 replace format extension 2023-11-16 13:15:37 +08:00
Lightczx
f1c50dc6c3 UnsafeAccessor 2023-11-16 10:47:07 +08:00
Lightczx
8e841b1295 remove using 2023-11-16 09:24:35 +08:00
DismissedLight
ea2000b03d Update DataSignAlgorithm.cs 2023-11-16 00:08:43 +08:00
DismissedLight
d186dd6f98 DynamicSecret -> DataSign 2023-11-16 00:08:01 +08:00
DismissedLight
5c46c92a1a rename constants 2023-11-15 22:36:57 +08:00
DismissedLight
98f2dd13d1 bulk refactor 2023-11-15 22:31:56 +08:00
Lightczx
3e0493be31 collection experssion 2 2023-11-15 17:26:18 +08:00
Lightczx
7025074170 fix collection style 2023-11-15 16:36:15 +08:00
Lightczx
0eade5e81a collection experssion 2023-11-15 16:19:43 +08:00
Lightczx
163d076ed5 Revert "fix shadow crash on announcement page"
This reverts commit cf5b377532.
2023-11-15 16:08:10 +08:00
Lightczx
fa5aac3366 Revert "remove AttachedCardShadow to fix startup crash"
This reverts commit afbc91abe8.
2023-11-15 16:07:57 +08:00
Lightczx
f98a40d080 fix NRE in DbCurrent and change UnsafeDateTimeOffset ns 2023-11-15 13:16:38 +08:00
Lightczx
cf5b377532 fix shadow crash on announcement page 2023-11-15 12:59:32 +08:00
Lightczx
afbc91abe8 remove AttachedCardShadow to fix startup crash 2023-11-15 12:53:20 +08:00
Lightczx
be572fa327 change csproj to net8 2023-11-15 12:52:52 +08:00
Lightczx
fe594864f2 use CoCreateInstance to activate ShellLinkW 2023-11-14 09:24:37 +08:00
DismissedLight
d8b6315e7e capture dispatched exceptions 2023-11-13 23:13:25 +08:00
Lightczx
d4e9c2aa9c localenames 2023-11-13 15:37:06 +08:00
DismissedLight
800c948c38 Merge pull request #1094 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-c2c108d172 2023-11-13 15:35:59 +08:00
dependabot[bot]
32589bc994 Bump the packages group in /src/Snap.Hutao with 1 update
Bumps the packages group in /src/Snap.Hutao with 1 update: [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest).

- [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.7.2...v17.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 07:08:36 +00:00
DismissedLight
afc91b1a29 remove unused using 2023-11-12 13:06:41 +08:00
DismissedLight
40db69825f Merge pull request #1090 from Tangweirui2021/main 2023-11-12 12:49:41 +08:00
DismissedLight
407dd7bac8 code style 2023-11-12 12:48:46 +08:00
Daniel
4708cd5629 fix user log in/register/unregister/reset password info bar localization 2023-11-11 18:42:14 +08:00
DismissedLight
2b93d31788 fix #1087 and improve some null checks 2023-11-11 12:55:47 +08:00
Lightczx
39831b0ae1 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-11-10 16:08:54 +08:00
Lightczx
51c9936018 1.7.17 package 2023-11-10 16:08:35 +08:00
Masterain
3466a98ffb Create MGMT-publish.yml 2023-11-09 23:48:29 -08:00
DismissedLight
a064cc10ee Merge pull request #1085 from DGP-Studio/develop 2023-11-10 15:41:33 +08:00
Masterain
3479a19164 New Crowdin updates (#1078)
Co-authored-by: DismissedLight <1686188646@qq.com>
2023-11-10 15:39:00 +08:00
Lightczx
d4549581c1 fix exception capture 2023-11-10 15:33:11 +08:00
Lightczx
f97bc344d0 fix announcement time incorrectness 2023-11-10 14:20:44 +08:00
Lightczx
26d23fec7f impl #830 in previous commit 2023-11-10 11:39:53 +08:00
Lightczx
7442f7f1ec support UIGF v2.4-preview 2023-11-10 11:37:45 +08:00
DismissedLight
3eb2556393 update gacha info endpoints 2023-11-09 23:39:51 +08:00
DismissedLight
cfff6f39fc adjust server timezone 2023-11-09 23:15:08 +08:00
Lightczx
3005031b39 add basic timezone support for gachaitem 2023-11-09 17:18:56 +08:00
Lightczx
71363f4d8d fix #1081 2023-11-09 15:23:51 +08:00
Lightczx
e833578334 rename jsbridge 2023-11-09 11:51:56 +08:00
Lightczx
d529b3cea6 fix #1079 2023-11-09 11:38:30 +08:00
Lightczx
1c0ce62885 fix gacha item corner radius 2023-11-08 13:34:55 +08:00
DismissedLight
acdf2baa9a improve webviewer & hotkey 2023-11-07 21:02:25 +08:00
DismissedLight
ec007d5d81 add fp to jsbridge 2023-11-07 19:08:48 +08:00
Lightczx
5e734ac689 impl #961 2023-11-07 15:37:53 +08:00
553 changed files with 18416 additions and 10376 deletions

12
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"cake.tool": {
"version": "4.0.0",
"commands": [
"dotnet-cake"
]
}
}
}

View File

@@ -85,7 +85,9 @@ body:
id: what-happened
attributes:
label: 发生了什么?
description: 详细的描述问题发生前后的行为,以便我们解决问题。**如果你的问题涉及程序崩溃,你应当检查 Windows 事件查看器,并将相关的 `.Net 错误`详情附上**
description: |
详细的描述问题发生前后的行为,以便我们解决问题。**如果你的问题涉及程序崩溃,你应当检查 Windows 事件查看器,并将相关的 `.Net 错误`详情附上**
如果你无法找到该日志,请下载并运行[此PowerShell脚本](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/dump_log_script/dump_log_zh.ps1),它将输出错误日志
validations:
required: true

View File

@@ -85,7 +85,9 @@ body:
id: what-happened
attributes:
label: What Happened?
description: Describe your issue in detail to help us identify the issue. **If your issue is about program crash, you should check Windows Event Viewer, and attach associated `.Net Error` details here**
description: |
Describe your issue in detail to help us identify the issue. **If your issue is about program crash, you should check Windows Event Viewer, and attach associated `.Net Error` details here**If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID.
If you cannot find it, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/dump_log_script/dump_log_en.ps1), it will dump the error log.
validations:
required: true

61
.github/ISSUE_TEMPLATE/MGMT-publish.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Publish Process
description: FOR ADMIN USE ONLY. WILL CAUSE A BAN IF NO PERMISSION.
title: "[Publish]: Version 1.9.98"
labels: ["Publish"]
assignees:
- Lightczx
body:
- type: textarea
id: main-body
attributes:
label: Publish Process
value: |
## 创建版本
- [ ] 同步一次 [Crowdin](https://crowdin.com/project/snap-hutao) 翻译
- [ ] 发布 RC 版本Optional
- [ ] 合并入主分支
- [ ] 整理更新内容,等待翻译
- [ ] 打包
- [ ] 提交微软商店
- [ ] 包含更新日志
- [ ] 在 [Snap.Hutao.Docs@next-patch](https://github.com/DGP-Studio/Snap.Hutao.Docs/tree/next-patch) 分支更新文档并直接开 PR
- [ ] 更新日志
- [ ] 功能文档更新
## 发布版本 [半自动]
- [ ] 在 GitHub 个人设置中更新 [Publish-Automate Beta PAT](https://github.com/settings/tokens?type=beta),有效期需小于预计发版需要天数
- [ ] 将更新的 PAT 更新至 Publish-Automate 库的 [Actions Secrets](https://github.com/DGP-Studio/Publish-Automate/settings/secrets/actions) 中
***
- [ ] 运行 [Auto Publish Action](https://github.com/DGP-Studio/Publish-Automate/actions/workflows/auto-publish.yml)
- [ ] 在 https://store.rg-adguard.net/ 下载新版本安装包
- [ ] Store URL: https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52
- [ ] 命名格式为 `Snap.Hutao x.x.x.msix`
- [ ] Merge 文档 PR
- [ ] 发布 Release
- [ ] 更新日志格式(以 1.6.2 版本为例)
```jsx
## Update log
https://hut.ao/en/statements/update-log.html#_1-6-2
## 更新日志
[此处从文档复制]
## What's Changed
**Full Changelog**: https://github.com/DGP-Studio/Snap.Hutao/compare/1.6.0...1.6.2
```
- [ ] 通知用户
- type: checkboxes
id: checklist-final
attributes:
label: Final Check
description: Understand what you are doing
options:
- label: I understand that I will get banned from repository if I don't have permission to use this template
required: true

View File

@@ -12,40 +12,9 @@ jobs:
runs-on: ubuntu-latest
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Checkout Repo
uses: actions/checkout@v3
# Download Assets
- name: Download Release
timeout-minutes: 5
uses: robinraju/release-downloader@v1.7
with:
repository: "DGP-Studio/Snap.Hutao"
latest: true
fileName: "*.msix"
out-file-path: ./release-download
# Upload to Drive
- name: Upload Drive
timeout-minutes: 15
env:
RCCONF: ${{ secrets.RCCONF }}
run: |
curl https://rclone.org/install.sh | sudo bash
mkdir -p ~/.config/rclone/
cat << EOF > ~/.config/rclone/rclone.conf
$RCCONF
EOF
rclone copy ./release-download/* dgpODCN:/releases/
# Purge Patch System Cache
- name: Purge Patch
env:
PATCH_HOSTS: ${{ secrets.PATCH_HOSTS }}
PURGE_TOKEN: ${{ secrets.PURGE_TOKEN }}
PURGE_URL: ${{ secrets.PURGE_URL }}
run: |
sudo echo "$PATCH_HOSTS" | sudo tee -a /etc/hosts
curl --header "Authorization: token $PURGE_TOKEN" $PURGE_URL
curl -X PATCH $PURGE_URL

63
.github/workflows/alpha.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: Snap Hutao Alpha
on:
workflow_dispatch:
push:
branches:
- main
- develop
paths-ignore:
- '.gitattributes'
- '.github/**'
- '.gitignore'
- '.gitmodules'
- '**.md'
- 'LICENSE'
jobs:
build:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Setup .NET
uses: actions/setup-dotnet@v4.0.0
with:
dotnet-version: 8.0
- name: Cake
id: cake
shell: pwsh
run: dotnet tool restore && dotnet cake
env:
VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }}
- name: Sign Msix
shell: pwsh
run: |
[System.Convert]::FromBase64String("${{ secrets.CERTIFICATE }}") | Set-Content -AsByteStream temp.pfx
signtool.exe sign /debug /v /a /fd SHA256 /f temp.pfx /p ${{ secrets.PW }} ${{ github.workspace }}\src\output\Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
- name: Upload signed msix
if: success()
uses: actions/upload-artifact@v3
with:
name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}
path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
- name: Add summary
if: success()
shell: pwsh
run: |
$summary = "
> [!WARNING]
> 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
> [!TIP]
> 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
> [!IMPORTANT]
> 请安装 [Snap.Hutao.CI.cer](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate/Snap.Hutao.CI.cer) 以安装测试版安装包
"
echo $summary >> $Env:GITHUB_STEP_SUMMARY

8
.gitignore vendored
View File

@@ -1,7 +1,6 @@
desktop.ini
*.csproj.user
*.pubxml
*.DotSettings.user
.vs/
@@ -16,8 +15,7 @@ src/Snap.Hutao/Snap.Hutao/Snap.Hutao_TemporaryKey.pfx
src/Snap.Hutao/Snap.Hutao.Win32/bin/
src/Snap.Hutao/Snap.Hutao.Win32/obj/
src/Snap.Hutao/Snap.Hutao.SourceGeneration/bin/
src/Snap.Hutao/Snap.Hutao.SourceGeneration/obj/
src/Snap.Hutao/Snap.Hutao.Test/bin/
src/Snap.Hutao/Snap.Hutao.Test/obj/
src/Snap.Hutao/Snap.Hutao.Test/obj/
src/Snap.Hutao/Snap.Hutao/Properties/PublishProfiles/FolderProfile.pubxml.user

View File

@@ -1,4 +1,5 @@
![](res/HutaoRepoBanner2.png)
![Banner2-large](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/742129f4-f903-4d16-bf1e-3cbfad873ee4)
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新 功能相结合,它提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
@@ -44,7 +45,29 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
* [Snap.Hutao.Server](https://github.com/DGP-Studio/Snap.Hutao.Server)
* [Snap.Metadata](https://github.com/DGP-Studio/Snap.Metadata)
## 赞助商 / Sponsorship
Snap Hutao is currently using sponsored software from the following service providers.
| [![](https://www.netlify.com/v3/img/components/netlify-light.svg)](https://www.netlify.com/) | [![](https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg)](https://crowdin.com/) | [![](https://gitlab.cn/images/icons/logos/logo-121-75.svg)](https://gitlab.cn/) |
|:----------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|
| [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/73ae8b90-f3c7-4033-b2b7-f4126331ce66)](https://about.signpath.io) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/49aed8ee-9f19-4a8a-998c-7b93ee286d65)](https://1password.com/) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/ad121220-d2d3-4f49-b215-b6d063dc229d)](https://about.signpath.io) |
- Netlify provides document and home page hosting service for Snap Hutao
- Crowdin provides its SaaS platform to help Snap Hutao's localization
- Jihu GitLab (极狐) provides Git repository and CI/CD SaaS service for Snap Hutao in China
- Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
- 1Password provides Snap Hutao development team with their amazing password management software
- DigitalOcean provides reliable cloud database for Snap Hutao database backup
## 开发 / Development
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)
[![Star History Chart](https://api.star-history.com/svg?repos=DGP-Studio/Snap.Hutao&type=Date)](https://star-history.com/#DGP-Studio/Snap.Hutao&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=DGP-Studio/Snap.Hutao&type=Date)](https://star-history.com/#DGP-Studio/Snap.Hutao&Date)

20
appveyor.yml Normal file
View File

@@ -0,0 +1,20 @@
version: 1.0.{build}
branches:
only:
- "release"
build_cloud: HUTAO-SERVER
image: Visual Studio 2022
clone_depth: 3
clone_folder: D:\appveyor\project\Snap.Hutao.Project
install:
- pwsh: dotnet tool restore
build_script:
- pwsh: dotnet cake
artifacts:
- path: src/output/*.msix
type: file
deploy:
- provider: Webhook
url: https://app.signpath.io/API/v1/7a941fa3-64d8-4c45-bd03-92a02bcd4964/Integrations/AppVeyor?ProjectSlug=Snap.Hutao&SigningPolicySlug=release-signing&ArtifactConfigurationSlug=msix
authorization:
secure: j8srQ5/UYWhI+jlm3Vo3D3QfXoRyQ9hOn3ynJGtwusKui4+uDi4gykdUFYCITZxK+C/fOCAZNJ+YaKSm/OaiXw==

View File

@@ -7,30 +7,32 @@
# 5. Connect the GitHub in project settings
# 6. Run
trigger:
branches:
include:
- main
- develop
paths:
exclude:
- README.md
- azure-pipelines.yml
- .github/ISSUE_TEMPLATE/*.yml
- .github/workflows/*.yml
- src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
pr:
branches:
include:
- main
paths:
exclude:
- README.md
- azure-pipelines.yml
- .github/ISSUE_TEMPLATE/*.yml
- .github/workflows/*.yml
- src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
trigger: none
pr: none
# trigger:
# branches:
# include:
# - main
# - develop
# paths:
# exclude:
# - README.md
# - azure-pipelines.yml
# - .github/ISSUE_TEMPLATE/*.yml
# - .github/workflows/*.yml
# - src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
# pr:
# branches:
# include:
# - main
# paths:
# exclude:
# - README.md
# - azure-pipelines.yml
# - .github/ISSUE_TEMPLATE/*.yml
# - .github/workflows/*.yml
# - src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
pool:
name: Default
@@ -42,150 +44,76 @@ variables:
project: $(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj'
buildPlatform: 'x64'
buildConfiguration: 'Release'
build_date: $[ format('{0:yyyy}.{0:M}.{0:d}', pipeline.startTime) ]
steps:
- task: GetRevision@1
displayName: get Pipelines revision number
inputs:
VariableName: 'rev_number'
- task: UseDotNet@2
displayName: Install dotNet
inputs:
packageType: 'sdk'
version: '7.x'
version: '8.x'
includePreviewVersions: true
- task: NuGetToolInstaller@1
name: 'NuGetToolInstaller'
displayName: 'NuGet Installer'
- task: NuGetCommand@2
displayName: NuGet restore
inputs:
command: 'restore'
restoreSolution: '$(solution)'
feedsToUse: 'config'
nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.Config'
- task: MsixPackaging@1
displayName: Build binary package
inputs:
outputPath: '$(Build.ArtifactStagingDirectory)/'
solution: '$(solution)'
clean: false
generateBundle: false
buildConfiguration: 'Release'
buildPlatform: 'x64'
updateAppVersion: false
appPackageDistributionMode: 'SideloadOnly'
msbuildLocationMethod: 'location'
msbuildLocation: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Msbuild\Current\Bin\MSBuild.exe'
- task: MagicChunks@2
inputs:
sourcePath: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\AppxManifest.xml'
fileType: 'Xml'
targetPathType: 'source'
transformationType: 'json'
transformations: |
{
"Package/Identity/@Name": "7f0db578-026f-4e0b-a75b-d5d06bb0a74c",
"Package/Identity/@Publisher": "CN=DGP Studio CI",
"Package/Identity/@Version": "$(build_date).$(rev_number)",
"Package/Properties/DisplayName": "胡桃 Alpha",
"Package/Properties/PublisherDisplayName":"DGP Studio CI",
"Package/Applications/Application/uap:VisualElements/@DisplayName": "胡桃 Alpha"
}
- task: CmdLine@2
displayName: Create resources folder
displayName: dotnet cake
inputs:
script: |
mkdir Assets
mkdir Resource
workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64'
- task: CopyFiles@2
displayName: Copy Assets Folder
inputs:
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Assets'
Contents: '**'
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\Assets'
- task: CopyFiles@2
displayName: Copy Resource Folder
inputs:
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Resource'
Contents: '**'
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\Resource'
- task: CmdLine@2
displayName: Build MSIX
inputs:
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'
script: dotnet tool restore && dotnet cake
- task: MsixSigning@1
name: signMsix
displayName: Sign MSIX package
inputs:
package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix'
certificate: 'DGP_Studio_CI.pfx'
passwordVariable: 'pw'
condition: succeeded()
#- task: PublishPipelineArtifact@1
# displayName: 'Upload Output'
# inputs:
# targetPath: '$(Build.ArtifactStagingDirectory)/'
# artifact: 'Output'
# publishLocation: 'pipeline'
- task: DownloadSecureFile@1
name: cerFile
displayName: Download Root CA
inputs:
secureFile: 'Snap.Hutao.CI.cer'
- task: GitHubRelease@1
- task: PublishPipelineArtifact@1
inputs:
gitHubConnection: 'github.com_Masterain'
repositoryName: 'DGP-Studio/Snap.Hutao'
action: 'create'
target: '$(Build.SourceVersion)'
tagSource: 'userSpecifiedTag'
tag: '$(build_date).$(rev_number)'
title: '$(build_date).$(rev_number)'
releaseNotesSource: 'inline'
releaseNotesInline: |
## 普通用户请勿下载
该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
targetPath: '$(Build.ArtifactStagingDirectory)'
artifact: 'Snap.Hutao.Alpha-$(version).msix'
publishLocation: 'pipeline'
普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
assets: |
$(Build.ArtifactStagingDirectory)/*
$(cerFile.secureFilePath)
isPreRelease: true
changeLogCompareToRelease: 'lastFullRelease'
changeLogType: 'commitBased'
#- task: GitHubRelease@1
# inputs:
# gitHubConnection: 'github.com_Masterain'
# repositoryName: 'DGP-Automation/Hutao-Auto-Release'
# action: 'create'
# target: '$(Build.SourceVersion)'
# tagSource: 'userSpecifiedTag'
# tag: '$(version)'
# title: '$(version)'
# releaseNotesSource: 'inline'
# releaseNotesInline: |
# ## 普通用户请勿下载
# 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
#
# 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
#
# assets: |
# $(Build.ArtifactStagingDirectory)/*
# $(cerFile.secureFilePath)
# isPreRelease: true
# changeLogCompareToRelease: 'lastFullRelease'
# changeLogType: 'commitBased'
- 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/'
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix downloadDGPCN:/releases/Alpha/'
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/'
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix downloadDGPCN:/releases/PR/'
configPath: 'C:\agent\_work\_tasks\rclone.conf'

199
build.cake Normal file
View File

@@ -0,0 +1,199 @@
#tool "nuget:?package=nuget.commandline&version=6.5.0"
#addin nuget:?package=Cake.Http&version=3.0.2
var target = Argument("target", "Build");
var configuration = Argument("configuration", "Release");
// Pre-define
var version = "version";
var repoDir = "repoDir";
var outputPath = "outputPath";
string solution
{
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao.sln");
}
string project
{
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Snap.Hutao.csproj");
}
string binPath
{
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "bin", "x64", "Release", "net8.0-windows10.0.22621.0", "win-x64");
}
string manifest
{
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Package.appxmanifest");
}
if (AzurePipelines.IsRunningOnAzurePipelines)
{
repoDir = AzurePipelines.Environment.Build.SourcesDirectory.FullPath;
outputPath = AzurePipelines.Environment.Build.ArtifactStagingDirectory.FullPath;
var versionAuth = HasEnvironmentVariable("VERSION_API_TOKEN") ? EnvironmentVariable("VERSION_API_TOKEN") : throw new Exception("Cannot find VERSION_API_TOKEN");
version = HttpGet(
"https://internal.snapgenshin.cn/BuildIntergration/RequestNewVersion",
new HttpSettings
{
Headers = new Dictionary<string, string>
{
{ "Authorization", versionAuth }
}
}
);
Information($"Version: {version}");
AzurePipelines.Commands.SetVariable("version", version);
}
else if (GitHubActions.IsRunningOnGitHubActions)
{
repoDir = GitHubActions.Environment.Workflow.Workspace.FullPath;
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
var versionAuth = HasEnvironmentVariable("VERSION_API_TOKEN") ? EnvironmentVariable("VERSION_API_TOKEN") : throw new Exception("Cannot find VERSION_API_TOKEN");
version = HttpGet(
"https://internal.snapgenshin.cn/BuildIntergration/RequestNewVersion",
new HttpSettings
{
Headers = new Dictionary<string, string>
{
{ "Authorization", versionAuth }
}
}
);
Information($"Version: {version}");
GitHubActions.Commands.SetOutputParameter("version", version);
}
else if (AppVeyor.IsRunningOnAppVeyor)
{
repoDir = AppVeyor.Environment.Build.Folder;
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
version = XmlPeek(manifest, "appx:Package/appx:Identity/@Version", new XmlPeekSettings
{
Namespaces = new Dictionary<string, string> { { "appx", "http://schemas.microsoft.com/appx/manifest/foundation/windows10" } }
})[..^2];
Information($"Version: {version}");
}
Task("Build")
.IsDependentOn("Build binary package")
.IsDependentOn("Copy files")
.IsDependentOn("Build MSIX");
Task("NuGet Restore")
.Does(() =>
{
Information("Restoring packages...");
var nugetConfig = System.IO.Path.Combine(repoDir, "NuGet.Config");
DotNetRestore(project, new DotNetRestoreSettings
{
Verbosity = DotNetVerbosity.Detailed,
Interactive = false,
ConfigFile = nugetConfig
});
});
Task("Generate AppxManifest")
.Does(() =>
{
Information("Generating AppxManifest...");
var content = System.IO.File.ReadAllText(manifest);
if (AzurePipelines.IsRunningOnAzurePipelines || GitHubActions.IsRunningOnGitHubActions)
{
Information("Using CI configuraion");
content = content
.Replace("Snap Hutao", "Snap Hutao Alpha")
.Replace("胡桃", "胡桃 Alpha")
.Replace("DGP Studio", "DGP Studio CI");
content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"7f0db578-026f-4e0b-a75b-d5d06bb0a74c\"");
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=DGP Studio CI\"");
content = System.Text.RegularExpressions.Regex.Replace(content, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\"");
}
else if (AppVeyor.IsRunningOnAppVeyor)
{
Information("Using Release configuration");
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US\"");
}
System.IO.File.WriteAllText(manifest, content);
Information("Generated.");
});
Task("Build binary package")
.IsDependentOn("NuGet Restore")
.IsDependentOn("Generate AppxManifest")
.Does(() =>
{
Information("Building binary package...");
var settings = new DotNetBuildSettings
{
Configuration = configuration
};
settings.MSBuildSettings = new DotNetMSBuildSettings
{
ArgumentCustomization = args => args.Append("/p:Platform=x64")
.Append("/p:UapAppxPackageBuildMode=SideloadOnly")
.Append("/p:AppxPackageSigningEnabled=false")
.Append("/p:AppxBundle=Never")
.Append("/p:AppxPackageOutput=" + outputPath)
};
DotNetBuild(project, settings);
});
Task("Copy files")
.IsDependentOn("Build binary package")
.Does(() =>
{
Information("Copying assets...");
CopyDirectory(
System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Assets"),
System.IO.Path.Combine(binPath, "Assets")
);
Information("Copying resource...");
CopyDirectory(
System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Resource"),
System.IO.Path.Combine(binPath, "Resource")
);
});
Task("Build MSIX")
.IsDependentOn("Build binary package")
.IsDependentOn("Copy files")
.Does(() =>
{
var arguments = "arguments";
if (AzurePipelines.IsRunningOnAzurePipelines || GitHubActions.IsRunningOnGitHubActions)
{
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix");
}
else if (AppVeyor.IsRunningOnAppVeyor)
{
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao-{version}.msix");
}
var p = StartProcess(
"makeappx.exe",
new ProcessSettings
{
Arguments = arguments
}
);
if (p != 0)
{
throw new InvalidOperationException("Build failed with exit code " + p);
}
});
RunTarget(target);

View File

@@ -108,7 +108,9 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_diagnostic.SA1629.severity = none
dotnet_diagnostic.SA1642.severity = none
dotnet_diagnostic.IDE0005.severity = warning
dotnet_diagnostic.IDE0060.severity = none
dotnet_diagnostic.IDE0290.severity = none
# SA1208: System using directives should be placed before other using directives
dotnet_diagnostic.SA1208.severity = none
@@ -322,6 +324,8 @@ dotnet_diagnostic.CA2227.severity = suggestion
# CA2251: 使用 “string.Equals”
dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = true:suggestion
dotnet_diagnostic.SA1010.severity = none
[*.vb]
#### 命名样式 ####

View File

@@ -1,147 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
namespace Snap.Hutao.SourceGeneration.Automation;
[Generator(LanguageNames.CSharp)]
internal sealed class AttributeGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(GenerateAllAttributes);
}
public static void GenerateAllAttributes(IncrementalGeneratorPostInitializationContext context)
{
string coreAnnotations = """
using System.Diagnostics;
namespace Snap.Hutao.Core.Annotation;
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class CommandAttribute : Attribute
{
public CommandAttribute(string name)
{
}
public bool AllowConcurrentExecutions { get; set; }
}
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
internal sealed class ConstructorGeneratedAttribute : Attribute
{
public ConstructorGeneratedAttribute()
{
}
public bool CallBaseConstructor { get; set; }
public bool ResolveHttpClient { get; set; }
}
[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)
{
}
public DependencyPropertyAttribute(string name, Type type, object defaultValue, string valueChangedCallbackName)
{
}
public bool IsAttached { get; set; }
public Type AttachedType { get; set; } = default;
}
[AttributeUsage(AttributeTargets.All, Inherited = false)]
[Conditional("DEBUG")]
internal sealed class HighQualityAttribute : Attribute
{
}
""";
context.AddSource("Snap.Hutao.Core.Annotation.Attributes.g.cs", coreAnnotations);
string coreDependencyInjectionAnnotationHttpClients = """
namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
internal sealed class HttpClientAttribute : Attribute
{
public HttpClientAttribute(HttpClientConfiguration configuration)
{
}
public HttpClientAttribute(HttpClientConfiguration configuration, Type interfaceType)
{
}
}
internal enum HttpClientConfiguration
{
/// <summary>
/// 默认配置
/// </summary>
Default,
/// <summary>
/// 米游社请求配置
/// </summary>
XRpc,
/// <summary>
/// 米游社登录请求配置
/// </summary>
XRpc2,
/// <summary>
/// Hoyolab app
/// </summary>
XRpc3,
}
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
internal sealed class PrimaryHttpMessageHandlerAttribute : Attribute
{
/// <inheritdoc cref="System.Net.Http.HttpClientHandler.MaxConnectionsPerServer"/>
public int MaxConnectionsPerServer { get; set; }
/// <summary>
/// <inheritdoc cref="System.Net.Http.HttpClientHandler.UseCookies"/>
/// </summary>
public bool UseCookies { get; set; }
}
""";
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.Attributes.g.cs", coreDependencyInjectionAnnotationHttpClients);
string coreDependencyInjectionAnnotations = """
namespace Snap.Hutao.Core.DependencyInjection.Annotation;
internal enum InjectAs
{
Singleton,
Transient,
Scoped,
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
internal sealed class InjectionAttribute : Attribute
{
public InjectionAttribute(InjectAs injectAs)
{
}
public InjectionAttribute(InjectAs injectAs, Type interfaceType)
{
}
}
""";
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.Attributes.g.cs", coreDependencyInjectionAnnotations);
}
}

View File

@@ -1,98 +0,0 @@
// 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 CommandGenerator : IIncrementalGenerator
{
public 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);
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

@@ -1,193 +0,0 @@
// 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;
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";
private const string CompilerGenerated = "System.Runtime.CompilerServices.CompilerGeneratedAttribute";
//private static readonly DiagnosticDescriptor genericTypeNotSupportedDescriptor = new("SH102", "Generic type is not supported to generate .ctor", "Type [{0}] is not supported", "Quality", DiagnosticSeverity.Error, true);
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);
bool callBaseConstructor = constructorInfo.HasNamedArgumentWith<bool>("CallBaseConstructor", value => value);
string httpclient = resolveHttpClient ? ", System.Net.Http.HttpClient httpClient" : string.Empty;
FieldValueAssignmentOptions options = new(resolveHttpClient, callBaseConstructor);
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
namespace {{context2.Symbol.ContainingNamespace}};
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(ConstructorGenerator)}}", "1.0.0.0")]
partial class {{context2.Symbol.ToDisplayString(SymbolDisplayFormats.QualifiedNonNullableFormat)}}
{
public {{context2.Symbol.Name}}(System.IServiceProvider serviceProvider{{httpclient}}){{(options.CallBaseConstructor ? " : base(serviceProvider)" : string.Empty)}}
{
""");
FillUpWithFieldValueAssignment(sourceBuilder, context2, options);
sourceBuilder.Append("""
}
}
""");
string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
production.AddSource($"{normalizedClassName}.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.Name.AsSpan()[0] is '<')
{
continue;
}
bool shoudSkip = false;
foreach (SyntaxReference syntaxReference in fieldSymbol.DeclaringSyntaxReferences)
{
if (syntaxReference.GetSyntax() is VariableDeclaratorSyntax declarator)
{
if (declarator.Initializer is not null)
{
// Skip field with initializer
builder.Append(" // Skip field with initializer: ").AppendLine(fieldSymbol.Name);
shoudSkip = true;
break;
}
}
}
if (shoudSkip)
{
continue;
}
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 readonly bool CallBaseConstructor;
public FieldValueAssignmentOptions(bool resolveHttpClient, bool callBaseConstructor)
{
ResolveHttpClient = resolveHttpClient;
CallBaseConstructor = callBaseConstructor;
}
}
}

View File

@@ -1,149 +0,0 @@
// 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.Generic;
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))
{
string owner = context2.Symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
Dictionary<string, TypedConstant> namedArguments = propertyInfo.NamedArguments.ToDictionary();
bool isAttached = namedArguments.TryGetValue("IsAttached", out TypedConstant constant) && (bool)constant.Value!;
string register = isAttached ? "RegisterAttached" : "Register";
ImmutableArray<TypedConstant> arguments = propertyInfo.ConstructorArguments;
string propertyName = (string)arguments[0].Value!;
string propertyType = arguments[1].Value!.ToString();
string defaultValue = GetLiteralString(arguments.ElementAtOrDefault(2)) ?? "default";
string propertyChangedCallback = arguments.ElementAtOrDefault(3) is { IsNull: false } arg3 ? $", {arg3.Value}" : string.Empty;
string code;
if (isAttached)
{
string objType = namedArguments.TryGetValue("AttachedType", out TypedConstant attachedType)
? attachedType.Value!.ToString()
: "object";
code = $$"""
using Microsoft.UI.Xaml;
namespace {{context2.Symbol.ContainingNamespace}};
partial class {{owner}}
{
private static readonly DependencyProperty {{propertyName}}Property =
DependencyProperty.RegisterAttached("{{propertyName}}", typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
public static {{propertyType}} Get{{propertyName}}({{objType}} obj)
{
return ({{propertyType}})obj?.GetValue({{propertyName}}Property);
}
public static void Set{{propertyName}}({{objType}} obj, {{propertyType}} value)
{
obj.SetValue({{propertyName}}Property, value);
}
}
""";
}
else
{
code = $$"""
using Microsoft.UI.Xaml;
namespace {{context2.Symbol.ContainingNamespace}};
partial class {{owner}}
{
private static readonly DependencyProperty {{propertyName}}Property =
DependencyProperty.Register(nameof({{propertyName}}), typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
public {{propertyType}} {{propertyName}}
{
get => ({{propertyType}})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.IsNull)
{
return default;
}
if (typedConstant.Value is bool boolValue)
{
return boolValue ? "true" : "false";
}
string result = typedConstant.Value!.ToString();
if (string.IsNullOrEmpty(result))
{
return default;
}
return result;
}
}

View File

@@ -1,72 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using System;
using System.Net.Http;
using System.Runtime.Serialization;
namespace Snap.Hutao.SourceGeneration.Automation;
[Generator(LanguageNames.CSharp)]
internal sealed class SaltConstantGenerator : IIncrementalGenerator
{
private static readonly HttpClient httpClient;
private static readonly Lazy<Response<SaltLatest>> lazySaltInfo;
static SaltConstantGenerator()
{
httpClient = new();
lazySaltInfo = new Lazy<Response<SaltLatest>>(() =>
{
string body = httpClient.GetStringAsync("https://internal.snapgenshin.cn/Archive/Salt/Latest").GetAwaiter().GetResult();
return JsonParser.FromJson<Response<SaltLatest>>(body)!;
});
}
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(GenerateSaltContstants);
}
private static void GenerateSaltContstants(IncrementalGeneratorPostInitializationContext context)
{
Response<SaltLatest> saltInfo = lazySaltInfo.Value;
string code = $$"""
namespace Snap.Hutao.Web.Hoyolab;
internal sealed class SaltConstants
{
public const string CNVersion = "{{saltInfo.Data.CNVersion}}";
public const string CNK2 = "{{saltInfo.Data.CNK2}}";
public const string CNLK2 = "{{saltInfo.Data.CNLK2}}";
public const string OSVersion = "{{saltInfo.Data.OSVersion}}";
public const string OSK2 = "{{saltInfo.Data.OSK2}}";
public const string OSLK2 = "{{saltInfo.Data.OSLK2}}";
}
""";
context.AddSource("SaltConstants.g.cs", code);
}
private sealed class Response<T>
{
[DataMember(Name = "data")]
public T Data { get; set; } = default!;
}
internal sealed class SaltLatest
{
public string CNVersion { get; set; } = default!;
public string CNK2 { get; set; } = default!;
public string CNLK2 { get; set; } = default!;
public string OSVersion { get; set; } = default!;
public string OSK2 { get; set; } = default!;
public string OSLK2 { get; set; } = default!;
}
}

View File

@@ -1,18 +0,0 @@
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

@@ -1,146 +0,0 @@
// 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.DependencyInjection;
[Generator(LanguageNames.CSharp)]
internal sealed class HttpClientGenerator : IIncrementalGenerator
{
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 CRLF = "\r\n";
private static readonly DiagnosticDescriptor injectionShouldOmitDescriptor = new("SH201", "Injection 特性可以省略", "HttpClient 特性已将 {0} 注册为 Transient 服务", "Quality", DiagnosticSeverity.Warning, true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
.Where(GeneratorSyntaxContext2.NotNull)
.Collect();
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddHttpClientsImplementation);
}
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
{
return node is ClassDeclarationSyntax classDeclarationSyntax
&& classDeclarationSyntax.HasAttributeLists();
}
private static GeneratorSyntaxContext2 HttpClientClass(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 GenerateAddHttpClientsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
{
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Net.Http;
namespace Snap.Hutao.Core.DependencyInjection;
internal static partial class IocHttpClientConfiguration
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(HttpClientGenerator)}}", "1.0.0.0")]
public static partial IServiceCollection AddHttpClients(this IServiceCollection services)
{
""");
FillUpWithAddHttpClient(sourceBuilder, context, context2s);
sourceBuilder.Append("""
return services;
}
}
""");
context.AddSource("IocHttpClientConfiguration.g.cs", sourceBuilder.ToString());
}
private static void FillUpWithAddHttpClient(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
{
List<string> lines = new();
StringBuilder lineBuilder = new();
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
{
if (context.SingleOrDefaultAttribute(InjectionGenerator.AttributeName) is AttributeData injectionData)
{
if (injectionData.ConstructorArguments[0].ToCSharpString() == InjectionGenerator.InjectAsTransientName)
{
if (injectionData.ConstructorArguments.Length < 2)
{
production.ReportDiagnostic(Diagnostic.Create(injectionShouldOmitDescriptor, context.Context.Node.GetLocation(), context.Context.Node));
}
}
}
lineBuilder.Clear().Append(CRLF);
lineBuilder.Append(@" services.AddHttpClient<");
AttributeData httpClientData = context.SingleAttribute(AttributeName);
ImmutableArray<TypedConstant> arguments = httpClientData.ConstructorArguments;
if (arguments.Length == 2)
{
lineBuilder.Append($"{arguments[1].Value}, ");
}
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>(");
lineBuilder.Append(arguments[0].ToCSharpString().Substring(HttpClientConfiguration.Length)).Append("Configuration)");
if (context.SingleOrDefaultAttribute(PrimaryHttpMessageHandlerAttributeName) is AttributeData handlerData)
{
ImmutableArray<KeyValuePair<string, TypedConstant>> properties = handlerData.NamedArguments;
lineBuilder.Append(@".ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() {");
foreach (KeyValuePair<string, TypedConstant> property in properties)
{
lineBuilder.Append(' ');
lineBuilder.Append(property.Key);
lineBuilder.Append(" = ");
lineBuilder.Append(property.Value.ToCSharpString());
lineBuilder.Append(',');
}
lineBuilder.Append(" })");
}
lineBuilder.Append(';');
lines.Add(lineBuilder.ToString());
}
foreach (string line in lines.OrderBy(x => x))
{
sourceBuilder.Append(line);
}
}
}

View File

@@ -1,126 +0,0 @@
// 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.DependencyInjection;
[Generator(LanguageNames.CSharp)]
internal sealed class InjectionGenerator : IIncrementalGenerator
{
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
public const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
public const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
public const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
private static readonly DiagnosticDescriptor invalidInjectionDescriptor = new("SH101", "无效的 InjectAs 枚举值", "尚未支持生成 {0} 配置", "Quality", DiagnosticSeverity.Error, true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
.Where(GeneratorSyntaxContext2.NotNull)
.Collect();
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddInjectionsImplementation);
}
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
{
return node is ClassDeclarationSyntax classDeclarationSyntax
&& classDeclarationSyntax.HasAttributeLists();
}
private static GeneratorSyntaxContext2 HttpClientClass(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 GenerateAddInjectionsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
{
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.DependencyInjection;
internal static partial class ServiceCollectionExtension
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(InjectionGenerator)}}", "1.0.0.0")]
public static partial IServiceCollection AddInjections(this IServiceCollection services)
{
""");
FillUpWithAddServices(sourceBuilder, context, context2s);
sourceBuilder.Append("""
return services;
}
}
""");
context.AddSource("ServiceCollectionExtension.g.cs", sourceBuilder.ToString());
}
private static void FillUpWithAddServices(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
{
List<string> lines = new();
StringBuilder lineBuilder = new();
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
{
lineBuilder.Clear().AppendLine();
AttributeData injectionInfo = context.SingleAttribute(AttributeName);
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
string injectAsName = arguments[0].ToCSharpString();
switch (injectAsName)
{
case InjectAsSingletonName:
lineBuilder.Append(" services.AddSingleton<");
break;
case InjectAsTransientName:
lineBuilder.Append(" services.AddTransient<");
break;
case InjectAsScopedName:
lineBuilder.Append(" services.AddScoped<");
break;
default:
production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, context.Context.Node.GetLocation(), injectAsName));
break;
}
if (arguments.Length == 2)
{
lineBuilder.Append($"{arguments[1].Value}, ");
}
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>();");
lines.Add(lineBuilder.ToString());
}
foreach (string line in lines.OrderBy(x => x))
{
sourceBuilder.Append(line);
}
}
}

View File

@@ -1,78 +0,0 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Collections.Immutable;
using System.Linq;
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class ServiceAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor NonSingletonUseServiceProviderDescriptor = new("SH301", "Non Singleton service should avoid direct use of IServiceProvider", "Non Singleton service should avoid direct use of IServiceProvider", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor SingletonServiceCaptureNonSingletonServiceDescriptor = new("SH302", "Singleton service should avoid keep reference of non singleton service", "Singleton service should avoid keep reference of non singleton service", "Quality", DiagnosticSeverity.Info, true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get => new DiagnosticDescriptor[]
{
NonSingletonUseServiceProviderDescriptor,
SingletonServiceCaptureNonSingletonServiceDescriptor,
}.ToImmutableArray();
}
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(CompilationStart);
}
private static void CompilationStart(CompilationStartAnalysisContext context)
{
context.RegisterSyntaxNodeAction(HandleNonSingletonUseServiceProvider, SyntaxKind.ClassDeclaration);
context.RegisterSyntaxNodeAction(HandleSingletonServiceCaptureNonSingletonService, SyntaxKind.ClassDeclaration);
}
private static void HandleNonSingletonUseServiceProvider(SyntaxNodeAnalysisContext context)
{
ClassDeclarationSyntax classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
if (classDeclarationSyntax.HasAttributeLists())
{
INamedTypeSymbol? classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
if (classSymbol is not null)
{
foreach (AttributeData attributeData in classSymbol.GetAttributes())
{
if (attributeData.AttributeClass!.ToDisplayString() is InjectionGenerator.AttributeName)
{
string serviceType = attributeData.ConstructorArguments[0].ToCSharpString();
if (serviceType is InjectionGenerator.InjectAsTransientName or InjectionGenerator.InjectAsScopedName)
{
HandleNonSingletonUseServiceProviderActual(context, classSymbol);
}
}
}
}
}
}
private static void HandleNonSingletonUseServiceProviderActual(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol)
{
ISymbol? symbol = classSymbol.GetMembers().Where(m => m is IFieldSymbol f && f.Type.ToDisplayString() == "System.IServiceProvider").SingleOrDefault();
if (symbol is not null)
{
Diagnostic diagnostic = Diagnostic.Create(NonSingletonUseServiceProviderDescriptor, symbol.Locations.FirstOrDefault());
context.ReportDiagnostic(diagnostic);
}
}
private static void HandleSingletonServiceCaptureNonSingletonService(SyntaxNodeAnalysisContext context)
{
//classSymbol.GetMembers().Where(m => m is IFieldSymbol { IsReadOnly: true, DeclaredAccessibility: Accessibility.Private } f);
}
}

View File

@@ -1,136 +0,0 @@
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.
using System.Globalization;
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, CultureInfo.CurrentCulture);
}
}
/// <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, CultureInfo.CurrentCulture);
}
}
""");
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

@@ -1,8 +0,0 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("", "RS2008")]

View File

@@ -1,202 +0,0 @@
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();
}
/// <inheritdoc/>
public override string ToString()
{
return Value.ToString();
}
}
""");
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

@@ -1,439 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
namespace Snap.Hutao.SourceGeneration;
// Really simple JSON parser in ~300 lines
// - Attempts to parse JSON files with minimal GC allocation
// - Nice and simple "[1,2,3]".FromJson<List<int>>() API
// - Classes and structs can be parsed too!
// class Foo { public int Value; }
// "{\"Value\":10}".FromJson<Foo>()
// - Can parse JSON without type information into Dictionary<string,object> and List<object> e.g.
// "[1,2,3]".FromJson<object>().GetType() == typeof(List<object>)
// "{\"Value\":10}".FromJson<object>().GetType() == typeof(Dictionary<string,object>)
// - No JIT Emit support to support AOT compilation on iOS
// - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead.
// - Only public fields and property setters on classes/structs will be written to
//
// Limitations:
// - No JIT Emit support to parse structures quickly
// - Limited to parsing <2GB JSON files (due to int.MaxValue)
// - Parsing of abstract classes or interfaces is NOT supported and will throw an exception.
public static class JsonParser
{
[ThreadStatic]
private static Stack<List<string>>? splitArrayPool;
[ThreadStatic]
private static StringBuilder? stringBuilder;
[ThreadStatic]
private static Dictionary<Type, Dictionary<string, FieldInfo>>? fieldInfoCache;
[ThreadStatic]
private static Dictionary<Type, Dictionary<string, PropertyInfo>>? propertyInfoCache;
public static T? FromJson<T>(this string json)
{
// Initialize, if needed, the ThreadStatic variables
propertyInfoCache ??= new Dictionary<Type, Dictionary<string, PropertyInfo>>();
fieldInfoCache ??= new Dictionary<Type, Dictionary<string, FieldInfo>>();
stringBuilder ??= new StringBuilder();
splitArrayPool ??= new Stack<List<string>>();
// Remove all whitespace not within strings to make parsing simpler
stringBuilder.Length = 0;
for (int i = 0; i < json.Length; i++)
{
char c = json[i];
if (c == '"')
{
i = AppendUntilStringEnd(true, i, json);
continue;
}
if (char.IsWhiteSpace(c))
{
continue;
}
stringBuilder.Append(c);
}
// Parse the thing!
return (T?)ParseValue(typeof(T), stringBuilder.ToString());
}
private static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json)
{
stringBuilder!.Append(json[startIdx]);
for (int i = startIdx + 1; i < json.Length; i++)
{
if (json[i] == '\\')
{
if (appendEscapeCharacter)
{
stringBuilder.Append(json[i]);
}
stringBuilder.Append(json[i + 1]);
i++;//Skip next character as it is escaped
}
else if (json[i] == '"')
{
stringBuilder.Append(json[i]);
return i;
}
else
{
stringBuilder.Append(json[i]);
}
}
return json.Length - 1;
}
// 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>();
splitArray.Clear();
if (json.Length == 2)
{
return splitArray;
}
int parseDepth = 0;
stringBuilder!.Length = 0;
for (int i = 1; i < json.Length - 1; i++)
{
switch (json[i])
{
case '[':
case '{':
parseDepth++;
break;
case ']':
case '}':
parseDepth--;
break;
case '"':
i = AppendUntilStringEnd(true, i, json);
continue;
case ',':
case ':':
if (parseDepth == 0)
{
splitArray.Add(stringBuilder.ToString());
stringBuilder.Length = 0;
continue;
}
break;
}
stringBuilder.Append(json[i]);
}
splitArray.Add(stringBuilder.ToString());
return splitArray;
}
internal static object? ParseValue(Type type, string json)
{
if (type == typeof(string))
{
if (json.Length <= 2)
{
return string.Empty;
}
StringBuilder parseStringBuilder = new(json.Length);
for (int i = 1; i < json.Length - 1; ++i)
{
if (json[i] == '\\' && i + 1 < json.Length - 1)
{
int j = "\"\\nrtbf/".IndexOf(json[i + 1]);
if (j >= 0)
{
parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]);
++i;
continue;
}
if (json[i + 1] == 'u' && i + 5 < json.Length - 1)
{
if (uint.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out uint c))
{
parseStringBuilder.Append((char)c);
i += 5;
continue;
}
}
}
parseStringBuilder.Append(json[i]);
}
return parseStringBuilder.ToString();
}
if (type.IsPrimitive)
{
object result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture);
return result;
}
if (type == typeof(decimal))
{
decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out decimal result);
return result;
}
if (type == typeof(DateTime))
{
DateTime.TryParse(json.Replace("\"", ""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime result);
return result;
}
if (json == "null")
{
return null;
}
if (type.IsEnum)
{
if (json[0] == '"')
{
json = json.Substring(1, json.Length - 2);
}
try
{
return System.Enum.Parse(type, json, false);
}
catch
{
return 0;
}
}
if (type.IsArray)
{
Type arrayType = type.GetElementType();
if (json[0] != '[' || json[json.Length - 1] != ']')
{
return null;
}
List<string> elems = Split(json);
Array newArray = Array.CreateInstance(arrayType, elems.Count);
for (int i = 0; i < elems.Count; i++)
{
newArray.SetValue(ParseValue(arrayType, elems[i]), i);
}
splitArrayPool!.Push(elems);
return newArray;
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
{
Type listType = type.GetGenericArguments()[0];
if (json[0] != '[' || json[json.Length - 1] != ']')
{
return null;
}
List<string> elems = Split(json);
IList list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count });
for (int i = 0; i < elems.Count; i++)
{
list.Add(ParseValue(listType, elems[i]));
}
splitArrayPool!.Push(elems);
return list;
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
Type keyType, valueType;
{
Type[] args = type.GetGenericArguments();
keyType = args[0];
valueType = args[1];
}
//Refuse to parse dictionary keys that aren't of type string
if (keyType != typeof(string))
{
return null;
}
//Must be a valid dictionary element
if (json[0] != '{' || json[json.Length - 1] != '}')
{
return null;
}
//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)
{
return null;
}
IDictionary dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 });
for (int i = 0; i < elems.Count; i += 2)
{
if (elems[i].Length <= 2)
{
continue;
}
string keyValue = elems[i].Substring(1, elems[i].Length - 2);
object? val = ParseValue(valueType, elems[i + 1]);
dictionary[keyValue] = val;
}
return dictionary;
}
if (type == typeof(object))
{
return ParseAnonymousValue(json);
}
if (json[0] == '{' && json[json.Length - 1] == '}')
{
return ParseObject(type, json);
}
return null;
}
private static object? ParseAnonymousValue(string json)
{
if (json.Length == 0)
{
return null;
}
if (json[0] == '{' && json[json.Length - 1] == '}')
{
List<string> elems = Split(json);
if (elems.Count % 2 != 0)
{
return null;
}
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]);
}
return dict;
}
if (json[0] == '[' && json[json.Length - 1] == ']')
{
List<string> items = Split(json);
List<object?> finalList = new(items.Count);
for (int i = 0; i < items.Count; i++)
{
finalList.Add(ParseAnonymousValue(items[i]));
}
return finalList;
}
if (json[0] == '"' && json[json.Length - 1] == '"')
{
string str = json.Substring(1, json.Length - 2);
return str.Replace("\\", string.Empty);
}
if (char.IsDigit(json[0]) || json[0] == '-')
{
if (json.Contains("."))
{
double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double result);
return result;
}
else
{
int.TryParse(json, out int result);
return result;
}
}
if (json == "true")
{
return true;
}
if (json == "false")
{
return false;
}
// handles json == "null" as well as invalid JSON
return null;
}
private static Dictionary<string, T> CreateMemberNameDictionary<T>(T[] members) where T : MemberInfo
{
Dictionary<string, T> nameToMember = new(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < members.Length; i++)
{
T member = members[i];
if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true))
{
continue;
}
string name = member.Name;
if (member.IsDefined(typeof(DataMemberAttribute), true))
{
DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true);
if (!string.IsNullOrEmpty(dataMemberAttribute.Name))
{
name = dataMemberAttribute.Name;
}
}
nameToMember.Add(name, member);
}
return nameToMember;
}
private static object ParseObject(Type type, string json)
{
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
List<string> elems = Split(json);
if (elems.Count % 2 != 0)
{
return instance;
}
if (!fieldInfoCache!.TryGetValue(type, out Dictionary<string, FieldInfo> nameToField))
{
nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
fieldInfoCache.Add(type, nameToField);
}
if (!propertyInfoCache!.TryGetValue(type, out Dictionary<string, PropertyInfo> nameToProperty))
{
nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
propertyInfoCache.Add(type, nameToProperty);
}
for (int i = 0; i < elems.Count; i += 2)
{
if (elems[i].Length <= 2)
{
continue;
}
string key = elems[i].Substring(1, elems[i].Length - 2);
string value = elems[i + 1];
if (nameToField.TryGetValue(key, out FieldInfo fieldInfo))
{
fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value));
}
else if (nameToProperty.TryGetValue(key, out PropertyInfo propertyInfo))
{
propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null);
}
}
return instance;
}
}

View File

@@ -1,16 +0,0 @@
// 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

@@ -1,41 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Linq;
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());
}
}
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> source)
{
return source.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
}

View File

@@ -1,77 +0,0 @@
// 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

@@ -1,18 +0,0 @@
// 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

@@ -1,21 +0,0 @@
// 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);
public static SymbolDisplayFormat QualifiedNonNullableFormat { get; } = new(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
}

View File

@@ -1,20 +0,0 @@
// 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

@@ -1,28 +0,0 @@
// 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,607 +0,0 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Xml.XPath;
namespace Snap.Hutao.SourceGeneration.Resx;
[Generator]
public sealed class ResxGenerator : IIncrementalGenerator
{
private static readonly DiagnosticDescriptor InvalidResx = new("SH401", "Couldn't parse Resx file", "Couldn't parse Resx file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
private static readonly DiagnosticDescriptor InvalidPropertiesForNamespace = new("SH402", "Couldn't compute namespace", "Couldn't compute namespace for file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
private static readonly DiagnosticDescriptor InvalidPropertiesForResourceName = new("SH403", "Couldn't compute resource name", "Couldn't compute resource name for file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
private static readonly DiagnosticDescriptor InconsistentProperties = new("SH404", "Inconsistent properties", "Property '{0}' values for '{1}' are inconsistent", "ResxGenerator", DiagnosticSeverity.Warning, true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<(string? AssemblyName, bool SupportNullableReferenceTypes)> compilationProvider = context.CompilationProvider
.Select(static (compilation, cancellationToken) => (compilation.AssemblyName, SupportNullableReferenceTypes: compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute") is not null));
IncrementalValueProvider<ImmutableArray<AdditionalText>> resxProvider = context.AdditionalTextsProvider
.Where(text => text.Path.EndsWith(".resx", StringComparison.OrdinalIgnoreCase))
.Collect();
context.RegisterSourceOutput(
source: context.AnalyzerConfigOptionsProvider.Combine(compilationProvider.Combine(resxProvider)),
action: (ctx, source) => Execute(ctx, source.Left, source.Right.Left.AssemblyName, source.Right.Left.SupportNullableReferenceTypes, source.Right.Right));
}
private static void Execute(SourceProductionContext context, AnalyzerConfigOptionsProvider options, string? assemblyName, bool supportNullableReferenceTypes, ImmutableArray<AdditionalText> files)
{
// Group additional file by resource kind ((a.resx, a.en.resx, a.en-us.resx), (b.resx, b.en-us.resx))
List<IGrouping<string, AdditionalText>> resxGroups = files
.GroupBy(file => GetResourceName(file.Path), StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x.Key, StringComparer.Ordinal)
.ToList();
foreach (IGrouping<string, AdditionalText>? resxGroug in resxGroups)
{
string? rootNamespaceConfiguration = GetMetadataValue(context, options, "RootNamespace", resxGroug);
string? projectDirConfiguration = GetMetadataValue(context, options, "ProjectDir", resxGroug);
string? namespaceConfiguration = GetMetadataValue(context, options, "Namespace", "DefaultResourcesNamespace", resxGroug);
string? resourceNameConfiguration = GetMetadataValue(context, options, "ResourceName", globalName: null, resxGroug);
string? classNameConfiguration = GetMetadataValue(context, options, "ClassName", globalName: null, resxGroug);
string rootNamespace = rootNamespaceConfiguration ?? assemblyName ?? "";
string projectDir = projectDirConfiguration ?? assemblyName ?? "";
string? defaultResourceName = ComputeResourceName(rootNamespace, projectDir, resxGroug.Key);
string? defaultNamespace = ComputeNamespace(rootNamespace, projectDir, resxGroug.Key);
string? ns = namespaceConfiguration ?? defaultNamespace;
string? resourceName = resourceNameConfiguration ?? defaultResourceName;
string className = classNameConfiguration ?? ToCSharpNameIdentifier(Path.GetFileName(resxGroug.Key));
if (ns == null)
{
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForNamespace, location: null, resxGroug.First().Path));
}
if (resourceName == null)
{
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForResourceName, location: null, resxGroug.First().Path));
}
List<ResxEntry>? entries = LoadResourceFiles(context, resxGroug);
string content = $"""
// Debug info:
// key: {resxGroug.Key}
// files: {string.Join(", ", resxGroug.Select(f => f.Path))}
// RootNamespace (metadata): {rootNamespaceConfiguration}
// ProjectDir (metadata): {projectDirConfiguration}
// Namespace / DefaultResourcesNamespace (metadata): {namespaceConfiguration}
// ResourceName (metadata): {resourceNameConfiguration}
// ClassName (metadata): {classNameConfiguration}
// AssemblyName: {assemblyName}
// RootNamespace (computed): {rootNamespace}
// ProjectDir (computed): {projectDir}
// defaultNamespace: {defaultNamespace}
// defaultResourceName: {defaultResourceName}
// Namespace: {ns}
// ResourceName: {resourceName}
// ClassName: {className}
""";
if (resourceName != null && entries != null)
{
content += GenerateCode(ns, className, resourceName, entries, supportNullableReferenceTypes);
}
context.AddSource($"{Path.GetFileName(resxGroug.Key)}.resx.g.cs", SourceText.From(content, Encoding.UTF8));
}
}
private static string GenerateCode(string? ns, string className, string resourceName, List<ResxEntry> entries, bool enableNullableAttributes)
{
StringBuilder sb = new();
sb.AppendLine();
sb.AppendLine("#nullable enable");
if (ns != null)
{
sb.AppendLine($$"""
namespace {{ns}};
""");
}
sb.AppendLine($$"""
internal partial class {{className}}
{
private static global::System.Resources.ResourceManager? resourceMan;
public {{className}}()
{
}
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager
{
get
{
if (resourceMan is null)
{
resourceMan = new global::System.Resources.ResourceManager("{{resourceName}}", typeof({{className}}).Assembly);
}
return resourceMan;
}
}
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo? Culture { get; set; }
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
public static object? GetObject(global::System.Globalization.CultureInfo? culture, string name, object? defaultValue)
{
culture ??= Culture;
object? obj = ResourceManager.GetObject(name, culture);
if (obj == null)
{
return defaultValue;
}
return obj;
}
public static object? GetObject(global::System.Globalization.CultureInfo? culture, string name)
{
return GetObject(culture: culture, name: name, defaultValue: null);
}
public static object? GetObject(string name)
{
return GetObject(culture: null, name: name, defaultValue: null);
}
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
public static object? GetObject(string name, object? defaultValue)
{
return GetObject(culture: null, name: name, defaultValue: defaultValue);
}
public static global::System.IO.Stream? GetStream(string name)
{
return GetStream(culture: null, name: name);
}
public static global::System.IO.Stream? GetStream(global::System.Globalization.CultureInfo? culture, string name)
{
culture ??= Culture;
return ResourceManager.GetStream(name, culture);
}
public static string? GetString(global::System.Globalization.CultureInfo? culture, string name)
{
return GetString(culture: culture, name: name, args: null);
}
public static string? GetString(global::System.Globalization.CultureInfo? culture, string name, params object?[]? args)
{
culture ??= Culture;
string? str = ResourceManager.GetString(name, culture);
if (str == null)
{
return null;
}
if (args != null)
{
return string.Format(culture, str, args);
}
else
{
return str;
}
}
public static string? GetString(string name, params object?[]? args)
{
return GetString(culture: null, name: name, args: args);
}
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
public static string? GetString(string name, string? defaultValue)
{
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: null);
}
public static string? GetString(string name)
{
return GetStringOrDefault(culture: null, name: name, defaultValue: null, args: null);
}
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
public static string? GetStringOrDefault(global::System.Globalization.CultureInfo? culture, string name, string? defaultValue)
{
return GetStringOrDefault(culture: culture, name: name, defaultValue: defaultValue, args: null);
}
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
public static string? GetStringOrDefault(global::System.Globalization.CultureInfo? culture, string name, string? defaultValue, params object?[]? args)
{
culture ??= Culture;
string? str = ResourceManager.GetString(name, culture);
if (str == null)
{
if (defaultValue == null || args == null)
{
return defaultValue;
}
else
{
return string.Format(culture, defaultValue, args);
}
}
if (args != null)
{
return string.Format(culture, str, args);
}
else
{
return str;
}
}
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
public static string? GetStringOrDefault(string name, string? defaultValue, params object?[]? args)
{
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: args);
}
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
public static string? GetStringOrDefault(string name, string? defaultValue)
{
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: null);
}
""");
foreach (ResxEntry? entry in entries.OrderBy(e => e.Name, StringComparer.Ordinal))
{
if (string.IsNullOrEmpty(entry.Name))
{
continue;
}
if (entry.IsText)
{
XElement summary = new("summary", new XElement("para", $"Looks up a localized string for \"{entry.Name}\"."));
if (!string.IsNullOrWhiteSpace(entry.Comment))
{
summary.Add(new XElement("para", entry.Comment));
}
if (!entry.IsFileRef)
{
summary.Add(new XElement("para", $"Value: \"{entry.Value}\"."));
}
string comment = summary.ToString().Replace("\r\n", "\r\n /// ", StringComparison.Ordinal);
sb.AppendLine($$"""
/// {{comment}}
public static string {{ToCSharpNameIdentifier(entry.Name!)}}
{
get => GetString("{{entry.Name}}")!;
}
""");
if (entry.Value != null)
{
int args = Regex.Matches(entry.Value, "\\{(?<num>[0-9]+)(\\:[^}]*)?\\}", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant)
.Cast<Match>()
.Select(m => int.Parse(m.Groups["num"].Value, CultureInfo.InvariantCulture))
.Distinct()
.DefaultIfEmpty(-1)
.Max();
if (args >= 0)
{
string inParams = string.Join(", ", Enumerable.Range(0, args + 1).Select(arg => "object? arg" + arg.ToString(CultureInfo.InvariantCulture)));
string callParams = string.Join(", ", Enumerable.Range(0, args + 1).Select(arg => "arg" + arg.ToString(CultureInfo.InvariantCulture)));
sb.AppendLine($$"""
/// {{comment}}
public static string Format{{ToCSharpNameIdentifier(entry.Name!)}}(global::System.Globalization.CultureInfo? provider, {{inParams}})
{
return GetString(provider, "{{entry.Name}}", {{callParams}})!;
}
/// {{comment}}
public static string Format{{ToCSharpNameIdentifier(entry.Name!)}}({{inParams}})
{
return GetString("{{entry.Name}}", {{callParams}})!;
}
""");
}
}
}
else
{
sb.AppendLine($$"""
public static global::{{entry.FullTypeName}}? {{ToCSharpNameIdentifier(entry.Name!)}}
{
get => (global::{{entry.FullTypeName}}?)GetObject("{{entry.Name}}");
}
""");
}
}
sb.AppendLine($$"""
}
internal partial class {{className}}Names
{
""");
foreach (ResxEntry entry in entries)
{
if (string.IsNullOrEmpty(entry.Name))
{
continue;
}
sb.AppendLine($$"""
public const string {{ToCSharpNameIdentifier(entry.Name!)}} = "entry.Name";
""");
}
sb.AppendLine("}");
return sb.ToString();
}
private static string? ComputeResourceName(string rootNamespace, string projectDir, string resourcePath)
{
string fullProjectDir = EnsureEndSeparator(Path.GetFullPath(projectDir));
string fullResourcePath = Path.GetFullPath(resourcePath);
if (fullProjectDir == fullResourcePath)
{
return rootNamespace;
}
if (fullResourcePath.StartsWith(fullProjectDir, StringComparison.Ordinal))
{
string relativePath = fullResourcePath.Substring(fullProjectDir.Length);
return rootNamespace + '.' + relativePath.Replace('/', '.').Replace('\\', '.');
}
return null;
}
private static string? ComputeNamespace(string rootNamespace, string projectDir, string resourcePath)
{
string fullProjectDir = EnsureEndSeparator(Path.GetFullPath(projectDir));
string fullResourcePath = EnsureEndSeparator(Path.GetDirectoryName(Path.GetFullPath(resourcePath))!);
if (fullProjectDir == fullResourcePath)
{
return rootNamespace;
}
if (fullResourcePath.StartsWith(fullProjectDir, StringComparison.Ordinal))
{
string relativePath = fullResourcePath.Substring(fullProjectDir.Length);
return rootNamespace + '.' + relativePath.Replace('/', '.').Replace('\\', '.').TrimEnd('.');
}
return null;
}
private static List<ResxEntry>? LoadResourceFiles(SourceProductionContext context, IGrouping<string, AdditionalText> resxGroug)
{
List<ResxEntry> entries = new();
foreach (AdditionalText? entry in resxGroug.OrderBy(file => file.Path, StringComparer.Ordinal))
{
SourceText? content = entry.GetText(context.CancellationToken);
if (content == null)
{
continue;
}
try
{
XDocument document = XDocument.Parse(content.ToString());
foreach (XElement? element in document.XPathSelectElements("/root/data"))
{
string? name = element.Attribute("name")?.Value;
string? type = element.Attribute("type")?.Value;
string? comment = element.Attribute("comment")?.Value;
string? value = element.Element("value")?.Value;
ResxEntry existingEntry = entries.Find(e => e.Name == name);
if (existingEntry != null)
{
existingEntry.Comment ??= comment;
}
else
{
entries.Add(new ResxEntry { Name = name, Value = value, Comment = comment, Type = type });
}
}
}
catch
{
context.ReportDiagnostic(Diagnostic.Create(InvalidResx, location: null, entry.Path));
return null;
}
}
return entries;
}
private static string? GetMetadataValue(SourceProductionContext context, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, string name, IEnumerable<AdditionalText> additionalFiles)
{
return GetMetadataValue(context, analyzerConfigOptionsProvider, name, name, additionalFiles);
}
private static string? GetMetadataValue(SourceProductionContext context, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, string name, string? globalName, IEnumerable<AdditionalText> additionalFiles)
{
string? result = null;
foreach (AdditionalText file in additionalFiles)
{
if (analyzerConfigOptionsProvider.GetOptions(file).TryGetValue("build_metadata.AdditionalFiles." + name, out string? value))
{
if (result != null && value != result)
{
context.ReportDiagnostic(Diagnostic.Create(InconsistentProperties, location: null, name, file.Path));
return null;
}
result = value;
}
}
if (!string.IsNullOrEmpty(result))
{
return result;
}
if (globalName != null && analyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property." + globalName, out string? globalValue) && !string.IsNullOrEmpty(globalValue))
{
return globalValue;
}
return null;
}
private static string ToCSharpNameIdentifier(string name)
{
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#identifiers
// https://docs.microsoft.com/en-us/dotnet/api/system.globalization.unicodecategory?view=net-5.0
StringBuilder sb = new();
foreach (char c in name)
{
UnicodeCategory category = char.GetUnicodeCategory(c);
switch (category)
{
case UnicodeCategory.UppercaseLetter:
case UnicodeCategory.LowercaseLetter:
case UnicodeCategory.TitlecaseLetter:
case UnicodeCategory.ModifierLetter:
case UnicodeCategory.OtherLetter:
case UnicodeCategory.LetterNumber:
sb.Append(c);
break;
case UnicodeCategory.DecimalDigitNumber:
case UnicodeCategory.ConnectorPunctuation:
case UnicodeCategory.Format:
if (sb.Length == 0)
{
sb.Append('_');
}
sb.Append(c);
break;
default:
sb.Append('_');
break;
}
}
return sb.ToString();
}
private static string EnsureEndSeparator(string path)
{
if (path[path.Length - 1] == Path.DirectorySeparatorChar)
{
return path;
}
return path + Path.DirectorySeparatorChar;
}
private static string GetResourceName(string path)
{
string pathWithoutExtension = Path.Combine(Path.GetDirectoryName(path)!, Path.GetFileNameWithoutExtension(path));
int indexOf = pathWithoutExtension.LastIndexOf('.');
if (indexOf < 0)
{
return pathWithoutExtension;
}
return Regex.IsMatch(pathWithoutExtension.Substring(indexOf + 1), "^[a-zA-Z]{2}(-[a-zA-Z]{2})?$", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1))
? pathWithoutExtension.Substring(0, indexOf)
: pathWithoutExtension;
}
private sealed class ResxEntry
{
public string? Name { get; set; }
public string? Value { get; set; }
public string? Comment { get; set; }
public string? Type { get; set; }
public bool IsText
{
get
{
if (Type == null)
{
return true;
}
if (Value != null)
{
string[] parts = Value.Split(';');
if (parts.Length > 1)
{
string type = parts[1];
if (type.StartsWith("System.String,", StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
}
public string? FullTypeName
{
get
{
if (IsText)
{
return "string";
}
if (Value != null)
{
string[] parts = Value.Split(';');
if (parts.Length > 1)
{
string type = parts[1];
return type.Split(',')[0];
}
}
return null;
}
}
public bool IsFileRef
{
get => Type != null && Type.StartsWith("System.Resources.ResXFileRef,", StringComparison.Ordinal);
}
}
}

View File

@@ -1,27 +0,0 @@
using System;
using System.Text;
namespace Snap.Hutao.SourceGeneration.Resx;
internal static class StringExtensions
{
public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
{
StringBuilder sb = new();
int previousIndex = 0;
int index = str.IndexOf(oldValue, comparison);
while (index is not -1)
{
sb.Append(str, previousIndex, index - previousIndex);
sb.Append(newValue);
index += oldValue.Length;
previousIndex = index;
index = str.IndexOf(oldValue, index, comparison);
}
sb.Append(str, previousIndex, str.Length - previousIndex);
return sb.ToString();
}
}

View File

@@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
</PropertyGroup>
<ItemGroup>
<None Remove="stylecop.json" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" Version="3.3.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
</ItemGroup>
</Project>

View File

@@ -1,312 +0,0 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Snap.Hutao.SourceGeneration.Primitive;
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);
private static readonly DiagnosticDescriptor useValueTaskIfPossibleDescriptor = new("SH003", "Use ValueTask instead of Task whenever possible", "Use ValueTask instead of Task", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor useIsNotNullPatternMatchingDescriptor = new("SH004", "Use \"is not null\" instead of \"!= null\" whenever possible", "Use \"is not null\" instead of \"!= null\"", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor useIsNullPatternMatchingDescriptor = new("SH005", "Use \"is null\" instead of \"== null\" whenever possible", "Use \"is null\" instead of \"== null\"", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor useIsPatternRecursiveMatchingDescriptor = new("SH006", "Use \"is { } obj\" whenever possible", "Use \"is {{ }} {0}\"", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor useArgumentNullExceptionThrowIfNullDescriptor = new("SH007", "Use \"ArgumentNullException.ThrowIfNull()\" instead of \"!\"", "Use \"ArgumentNullException.ThrowIfNull()\"", "Quality", DiagnosticSeverity.Info, true);
private static readonly ImmutableHashSet<string> RefLikeKeySkipTypes = new HashSet<string>()
{
"System.Threading.CancellationToken",
"System.Guid"
}.ToImmutableHashSet();
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
return new DiagnosticDescriptor[]
{
typeInternalDescriptor,
readOnlyStructRefDescriptor,
useValueTaskIfPossibleDescriptor,
useIsNotNullPatternMatchingDescriptor,
useIsNullPatternMatchingDescriptor,
useIsPatternRecursiveMatchingDescriptor,
useArgumentNullExceptionThrowIfNullDescriptor
}.ToImmutableArray();
}
}
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(CompilationStart);
}
private static void CompilationStart(CompilationStartAnalysisContext context)
{
SyntaxKind[] types =
{
SyntaxKind.ClassDeclaration,
SyntaxKind.InterfaceDeclaration,
SyntaxKind.StructDeclaration,
SyntaxKind.EnumDeclaration,
};
context.RegisterSyntaxNodeAction(HandleTypeShouldBeInternal, types);
context.RegisterSyntaxNodeAction(HandleMethodParameterShouldUseRefLikeKeyword, SyntaxKind.MethodDeclaration);
context.RegisterSyntaxNodeAction(HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask, SyntaxKind.MethodDeclaration);
context.RegisterSyntaxNodeAction(HandleConstructorParameterShouldUseRefLikeKeyword, SyntaxKind.ConstructorDeclaration);
SyntaxKind[] expressions =
{
SyntaxKind.EqualsExpression,
SyntaxKind.NotEqualsExpression,
};
context.RegisterSyntaxNodeAction(HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching, expressions);
context.RegisterSyntaxNodeAction(HandleIsPatternShouldUseRecursivePattern, SyntaxKind.IsPatternExpression);
context.RegisterSyntaxNodeAction(HandleArgumentNullExceptionThrowIfNull, SyntaxKind.SuppressNullableWarningExpression);
// TODO add analyzer for unnecessary IServiceProvider registration
// TODO add analyzer for Singlton service use Scoped or Transient services
}
private static void HandleTypeShouldBeInternal(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 static void HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask(SyntaxNodeAnalysisContext context)
{
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
// 跳过重载方法
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.OverrideKeyword)))
{
return;
}
// ICommand can only use Task or Task<T>
if (methodSymbol.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == Automation.CommandGenerator.AttributeName))
{
return;
}
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
{
Location location = methodSyntax.ReturnType.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(useValueTaskIfPossibleDescriptor, location);
context.ReportDiagnostic(diagnostic);
}
}
private static void HandleMethodParameterShouldUseRefLikeKeyword(SyntaxNodeAnalysisContext context)
{
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
// 跳过方法定义 如 接口
if (methodSyntax.Body == null)
{
return;
}
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
{
return;
}
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.ValueTask"))
{
return;
}
foreach (SyntaxToken token in methodSyntax.Modifiers)
{
// 跳过异步方法,因为异步方法无法使用 ref/in/out
if (token.IsKind(SyntaxKind.AsyncKeyword))
{
return;
}
// 跳过重载方法
if (token.IsKind(SyntaxKind.OverrideKeyword))
{
return;
}
}
foreach (ParameterSyntax parameter in methodSyntax.ParameterList.Parameters)
{
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
{
if (IsBuiltInType(symbol.Type))
{
continue;
}
if (RefLikeKeySkipTypes.Contains(symbol.Type.ToDisplayString()))
{
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 static void HandleConstructorParameterShouldUseRefLikeKeyword(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);
}
}
}
}
public static void HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching(SyntaxNodeAnalysisContext context)
{
BinaryExpressionSyntax syntax = (BinaryExpressionSyntax)context.Node;
if (syntax.IsKind(SyntaxKind.NotEqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
{
Location location = syntax.OperatorToken.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(useIsNotNullPatternMatchingDescriptor, location);
context.ReportDiagnostic(diagnostic);
}
else if (syntax.IsKind(SyntaxKind.EqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
{
Location location = syntax.OperatorToken.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(useIsNullPatternMatchingDescriptor, location);
context.ReportDiagnostic(diagnostic);
}
}
private static void HandleIsPatternShouldUseRecursivePattern(SyntaxNodeAnalysisContext context)
{
IsPatternExpressionSyntax syntax = (IsPatternExpressionSyntax)context.Node;
if (syntax.Pattern is DeclarationPatternSyntax declaration)
{
ITypeSymbol? leftType = context.SemanticModel.GetTypeInfo(syntax.Expression).ConvertedType;
ITypeSymbol? rightType = context.SemanticModel.GetTypeInfo(declaration).ConvertedType;
if (SymbolEqualityComparer.Default.Equals(leftType, rightType))
{
Location location = declaration.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(useIsPatternRecursiveMatchingDescriptor, location, declaration.Designation);
context.ReportDiagnostic(diagnostic);
}
}
}
private static void HandleArgumentNullExceptionThrowIfNull(SyntaxNodeAnalysisContext context)
{
PostfixUnaryExpressionSyntax syntax = (PostfixUnaryExpressionSyntax)context.Node;
if (syntax.Operand is LiteralExpressionSyntax literal)
{
if (literal.IsKind(SyntaxKind.DefaultLiteralExpression))
{
return;
}
}
if (syntax.Operand is DefaultExpressionSyntax expression)
{
return;
}
Location location = syntax.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(useArgumentNullExceptionThrowIfNullDescriptor, location);
context.ReportDiagnostic(diagnostic);
}
private static 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,23 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings": {
"documentationRules": {
"companyName": "DGP Studio",
"copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license.",
"xmlHeader": false,
"variables": {
"licenseName": "MIT"
}
},
"orderingRules": {
"elementOrder": [
"kind",
"accessibility",
"constant",
"static",
"readonly"
],
"usingDirectivesPlacement": "outsideNamespace"
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public class CollectionsMarshalTest
{
[TestMethod]
public void DictionaryMarshalGetValueRefOrNullRefIsNullRef()
{
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
Dictionary<string, string> dictionaryRefKeyRefValue = [];
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyRefValue, 1U)));
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyValueValue, 1U)));
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyValueValue, "no such key")));
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyRefValue, "no such key")));
}
[TestMethod]
public void DictionaryMarshalGetValueRefOrAddDefaultIsDefault()
{
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
Dictionary<string, string> dictionaryRefKeyRefValue = [];
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyRefValue, 1U, out _) == default);
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyValueValue, 1U, out _) == default);
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyValueValue, "no such key", out _) == default);
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyRefValue, "no such key", out _) == default);
}
}

View File

@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public sealed class JsonSerializeTest
{
private readonly JsonSerializerOptions AlowStringNumberOptions = new()
{
NumberHandling = JsonNumberHandling.AllowReadingFromString,
};
private const string SmapleObjectJson = """
{
"A" :1
}
""";
private const string SmapleEmptyStringObjectJson = """
{
"A" : ""
}
""";
private const string SmapleNumberKeyDictionaryJson = """
{
"111" : "12",
"222" : "34"
}
""";
[TestMethod]
public void DelegatePropertyCanSerialize()
{
SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SmapleObjectJson)!;
Assert.AreEqual(sample.B, 1);
}
[TestMethod]
[ExpectedException(typeof(JsonException))]
public void EmptyStringCannotSerializeAsNumber()
{
SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SmapleEmptyStringObjectJson)!;
Assert.AreEqual(sample.A, 0);
}
[TestMethod]
public void NumberStringKeyCanSerializeAsKey()
{
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, AlowStringNumberOptions)!;
Assert.AreEqual(sample[111], "12");
}
[TestMethod]
public void ByteArraySerializeAsBase64()
{
SampleByteArrayPropertyClass sample = new()
{
Array = [1, 2, 3, 4, 5],
};
string result = JsonSerializer.Serialize(sample);
Assert.AreEqual(result, """{"Array":"AQIDBAU="}""");
}
[TestMethod]
public void InterfaceDefaultMethodCanSerializeActualInstanceMember()
{
ISampleInterface sample = new SampleClassImplementedInterface()
{
A = 1,
B = 2,
};
string result = sample.ToJson();
Console.WriteLine(result);
Assert.AreEqual(result, """{"A":1,"B":2}""");
}
private sealed class SampleDelegatePropertyClass
{
public int A { get => B; set => B = value; }
public int B { get; set; }
}
private sealed class SampleStringReadWriteNumberPropertyClass
{
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
public int A { get; set; }
}
private sealed class SampleByteArrayPropertyClass
{
public byte[]? Array { get; set; }
}
private sealed class SampleClassImplementedInterface : ISampleInterface
{
public int A { get; set; }
public int B { get; set; }
}
[JsonDerivedType(typeof(SampleClassImplementedInterface))]
private interface ISampleInterface
{
int A { get; set; }
string ToJson()
{
return JsonSerializer.Serialize(this);
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public sealed class LinqTest
{
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void LinqOrderByWithWrapperStructThrow()
{
List<MyUInt32> list = [1, 5, 2, 6, 3, 7, 4, 8];
string result = string.Join(", ", list.OrderBy(i => i).Select(i => i.Value));
Console.WriteLine(result);
}
private readonly struct MyUInt32
{
public readonly uint Value;
public MyUInt32(uint value)
{
Value = value;
}
public static implicit operator MyUInt32(uint value)
{
return new(value);
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public sealed class TypeReflectionTest
{
[TestMethod]
public void TypeCodeOfEnumIsUserlyingTypeTypeCode()
{
Assert.AreEqual(Type.GetTypeCode(typeof(TestEnum)), TypeCode.Int32);
}
private enum TestEnum
{
A,
B,
}
}

View File

@@ -0,0 +1,209 @@
using System;
using System.Buffers.Binary;
using System.Globalization;
using System.Text;
namespace Snap.Hutao.Test.IncomingFeature;
[TestClass]
public sealed class GeniusInvokationDecoding
{
public TestContext? TestContext { get; set; }
/// <summary>
/// https://www.bilibili.com/video/av278125720
/// </summary>
[TestMethod]
public unsafe void GeniusInvokationShareCodeDecoding()
{
// 51 bytes obfuscated data
byte[] bytes = Convert.FromBase64String("BCHBwxQNAYERyVANCJGBynkOCZER2pgOCrFx8poQChGR9bYQDEGB9rkQDFKRD7oRDeEB");
// ---------------------------------------------
// | Data | Caesar Cipher Key |
// |----------|-------------------|
// | 50 Bytes | 1 Byte |
// ---------------------------------------------
// Data:
// 00000100 00100001 11000001 11000011 00010100
// 00001101 00000001 10000001 00010001 11001001
// 01010000 00001101 00001000 10010001 10000001
// 11001010 01111001 00001110 00001001 10010001
// 00010001 11011010 10011000 00001110 00001010
// 10110001 01110001 11110010 10011010 00010000
// 00001010 00010001 10010001 11110101 10110110
// 00010000 00001100 01000001 10000001 11110110
// 10111001 00010000 00001100 01010010 10010001
// 00001111 10111010 00010001 00001101 11100001
// ---------------------------------------------
// Caesar Cipher Key:
// 00000001
// ---------------------------------------------
fixed (byte* ptr = bytes)
{
// Reinterpret as 50 byte actual data and 1 deobfuscate key byte
EncryptedDataAndKey* data = (EncryptedDataAndKey*)ptr;
byte* dataPtr = data->Data;
// ----------------------------------------------------------
// | First | Second | Padding |
// |-----------|----------|---------|
// | 25 Bytes | 25 Bytes | 1 Byte |
// ----------------------------------------------------------
// We are doing two things here:
// 1. Retrieve actual data by subtracting key
// 2. Store data into two halves by alternating between them
// ----------------------------------------------------------
// What we will get after this step:
// ----------------------------------------------------------
// First:
// 00000011 11000000 00010011 00000000 00010000
// 01001111 00000111 10000000 01111000 00001000
// 00010000 10010111 00001001 01110000 10011001
// 00001001 10010000 10110101 00001011 10000000
// 10111000 00001011 10010000 10111001 00001100
// ----------------------------------------------------------
// Second:
// 00100000 11000010 00001100 10000000 11001000
// 00001100 10010000 11001001 00001101 10010000
// 11011001 00001101 10110000 11110001 00001111
// 00010000 11110100 00001111 01000000 11110101
// 00001111 01010001 00001110 00010000 11100000
// ----------------------------------------------------------
RearrangeBuffer rearranged = default;
byte* pFirst = rearranged.First;
byte* pSecond = rearranged.Second;
for (int i = 0; i < 50; i++)
{
// Determine which half are we going to insert
byte** ppTarget = i % 2 == 0 ? &pFirst : &pSecond;
// (actual data = data - key) and store it directly to the target half
**ppTarget = unchecked((byte)(dataPtr[i] - data->Key));
(*ppTarget)++;
}
// Prepare decoded data result storage
DecryptedData decoded = default;
ushort* pDecoded = decoded.Data;
// ----------------------------------------------------------
// | Data |
// |----------| x 17 = 51 Bytes
// | 3 Bytes |
// ----------------------------------------------------------
// Grouping each 3 bytes and read out as 2 ushort with
// 12 bits each (Big Endian)
// ----------------------------------------------------------
// 00000011 1100·0000 00010011|
// 00000000 0001·0000 01001111|
// 00000111 1000·0000 01111000|
// 00001000 0001·0000 10010111|
// 00001001 0111·0000 10011001|
// 00001001 1001·0000 10110101|
// 00001011 1000·0000 10111000|
// 00001011 1001·0000 10111001|
// 00001100 0010·0000 11000010|
// 00001100 1000·0000 11001000|
// 00001100 1001·0000 11001001|
// 00001101 1001·0000 11011001|
// 00001101 1011·0000 11110001|
// 00001111 0001·0000 11110100|
// 00001111 0100·0000 11110101|
// 00001111 0101·0001 00001110|
// 00010000 1110·0000 -padding|[padding32]
// ----------------------------------------------------------
// reinterpret as DecodeGroupingHelper for each 3 bytes
DecodeGroupingHelper* pGroup = (DecodeGroupingHelper*)&rearranged;
for (int i = 0; i < 17; i++)
{
(ushort first, ushort second) = pGroup->GetData();
*pDecoded = first;
*(pDecoded + 1) = second;
pDecoded += 2;
pGroup++;
}
// Now we get
// 60, 19, 1,
// 79,120,120,
// 129,151,151,
// 153,153,181,
// 184,184,185,
// 185,194,194,
// 200,200,201,
// 201,217,217,
// 219,241,241,
// 244,244,245,
// 245,270,270,
StringBuilder stringBuilder = new();
for (int i = 0; i < 33; i++)
{
stringBuilder
.AppendFormat(CultureInfo.InvariantCulture, "{0,3}", decoded.Data[i])
.Append(',');
if (i % 11 == 10)
{
stringBuilder.Append('\n');
}
}
TestContext?.WriteLine(stringBuilder.ToString(0, stringBuilder.Length - 1));
ushort[] resultArray = new ushort[33];
Span<ushort> result = new((ushort*)&decoded, 33);
result.CopyTo(resultArray);
ushort[] testKnownResult =
[
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
];
CollectionAssert.AreEqual(resultArray, testKnownResult);
}
}
private struct EncryptedDataAndKey
{
public unsafe fixed byte Data[50];
public byte Key;
}
private struct RearrangeBuffer
{
public unsafe fixed byte First[25];
public unsafe fixed byte Second[25];
// Make it 51 bytes
// allow to be group as 17 DecodeGroupingHelper later
public byte padding;
// prevent accidently int32 cast access violation
public byte paddingTo32;
}
private struct DecodeGroupingHelper
{
public unsafe fixed byte Data[3];
public unsafe (ushort First, ushort Second) GetData()
{
fixed (byte* ptr = Data)
{
uint value = BinaryPrimitives.ReverseEndianness((*(uint*)ptr) & 0x00FFFFFF) >> 8; // keep low 24 bits only
return ((ushort)((value >> 12) & 0x0FFF), (ushort)(value & 0x0FFF));
}
}
}
private struct DecryptedData
{
public unsafe fixed ushort Data[33];
}
}

View File

@@ -0,0 +1,43 @@
using System;
namespace Snap.Hutao.Test.IncomingFeature;
[TestClass]
public class SpiralAbyssScheduleIdTest
{
private static readonly TimeSpan Utc8 = new(8, 0, 0);
[TestMethod]
public void Test()
{
Console.WriteLine($"当前第 {GetForDateTimeOffset(DateTimeOffset.Now)} 期");
DateTimeOffset dateTimeOffset = new(2020, 7, 1, 4, 0, 0, Utc8);
Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(dateTimeOffset)} 期");
}
public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset)
{
// Force time in UTC+08
dateTimeOffset = dateTimeOffset.ToOffset(Utc8);
((int year, int mouth, int day), (int hour, _), _) = dateTimeOffset;
// 2020-07-01 04:00:00 为第 1 期
int periodNum = (((year - 2020) * 12) + (mouth - 6)) * 2;
// 上半月1-15 日, 以及 16 日 00:00-04:00
if (day < 16 || (day == 16 && hour < 4))
{
periodNum--;
}
// 上个月1 日 00:00-04:00
if (day is 1 && hour < 4)
{
periodNum--;
}
return periodNum;
}
}

View File

@@ -1,67 +0,0 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Test;
[TestClass]
public class JsonSerializeTest
{
private const string SmapleObjectJson = """
{
"A" :1
}
""";
private const string SmapleEmptyStringObjectJson = """
{
"A" : ""
}
""";
private const string SmapleNumberKeyDictionaryJson = """
{
"111" : "12",
"222" : "34"
}
""";
[TestMethod]
public void DelegatePropertyCanSerialize()
{
Sample sample = JsonSerializer.Deserialize<Sample>(SmapleObjectJson)!;
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 sealed class Sample
{
public int A { get => B; set => B = value; }
public int B { get; set; }
}
private sealed class StringNumberSample
{
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
public int A { get; set; }
}
}

View File

@@ -1,7 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using System;
namespace Snap.Hutao.Test;
namespace Snap.Hutao.Test.PlatformExtensions;
[TestClass]
public sealed class DependencyInjectionTest

View File

@@ -9,11 +9,12 @@ public sealed class ForEachRuntimeBehaviorTest
[TestMethod]
public void ListOfStringCanEnumerateAsReadOnlySpanOfChar()
{
List<string> strings = new()
{
"a", "b", "c"
};
List<string> strings =
#if NET8_0_OR_GREATER
["a", "b", "c"];
#else
new() { "a", "b", "c" };
#endif
int count = 0;
foreach (ReadOnlySpan<char> chars in strings)
{

View File

@@ -8,8 +8,13 @@ public sealed class RangeRuntimeBehaviorTest
[TestMethod]
public void RangeTrimLastOne()
{
#if NET8_0_OR_GREATER
int[] array = [1, 2, 3, 4];
int[] test = [1, 2, 3];
#else
int[] array = { 1, 2, 3, 4 };
int[] test = { 1, 2, 3 };
#endif
int[] result = array[..^1];
Assert.AreEqual(3, result.Length);
Assert.IsTrue(MemoryExtensions.SequenceEqual<int>(test, result));

View File

@@ -1,16 +1,71 @@
namespace Snap.Hutao.Test.RuntimeBehavior;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Test.RuntimeBehavior;
[TestClass]
public sealed class UnsafeRuntimeBehaviorTest
{
[TestMethod]
public unsafe void UInt32AllSetIs()
public unsafe void UInt32AllSetIsUInt32MaxValue()
{
byte[] bytes = { 0xFF, 0xFF, 0xFF, 0xFF, };
byte[] bytes =
#if NET8_0_OR_GREATER
[0xFF, 0xFF, 0xFF, 0xFF];
#else
{ 0xFF, 0xFF, 0xFF, 0xFF, };
#endif
fixed (byte* pBytes = bytes)
{
Assert.AreEqual(uint.MaxValue, *(uint*)pBytes);
}
}
[TestMethod]
public unsafe void UInt32LayoutIsLittleEndian()
{
ulong testValue = 0x1234567887654321;
ref BuildVersion version = ref Unsafe.As<ulong, BuildVersion>(ref testValue);
Assert.AreEqual(0x1234, version.Major);
Assert.AreEqual(0x5678, version.Minor);
Assert.AreEqual(0x8765, version.Patch);
Assert.AreEqual(0x4321, version.Build);
}
[TestMethod]
public unsafe void ReadOnlyStructCanBeModifiedInCtor()
{
TestStruct testStruct = new([4444, 7878, 5656, 1212]);
Assert.AreEqual(4444, testStruct.Value1);
Assert.AreEqual(7878, testStruct.Value2);
Assert.AreEqual(5656, testStruct.Value3);
Assert.AreEqual(1212, testStruct.Value4);
}
private readonly struct TestStruct
{
public readonly int Value1;
public readonly int Value2;
public readonly int Value3;
public readonly int Value4;
public TestStruct(List<int> list)
{
CollectionsMarshal.AsSpan(list).CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.As<TestStruct, int>(ref this), 4));
}
}
private readonly struct BuildVersion
{
public readonly ushort Build;
public readonly ushort Patch;
public readonly ushort Minor;
public readonly ushort Major;
}
}

View File

@@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFrameworks>net8.0</TargetFrameworks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">

View File

@@ -1,5 +1,11 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
"allowMarshaling": true,
"useSafeHandles": false
"useSafeHandles": false,
"comInterop": {
"preserveSigMethods": [
"IFileOpenDialog.Show",
"IFileSaveDialog.Show"
]
}
}

View File

@@ -10,15 +10,18 @@ DwmSetWindowAttribute
GetDeviceCaps
// KERNEL32
AllocConsole
CloseHandle
CreateEventW
CreateRemoteThread
FreeConsole
GetModuleHandleW
GetProcAddress
K32EnumProcessModules
K32GetModuleBaseNameW
K32GetModuleInformation
ReadProcessMemory
SetConsoleTitle
SetEvent
VirtualAlloc
VirtualAllocEx
@@ -28,8 +31,12 @@ WaitForSingleObject
WriteProcessMemory
// OLE32
CoCreateInstance
CoWaitForMultipleObjects
// SHELL32
SHCreateItemFromParsingName
// USER32
AttachThreadInput
FindWindowExW
@@ -46,6 +53,10 @@ SetForegroundWindow
UnregisterHotKey
// COM
FileOpenDialog
FileSaveDialog
IFileOpenDialog
IFileSaveDialog
IPersistFile
IShellLinkW
ShellLink
@@ -56,6 +67,8 @@ IMemoryBufferByteAccess
// Const value
INFINITE
RPC_E_WRONG_THREAD
MAX_PATH
WM_GETMINMAXINFO
WM_HOTKEY
WM_NCRBUTTONDOWN
@@ -63,6 +76,8 @@ WM_NCRBUTTONUP
WM_NULL
// Type & Enum definition
HRESULT_FROM_WIN32
SLGP_FLAGS
// System.Threading
LPTHREAD_START_ROUTINE

View File

@@ -0,0 +1,17 @@
using System;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
namespace Windows.Win32;
internal static partial class PInvoke
{
/// <inheritdoc cref="CoCreateInstance(Guid*, object, CLSCTX, Guid*, out object)"/>
internal static unsafe HRESULT CoCreateInstance<TClass, TInterface>(object? pUnkOuter, CLSCTX dwClsContext, out TInterface ppv)
where TInterface : class
{
HRESULT hr = CoCreateInstance(typeof(TClass).GUID, pUnkOuter, dwClsContext, typeof(TInterface).GUID, out object o);
ppv = (TInterface)o;
return hr;
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -19,18 +19,6 @@ internal static class StructMarshal
/// <returns>新的实例</returns>
public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
{
return new() { length = SizeOf<WINDOWPLACEMENT>() };
}
/// <summary>
/// 获取结构的大小
/// </summary>
/// <typeparam name="TStruct">结构类型</typeparam>
/// <returns>结构的大小</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe uint SizeOf<TStruct>()
where TStruct : unmanaged
{
return unchecked((uint)sizeof(TStruct));
return new() { length = unchecked((uint)sizeof(WINDOWPLACEMENT)) };
}
}

View File

@@ -69,4 +69,4 @@ internal class WinRTCustomMarshaler : ICustomMarshaler
return Marshal.GetObjectForIUnknown(pNativeData);
}
}
}
}

View File

@@ -10,8 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Win32", "Snap.Hutao.Win32\Snap.Hutao.Win32.csproj", "{0F7ABEB2-5107-4037-B9DC-84D288FB0801}"
@@ -52,22 +50,6 @@ 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|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
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.ActiveCfg = Debug|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.Build.0 = Debug|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.Build.0 = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.Build.0 = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.ActiveCfg = Release|x64
{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|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
@@ -105,11 +87,11 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
RESX_ShowErrorsInErrorList = False
RESX_SortFileContentOnSave = True
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
RESX_NeutralResourcesLanguage = zh-CN
RESX_AutoApplyExistingTranslations = False
RESX_NeutralResourcesLanguage = zh-CN
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
RESX_SortFileContentOnSave = True
RESX_ShowErrorsInErrorList = False
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
EndGlobalSection
EndGlobal

View File

@@ -4,12 +4,18 @@
"add": {
"extensionToExtension": {
"add": {
".json": [ ".txt" ]
".json": [
".txt"
]
}
},
"pathSegment": {
"add": {
".*": [ ".cs", ".resx" ]
".*": [
".cs",
".resx",
".appxmanifest"
]
}
},
"fileSuffixToExtension": {
@@ -19,12 +25,24 @@
},
"fileToFile": {
"add": {
".filenesting.json": [ "App.xaml.cs" ],
"app.manifest": [ "App.xaml.cs" ],
"Package.appxmanifest": [ "App.xaml" ],
"Package.StoreAssociation.xml": [ "App.xaml" ],
".editorconfig": [ "Program.cs" ],
"GlobalUsing.cs": [ "Program.cs" ]
".filenesting.json": [
"App.xaml.cs"
],
"app.manifest": [
"App.xaml.cs"
],
"Package.appxmanifest": [
"App.xaml"
],
"Package.StoreAssociation.xml": [
"App.xaml"
],
".editorconfig": [
"Program.cs"
],
"GlobalUsing.cs": [
"Program.cs"
]
}
}
}

View File

@@ -10,6 +10,7 @@
<ResourceDictionary Source="ms-appx:///Control/Image/CachedImage.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Card.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Color.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/ComboBox.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Converter.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/CornerRadius.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/FlyoutStyle.xaml"/>
@@ -20,6 +21,7 @@
<ResourceDictionary Source="ms-appx:///Control/Theme/NumericValue.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/PageOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/SettingsStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>

View File

@@ -20,7 +20,24 @@ namespace Snap.Hutao;
[SuppressMessage("", "SH001")]
public sealed partial class App : Application
{
private const string ConsoleBanner = """
----------------------------------------------------------------
_____ _ _ _
/ ____| | | | | | |
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
____) || | | || (_| || |_) |_ | | | || |_| || |_| (_| || (_) |
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
| |
|_|
Snap.Hutao is a open source software developed by DGP Studio.
Copyright (C) 2022 - 2024 DGP Studio, All Rights Reserved.
----------------------------------------------------------------
""";
private const string AppInstanceKey = "main";
private readonly IServiceProvider serviceProvider;
private readonly IActivation activation;
private readonly ILogger<App> logger;
@@ -51,6 +68,8 @@ public sealed partial class App : Application
if (firstInstance.IsCurrent)
{
logger.LogInformation(ConsoleBanner);
// manually invoke
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
activation.InitializeWith(firstInstance);

View File

@@ -3,12 +3,18 @@
namespace Snap.Hutao.Control.Animation;
/// <summary>
/// 动画时长
/// </summary>
[HighQuality]
internal static class AnimationDurations
internal static class ControlAnimationConstants
{
/// <summary>
/// 1
/// </summary>
public const string One = "1";
/// <summary>
/// 1.1
/// </summary>
public const string OnePointOne = "1.1";
/// <summary>
/// 图片缩放动画
/// </summary>

View File

@@ -19,10 +19,10 @@ internal sealed class ImageZoomInAnimation : ImplicitAnimation<string, Vector3>
/// </summary>
public ImageZoomInAnimation()
{
Duration = AnimationDurations.ImageZoom;
Duration = ControlAnimationConstants.ImageZoom;
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
To = Core.StringLiterals.OnePointOne;
To = ControlAnimationConstants.OnePointOne;
}
/// <inheritdoc/>

View File

@@ -19,10 +19,10 @@ internal sealed class ImageZoomOutAnimation : ImplicitAnimation<string, Vector3>
/// </summary>
public ImageZoomOutAnimation()
{
Duration = AnimationDurations.ImageZoom;
Duration = ControlAnimationConstants.ImageZoom;
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
To = Core.StringLiterals.One;
To = ControlAnimationConstants.One;
}
/// <inheritdoc/>

View File

@@ -35,6 +35,11 @@ internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIEle
private void TryExecuteCommand()
{
if (AssociatedObject is null)
{
return;
}
if (executed)
{
return;

View File

@@ -11,12 +11,17 @@ namespace Snap.Hutao.Control.Behavior;
/// 打开附着的浮出控件操作
/// </summary>
[HighQuality]
internal sealed class OpenAttachedFlyoutAction : DependencyObject, IAction
internal sealed class ShowAttachedFlyoutAction : DependencyObject, IAction
{
/// <inheritdoc/>
public object Execute(object sender, object parameter)
public object? Execute(object sender, object parameter)
{
if (sender is null)
{
return default;
}
FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement);
return default!;
return default;
}
}

View File

@@ -1,21 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control;
/// <summary>
/// 封装的值
/// </summary>
[HighQuality]
internal static class BoxedValues
{
/// <summary>
/// <see cref="true"/>
/// </summary>
public static readonly object True = true;
/// <summary>
/// <see cref="false"/>
/// </summary>
public static readonly object False = false;
}

View File

@@ -35,9 +35,9 @@ internal sealed partial class SegmentedBar : ContentControl
double offset = 0;
foreach (ref readonly IColorSegment segment in CollectionsMarshal.AsSpan(list))
{
collection.Add(new GradientStop() { Color = segment.Color, Offset = offset, });
collection.Add(new() { Color = segment.Color, Offset = offset, });
offset += segment.Value / total;
collection.Add(new GradientStop() { Color = segment.Color, Offset = offset, });
collection.Add(new() { Color = segment.Color, Offset = offset, });
}
}
}

View File

@@ -16,22 +16,7 @@ internal abstract class DependencyValueConverter<TFrom, TTo> : DependencyObject,
/// <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/>

View File

@@ -24,7 +24,7 @@ internal struct ContentDialogHideToken : IDisposable, IAsyncDisposable
if (!disposed && !disposing)
{
disposing = true;
taskContext.InvokeOnMainThread(contentDialog.Hide); // Hide() must be called on main thread.
taskContext.InvokeOnMainThread(contentDialog.Hide);
disposing = false;
disposed = true;
}

View File

@@ -1,16 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using System.Diagnostics;
namespace Snap.Hutao.Web.Bridge;
namespace Snap.Hutao.Control.Extension;
/// <summary>
/// Bridge 拓展
/// </summary>
[HighQuality]
internal static class CoreWebView2Extension
internal static class WebView2Extension
{
[Conditional("RELEASE")]
public static void DisableDevToolsForReleaseBuild(this CoreWebView2 webView)
@@ -37,4 +38,9 @@ internal static class CoreWebView2Extension
manager.DeleteCookie(item);
}
}
public static bool IsDisposed(this WebView2 webView2)
{
return WinRTExtension.IsDisposed(webView2);
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("LeftPanelMaxWidth", typeof(double), IsAttached = true, AttachedType = typeof(ScrollViewer))]
[DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))]
public sealed partial class ScrollViewerHelper
{
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control;
internal interface IScopedPageScopeReferenceTracker
{
IServiceScope CreateScope();
}

View File

@@ -20,7 +20,7 @@ internal sealed class CachedImage : Implementation.ImageEx
public CachedImage()
{
IsCacheEnabled = true;
EnableLazyLoading = true;
EnableLazyLoading = false;
}
/// <inheritdoc/>

View File

@@ -19,7 +19,6 @@
Name="PlaceholderImage"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Opacity="1.0"
Source="{TemplateBinding PlaceholderSource}"
Stretch="{TemplateBinding PlaceholderStretch}"/>
<Image
@@ -27,7 +26,6 @@
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
NineGrid="{TemplateBinding NineGrid}"
Opacity="0.0"
Stretch="{TemplateBinding Stretch}"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">

View File

@@ -30,7 +30,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
private readonly IServiceProvider serviceProvider;
private readonly RoutedEventHandler unloadEventHandler;
private readonly SizeChangedEventHandler sizeChangedEventHandler;
private readonly TypedEventHandler<LoadedImageSurface, LoadedImageSourceLoadCompletedEventArgs> loadedImageSourceLoadCompletedEventHandler;
@@ -46,9 +45,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
serviceProvider = this.ServiceProvider();
this.DisableInteraction();
unloadEventHandler = OnUnload;
Unloaded += unloadEventHandler;
sizeChangedEventHandler = OnSizeChanged;
SizeChanged += sizeChangedEventHandler;
@@ -67,10 +63,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{
}
protected virtual void Unloading()
{
}
/// <summary>
/// 更新视觉对象
/// </summary>
@@ -88,19 +80,22 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
ILogger<CompositionImage> logger = serviceProvider.GetRequiredService<ILogger<CompositionImage>>();
// source is valid
if (arg.NewValue is Uri inner && !string.IsNullOrEmpty(inner.OriginalString))
if (arg.NewValue is Uri inner)
{
// value is different from old one
if (inner != (arg.OldValue as Uri))
if (!string.IsNullOrEmpty(inner.OriginalString))
{
image
.ApplyImageAsync(inner, token)
.SafeForget(logger, ex => OnApplyImageFailed(serviceProvider, inner, ex));
// value is different from old one
if (inner != (arg.OldValue as Uri))
{
image
.ApplyImageAsync(inner, token)
.SafeForget(logger, ex => OnApplyImageFailed(serviceProvider, inner, ex));
}
}
else
{
image.HideAsync(token).SafeForget(logger);
}
}
else
{
image.HideAsync(token).SafeForget(logger);
}
}
@@ -110,7 +105,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
if (exception is HttpRequestException httpRequestException)
{
infoBarService.Error(httpRequestException, SH.ControlImageCompositionImageHttpRequest.Format(uri));
infoBarService.Error(httpRequestException, SH.FormatControlImageCompositionImageHttpRequest(uri));
}
else
{
@@ -138,8 +133,9 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{
imageSurface = await LoadImageSurfaceAsync(file, token).ConfigureAwait(true);
}
catch (COMException)
catch (COMException ex)
{
_ = ex;
imageCache.Remove(uri);
}
catch (IOException)
@@ -171,7 +167,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
surface.LoadCompleted += loadedImageSourceLoadCompletedEventHandler;
if (surface.DecodedPhysicalSize.Size() <= 0D)
{
await surfaceLoadTaskCompletionSource.Task.ConfigureAwait(true);
await Task.WhenAny(surfaceLoadTaskCompletionSource.Task, Task.Delay(5000, token)).ConfigureAwait(true);
}
LoadImageSurfaceCompleted(surface);
@@ -196,7 +192,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{
await AnimationBuilder
.Create()
.Opacity(from: 0D, to: 1D, duration: AnimationDurations.ImageFadeIn)
.Opacity(from: 0D, to: 1D, duration: ControlAnimationConstants.ImageFadeIn)
.StartAsync(this, token)
.ConfigureAwait(true);
}
@@ -217,7 +213,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{
await AnimationBuilder
.Create()
.Opacity(from: 1D, to: 0D, duration: AnimationDurations.ImageFadeOut)
.Opacity(from: 1D, to: 0D, duration: ControlAnimationConstants.ImageFadeOut)
.StartAsync(this, token)
.ConfigureAwait(true);
}
@@ -240,14 +236,4 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
UpdateVisual(spriteVisual);
}
}
private void OnUnload(object sender, RoutedEventArgs e)
{
Unloading();
spriteVisual?.Dispose();
spriteVisual = null;
SizeChanged -= sizeChangedEventHandler;
Unloaded -= unloadEventHandler;
}
}

View File

@@ -45,16 +45,6 @@ internal sealed class MonoChrome : CompositionImage
return compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
}
protected override void Unloading()
{
ActualThemeChanged -= actualThemeChangedEventHandler;
backgroundBrush?.Dispose();
backgroundBrush = null;
base.Unloading();
}
private void OnActualThemeChanged(FrameworkElement sender, object args)
{
if (backgroundBrush is not null)

View File

@@ -27,9 +27,9 @@ internal sealed class DefaultItemCollectionTransitionProvider : ItemCollectionTr
protected override void StartTransitions(IList<ItemCollectionTransition> transitions)
{
List<ItemCollectionTransition> addTransitions = new();
List<ItemCollectionTransition> removeTransitions = new();
List<ItemCollectionTransition> moveTransitions = new();
List<ItemCollectionTransition> addTransitions = [];
List<ItemCollectionTransition> removeTransitions = [];
List<ItemCollectionTransition> moveTransitions = [];
foreach (ItemCollectionTransition transition in addTransitions)
{

View File

@@ -2,13 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Snap.Hutao.Control.Layout;

View File

@@ -118,7 +118,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
Span<double> columnHeights = new double[numberOfColumns];
Span<int> itemsPerColumn = new int[numberOfColumns];
HashSet<int> deadColumns = new();
HashSet<int> deadColumns = [];
for (int i = 0; i < context.ItemCount; i++)
{
@@ -202,11 +202,8 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
{
UniformStaggeredColumnLayout layout = state.GetColumnLayout(columnIndex);
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
for (int i = 0; i < layoutSpan.Length; i++)
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(layout))
{
ref readonly UniformStaggeredItem item = ref layoutSpan[i];
double bottom = item.Top + item.Height;
if (bottom < context.RealizationRect.Top)
{

View File

@@ -9,9 +9,9 @@ namespace Snap.Hutao.Control.Layout;
internal sealed class UniformStaggeredLayoutState
{
private readonly List<UniformStaggeredItem> items = new();
private readonly List<UniformStaggeredItem> items = [];
private readonly VirtualizingLayoutContext context;
private readonly Dictionary<int, UniformStaggeredColumnLayout> columnLayout = new();
private readonly Dictionary<int, UniformStaggeredColumnLayout> columnLayout = [];
private double lastAverageHeight;
public UniformStaggeredLayoutState(VirtualizingLayoutContext context)
@@ -32,7 +32,7 @@ internal sealed class UniformStaggeredLayoutState
{
if (!this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout))
{
columnLayout = new();
columnLayout = [];
this.columnLayout[columnIndex] = columnLayout;
}
@@ -62,11 +62,9 @@ internal sealed class UniformStaggeredLayoutState
}
}
[SuppressMessage("", "SH007")]
internal UniformStaggeredColumnLayout GetColumnLayout(int columnIndex)
{
this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout);
return columnLayout!;
return columnLayout[columnIndex];
}
/// <summary>
@@ -74,6 +72,21 @@ internal sealed class UniformStaggeredLayoutState
/// </summary>
internal void Clear()
{
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
// The first element must be force refreshed otherwise
// it will use the old one realized
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Now we need to refresh the first element of each column
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Finally we need to refresh the whole layout when we reset
if (context.ItemCount > 0)
{
for (int i = 0; i < context.ItemCount; i++)
{
RecycleElementAt(i);
}
}
columnLayout.Clear();
items.Clear();
}

View File

@@ -11,7 +11,6 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
{
public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(Loading), new PropertyMetadata(default(bool), IsLoadingPropertyChanged));
[SuppressMessage("", "IDE0052")]
private FrameworkElement? presenter;
public Loading()

View File

@@ -17,9 +17,6 @@ namespace Snap.Hutao.Control;
[SuppressMessage("", "CA1001")]
internal class ScopedPage : Page
{
// Allow GC to Collect the IServiceScope
private static readonly WeakReference<IServiceScope> PreviousScopeReference = new(default!);
private readonly RoutedEventHandler unloadEventHandler;
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope currentScope;
@@ -31,22 +28,7 @@ internal class ScopedPage : Page
{
unloadEventHandler = OnUnloaded;
Unloaded += unloadEventHandler;
currentScope = Ioc.Default.CreateScope();
DisposePreviousScope();
// track current
PreviousScopeReference.SetTarget(currentScope);
}
/// <summary>
/// 释放上个范围
/// </summary>
public static void DisposePreviousScope()
{
if (PreviousScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
}
/// <summary>

View File

@@ -0,0 +1,41 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control;
/// <summary>
/// By injecting into services, we take dvantage of the fact that
/// IServiceProvider disposes all injected services when it is disposed.
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IScopedPageScopeReferenceTracker))]
internal sealed partial class ScopedPageScopeReferenceTracker : IScopedPageScopeReferenceTracker, IDisposable
{
private readonly IServiceProvider serviceProvider;
private readonly WeakReference<IServiceScope> previousScopeReference = new(default!);
public void Dispose()
{
DisposePreviousScope();
}
public IServiceScope CreateScope()
{
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
IServiceScope currentScope = serviceProvider.CreateScope();
// In case previous one is not disposed.
DisposePreviousScope();
previousScopeReference.SetTarget(currentScope);
return currentScope;
}
private void DisposePreviousScope()
{
if (previousScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
namespace Snap.Hutao.Control;
[DependencyProperty("IsWidthRestricted", typeof(bool), true)]
[DependencyProperty("IsHeightRestricted", typeof(bool), true)]
internal sealed partial class SizeRestrictedContentControl : ContentControl
{
private double minContentWidth;
private double minContentHeight;
protected override Size MeasureOverride(Size availableSize)
{
if (Content is FrameworkElement element)
{
element.Measure(availableSize);
Size contentDesiredSize = element.DesiredSize;
Size contentActualOrDesiredSize = new(
Math.Max(element.ActualWidth, contentDesiredSize.Width),
Math.Max(element.ActualHeight, contentDesiredSize.Height));
if (IsWidthRestricted)
{
if (contentActualOrDesiredSize.Width > minContentWidth)
{
minContentWidth = contentActualOrDesiredSize.Width;
}
element.MinWidth = minContentWidth;
}
if (IsHeightRestricted)
{
if (contentActualOrDesiredSize.Height > minContentHeight)
{
minContentHeight = contentActualOrDesiredSize.Height;
}
element.MinHeight = minContentHeight;
}
}
return base.MeasureOverride(availableSize);
}
}

View File

@@ -2,6 +2,22 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwm="using:CommunityToolkit.WinUI.Media">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
BlurRadius="8"
Opacity="0.14"
Offset="0,4,0"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
BlurRadius="8"
Opacity="0.28"
Offset="0,4,0"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Style x:Key="BorderCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
@@ -14,8 +30,4 @@
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
</Style>
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
Opacity="0.1"
Offset="0,4,0"/>
</ResourceDictionary>

View File

@@ -23,7 +23,7 @@
<SolidColorBrush x:Key="PurpleColorBrush" Color="{ThemeResource PurpleColor}"/>
<SolidColorBrush x:Key="OrangeColorBrush" Color="{ThemeResource OrangeColor}"/>
<SolidColorBrush x:Key="GuaranteePullCoolorBrush" Color="{ThemeResource GuaranteePullColor}"/>
<SolidColorBrush x:Key="GuaranteePullColorBrush" Color="{ThemeResource GuaranteePullColor}"/>
<SolidColorBrush x:Key="UpPullColorBrush" Color="{ThemeResource UpPullColor}"/>
<SolidColorBrush x:Key="DarkOnlyOverlayMaskColorBrush" Color="{ThemeResource DarkOnlyOverlayMaskColor}"/>

View File

@@ -0,0 +1,11 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style
x:Key="CommandBarComboBoxStyle"
BasedOn="{StaticResource DefaultComboBoxStyle}"
TargetType="ComboBox">
<Setter Property="Padding" Value="12,7,0,7"/>
<Setter Property="Height" Value="36"/>
</Style>
<!-- https://github.com/microsoft/microsoft-ui-xaml/issues/4811 -->
<x:Int32 x:Key="__DiscardPageOverride">0</x:Int32>
</ResourceDictionary>

View File

@@ -1,4 +1,5 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<CornerRadius x:Key="ControlCornerRadiusTop">4,4,0,0</CornerRadius>
<CornerRadius x:Key="ControlCornerRadiusBottom">0,0,4,4</CornerRadius>
<CornerRadius x:Key="ControlCornerRadiusTopRightAndBottomLeft">0,4,0,4</CornerRadius>
</ResourceDictionary>

View File

@@ -4,7 +4,6 @@
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
</Style>
<Style

View File

@@ -93,4 +93,19 @@
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<SolidColorBrush x:Key="TextControlTextForeground" Color="{ThemeResource TextFillColorPrimary}"/>
<Thickness x:Key="TextControlTextMargin">1,0</Thickness>
<Style
x:Key="TextControlTextBlockStyle"
BasedOn="{StaticResource BodyTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource TextControlTextForeground}"/>
<Setter Property="Margin" Value="{StaticResource TextControlTextMargin}"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalTextAlignment" Value="Center"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
</ResourceDictionary>

View File

@@ -1,4 +1,5 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- https://learn.microsoft.com/en-us/windows/apps/design/style/segoe-fluent-icons-font -->
<x:String x:Key="FontIconContentAdd">&#xE710;</x:String>
<x:String x:Key="FontIconContentSetting">&#xE713;</x:String>
<x:String x:Key="FontIconContentRefresh">&#xE72C;</x:String>
@@ -12,6 +13,7 @@
<x:String x:Key="FontIconContentBulletedList">&#xE8FD;</x:String>
<x:String x:Key="FontIconContentCheckList">&#xE9D5;</x:String>
<x:String x:Key="FontIconContentWebsite">&#xEB41;</x:String>
<x:String x:Key="FontIconContentQRCode">&#xED14;</x:String>
<x:String x:Key="FontIconContentHomeGroup">&#xEC26;</x:String>
<x:String x:Key="FontIconContentAsteriskBadge12">&#xEDAD;</x:String>
<x:String x:Key="FontIconContentZipFolder">&#xF012;</x:String>

View File

@@ -17,6 +17,9 @@
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing2Template">
<StackPanel Orientation="Horizontal" Spacing="2"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
<StackPanel Spacing="4"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
<cwcont:UniformGrid
ColumnSpacing="2"

View File

@@ -0,0 +1,287 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shch="using:Snap.Hutao.Control.Helper">
<Style x:Key="TwoPanelScrollViewerStyle" TargetType="ScrollViewer">
<Setter Property="HorizontalScrollMode" Value="Auto"/>
<Setter Property="VerticalScrollMode" Value="Auto"/>
<Setter Property="IsHorizontalRailEnabled" Value="True"/>
<Setter Property="IsVerticalRailEnabled" Value="True"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="ZoomMode" Value="Disabled"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="VerticalScrollBarVisibility" Value="Visible"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollViewer">
<Border
x:Name="Root"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid
Grid.RowSpan="2"
Grid.ColumnSpan="2"
Margin="{TemplateBinding Padding}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MaxWidth="{Binding Path=(shch:ScrollViewerHelper.LeftPanelMaxWidth), RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ScrollContentPresenter x:Name="ScrollContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}"/>
<ContentPresenter Grid.Column="1" Content="{Binding Path=(shch:ScrollViewerHelper.RightPanel), RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
</Grid>
<Grid Grid.RowSpan="2" Grid.ColumnSpan="2"/>
<Grid
Grid.Column="1"
Padding="{ThemeResource ScrollViewerScrollBarMargin}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}">
<ScrollBar
x:Name="VerticalScrollBar"
HorizontalAlignment="Right"
IsTabStop="False"
Maximum="{TemplateBinding ScrollableHeight}"
Orientation="Vertical"
ViewportSize="{TemplateBinding ViewportHeight}"
Value="{TemplateBinding VerticalOffset}"/>
</Grid>
<Grid
Grid.Row="1"
Padding="{ThemeResource ScrollViewerScrollBarMargin}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}">
<ScrollBar
x:Name="HorizontalScrollBar"
IsTabStop="False"
Maximum="{TemplateBinding ScrollableWidth}"
Orientation="Horizontal"
ViewportSize="{TemplateBinding ViewportWidth}"
Value="{TemplateBinding HorizontalOffset}"/>
</Grid>
<Border
x:Name="ScrollBarSeparator"
Grid.Row="1"
Grid.Column="1"
Background="{ThemeResource ScrollViewerScrollBarSeparatorBackground}"
Opacity="0"/>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ScrollingIndicatorStates">
<VisualStateGroup.Transitions>
<VisualTransition From="MouseIndicator" To="NoIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition From="MouseIndicatorFull" To="NoIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition From="MouseIndicatorFull" To="MouseIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition From="TouchIndicator" To="NoIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0:0:0.5">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0:0:0.5">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="NoIndicator"/>
<VisualState x:Name="TouchIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>TouchIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>TouchIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="MouseIndicator">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="MouseIndicatorFull">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ScrollBarSeparatorStates">
<VisualStateGroup.Transitions>
<VisualTransition From="ScrollBarSeparatorExpanded" To="ScrollBarSeparatorCollapsed">
<Storyboard>
<DoubleAnimation
BeginTime="{ThemeResource ScrollViewerSeparatorContractBeginTime}"
Storyboard.TargetName="ScrollBarSeparator"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="{ThemeResource ScrollViewerSeparatorContractDuration}"/>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="ScrollBarSeparatorCollapsed"/>
<VisualState x:Name="ScrollBarSeparatorExpanded">
<Storyboard>
<DoubleAnimation
BeginTime="{ThemeResource ScrollViewerSeparatorExpandBeginTime}"
Storyboard.TargetName="ScrollBarSeparator"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="{ThemeResource ScrollViewerSeparatorExpandDuration}"/>
</Storyboard>
</VisualState>
<VisualState x:Name="ScrollBarSeparatorExpandedWithoutAnimation">
<Storyboard>
<DoubleAnimation
BeginTime="{ThemeResource ScrollViewerSeparatorExpandBeginTime}"
Storyboard.TargetName="ScrollBarSeparator"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="ScrollBarSeparatorCollapsedWithoutAnimation">
<Storyboard>
<DoubleAnimation
BeginTime="{ThemeResource ScrollViewerSeparatorContractBeginTime}"
Storyboard.TargetName="ScrollBarSeparator"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -10,25 +10,25 @@
<x:String x:Key="Sponsor_Afadian">https://afdian.net/a/DismissedLight</x:String>
<!-- AvatarCard -->
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://static.snapgenshin.com/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://api.snapgenshin.com/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
<!-- Bg -->
<x:String x:Key="UI_Icon_Intee_Explore_1">https://static.snapgenshin.com/Bg/UI_Icon_Intee_Explore_1.png</x:String>
<x:String x:Key="UI_ImgSign_ItemIcon">https://static.snapgenshin.com/Bg/UI_ImgSign_ItemIcon.png</x:String>
<x:String x:Key="UI_ItemIcon_None">https://static.snapgenshin.com/Bg/UI_ItemIcon_None.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Proce">https://static.snapgenshin.com/Bg/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkTower">https://static.snapgenshin.com/Bg/UI_MarkTower.png</x:String>
<x:String x:Key="UI_Icon_Intee_Explore_1">https://api.snapgenshin.com/static/raw/Bg/UI_Icon_Intee_Explore_1.png</x:String>
<x:String x:Key="UI_ImgSign_ItemIcon">https://api.snapgenshin.com/static/raw/Bg/UI_ImgSign_ItemIcon.png</x:String>
<x:String x:Key="UI_ItemIcon_None">https://api.snapgenshin.com/static/raw/Bg/UI_ItemIcon_None.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Bg/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Bg/UI_MarkTower.png</x:String>
<!-- ItemIcon -->
<x:String x:Key="UI_ItemIcon_201">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_201.png</x:String>
<x:String x:Key="UI_ItemIcon_204">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_204.png</x:String>
<x:String x:Key="UI_ItemIcon_210">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_210.png</x:String>
<x:String x:Key="UI_ItemIcon_220021">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_220021.png</x:String>
<x:String x:Key="UI_ItemIcon_201">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_201.png</x:String>
<x:String x:Key="UI_ItemIcon_204">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_204.png</x:String>
<x:String x:Key="UI_ItemIcon_210">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_210.png</x:String>
<x:String x:Key="UI_ItemIcon_220021">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_220021.png</x:String>
<!-- EmotionIcon -->
<x:String x:Key="UI_EmotionIcon25">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon25.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon272.png</x:String>
<x:String x:Key="UI_EmotionIcon293">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon293.png</x:String>
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
</ResourceDictionary>

View File

@@ -3,18 +3,12 @@
namespace Snap.Hutao.Core.Abstraction;
/// <summary>
/// 指示该类可以解构为元组
/// </summary>
/// <typeparam name="T1">元组的第一个类型</typeparam>
/// <typeparam name="T2">元组的第二个类型</typeparam>
[HighQuality]
internal interface IDeconstruct<T1, T2>
{
/// <summary>
/// 解构
/// </summary>
/// <param name="t1">第一个元素</param>
/// <param name="t2">第二个元素</param>
void Deconstruct(out T1 t1, out T2 t2);
}
internal interface IDeconstruct<T1, T2, T3>
{
void Deconstruct(out T1 t1, out T2 t2, out T3 t3);
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
internal interface IPinnable<TData>
{
ref readonly TData GetPinnableReference();
}

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.IO;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.IO;
using System.Net;
using System.Net.Http;
@@ -17,42 +18,30 @@ namespace Snap.Hutao.Core.Caching;
/// The class's name will become the cache folder's name
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IImageCache))]
[HttpClient(HttpClientConfiguration.Default)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string CacheFolderName = nameof(ImageCache);
// TODO: use FrozenDictionary
private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new()
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = new Dictionary<int, TimeSpan>()
{
[0] = TimeSpan.FromSeconds(4),
[1] = TimeSpan.FromSeconds(16),
[2] = TimeSpan.FromSeconds(64),
};
private readonly ILogger logger;
private readonly IHttpClientFactory httpClientFactory;
private readonly IServiceProvider serviceProvider;
}.ToFrozenDictionary();
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
private readonly IHttpClientFactory httpClientFactory;
private readonly IServiceProvider serviceProvider;
private readonly ILogger<ImageCache> logger;
private string? baseFolder;
private string? cacheFolder;
/// <summary>
/// Initializes a new instance of the <see cref="ImageCache"/> class.
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public ImageCache(IServiceProvider serviceProvider)
{
logger = serviceProvider.GetRequiredService<ILogger<ImageCache>>();
httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
this.serviceProvider = serviceProvider;
}
/// <inheritdoc/>
public void RemoveInvalid()
{
@@ -62,7 +51,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
/// <inheritdoc/>
public void Remove(Uri uriForCachedItem)
{
Remove(new ReadOnlySpan<Uri>(uriForCachedItem));
Remove([uriForCachedItem]);
}
/// <inheritdoc/>
@@ -76,7 +65,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
string folder = GetCacheFolder();
string[] files = Directory.GetFiles(folder);
List<string> filesToDelete = new();
List<string> filesToDelete = [];
foreach (ref readonly Uri uri in uriForCachedItems)
{
string filePath = Path.Combine(folder, GetCacheFileName(uri));
@@ -125,7 +114,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
/// <inheritdoc/>
public ValueFile GetFileFromCategoryAndName(string category, string fileName)
{
Uri dummyUri = Web.HutaoEndpoints.StaticFile(category, fileName).ToUri();
Uri dummyUri = Web.HutaoEndpoints.StaticRaw(category, fileName).ToUri();
return Path.Combine(GetCacheFolder(), GetCacheFileName(dummyUri));
}
@@ -191,7 +180,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
case HttpStatusCode.TooManyRequests:
{
retryCount++;
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount];
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
logger.LogInformation("Retry {Uri} after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;

View File

@@ -12,7 +12,8 @@ namespace Snap.Hutao.Core;
internal sealed class CommandLineBuilder
{
private const char WhiteSpace = ' ';
private readonly Dictionary<string, string?> options = new();
private readonly Dictionary<string, string?> options = [];
/// <summary>
/// 当符合条件时添加参数

View File

@@ -38,7 +38,11 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
return;
}
// TODO: Troubeshooting why the serviceProvider will NRE
if (serviceProvider.IsDisposedSlow())
{
return;
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
@@ -92,7 +96,11 @@ internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
return;
}
// TODO: Troubeshooting why the serviceProvider will NRE
if (serviceProvider.IsDisposedSlow())
{
return;
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();

View File

@@ -3,6 +3,12 @@
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
/// <summary>
/// 由于 AddHttpClient 不支持 KeyedService, 所以使用工厂模式
/// </summary>
/// <typeparam name="TClient">抽象类型</typeparam>
/// <typeparam name="TClientCN">官服/米游社类型</typeparam>
/// <typeparam name="TClientOS">国际/HoYoLAB类型</typeparam>
internal abstract class OverseaSupportFactory<TClient, TClientCN, TClientOS> : IOverseaSupportFactory<TClient>
where TClientCN : notnull, TClient
where TClientOS : notnull, TClient

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Windows.ApplicationModel.Resources;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Service;
using System.Globalization;
@@ -25,7 +24,7 @@ internal static class DependencyInjection
ServiceProvider serviceProvider = new ServiceCollection()
// Microsoft extension
.AddLogging(builder => builder.AddUnconditionalDebug())
.AddLogging(builder => builder.AddDebug().AddConsoleWindow())
.AddMemoryCache()
// Hutao extensions
@@ -36,11 +35,11 @@ internal static class DependencyInjection
// Discrete services
.AddSingleton<IMessenger, WeakReferenceMessenger>()
.BuildServiceProvider(true);
Ioc.Default.ConfigureServices(serviceProvider);
serviceProvider.InitializeConsoleWindow();
serviceProvider.InitializeCulture();
return serviceProvider;
@@ -54,8 +53,18 @@ internal static class DependencyInjection
CultureInfo cultureInfo = appOptions.CurrentCulture;
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo;
ApplicationLanguages.PrimaryLanguageOverride = cultureInfo.Name;
SH.Culture = cultureInfo;
}
private static void InitializeConsoleWindow(this IServiceProvider serviceProvider)
{
_ = serviceProvider.GetRequiredService<ConsoleWindowLifeTime>();
}
}

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