Compare commits

..

355 Commits
1.3.3 ... 1.6.4

Author SHA1 Message Date
Masterain
099c5d3f6d Update SECURITY.md 2023-05-29 00:28:50 -07:00
DismissedLight
5f15c05e3d Merge pull request #736 from DGP-Studio/l10n_main
New Crowdin updates
2023-05-27 14:33:54 +08:00
Masterain
de95fd6419 New translations SH.resx (English) 2023-05-26 23:17:16 -07:00
Masterain
bd0901a8ab New translations SH.resx (Chinese Traditional) 2023-05-26 23:17:15 -07:00
Masterain
84fa2dadcc New translations SH.resx (Korean) 2023-05-26 23:17:14 -07:00
DismissedLight
e204b65afc Merge pull request #738 from Masterain98/main
fix #737 Localization Update
2023-05-27 09:47:55 +08:00
Masterain98
9c71c4ffcc Automatic Config File Push 2023-05-27 00:13:32 +00:00
Masterain
62694be22b Localization update 2023-05-26 14:20:00 -07:00
Masterain
20f16ceb3e New translations SH.resx (English) 2023-05-26 06:04:04 -07:00
Masterain
a1e8f90710 New translations SH.resx (Chinese Traditional) 2023-05-26 06:04:03 -07:00
Masterain
0d9baa2cac New translations SH.resx (Korean) 2023-05-26 06:04:02 -07:00
DismissedLight
96b3e6d092 Merge pull request #734 from DGP-Studio/fix-unlock-fps
Fix unlock fps
2023-05-26 16:22:14 +08:00
Lightczx
4178f1b89b fix unlock fps 2023-05-26 16:20:12 +08:00
Lightczx
e44a20f202 Try to fix unlock fps 2023-05-25 20:50:37 +08:00
Masterain98
a12874d114 Automatic Config File Push 2023-05-25 00:12:55 +00:00
Lightczx
c7e3b18d62 fix 3.7 metadata crash 2023-05-23 13:13:41 +08:00
Lightczx
b607bbf819 Update .gitignore 2023-05-21 21:59:49 +08:00
Lightczx
5b16313b65 Delete desktop.ini 2023-05-21 21:58:40 +08:00
Lightczx
577aed0f80 Typo 2023-05-21 21:27:10 +08:00
Masterain98
39a3e6eb37 Automatic Config File Push 2023-05-20 04:14:35 +00:00
Lightczx
96f503d30c Introducing Elevation Badge 2023-05-17 20:58:40 +08:00
Lightczx
f104554661 remove installer 2023-05-16 22:45:09 +08:00
DismissedLight
451823ebf9 Merge pull request #723 from Xhichn/Dev
Improve HoYoLAB API request
2023-05-16 16:21:39 +08:00
Lightczx
7346c5eb4e typo 2023-05-16 16:20:41 +08:00
Lightczx
73f0e356c4 move InitializeCulture 2023-05-16 16:14:49 +08:00
Lightczx
168bed4b2c shell interop 2023-05-16 15:32:44 +08:00
Lightczx
d634eb6818 Unify Encoding to UIT-8 with BOM 2023-05-15 15:43:03 +08:00
DismissedLight
0cb9e59b8a Merge pull request #725 from DGP-Studio/feat/uigf_v2.3
fully support UIGF v2.3
2023-05-15 12:38:57 +08:00
Lightczx
ca10afa25a fully support UIGF v2.3 2023-05-15 12:37:46 +08:00
Lightczx
5fe38f305b refine game service 2023-05-14 22:43:50 +08:00
Lightczx
46f58730dc fix # 722 2023-05-14 22:20:45 +08:00
Xhichn
d144859d94 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-05-14 21:55:18 +08:00
Xhichn
40439aea8a Removed some unused endpoints 2023-05-14 21:52:46 +08:00
Lightczx
d54828ff55 fix #720 2023-05-14 21:15:22 +08:00
Xhichn
7e0c0fa5bf Change hoyolab api host 2023-05-14 17:49:03 +08:00
Xhichn
8312c7c88b Update hoyolab requests 2023-05-14 12:24:27 +08:00
Lightczx
456d87003f fix #709 2023-05-13 22:37:11 +08:00
Lightczx
7a63013dfd fix #719 2023-05-13 20:47:49 +08:00
Lightczx
15c3019576 fix jumplist launch 2023-05-11 22:13:06 +08:00
Lightczx
c15e948659 增加米奇妙妙工具 2023-05-11 18:17:10 +08:00
Lightczx
c5a4226662 fix scheme displayname 2023-05-10 22:12:02 +08:00
DismissedLight
0f9b906b7b Merge pull request #712 from DGP-Studio/l10n_main
New Crowdin updates
2023-05-10 21:26:39 +08:00
Masterain
f55dc038e2 New translations SH.resx (English) 2023-05-10 06:26:16 -07:00
Masterain
8540c1eebc New translations SH.resx (English) 2023-05-10 06:24:15 -07:00
Masterain
8f71994574 New translations SH.resx (Chinese Traditional) 2023-05-10 06:24:14 -07:00
Masterain
fe3e835211 New translations SH.resx (Korean) 2023-05-10 06:24:13 -07:00
Lightczx
6b5477ba44 1.16.3 release candidate 1 2023-05-10 21:19:26 +08:00
Lightczx
e147b8773f support dynamically change game fps 2023-05-10 19:52:22 +08:00
Masterain98
b386a35f07 Automatic Config File Push 2023-05-10 05:47:11 +00:00
Lightczx
42a19239e6 fix launch scheme detection 2023-05-10 13:44:09 +08:00
Masterain98
404cd9d705 Automatic Config File Push 2023-05-10 00:17:39 +00:00
Lightczx
33935dabc2 code style 2023-05-06 22:43:09 +08:00
Lightczx
5e04c51456 Address several db sync issues 2023-05-06 15:34:40 +08:00
Lightczx
50c5bb44ff auto dependency injection 2023-05-06 13:05:19 +08:00
Lightczx
60641c3f57 fix GetCharacterAsync 2023-05-01 15:47:58 +08:00
Lightczx
40753177cb Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-05-01 10:29:14 +08:00
Lightczx
ba2f2d5708 fix gacha fetch 2023-05-01 10:29:11 +08:00
Masterain
6f5a65c0e3 Update azure-pipelines.yml 2023-04-30 13:05:13 -07:00
Lightczx
20f353d9eb fix avatar property 2023-04-30 21:05:07 +08:00
DismissedLight
1a1a865b8d Merge pull request #700 from DGP-Studio/refactor/application-model
Refactor Application Model
2023-04-30 20:28:10 +08:00
Lightczx
7b3b1f1317 remove all ThreadHelper usage 2023-04-30 20:26:07 +08:00
Lightczx
0891c3f521 fixup all services 2023-04-29 20:54:17 +08:00
Lightczx
9657df36c2 Generate Enum Localization 2023-04-26 21:43:21 +08:00
Lightczx
9a54df4163 remove CoreEnvironment 2023-04-24 19:34:58 +08:00
Lightczx
8ac410fb4d miyoushe 2.49.1 2023-04-23 13:44:09 +08:00
Lightczx
538a076b69 increase boot speed 2023-04-20 19:11:40 +08:00
Lightczx
7db14a9c2a config utf-8 bom 2023-04-20 17:32:33 +08:00
Masterain
0e710f92d2 fix pipelines env 2023-04-20 02:24:08 -07:00
Masterain
fec2dc6c99 Use dedicated build sever 2023-04-20 02:19:24 -07:00
Lightczx
bdb40aca6a daily note card 2023-04-20 15:54:57 +08:00
Masterain
c90f147564 i10n refinement 2023-04-19 20:04:43 -07:00
DismissedLight
2b36b01145 Merge pull request #690 from Xhichn/SignIn
Feat: Daily check-in support for hoyolab user
2023-04-19 21:28:07 +08:00
Lightczx
d876f269fd typo 2023-04-19 21:27:35 +08:00
Lightczx
729f6717e1 Update GachaStatisticsCard.xaml 2023-04-19 19:06:27 +08:00
Xhichn
b5371a9656 Use seperated js interface for hoyolab check-in 2023-04-19 18:23:45 +08:00
Lightczx
04dae7ccd8 achievement card 2023-04-19 18:22:30 +08:00
Xhichn
a5fcfca609 Feat: Daily check-in support for hoyolab user 2023-04-19 13:18:01 +08:00
Lightczx
aee5271a2d more ref-like parameters 2023-04-18 21:57:13 +08:00
Lightczx
89fe93b3eb use ref-like keyword to accelerate runtime speed 2023-04-18 18:54:43 +08:00
Lightczx
dc38def97c fixup cookie creation key collision 2023-04-18 11:24:56 +08:00
Lightczx
2432b1ec5d fix #675 2023-04-17 12:41:06 +08:00
Lightczx
97c7671595 use hoyoverse instead of hoyolab to login 2023-04-17 12:00:48 +08:00
Lightczx
b8895d8250 update to was 1.3 and fix upload spiralabyss 2023-04-14 15:48:00 +08:00
Lightczx
cb882ab062 Update GachaLogViewModelSlim.cs 2023-04-12 22:36:01 +08:00
Lightczx
27c7875c26 refactor gachalog service 2023-04-12 22:34:49 +08:00
Lightczx
3cf505d9b2 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-04-11 18:42:47 +08:00
Lightczx
4407166005 temp state 2023-04-11 18:42:44 +08:00
DismissedLight
7b6e63a932 Merge pull request #671 from DGP-Studio/l10n_main
New Crowdin updates
2023-04-11 18:41:48 +08:00
Masterain
4a452c205e New translations SH.resx (English) 2023-04-11 01:37:14 -07:00
Masterain
4dd2ba3c6d New translations SH.resx (English) 2023-04-11 01:28:25 -07:00
Masterain
1e7155a902 New translations SH.resx (Chinese Traditional) 2023-04-11 01:28:24 -07:00
Masterain
129a1a7fa8 New translations SH.resx (Korean) 2023-04-11 01:28:24 -07:00
Masterain
f37f4fe37d Update SH.resx 2023-04-11 01:27:38 -07:00
DismissedLight
0df746e4c6 Merge pull request #670 from Masterain98/main
WIKI Pages Localization
2023-04-11 15:39:26 +08:00
Masterain
285f788015 fix typo 2023-04-11 00:37:54 -07:00
Masterain
d0525dd814 WIKI pages localization 2023-04-11 00:25:38 -07:00
Masterain
4372eb0ded =w= 2023-04-10 18:09:04 -07:00
Masterain98
15e6964340 Automatic Config File Push 2023-04-11 00:58:30 +00:00
Lightczx
e5c5c1c95c improve cloud service experience 2023-04-08 22:09:07 +08:00
DismissedLight
a4f555ea40 Merge pull request #663 from DGP-Studio/l10n_main
New Crowdin updates
2023-04-08 16:19:14 +08:00
Masterain
1ea0a14926 New translations SH.resx (English) 2023-04-08 01:17:53 -07:00
Masterain
a26b6fc528 New translations SH.resx (Chinese Traditional) 2023-04-08 01:17:52 -07:00
Masterain
e4523d540c New translations SH.resx (Korean) 2023-04-08 01:17:51 -07:00
Lightczx
6a309321f6 source generate reliquary score weight 2023-04-08 16:17:16 +08:00
Lightczx
512b1257a8 fix DbCurrent 2023-04-07 18:44:08 +08:00
Lightczx
d5b668cb8d fix cloud gacha download crash 2023-04-07 16:41:57 +08:00
Lightczx
32177491da gacha log upload 2023-04-07 15:08:20 +08:00
Lightczx
e5012d9051 improve welcome download experience 2023-04-06 15:12:51 +08:00
Chen Hill
cad1182ade Remove BITS 2023-04-05 13:03:33 +08:00
DismissedLight
e58d982e72 Merge pull request #652 from Xhichn/main
Fix hoyolab userinfo request
2023-04-05 11:12:01 +08:00
xhichn
9749c6c342 Fix hoyolab userinfo request 2023-04-05 10:02:08 +08:00
DismissedLight
179b78ca83 Launch Game QoL 2023-04-04 18:44:19 +08:00
DismissedLight
79118cdb4d fix #649 2023-04-04 16:53:01 +08:00
DismissedLight
35e7aaef4e fix HTTPS usage 2023-04-03 21:17:52 +08:00
DismissedLight
97f5904efa fix cn login 2023-03-31 13:09:03 +08:00
DismissedLight
50112cb3f6 Merge pull request #634 from Masterain98/main
Update PWA assets
2023-03-30 13:12:19 +08:00
DismissedLight
3dd7a8c85f fix HoYoLAB webview login 2023-03-30 13:07:15 +08:00
Masterain
ca06fe7b02 Remove badge logo assets 2023-03-29 17:43:57 -07:00
DismissedLight
04e9f7db72 Merge pull request #637 from DGP-Studio/l10n_main
New Crowdin updates
2023-03-29 18:41:33 +08:00
Masterain
7facf43332 New translations SH.resx (English) 2023-03-29 03:40:37 -07:00
Masterain
bc7cf93137 New translations SH.resx (Chinese Traditional) 2023-03-29 03:40:36 -07:00
Masterain
877dd93bad New translations SH.resx (Korean) 2023-03-29 03:40:35 -07:00
DismissedLight
98b3f2d202 add hutao passport 2023-03-29 17:35:43 +08:00
DismissedLight
0a7bd55dab remove using statements 2023-03-28 21:26:18 +08:00
DismissedLight
e9a8e906da Merge pull request #635 from DGP-Studio/l10n_main
New Crowdin updates
2023-03-28 18:06:03 +08:00
Masterain
93523c8832 New translations SH.resx (English) 2023-03-28 02:50:46 -07:00
Masterain
541455e51d New translations SH.resx (Chinese Traditional) 2023-03-28 02:50:45 -07:00
Masterain
cdc48a1a4f New translations SH.resx (Korean) 2023-03-28 02:50:43 -07:00
DismissedLight
ab65a62c11 fix hoyolab user creation 2023-03-28 16:43:37 +08:00
Masterain
9e90cfb1c8 Update PWA assets 2023-03-27 23:09:01 -07:00
DismissedLight
b1ace71648 fixup dependency injection 2023-03-27 20:08:00 +08:00
DismissedLight
015c731df0 Merge pull request #608 from Xhichn/main
Add basic support for hoyoverse account
2023-03-27 19:03:53 +08:00
DismissedLight
8ea4411e2a phase final 2023-03-27 19:02:09 +08:00
DismissedLight
f90b828bb4 phase 3 2023-03-27 18:39:02 +08:00
DismissedLight
97cbe7cf55 phase 2 2023-03-27 16:17:36 +08:00
DismissedLight
09abb46159 fixup some code style phase 1 2023-03-25 21:26:17 +08:00
Xhichn
523374ed3d Upload spiral abyss record for hoyolab user & clean 2023-03-24 21:44:05 +08:00
Xhichn
9decb67cff Merge branch 'DGP-Studio:main' into main 2023-03-24 20:24:37 +08:00
DismissedLight
af26c06cfc Merge pull request #623 from Masterain98/main
Update Project Information
2023-03-23 22:42:49 +08:00
DismissedLight
b550b91cc9 update banner 2023-03-23 22:24:19 +08:00
DismissedLight
2ba6377088 update bug report template 2023-03-23 12:47:01 +08:00
DismissedLight
18f8137f41 fix cookie token 2023-03-22 18:24:48 +08:00
Xhichn
32ae92e49d Login from hoyolab account page to get stoken 2023-03-22 18:13:55 +08:00
Xhichn
805fd31bf8 Login from hoyolab account website to get stoken 2023-03-22 17:44:56 +08:00
Masterain
b65e0c94f0 Update Project Information 2023-03-22 02:07:06 -07:00
DismissedLight
7e3d7e9076 remove MemoryExtension [skip ci] 2023-03-21 13:16:09 +08:00
DismissedLight
2c162d1fef fix #616 [skip ci] 2023-03-20 12:36:49 +08:00
DismissedLight
bfacf42d71 refactor user initialization 2023-03-19 21:57:49 +08:00
DismissedLight
eeee171b78 fix #611 2023-03-19 20:45:36 +08:00
DismissedLight
9861a3df37 improve fps unlocker 2023-03-19 12:27:17 +08:00
Masterain
84b9a9de23 Update azure-pipelines.yml for Azure Pipelines 2023-03-18 02:52:38 -07:00
DismissedLight
11828bd280 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-03-18 16:42:46 +08:00
DismissedLight
b29d66d6b6 fix ci build 2023-03-18 16:42:35 +08:00
Masterain
a25d1ba4ce Update azure-pipelines.yml for Azure Pipelines 2023-03-18 01:30:31 -07:00
DismissedLight
5505927ca1 fix nuget sources 2023-03-18 16:00:58 +08:00
DismissedLight
d3a60451a4 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-03-18 14:45:40 +08:00
DismissedLight
915635a843 fix material 2023-03-18 14:44:24 +08:00
Masterain
918e84be61 Update azure-pipelines.yml for Azure Pipelines 2023-03-17 19:56:09 -07:00
DismissedLight
a8a3354217 Add IsOverSea on User Entity 2023-03-17 23:20:47 +08:00
Xhichn
b6f6c5d54b Merge branch 'DGP-Studio:main' into main 2023-03-16 20:15:43 +08:00
DismissedLight
a9e0e3db39 Update README.md [skip ci] 2023-03-16 18:52:49 +08:00
DismissedLight
ece3f2cd08 migrate to CommunityToolkit.Labs.WinUI.SettingsControls 2023-03-16 18:22:30 +08:00
Xhichn
7612a2e7c0 Merge branch 'DGP-Studio:main' into main 2023-03-15 23:40:21 +08:00
Xhichn
eee34ec4c7 Merge branch 'main' of https://github.com/Xhichn/Snap.Hutao 2023-03-15 23:38:58 +08:00
Xhichn
e24a7436b4 Add daily reward claim support for hoyolab user & fix typo 2023-03-15 23:38:42 +08:00
Xhichn
a543bf3091 Fix dailynote notification for hoyoverse account, block some unsupported operations 2023-03-15 22:39:42 +08:00
DismissedLight
1d88360528 fix app options 2023-03-15 20:13:24 +08:00
DismissedLight
f21d04fadc Merge pull request #604 from GashByte/main
add 'Enabled Advanced' option to setting page
2023-03-15 19:15:02 +08:00
x3zF Love U
0cd6bf95a9 change the field name of 'EnabledAdvanced' 2023-03-15 19:12:41 +08:00
x3zF Love U
4de01d2f62 add 'Enabled Advanced' option to setting page 2023-03-15 19:04:55 +08:00
DismissedLight
05d226ad15 move viewmodels 2023-03-15 18:28:14 +08:00
Xhichn
eabf2a9ea8 Merge branch 'DGP-Studio:main' into main 2023-03-15 14:56:38 +08:00
DismissedLight
e796afbbb0 Merge pull request #603 from HolographicHat/main
Use exclusive access instead of renaming
2023-03-15 14:27:36 +08:00
Xhichn
d6099f10ad Merge branch 'DGP-Studio:main' into main 2023-03-15 13:49:00 +08:00
Xhichn
f837b39ecf Refresh gacha log by stoken is unsupported for oversea players currently 2023-03-15 13:43:03 +08:00
HolographicHat
770cabce81 Update ProcessInterop.cs 2023-03-15 00:23:14 +08:00
HolographicHat
cec4b2a23a Use exclusive access instead of renaming 2023-03-14 15:53:28 +00:00
DismissedLight
c60748626a Create .github/dependabot.yml 2023-03-14 16:33:03 +08:00
Xhichn
51d8ee5d6e Add some auth endpoints 2023-03-14 16:29:51 +08:00
Xhichn
64c020a5fb Support get avatars info from hoyolab dev calc & add some apis 2023-03-14 16:02:10 +08:00
Xhichn
934fb75fa5 Merge branch 'Globalization' 2023-03-13 21:26:18 +08:00
DismissedLight
85b9fc1a08 fix setting restart 2023-03-13 21:01:55 +08:00
Xhichn
314f86966a Remove unnecessary cookie method 2023-03-13 19:36:28 +08:00
DismissedLight
5aba2eab97 support multi-clienting 2023-03-13 19:13:35 +08:00
Xhichn
fdf2311f0c Support website login for adding hoyoverse account 2023-03-13 12:36:38 +08:00
Xhichn
b0b3553d0c Support daily notes for global server player 2023-03-13 12:16:38 +08:00
Xhichn
388cdf1848 Support sync from hoyolab my characters 2023-03-13 11:41:18 +08:00
xhichn
22f4f411ea Support spiral abyss info request for global server players 2023-03-12 23:26:00 +08:00
xhichn
b89c66fd6b Support adding hoyoverse accounts by cookie input 2023-03-12 23:25:25 +08:00
xhichn
0295d4fc22 Add client config and DS salt for oversea server request 2023-03-12 23:21:52 +08:00
xhichn
d85811ee99 Add some hoyolab endpoints 2023-03-12 23:20:09 +08:00
DismissedLight
dc8d7ac913 Merge pull request #598 from GashByte/main
Add 'Mult-Start' & fix a start game bug
2023-03-12 22:32:23 +08:00
x3zF Love U
a832ea96ea repair 'LaunchMultipleInstancesGameAsync' 2023-03-12 22:24:29 +08:00
x3zF Love U
29582efaee reduce the Task waiting time of mult-start 2023-03-12 18:15:40 +08:00
x3zF Love U
6caeb1d238 Add 'Mult-Start' & fix a start game bug 2023-03-12 16:32:59 +08:00
DismissedLight
439a8dd475 Merge pull request #594 from GashByte/main
add 'Copy' Button to 'SettingPage - Device Id'
2023-03-11 16:04:56 +08:00
x3zF Love U
60c7e65abb add 'Copy' Button to 'SettingPage - Device Id' 2023-03-11 15:42:21 +08:00
DismissedLight
5c0984b064 rescale assets [skip ci] 2023-03-11 13:19:45 +08:00
DismissedLight
9b8cce30a7 fix package logo 2023-03-11 13:02:26 +08:00
DismissedLight
463a842cb1 update assets 2023-03-10 21:48:14 +08:00
DismissedLight
8ee2908633 launch screenshot folder 2023-03-10 20:32:49 +08:00
DismissedLight
c71ecd89e3 support game resource download switch 2023-03-07 16:28:00 +08:00
DismissedLight
128b985609 fix game resource convert 2023-03-06 11:29:53 +08:00
DismissedLight
b1a03662d9 launch game impl switch monitor 2023-03-05 18:47:36 +08:00
DismissedLight
706fb3404b refactor localization 2023-03-03 16:49:27 +08:00
DismissedLight
63e1273d6a Merge pull request #582 from DGP-Studio/l10n_main
New Crowdin updates
2023-03-03 15:33:45 +08:00
Masterain
400a3b99ae New translations SH.resx (English) 2023-03-02 23:31:00 -08:00
Masterain
c0c1774db8 New translations SH.resx (Chinese Traditional) 2023-03-02 23:30:59 -08:00
Masterain
099fbf4052 New translations SH.resx (Korean) 2023-03-02 23:30:58 -08:00
Masterain
5c4405e545 Update Crowdin configuration file 2023-03-02 23:30:30 -08:00
DismissedLight
ac68579d6a fix #567 2023-02-28 12:35:49 +08:00
DismissedLight
f3387bb8c8 fix #566 2023-02-28 11:52:43 +08:00
DismissedLight
e50c1b9184 monster wiki fullfilled 2023-02-25 19:12:38 +08:00
DismissedLight
54535cd822 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-24 13:54:47 +08:00
DismissedLight
ece2737633 avatar & weapon level slider 2023-02-24 13:54:43 +08:00
Masterain
4b012424b9 Update bug-report.yml 2023-02-23 11:22:24 -08:00
DismissedLight
6d66af6c84 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-20 19:39:21 +08:00
DismissedLight
027874d4cf adjust avatar reliquary score weight 2023-02-20 19:38:48 +08:00
Masterain
3001936ab3 Update README.md
- optimze image size
- update i18n URL
2023-02-20 01:03:29 -08:00
DismissedLight
400e097fa7 support language switch 2023-02-20 16:04:23 +08:00
DismissedLight
ffce055d75 Merge pull request #539 from DGP-Studio/l10n_main
New Crowdin updates
2023-02-20 14:40:11 +08:00
Masterain
a820c41ad7 New translations SH.resx (Korean) 2023-02-19 22:34:43 -08:00
Masterain
485010c895 New translations SH.resx (English) 2023-02-19 22:34:42 -08:00
Masterain
5feddf566e New translations SH.resx (Chinese Traditional) 2023-02-19 22:34:41 -08:00
Masterain
cdfe306b16 New translations SH.resx (Russian) 2023-02-19 22:34:40 -08:00
Masterain
189c61ddea New translations SH.resx (Japanese) 2023-02-19 22:34:39 -08:00
Masterain
0ac7d6e94d Update PublishDistribution.yml 2023-02-18 02:18:40 -08:00
DismissedLight
042e3b5747 fix #527 2023-02-18 12:51:42 +08:00
DismissedLight
0372f1a8e3 fix #525 2023-02-18 12:50:10 +08:00
DismissedLight
08a630fd43 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-17 13:50:07 +08:00
DismissedLight
3781cad896 make all type internal 2023-02-17 13:49:37 +08:00
Masterain
78bc2f09d5 Create SECURITY.md 2023-02-15 02:17:43 -08:00
DismissedLight
8c52921b3e refactor codebase 2023-02-14 14:30:59 +08:00
DismissedLight
605aecb216 remove strings 2023-02-13 14:51:18 +08:00
DismissedLight
e3e124d52f Merge pull request #500 from DGP-Studio/l10n_main
New Crowdin updates
2023-02-13 14:48:40 +08:00
Masterain
0866e1947b New translations SH.resx (English) 2023-02-12 22:47:53 -08:00
Masterain
dbcb7dd879 New translations SH.resx (Chinese Traditional) 2023-02-12 22:47:52 -08:00
Masterain
44687dd87b New translations SH.resx (English) 2023-02-12 21:59:02 -08:00
Masterain
1a209f6c8d New translations SH.resx (Chinese Traditional) 2023-02-12 21:59:01 -08:00
Masterain
8633b78725 New translations SH.resx (Russian) 2023-02-12 21:59:00 -08:00
Masterain
2e20701c6c New translations SH.resx (Japanese) 2023-02-12 21:58:59 -08:00
DismissedLight
9c4d4cda1e fix localization 2023-02-13 13:51:15 +08:00
DismissedLight
a32481980b code style [skip ci] 2023-02-12 16:18:17 +08:00
DismissedLight
b5577e76a5 add hint for #493 2023-02-12 16:15:30 +08:00
DismissedLight
6c2ff9b3c9 fix process name detection 2023-02-11 18:32:00 +08:00
DismissedLight
818365b816 fix #486 2023-02-11 12:32:00 +08:00
DismissedLight
d7dd8c6f0d code style 2023-02-10 16:07:01 +08:00
DismissedLight
faad104e0e fix import thread issue 2023-02-10 11:57:55 +08:00
DismissedLight
2f6ee75f80 Merge pull request #479 from DGP-Studio/l10n_main
New Crowdin updates
2023-02-09 20:32:35 +08:00
Masterain
34f319bdac New translations SH.resx (English) 2023-02-09 04:32:02 -08:00
Masterain
f242808768 New translations SH.resx (Chinese Traditional) 2023-02-09 04:32:01 -08:00
DismissedLight
98f18f91d8 use WScript.Shell to run scheduled tasks 2023-02-09 19:23:24 +08:00
Masterain
0fd1f6959a New translations SH.resx (English) 2023-02-08 20:27:42 -08:00
Masterain
ba46ed64db New translations SH.resx (Chinese Traditional) 2023-02-08 20:27:41 -08:00
Masterain
0fb8312605 New translations SH.resx (Russian) 2023-02-08 20:27:40 -08:00
Masterain
b722554950 New translations SH.resx (Japanese) 2023-02-08 20:27:39 -08:00
DismissedLight
165c33ef2c fix translation 2023-02-09 12:26:42 +08:00
Masterain
54bb3d634b New translations SH.resx (English) 2023-02-08 20:26:02 -08:00
Masterain
629975480a New translations SH.resx (Chinese Traditional) 2023-02-08 20:26:01 -08:00
Masterain
5a36448c23 New translations SH.resx (Russian) 2023-02-08 20:26:00 -08:00
Masterain
80a6aaab46 New translations SH.resx (Japanese) 2023-02-08 20:25:58 -08:00
Masterain
6c83cd3da5 Update azure-pipelines.yml for Azure Pipelines 2023-02-07 21:54:23 -08:00
DismissedLight
e60a04a2bc impl #117 2023-02-08 12:28:31 +08:00
DismissedLight
aec483510f fix #460 2023-02-08 10:07:10 +08:00
DismissedLight
c245fe654e add gacha import validation 2023-02-07 16:57:53 +08:00
DismissedLight
898d95bb1d add more globalization strings 2023-02-07 15:36:50 +08:00
DismissedLight
1df22e5b75 fix L10n issues 2023-02-07 14:44:36 +08:00
DismissedLight
332e09fef0 fix #439 2023-02-07 13:41:53 +08:00
DismissedLight
2a77daf2ca Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-07 10:21:51 +08:00
DismissedLight
8a47ea8727 fix enka api 2023-02-07 10:21:36 +08:00
DismissedLight
b3937ac810 Merge pull request #453 from DGP-Studio/l10n_main
New Crowdin updates
2023-02-06 16:49:06 +08:00
Masterain
ed5c52dc63 New translations SH.resx (English) 2023-02-06 00:44:03 -08:00
Masterain
461d139602 New translations SH.resx (English) 2023-02-05 22:49:55 -08:00
Masterain
164ec2af33 New translations SH.resx (Chinese Traditional) 2023-02-05 22:49:54 -08:00
Masterain
e30523c621 New translations SH.resx (Russian) 2023-02-05 22:49:53 -08:00
Masterain
11d0405102 New translations SH.resx (Japanese) 2023-02-05 22:49:52 -08:00
DismissedLight
a1c0b4f830 fix zh-cn showcase 2 [skip ci] 2023-02-06 14:46:04 +08:00
Masterain
e476ed5960 New translations SH.resx (English) 2023-02-05 22:21:50 -08:00
Masterain
ffc999360d New translations SH.resx (Chinese Traditional) 2023-02-05 22:21:49 -08:00
Masterain
84058011c7 New translations SH.resx (Russian) 2023-02-05 22:21:48 -08:00
Masterain
c18e0c40c5 New translations SH.resx (Japanese) 2023-02-05 22:21:48 -08:00
DismissedLight
ad78515094 fix zh-cn task [skip ci] 2023-02-06 14:20:10 +08:00
Masterain
38367a090d New translations SH.resx (English) 2023-02-05 22:12:29 -08:00
Masterain
ce30f609fb New translations SH.resx (Chinese Traditional) 2023-02-05 22:12:28 -08:00
Masterain
f4b9cc7c48 New translations SH.resx (Russian) 2023-02-05 22:12:27 -08:00
Masterain
7c2212f44c New translations SH.resx (Japanese) 2023-02-05 22:12:26 -08:00
DismissedLight
95eddef457 fix zh-cn showcase 2023-02-06 14:11:43 +08:00
Masterain
02447bc966 New translations SH.resx (English) 2023-02-05 21:49:52 -08:00
Masterain
fb88e33d16 New translations SH.resx (Chinese Traditional) 2023-02-05 21:49:51 -08:00
Masterain
5fa36416ef New translations SH.resx (Russian) 2023-02-05 21:49:50 -08:00
Masterain
7076caaa5d New translations SH.resx (Japanese) 2023-02-05 21:49:49 -08:00
DismissedLight
b7b1155cfc adjust achievement UI 2023-02-06 13:45:41 +08:00
Masterain
6351f2b460 Update SH.resx
- fix typo in based language
2023-02-05 20:09:28 -08:00
DismissedLight
35ac2f33ba Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-05 21:06:45 +08:00
DismissedLight
f8ff1988bb update readme 2023-02-05 21:06:41 +08:00
Masterain
907d70ba71 Update azure-pipelines.yml for Azure Pipelines
[skip ci]
2023-02-05 05:04:47 -08:00
Masterain
a5bdc17712 Update azure-pipelines.yml
[skip ci]
2023-02-05 04:50:00 -08:00
Masterain
f078d92f33 Update Crowdin configuration file 2023-02-05 04:46:19 -08:00
DismissedLight
f2d4f0f1d3 locale start 2023-02-05 19:52:00 +08:00
DismissedLight
fcde9b21ae fix #442 2023-02-03 20:05:32 +08:00
DismissedLight
24f09861fd locale zh-cn phase 2 2023-02-02 20:48:48 +08:00
DismissedLight
47708adc83 update readme 2023-02-02 16:35:48 +08:00
DismissedLight
79a254235a Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-02-01 20:28:40 +08:00
DismissedLight
d9bcb3b16b locale zh-cn 2023-02-01 20:28:32 +08:00
Masterain
cf7dd548a2 Update network-issue.yml 2023-01-30 17:34:07 -08:00
DismissedLight
04deeb7086 Create FUNDING.yml 2023-01-30 19:26:25 +08:00
DismissedLight
9fb2da41cd store migration 2023-01-30 16:22:54 +08:00
DismissedLight
bb01f3a3cb fix package convert issue 2023-01-30 10:43:05 +08:00
DismissedLight
f7f2d9c867 fix #406 2023-01-28 20:03:37 +08:00
DismissedLight
01b7e58b3e fix convert cache 2023-01-27 16:51:43 +08:00
DismissedLight
2518ae0b90 package convert impl 2023-01-27 11:22:25 +08:00
DismissedLight
7d4a8cdcd9 fix empty statistics [skip ci] 2023-01-23 13:06:56 +08:00
DismissedLight
623893e00e remove visual transition gap in gacha log initialization 2023-01-23 12:58:00 +08:00
Masterain
0d34c81bcf Merge pull request #388 from wordlesswind/patch-1
Update version information and fix broken links
2023-01-22 01:07:49 -08:00
清靈語
5f3d0126b3 Update version information 2023-01-22 12:51:57 +08:00
DismissedLight
5d1fe3f38a move dispatcher queue to thread helper 2023-01-21 13:14:54 +08:00
DismissedLight
c810ffa625 remove unnecessary converters 2023-01-20 17:30:16 +08:00
DismissedLight
ee70205245 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-01-20 15:47:18 +08:00
DismissedLight
06c8b347d3 Announcement Viewer 2023-01-20 15:47:05 +08:00
Masterain
5c6ab1dee9 Update azure-pipelines.yml for Azure Pipelines 2023-01-19 15:23:50 -08:00
DismissedLight
ad440e0561 fix #377 2023-01-19 14:55:53 +08:00
DismissedLight
ca56d8c636 remove async relay command factory 2023-01-18 15:29:22 +08:00
Masterain
da0ee0cca6 Update PublishDistribution.yml
[skip ci]
2023-01-16 13:35:57 -08:00
Masterain
5d00d9cc0d Update azure-pipelines.yml for Azure Pipelines
[force ci]
2023-01-16 13:15:05 -08:00
Masterain
e8b27e6655 Update azure-pipelines.yml for Azure Pipelines 2023-01-16 13:05:55 -08:00
DismissedLight
0ac79012d1 fix #368 2023-01-16 18:12:12 +08:00
DismissedLight
bb2665b75e Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-01-16 14:27:45 +08:00
DismissedLight
d22ac39c1d fix dupe download items [skip ci] 2023-01-16 14:27:31 +08:00
Masterain
a312603d61 Update azure-pipelines.yml for Azure Pipelines 2023-01-15 22:22:34 -08:00
DismissedLight
0732ea0e06 replace font 2023-01-16 14:10:28 +08:00
Masterain
e4d2b3055c Update azure-pipelines.yml for Azure Pipelines
[skip ci]
2023-01-14 17:28:43 -08:00
Masterain
5668931230 Update PublishDistribution.yml
[skip ci]
2023-01-14 17:08:50 -08:00
DismissedLight
5126337138 code style [skip ci] 2023-01-12 19:42:45 +08:00
DismissedLight
4d634d3264 improve concurrent execution 2023-01-12 19:38:06 +08:00
Masterain
15a69fd0de Delete PrereleaseDistribution.yml 2023-01-11 20:42:42 -08:00
Masterain
c232891fe7 Update azure-pipelines.yml for Azure Pipelines 2023-01-11 20:41:03 -08:00
Masterain
c35c2a5700 Update workflows 2023-01-11 19:36:23 -08:00
Masterain
42305094f8 Update network-issue.yml 2023-01-11 16:12:16 -08:00
DismissedLight
9ef48ab05c fix crashes 2023-01-11 16:02:14 +08:00
DismissedLight
eec010870a improve web request experience 2023-01-11 13:11:09 +08:00
DismissedLight
a24fbf535d fix up oversea api style 2023-01-10 15:52:10 +08:00
DismissedLight
f7bd184a3c Merge pull request #352 from solacens/globalization/gotcha-histories
[Globalization] Read gotcha history from input of url or from cache/data_2
2023-01-10 15:22:06 +08:00
DismissedLight
267f285101 fix up oversea launcher support 2023-01-10 15:05:21 +08:00
solacens
2a1e77a9db add intl version gatcha history support 2023-01-10 17:46:29 +11:00
DismissedLight
abdc8e2e9f Merge pull request #334 from solacens/globalization/support-intl-version-game-launching
[Globalization] Support intl version game launching
2023-01-10 14:01:07 +08:00
solacens
64f1af293b merge main 2023-01-10 16:36:40 +11:00
DismissedLight
e0336d6b30 fix content dialog thread issue 2023-01-10 10:21:32 +08:00
DismissedLight
23f3e5df77 ContentDialogFactory 2023-01-09 12:15:11 +08:00
DismissedLight
4a027a8d3f remove filesystem context 2023-01-08 14:54:16 +08:00
DismissedLight
80459708a7 fix panel selector global group name 2023-01-08 12:32:43 +08:00
DismissedLight
650b67bea0 download static resource at startup 2023-01-07 18:27:45 +08:00
Masterain
18b3d23b1c Update azure-pipelines.yml for Azure Pipelines
- optimize CI logic for RPs
2023-01-03 19:17:14 -08:00
solacens
bf08ffa89e Missing changes 2023-01-03 12:20:13 +11:00
solacens
af4180bdeb Add GenshinImpact.exe to check game running 2023-01-01 23:14:35 +11:00
solacens
a70593c529 Support international version game launching 2023-01-01 22:45:10 +11:00
1076 changed files with 47064 additions and 19012 deletions

13
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: https://afdian.net/a/DismissedLight

View File

@@ -15,17 +15,15 @@ body:
description: |-
请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略
options:
- label: 完整阅读[胡桃工具箱文档](https://hut.ao/FAQ/most-frequent-questions.html),并认为我的问题没有在文档中得到解答
required: true
- label: 并未完整阅读[胡桃工具箱文档](https://hut.ao/advanced/FAQ.html)
- label: 我不知道文档站的导航栏中有**搜索功能**,也没有搜索过相关关键词
- label: 我使用的操作系统是[受支持的版本](https://hut.ao/quick-start.html#%E6%9C%80%E4%BD%8E%E7%B3%BB%E7%BB%9F%E8%A6%81%E6%B1%82)
required: true
- label: 我使用的操作系统是[受支持的版本](https://hut.ao/quick-start.html#%E6%9C%80%E4%BD%8E%E7%B3%BB%E7%BB%9F%E8%A6%81%E6%B1%82)
- label: 确认没有其他人已经提出相同或类似的问题
required: true
- label: 我会在下方的表单中附上充足的信息以帮助开发人员确定问题
required: true
- label: 我没有**通过搜索功能**确认其他人提出相同或类似的问题
- label: 我不明白上述的勾选项是**一个有助于快速排查问题的检查清单**,而是随手确认的选项
- type: input
id: winver
@@ -33,7 +31,7 @@ body:
label: Windows 版本
description: |
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
placeholder: 22000.556
placeholder: 22621.1105
validations:
required: true
@@ -42,7 +40,7 @@ body:
attributes:
label: Snap Hutao 版本
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
placeholder: 1.1.0
placeholder: 1.4.15.0
validations:
required: true
@@ -96,14 +94,4 @@ body:
description: 详细的描述你期望发生的行为,突出与目前(可能不正确的)行为的不同
validations:
required: false
- type: textarea
id: logs
attributes:
label: 相关的崩溃日志
description: |
在资源管理器中直接输入`%userprofile%/Documents/Hutao`即可进入文件夹
如果应用程序崩溃了,请将`log.db` 文件上传,文件包含了敏感信息,谨慎上传
如果这个表单是关于导入祈愿记录的问题,请包含你导入的`Json`文件
> **务必不要上传`user.db`文件,该文件包含你的帐号敏感信息**

View File

@@ -14,7 +14,7 @@ body:
id: back
attributes:
label: 背景与动机
description: 添加此功能的理由
description: 添加此功能的理由,如果你想要实现多个功能,请分别发起多个单独的 Issue
validations:
required: true

View File

@@ -21,8 +21,8 @@ body:
**在填写下面的问题之前请先使用我们的网络诊断工具**
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
- 你可以点击下面的链接以下载网络诊断工具:
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-tool.exe)
- [GitHub](https://github.com/DGP-Studio/Snap.Hutao/files/10081999/network-diagnosis-hutao.zip)
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-hutao.exe)
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/network-diagnosis-hutao.exe)
validations:
required: true
@@ -41,7 +41,7 @@ body:
id: user-isp
attributes:
label: 你的运营商
description: 中国用户请精确到省级行政区,海外地区请精确到国家
description: 海外用户请选其它
options:
- 中国电信
- 中国联通
@@ -60,7 +60,9 @@ body:
- 完全无法连接服务器
- 连接速度慢
- 获取到了不正确的页面或数据
- 图片下载错误(429 Error
- 客户端提示 429 Error
- 客户端图片下载错误
- 客户端图片预下载错误
- 其它
validations:
required: true

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "nuget" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -2,7 +2,7 @@ name: PublishDistribution
on:
release:
types: [published]
types: [released]
workflow_dispatch:
@@ -16,14 +16,14 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@v3
# Download Publish.zip
# Download Assets
- name: Download Release
timeout-minutes: 5
uses: robinraju/release-downloader@v1.5
uses: robinraju/release-downloader@v1.7
with:
repository: "DGP-Studio/Snap.Hutao"
latest: true
fileName: "*.zip"
fileName: "*.msix"
out-file-path: ./release-download
# Upload to Drive
@@ -38,4 +38,14 @@ jobs:
$RCCONF
EOF
rclone copy ./release-download/* dgpODCN:/snaphutao/Releases/
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

5
.gitignore vendored
View File

@@ -1,3 +1,4 @@
desktop.ini
*.csproj.user
*.pubxml
@@ -15,5 +16,5 @@ src/Snap.Hutao/Snap.Hutao.Installer/Properties/PublishProfiles/FolderProfile.pub
src/Snap.Hutao/Snap.Hutao.SourceGeneration/bin/
src/Snap.Hutao/Snap.Hutao.SourceGeneration/obj/
src/Snap.Hutao/Snap.Hutao.Win32/bin/
src/Snap.Hutao/Snap.Hutao.Win32/obj/
src/Snap.Hutao/Snap.Hutao.Test/bin/
src/Snap.Hutao/Snap.Hutao.Test/obj/

13
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,13 @@
# Code of Conduct
> Snap Hutao is adapting the following rules to keep the community safety.
When participating in our open source community, we want all members to respect and support each other. To ensure the comfort and safety of our community members, we have established the following code of conduct:
1. Respect diversity and inclusivity. We welcome people from different countries, regions, genders, sexual orientations, abilities, religions, and cultural backgrounds to participate in our community, and we encourage respect for all differences.
2. Prohibit discrimination and harassment. We do not tolerate any form of discrimination, harassment, personal attacks, or insults. This includes but is not limited to race, gender, sexual orientation, age, religion, nationality, cultural background, physical and mental health status.
3. Respect privacy and personal information. We protect the privacy and personal information of community members and prohibit the public disclosure of any private information. If you need to disclose certain information, please make sure you have obtained the relevant person's permission.
4. Keep honesty and transparency. We expect community members to maintain honesty and transparency and not intentionally mislead or deceive others.
5. Respect community rules and other members. We encourage community members to follow community rules and guidelines and maintain a polite and respectful attitude towards other members. If you find that other members are violating community rules, please report it to community administrators or organizers in a timely manner.
The above is our community's code of conduct, and we expect all community members to abide by these rules. We will actively address behaviors that violate these rules. We believe that through mutual respect and support, we can build a friendly, inclusive, and beneficial open source community.

19
NuGet.Config Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="Microsoft CsWin32" value="https://pkgs.dev.azure.com/azure-public/winsdk/_packaging/CI/nuget/v3/index.json" />
<add key="CommunityToolkit Labs" value="https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-Labs/nuget/v3/index.json" />
</packageSources>
<packageRestore>
<add key="enabled" value="True" />
<add key="automatic" value="True" />
</packageRestore>
<bindingRedirects>
<add key="skip" value="False" />
</bindingRedirects>
<packageManagement>
<add key="format" value="1" />
<add key="disabled" value="False" />
</packageManagement>
</configuration>

View File

@@ -1,24 +1,34 @@
# [Snap.Hutao](https://hut.ao)
> 唷,找本堂主有何贵干呀?
![](https://repository-images.githubusercontent.com/482734649/c47a0a8e-868d-4d07-a66f-6d4473abfe46)
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)
胡桃工具箱是一个 Windows 平台的开源的原神工具箱,旨在帮助玩家获得更好的游戏体验; 它是对官方移动端工具的一种非破坏性功能扩展,为不习惯在移动端进行原神游戏的 PC 玩家提供一个在 Windows 平台下获得接近移动端功能权利的途径
# 特别感谢
Snap Hutao is an open-source Genshin Impact toolbox on Windows platform, aim to provide a better gaming experience for players. It's an nondestructive feature extension from Genshin Impact's official mobile application, to provide similar feature on desktop, to allow PC gamers gain deserved benefits from mobile platforms.
### 原神组织与个人
## 下载使用 / Download
[<img src="https://get.microsoft.com/images/zh-cn%20light.svg" width="30%" height="30%">](https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52)
## 贡献 / Contribute
* [向我们提交 PR / Make Pull Requests](https://github.com/DGP-Studio/Snap.Hutao/pulls)
* [在 Crowdin 上进行本地化 / Translate project on Crowdin](https://translate.hut.ao/)
* [为我们更新文档 / Enhance our Document ](https://github.com/DGP-Studio/Snap.Hutao.Docs)
## 特别感谢 / Special Thanks
* [HolographicHat](https://github.com/HolographicHat)
* [UIGF organization](https://uigf.org)
### 特定的原神项目
### 特定的原神项目 / Specific Genshin-related Project
* [biuuu/genshin-wish-export](https://github.com/biuuu/genshin-wish-export)
* [xunkong/xunkong](https://github.com/xunkong/xunkong)
* [YuehaiTeam/cocogoat](https://github.com/YuehaiTeam/cocogoat)
### 使用的技术栈
### 使用的技术栈 / Tech Stack
* [CommunityToolkit/dotnet](https://github.com/CommunityToolkit/dotnet)
* [CommunityToolkit/Labs-Windows](https://github.com/CommunityToolkit/Labs-Windows)
* [CommunityToolkit/WindowsCommunityToolkit](https://github.com/CommunityToolkit/WindowsCommunityToolkit)
* [dahall/taskscheduler](https://github.com/dahall/taskscheduler)
* [dotnet/efcore](https://github.com/dotnet/efcore)
@@ -29,4 +39,12 @@
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
* [WinUICommunity/SettingsUI](https://github.com/WinUICommunity/SettingsUI)
### 支撑项目 / Supporter Project
* [Snap.Hutao.Server](https://github.com/DGP-Studio/Snap.Hutao.Server)
* [Snap.Metadata](https://github.com/DGP-Studio/Snap.Metadata)
* [Snap.Data.Mapper](https://github.com/DGP-Studio/Snap.Data.Mapper)
## 近期活跃数据 / Active Stat
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)

12
SECURITY.md Normal file
View File

@@ -0,0 +1,12 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| >=1.6.0 | :white_check_mark: |
| <1.6.0 | :x: |
## Reporting a Vulnerability
Please [open an issue](https://github.com/DGP-Studio/Snap.Hutao/issues/new/choose)

View File

@@ -17,10 +17,23 @@ trigger:
- 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:
vmImage: 'windows-2022'
name: Default
demands: agent.name -equals Hutao-Server
variables:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
@@ -53,7 +66,8 @@ steps:
inputs:
command: 'restore'
restoreSolution: '$(solution)'
feedsToUse: 'select'
feedsToUse: 'config'
nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.Config'
- task: MsixPackaging@1
displayName: Build binary package
@@ -71,7 +85,7 @@ steps:
- task: MagicChunks@2
inputs:
sourcePath: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64\AppxManifest.xml'
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'
@@ -81,7 +95,8 @@ steps:
"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/Properties/PublisherDisplayName":"DGP Studio CI",
"Package/Applications/Application/uap:VisualElements/@DisplayName": "胡桃 Alpha"
}
- task: CmdLine@2
@@ -91,7 +106,7 @@ steps:
mkdir Assets
mkdir Resource
workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64'
workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64'
- task: CopyFiles@2
@@ -99,33 +114,36 @@ steps:
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.18362.0\win10-x64\Assets'
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.18362.0\win10-x64\Resource'
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.22000.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
- task: MsixSigning@1
name: signMsix
displayName: Sign MSIX package
inputs:
package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).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: PublishPipelineArtifact@1
# displayName: 'Upload Output'
# inputs:
# targetPath: '$(Build.ArtifactStagingDirectory)/'
# artifact: 'Output'
# publishLocation: 'pipeline'
- task: DownloadSecureFile@1
name: cerFile
@@ -144,13 +162,29 @@ steps:
title: '$(build_date).$(rev_number)'
releaseNotesSource: 'inline'
releaseNotesInline: |
## 提示 (Hint)
发布版本由 CI 程序自动打包生成,属于 `Alpha` 测试版,仅用于开发调试和内部测试用途。使用该版本可能存在意料之外的风险,请仅在有明确用途的情况下使用该版本。
This release is a Alpha Testing version generated by CI program automatically in a purpose of debugging and interal testing. Using this release may have unexpected risk, please only use it when you know what you are doing.
## 普通用户请勿下载
该版本由 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/'
configPath: 'C:\agent\_work\_tasks\rclone.conf'
- task: rclone@1
displayName: Upload PR CI via Rclone
condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
inputs:
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix downloadDGPCN:/releases/PR/'
configPath: 'C:\agent\_work\_tasks\rclone.conf'

3
crowdin.yml Normal file
View File

@@ -0,0 +1,3 @@
files:
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.%osx_locale%.resx

View File

@@ -1,6 +0,0 @@
[.ShellClassInfo]
IconResource=D:\Develop\Projects\Snap.Hutao\src\Snap.Hutao\Snap.Hutao\Assets\Logo.ico,0
[ViewState]
Mode=
Vid=
FolderType=Generic

Binary file not shown.

BIN
res/HutaoIcon2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 KiB

BIN
res/HutaoIcon2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
res/HutaoIconSource.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
res/HutaoRepoBanner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 KiB

BIN
res/HutaoRepoBanner.psd Normal file

Binary file not shown.

3
res/LEGAL NOTICE.md Normal file
View File

@@ -0,0 +1,3 @@
本文件夹中的所有图片,均由 [DGP Studio](https://github.com/DGP-Studio) 委托 [Bilibili 画画的芦苇](https://space.bilibili.com/274422134) 绘制
Copyright ©2023 DGP Studio, All Rights Reserved.

File diff suppressed because one or more lines are too long

View File

@@ -10,13 +10,13 @@ csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = false:silent
csharp_style_expression_bodied_indexers = false:silent
csharp_style_expression_bodied_accessors = when_on_single_line:silent
csharp_style_expression_bodied_accessors = when_on_single_line:suggestion
csharp_style_expression_bodied_lambdas = when_on_single_line:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_conditional_delegate_call = true:suggestion
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_when_type_is_apparent = false:warning
csharp_style_var_elsewhere = false:warning
csharp_prefer_simple_using_statement = false:suggestion
csharp_prefer_braces = true:silent
csharp_style_namespace_declarations = file_scoped:silent
@@ -24,11 +24,11 @@ csharp_prefer_static_local_function = false:suggestion
[*.{cs,vb}]
end_of_line = crlf
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
dotnet_code_quality_unused_parameters = non_public:suggestion
dotnet_style_readonly_field = true:suggestion
indent_size = 4
@@ -165,6 +165,7 @@ dotnet_diagnostic.CA1805.severity = suggestion
dotnet_diagnostic.VSTHRD111.severity = suggestion
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
[*.vb]
#### 命名样式 ####

View File

@@ -1,73 +0,0 @@
using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace Snap.Hutao.Installer;
internal class Program
{
private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Appx";
private const string ValueName = "AllowDevelopmentWithoutDevLicense";
public static async Task Main(string[] args)
{
_ = args;
string ps1File = Path.Combine(AppContext.BaseDirectory, "Install.ps1");
if (!File.Exists(ps1File))
{
Console.WriteLine("未检测到 Install.ps1 文件");
Console.WriteLine("请勿移动该安装程序,按下任意键退出...");
Console.ReadKey();
return;
}
try
{
//以管理策略打开开发者模式
Registry.SetValue(AppxKey, ValueName, 1, RegistryValueKind.DWord);
}
catch (Exception)
{
Console.WriteLine("开发者模式未开启,请手动开启,参阅下方链接");
Console.WriteLine("https://learn.microsoft.com/zh-CN/windows/apps/get-started/developer-mode-features-and-debugging");
}
await InstallAsync(ps1File).ConfigureAwait(false);
Console.WriteLine();
Console.WriteLine("官方文档与使用教程");
Console.WriteLine("https://hut.ao");
Console.WriteLine();
Console.WriteLine("在开始菜单中启动 Snap.Hutao ,按下任意键退出...");
Console.ReadKey();
}
private static async Task InstallAsync(string ps1File)
{
Console.WriteLine("请注意 PowerShell 中的提示");
Process ps = new()
{
StartInfo = new ProcessStartInfo()
{
FileName = "powershell.exe",
Arguments = $"-ExecutionPolicy Unrestricted \"{ps1File}\"",
UseShellExecute = true,
}
};
try
{
ps.Start();
await ps.WaitForExitAsync();
Console.WriteLine("安装脚本运行完成");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}

View File

@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishTrimmed>true</PublishTrimmed>
<ApplicationManifest>app.manifest</ApplicationManifest>
<DebugType>embedded</DebugType>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
</Project>

View File

@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC 清单选项
如果想要更改 Windows 用户帐户控制级别,请使用
以下节点之一替换 requestedExecutionLevel 节点。
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
元素。
-->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
Windows 版本的列表。取消评论适当的元素,
Windows 将自动选择最兼容的环境。 -->
<!-- Windows Vista -->
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!-- Windows 7 -->
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
<!-- Windows 8 -->
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
<!-- Windows 8.1 -->
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI无需
选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
<!--
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
-->
<!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
<!--
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
-->
</assembly>

View File

@@ -0,0 +1,102 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading;
namespace Snap.Hutao.SourceGeneration.Automation;
[Generator(LanguageNames.CSharp)]
internal sealed class CommandGenerator : IIncrementalGenerator
{
private const string AttributeName = "Snap.Hutao.Core.Annotation.CommandAttribute";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2<IMethodSymbol>>> commands =
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedMethods, CommandMethod)
.Where(GeneratorSyntaxContext2<IMethodSymbol>.NotNull)
.Collect();
context.RegisterImplementationSourceOutput(commands, GenerateCommandImplementations);
}
private static bool FilterAttributedMethods(SyntaxNode node, CancellationToken token)
{
return node is MethodDeclarationSyntax methodDeclarationSyntax
&& methodDeclarationSyntax.Parent is ClassDeclarationSyntax classDeclarationSyntax
&& classDeclarationSyntax.Modifiers.Count > 1
&& methodDeclarationSyntax.HasAttributeLists();
}
private static GeneratorSyntaxContext2<IMethodSymbol> CommandMethod(GeneratorSyntaxContext context, CancellationToken token)
{
if (context.TryGetDeclaredSymbol(token, out IMethodSymbol? methodSymbol))
{
ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
{
return new(context, methodSymbol, attributes);
}
}
return default;
}
private static void GenerateCommandImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2<IMethodSymbol>> context2s)
{
foreach (GeneratorSyntaxContext2<IMethodSymbol> context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
{
GenerateCommandImplementation(production, context2);
}
}
private static void GenerateCommandImplementation(SourceProductionContext production, GeneratorSyntaxContext2<IMethodSymbol> context2)
{
INamedTypeSymbol classSymbol = context2.Symbol.ContainingType;
AttributeData commandInfo = context2.SingleAttribute(AttributeName);
string commandName = (string)commandInfo.ConstructorArguments[0].Value!;
string commandType = context2.Symbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task")
? "AsyncRelayCommand"
: "RelayCommand";
string genericParameter = context2.Symbol.Parameters.ElementAtOrDefault(0) is IParameterSymbol parameter
? $"<{parameter.Type.ToDisplayString(SymbolDisplayFormats.FullyQualifiedNonNullableFormat)}>"
: string.Empty;
string concurrentExecution = commandInfo.HasNamedArgumentWith<bool>("AllowConcurrentExecutions", value => value)
? ", AsyncRelayCommandOptions.AllowConcurrentExecutions"
: string.Empty;
string className = classSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
// TODO: 支持嵌套类
string code = $$"""
using CommunityToolkit.Mvvm.Input;
namespace {{classSymbol.ContainingNamespace}};
partial class {{className}}
{
private ICommand _{{commandName}};
public ICommand {{commandName}}
{
get => _{{commandName}} ??= new {{commandType}}{{genericParameter}}({{context2.Symbol.Name}}{{concurrentExecution}});
}
}
""";
string normalizedClassName = classSymbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
production.AddSource($"{normalizedClassName}.{commandName}.g.cs", code);
}
}

View File

@@ -0,0 +1,161 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading;
namespace Snap.Hutao.SourceGeneration.Automation;
[Generator(LanguageNames.CSharp)]
internal sealed class ConstructorGenerator : IIncrementalGenerator
{
private const string AttributeName = "Snap.Hutao.Core.Annotation.ConstructorGeneratedAttribute";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses =
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, ConstructorGeneratedClass)
.Where(GeneratorSyntaxContext2.NotNull)
.Collect();
context.RegisterSourceOutput(injectionClasses, GenerateConstructorImplementations);
}
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
{
return node is ClassDeclarationSyntax classDeclarationSyntax
&& classDeclarationSyntax.Modifiers.Count > 1
&& classDeclarationSyntax.HasAttributeLists();
}
private static GeneratorSyntaxContext2 ConstructorGeneratedClass(GeneratorSyntaxContext context, CancellationToken token)
{
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
{
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
{
return new(context, classSymbol, attributes);
}
}
return default;
}
private static void GenerateConstructorImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> context2s)
{
foreach (GeneratorSyntaxContext2 context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
{
GenerateConstructorImplementation(production, context2);
}
}
private static void GenerateConstructorImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2)
{
AttributeData constructorInfo = context2.SingleAttribute(AttributeName);
bool resolveHttpClient = constructorInfo.HasNamedArgumentWith<bool>("ResolveHttpClient", value => value);
string httpclient = resolveHttpClient ? ", System.Net.Http.HttpClient httpClient" : string.Empty;
FieldValueAssignmentOptions options = new(resolveHttpClient);
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
namespace {{context2.Symbol.ContainingNamespace}};
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(ConstructorGenerator)}}", "1.0.0.0")]
partial class {{context2.Symbol.Name}}
{
public {{context2.Symbol.Name}}(System.IServiceProvider serviceProvider{{httpclient}})
{
""");
FillUpWithFieldValueAssignment(sourceBuilder, context2, options);
sourceBuilder.Append("""
}
}
""");
production.AddSource($"{context2.Symbol.ToDisplayString()}.ctor.g.cs", sourceBuilder.ToString());
}
private static void FillUpWithFieldValueAssignment(StringBuilder builder, GeneratorSyntaxContext2 context2, FieldValueAssignmentOptions options)
{
IEnumerable<IFieldSymbol> fields = context2.Symbol.GetMembers()
.Where(m => m.Kind == SymbolKind.Field)
.OfType<IFieldSymbol>();
foreach (IFieldSymbol fieldSymbol in fields)
{
if (fieldSymbol.IsReadOnly && !fieldSymbol.IsStatic)
{
switch (fieldSymbol.Type.ToDisplayString())
{
case "System.IServiceProvider":
builder
.Append(" this.")
.Append(fieldSymbol.Name)
.AppendLine(" = serviceProvider;");
break;
case "System.Net.Http.HttpClient":
if (options.ResolveHttpClient)
{
builder
.Append(" this.")
.Append(fieldSymbol.Name)
.AppendLine(" = httpClient;");
}
else
{
builder
.Append(" this.")
.Append(fieldSymbol.Name)
.Append(" = serviceProvider.GetRequiredService<System.Net.Http.IHttpClientFactory>().CreateClient(nameof(")
.Append(context2.Symbol.Name)
.AppendLine("));");
}
break;
default:
builder
.Append(" this.")
.Append(fieldSymbol.Name)
.Append(" = serviceProvider.GetRequiredService<")
.Append(fieldSymbol.Type)
.AppendLine(">();");
break;
}
}
}
foreach(INamedTypeSymbol interfaceSymbol in context2.Symbol.Interfaces)
{
if (interfaceSymbol.Name == "IRecipient")
{
builder
.Append(" CommunityToolkit.Mvvm.Messaging.IMessengerExtensions.Register<")
.Append(interfaceSymbol.TypeArguments[0])
.AppendLine(">(serviceProvider.GetRequiredService<CommunityToolkit.Mvvm.Messaging.IMessenger>(), this);");
}
}
}
private readonly struct FieldValueAssignmentOptions
{
public readonly bool ResolveHttpClient;
public FieldValueAssignmentOptions(bool resolveHttpClient)
{
ResolveHttpClient = resolveHttpClient;
}
}
}

View File

@@ -0,0 +1,15 @@
namespace System.Diagnostics.CodeAnalysis;
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}

View File

@@ -1,178 +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 Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
/// <summary>
/// 注入HttpClient代码生成器
/// 旨在使用源生成器提高注入效率
/// 防止在运行时动态查找注入类型
/// </summary>
[Generator]
public class HttpClientGenerator : ISourceGenerator
{
private const string DefaultName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.Default";
private const string XRpcName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc";
private const string XRpc2Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc2";
private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute";
private const string DynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
{
// Register a syntax receiver that will be created for each generation pass
context.RegisterForSyntaxNotifications(() => new HttpClientSyntaxContextReceiver());
}
/// <inheritdoc/>
public void Execute(GeneratorExecutionContext context)
{
// retrieve the populated receiver
if (context.SyntaxContextReceiver is not HttpClientSyntaxContextReceiver receiver)
{
return;
}
string toolName = this.GetGeneratorType().FullName;
StringBuilder sourceCodeBuilder = new();
sourceCodeBuilder.Append($@"// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// This class is generated by Snap.Hutao.SourceGeneration
using Microsoft.Extensions.DependencyInjection;
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(""{toolName}"",""1.0.0.0"")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static partial IServiceCollection AddHttpClients(this IServiceCollection services)
{{");
FillWithInjectionServices(receiver, sourceCodeBuilder);
sourceCodeBuilder.Append(@"
return services;
}
}");
context.AddSource("IocHttpClientConfiguration.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
}
private static void FillWithInjectionServices(HttpClientSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
{
List<string> lines = new();
StringBuilder lineBuilder = new();
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
{
lineBuilder.Clear().Append("\r\n");
lineBuilder.Append(@" services.AddHttpClient<");
lineBuilder.Append($"{classSymbol.ToDisplayString()}>(");
AttributeData httpClientInfo = classSymbol
.GetAttributes()
.Single(attr => attr.AttributeClass!.ToDisplayString() == HttpClientSyntaxContextReceiver.AttributeName);
ImmutableArray<TypedConstant> arguments = httpClientInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.ToCSharpString();
switch (injectAsName)
{
case DefaultName:
lineBuilder.Append(@"DefaultConfiguration)");
break;
case XRpcName:
lineBuilder.Append(@"XRpcConfiguration)");
break;
case XRpc2Name:
lineBuilder.Append(@"XRpc2Configuration)");
break;
default:
throw new InvalidOperationException($"非法的HttpClientConfigration值: [{injectAsName}]");
}
AttributeData? handlerInfo = classSymbol
.GetAttributes()
.SingleOrDefault(attr => attr.AttributeClass!.ToDisplayString() == PrimaryHttpMessageHandlerAttributeName);
if (handlerInfo != null)
{
ImmutableArray<KeyValuePair<string, TypedConstant>> properties = handlerInfo.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(" })");
}
if (classSymbol.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == DynamicSecretAttributeName))
{
lineBuilder.Append(".AddHttpMessageHandler<DynamicSecretHandler>()");
}
lineBuilder.Append(";");
lines.Add(lineBuilder.ToString());
}
foreach (string line in lines.OrderBy(x => x))
{
sourceCodeBuilder.Append(line);
}
}
private class HttpClientSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
}
}
}

View File

@@ -1,156 +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 Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
/// <summary>
/// 注入代码生成器
/// 旨在使用源生成器提高注入效率
/// 防止在运行时动态查找注入类型
/// </summary>
[Generator]
public class InjectionGenerator : ISourceGenerator
{
private const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
private const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
private const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
{
// Register a syntax receiver that will be created for each generation pass
context.RegisterForSyntaxNotifications(() => new InjectionSyntaxContextReceiver());
}
/// <inheritdoc/>
public void Execute(GeneratorExecutionContext context)
{
// retrieve the populated receiver
if (context.SyntaxContextReceiver is not InjectionSyntaxContextReceiver receiver)
{
return;
}
string toolName = this.GetGeneratorType().FullName;
StringBuilder sourceCodeBuilder = new();
sourceCodeBuilder.Append($@"// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// This class is generated by Snap.Hutao.SourceGeneration
using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
internal static partial class ServiceCollectionExtension
{{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{toolName}"",""1.0.0.0"")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static partial IServiceCollection AddInjections(this IServiceCollection services)
{{");
FillWithInjectionServices(receiver, sourceCodeBuilder);
sourceCodeBuilder.Append(@"
return services;
}
}");
context.AddSource("ServiceCollectionExtension.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
}
private static void FillWithInjectionServices(InjectionSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
{
List<string> lines = new();
StringBuilder lineBuilder = new();
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
{
IEnumerable<AttributeData> datas = classSymbol
.GetAttributes()
.Where(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
foreach (AttributeData injectionInfo in datas)
{
lineBuilder
.Clear()
.Append("\r\n");
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.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:
throw new InvalidOperationException($"非法的 InjectAs 值: [{injectAsName}]");
}
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.ToCSharpString()}, ");
}
lineBuilder.Append($"typeof({classSymbol.ToDisplayString()}));");
lines.Add(lineBuilder.ToString());
}
}
foreach (string line in lines.OrderBy(x => x))
{
sourceCodeBuilder.Append(line);
}
}
private class InjectionSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
}
}
}

View File

@@ -0,0 +1,140 @@
// 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;
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 UseDynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
private const string CRLF = "\r\n";
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()))
{
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(" })");
}
if (context.HasAttributeWithName(UseDynamicSecretAttributeName))
{
lineBuilder.Append(".AddHttpMessageHandler<DynamicSecretHandler>()");
}
lineBuilder.Append(';');
lines.Add(lineBuilder.ToString());
}
foreach (string line in lines.OrderBy(x => x))
{
sourceBuilder.Append(line);
}
}
}

View File

@@ -0,0 +1,127 @@
// 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
{
private const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
private const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
private const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
private const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
private const string CRLF = "\r\n";
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().Append(CRLF);
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, null, 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

@@ -0,0 +1,134 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading;
namespace Snap.Hutao.SourceGeneration.Enum;
[Generator(LanguageNames.CSharp)]
internal class LocalizedEnumGenerator : IIncrementalGenerator
{
private const string AttributeName = "Snap.Hutao.Resource.Localization.LocalizationAttribute";
private const string LocalizationKeyName = "Snap.Hutao.Resource.Localization.LocalizationKeyAttribute";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<GeneratorSyntaxContext2> localizationEnums = context.SyntaxProvider
.CreateSyntaxProvider(FilterAttributedEnums, LocalizationEnum)
.Where(GeneratorSyntaxContext2.NotNull);
context.RegisterSourceOutput(localizationEnums, GenerateGetLocalizedDescriptionImplementation);
}
private static bool FilterAttributedEnums(SyntaxNode node, CancellationToken token)
{
return node is EnumDeclarationSyntax enumDeclarationSyntax
&& enumDeclarationSyntax.HasAttributeLists();
}
private static GeneratorSyntaxContext2 LocalizationEnum(GeneratorSyntaxContext context, CancellationToken token)
{
if (context.SemanticModel.GetDeclaredSymbol(context.Node, token) is INamedTypeSymbol enumSymbol)
{
ImmutableArray<AttributeData> attributes = enumSymbol.GetAttributes();
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
{
return new(context, enumSymbol, attributes);
}
}
return default;
}
private static void GenerateGetLocalizedDescriptionImplementation(SourceProductionContext context, GeneratorSyntaxContext2 context2)
{
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Resource.Localization;
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(LocalizedEnumGenerator)}}", "1.0.0.0")]
internal static class {{context2.Symbol.Name}}Extension
{
/// <summary>
/// 获取本地化的描述
/// </summary>
/// <param name="value">枚举值</param>
/// <returns>本地化的描述</returns>
public static string GetLocalizedDescription(this {{context2.Symbol}} value)
{
string key = value switch
{
""");
FillUpWithSwitchBranches(sourceBuilder, context2);
sourceBuilder.Append($$"""
_ => string.Empty,
};
if (string.IsNullOrEmpty(key))
{
return Enum.GetName(value);
}
else
{
return SH.ResourceManager.GetString(key);
}
}
/// <summary>
/// 获取本地化的描述
/// </summary>
/// <param name="value">枚举值</param>
/// <returns>本地化的描述</returns>
[return:MaybeNull]
public static string GetLocalizedDescriptionOrDefault(this {{context2.Symbol}} value)
{
string key = value switch
{
""");
FillUpWithSwitchBranches(sourceBuilder, context2);
sourceBuilder.Append($$"""
_ => string.Empty,
};
return SH.ResourceManager.GetString(key);
}
}
""");
context.AddSource($"{context2.Symbol.Name}Extension.g.cs", sourceBuilder.ToString());
}
private static void FillUpWithSwitchBranches(StringBuilder sourceBuilder, GeneratorSyntaxContext2 context)
{
IEnumerable<IFieldSymbol> fields = context.Symbol.GetMembers()
.Where(m => m.Kind == SymbolKind.Field)
.Cast<IFieldSymbol>();
foreach(IFieldSymbol fieldSymbol in fields)
{
AttributeData? localizationKeyInfo = fieldSymbol.GetAttributes()
.SingleOrDefault(data => data.AttributeClass!.ToDisplayString() == LocalizationKeyName);
if (localizationKeyInfo != null)
{
sourceBuilder
.Append(" ")
.Append(fieldSymbol)
.Append(" => \"")
.Append(localizationKeyInfo.ConstructorArguments[0].Value)
.AppendLine("\",");
}
}
}
}

View File

@@ -5,4 +5,4 @@
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:启用分析器发布跟踪", Justification = "<挂起>", Scope = "member", Target = "~F:Snap.Hutao.SourceGeneration.TodoAnalyzer.Descriptor")]
[assembly: SuppressMessage("", "RS2008")]

View File

@@ -0,0 +1,177 @@
using Microsoft.CodeAnalysis;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
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())
{
GenerateIdentityConverter(context);
foreach (IdentityStructMetadata identityStruct in identities)
{
GenerateIdentityStruct(context, identityStruct);
}
}
}
private static void GenerateIdentityConverter(SourceProductionContext context)
{
string source = $$"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Primitive.Converter;
/// <summary>
/// Id 转换器
/// </summary>
/// <typeparam name="TWrapper">包装类型</typeparam>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(IdentityGenerator)}}", "1.0.0.0")]
internal unsafe sealed class IdentityConverter<TWrapper> : JsonConverter<TWrapper>
where TWrapper : unmanaged
{
/// <inheritdoc/>
public override TWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
{
int value = reader.GetInt32();
return *(TWrapper*)&value;
}
throw new JsonException();
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, TWrapper value, JsonSerializerOptions options)
{
writer.WriteNumberValue(*(int*)&value);
}
}
""";
context.AddSource("IdentityConverter.g.cs", source);
}
private static void GenerateIdentityStruct(SourceProductionContext context, IdentityStructMetadata identityStruct)
{
string name = identityStruct.Name;
string type = identityStruct.Type;
string source = $$"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive.Converter;
namespace Snap.Hutao.Model.Primitive;
/// <summary>
/// {{identityStruct.Documentation}}
/// </summary>
[JsonConverter(typeof(IdentityConverter<{{name}}>))]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(IdentityGenerator)}}","1.0.0.0")]
internal readonly struct {{name}} : IEquatable<{{name}}>
{
/// <summary>
/// 值
/// </summary>
public readonly {{type}} Value;
/// <summary>
/// Initializes a new instance of the <see cref="{{name}}"/> struct.
/// </summary>
/// <param name="value">value</param>
public {{name}}({{type}} value)
{
Value = value;
}
public static implicit operator {{type}}({{name}} value)
{
return value.Value;
}
public static implicit operator {{name}}({{type}} value)
{
return new(value);
}
public static bool operator ==({{name}} left, {{name}} right)
{
return left.Value == right.Value;
}
public static bool operator !=({{name}} left, {{name}} right)
{
return !(left == right);
}
/// <inheritdoc/>
public bool Equals({{name}} other)
{
return Value == other.Value;
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
return obj is {{name}} other && Equals(other);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return Value.GetHashCode();
}
}
""";
context.AddSource($"{name}.g.cs", source);
}
private sealed class IdentityStructMetadata
{
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 基底类型
/// </summary>
public string Type { get; set; } = default!;
/// <summary>
/// 文档
/// </summary>
public string? Documentation { get; set; }
}
}

View File

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

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using System;
using System.Linq;
namespace Snap.Hutao.SourceGeneration.Primitive;
internal static class AttributeDataExtension
{
public static bool HasNamedArgumentWith<TValue>(this AttributeData data, string key, Func<TValue, bool> predicate)
{
return data.NamedArguments.Any(a => a.Key == key && predicate((TValue)a.Value.Value!));
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
namespace Snap.Hutao.SourceGeneration.Primitive;
internal static class EnumerableExtension
{
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
return DistinctByIterator(source, keySelector);
}
private static IEnumerable<TSource> DistinctByIterator<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
using IEnumerator<TSource> enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
{
HashSet<TKey> set = new();
do
{
TSource element = enumerator.Current;
if (set.Add(keySelector(element)))
{
yield return element;
}
}
while (enumerator.MoveNext());
}
}
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using System.Collections.Immutable;
using System.Linq;
namespace Snap.Hutao.SourceGeneration.Primitive;
internal readonly struct GeneratorSyntaxContext2
{
public readonly GeneratorSyntaxContext Context;
public readonly INamedTypeSymbol Symbol;
public readonly ImmutableArray<AttributeData> Attributes;
public readonly bool HasValue = false;
public GeneratorSyntaxContext2(GeneratorSyntaxContext context, INamedTypeSymbol symbol, ImmutableArray<AttributeData> attributes)
{
Context = context;
Symbol = symbol;
Attributes = attributes;
HasValue = true;
}
public static bool NotNull(GeneratorSyntaxContext2 context)
{
return context.HasValue;
}
public bool HasAttributeWithName(string name)
{
return Attributes.Any(attr => attr.AttributeClass!.ToDisplayString() == name);
}
public AttributeData SingleAttribute(string name)
{
return Attributes.Single(attribute => attribute.AttributeClass!.ToDisplayString() == name);
}
public AttributeData? SingleOrDefaultAttribute(string name)
{
return Attributes.SingleOrDefault(attribute => attribute.AttributeClass!.ToDisplayString() == name);
}
public TSyntaxNode Node<TSyntaxNode>()
where TSyntaxNode : SyntaxNode
{
return (TSyntaxNode)Context.Node;
}
}
internal readonly struct GeneratorSyntaxContext2<TSymbol>
where TSymbol : ISymbol
{
public readonly GeneratorSyntaxContext Context;
public readonly TSymbol Symbol;
public readonly ImmutableArray<AttributeData> Attributes;
public readonly bool HasValue = false;
public GeneratorSyntaxContext2(GeneratorSyntaxContext context, TSymbol symbol, ImmutableArray<AttributeData> attributes)
{
Context = context;
Symbol = symbol;
Attributes = attributes;
HasValue = true;
}
public static bool NotNull(GeneratorSyntaxContext2<TSymbol> context)
{
return context.HasValue;
}
public AttributeData SingleAttribute(string name)
{
return Attributes.Single(attribute => attribute.AttributeClass!.ToDisplayString() == name);
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Diagnostics.CodeAnalysis;
namespace Snap.Hutao.SourceGeneration.Primitive;
internal static class GeneratorSyntaxContextExtension
{
public static bool TryGetDeclaredSymbol<TSymbol>(this GeneratorSyntaxContext context, System.Threading.CancellationToken token,[NotNullWhen(true)] out TSymbol? symbol)
where TSymbol : class, ISymbol
{
symbol = context.SemanticModel.GetDeclaredSymbol(context.Node, token) as TSymbol;
return symbol != null;
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
namespace Snap.Hutao.SourceGeneration.Primitive;
internal static class SymbolDisplayFormats
{
public static SymbolDisplayFormat FullyQualifiedNonNullableFormat { get; } = new(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Snap.Hutao.SourceGeneration.Primitive;
internal static class SyntaxExtension
{
/// <summary>
/// Checks whether a given <see cref="MemberDeclarationSyntax"/> has or could potentially have any attribute lists.
/// </summary>
/// <param name="declaration">The input <see cref="MemberDeclarationSyntax"/> to check.</param>
/// <returns>Whether <paramref name="declaration"/> has or potentially has any attribute lists.</returns>
public static bool HasAttributeLists<TSyntax>(this TSyntax declaration)
where TSyntax : MemberDeclarationSyntax
{
return declaration.AttributeLists.Count > 0;
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
namespace Snap.Hutao.SourceGeneration.Primitive;
internal static class TypeSymbolExtension
{
/// <summary>
/// Checks whether or not a given <see cref="ITypeSymbol"/> has or inherits from a specified type.
/// </summary>
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
/// <param name="name">The full name of the type to check for inheritance.</param>
/// <returns>Whether or not <paramref name="typeSymbol"/> is or inherits from <paramref name="name"/>.</returns>
public static bool IsOrInheritsFrom(this ITypeSymbol typeSymbol, string name)
{
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
{
if (currentType.ToDisplayString() == name)
{
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,133 @@
using Microsoft.CodeAnalysis;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
namespace Snap.Hutao.SourceGeneration.Reliquary;
[Generator(LanguageNames.CSharp)]
internal sealed class ReliquaryWeightConfigurationGenerator : IIncrementalGenerator
{
private const string FileName = "ReliquaryWeightConfiguration.json";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ImmutableArray<AdditionalText>> provider = context.AdditionalTextsProvider.Where(MatchFileName).Collect();
context.RegisterSourceOutput(provider, GenerateReliquaryWeightConfiguration);
}
private static bool MatchFileName(AdditionalText text)
{
return Path.GetFileName(text.Path) == FileName;
}
private static void GenerateReliquaryWeightConfiguration(SourceProductionContext context, ImmutableArray<AdditionalText> texts)
{
AdditionalText jsonFile = texts.Single();
string configurationJson = jsonFile.GetText(context.CancellationToken)!.ToString();
Dictionary<string, ReliquaryWeightConfigurationMetadata> metadataMap =
configurationJson.FromJson<Dictionary<string, ReliquaryWeightConfigurationMetadata>>()!;
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 圣遗物评分权重配置
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(ReliquaryWeightConfigurationGenerator)}}","1.0.0.0")]
internal static class ReliquaryWeightConfiguration
{
/// <summary>
/// 默认
/// </summary>
public static readonly AffixWeight Default = new(0, 100, 75, 0, 100, 100, 0, 55, 0);
/// <summary>
/// 词条权重
/// </summary>
public static readonly List<AffixWeight> AffixWeights = new()
{
""");
foreach (KeyValuePair<string, ReliquaryWeightConfigurationMetadata> kvp in metadataMap.OrderBy(kvp => kvp.Key))
{
AppendAffixWeight(sourceBuilder, kvp.Key, kvp.Value);
}
sourceBuilder.Append($$"""
};
}
""");
context.AddSource("ReliquaryWeightConfiguration.g.cs", sourceBuilder.ToString());
}
private static void AppendAffixWeight(StringBuilder builder, string id, ReliquaryWeightConfigurationMetadata metadata)
{
StringBuilder lineBuilder = new StringBuilder()
.Append(" new AffixWeight(").Append(id).Append(',')
.Append(' ').Append(metadata.Hp).Append(',')
.Append(' ').Append(metadata.Attack).Append(',')
.Append(' ').Append(metadata.Defense).Append(',')
.Append(' ').Append(metadata.CritRate).Append(',')
.Append(' ').Append(metadata.CritHurt).Append(',')
.Append(' ').Append(metadata.Mastery).Append(',')
.Append(' ').Append(metadata.Recharge).Append(',')
.Append(' ').Append(metadata.Healing).Append(')')
.Append('.').Append(metadata.ElementType).Append("(").Append(metadata.ElementHurt).Append(')');
if (metadata.PhysicialHurt != 0)
{
lineBuilder.Append(".Physical(").Append(metadata.PhysicialHurt).Append(')');
}
lineBuilder.Append(',');
builder.AppendLine(lineBuilder.ToString());
}
private sealed class ReliquaryWeightConfigurationMetadata
{
[DataMember(Name = "hp")]
public int Hp { get; set; }
[DataMember(Name = "atk")]
public int Attack { get; set; }
[DataMember(Name = "def")]
public int Defense { get; set; }
[DataMember(Name = "cpct")]
public int CritRate { get; set; }
[DataMember(Name = "cdmg")]
public int CritHurt { get; set; }
[DataMember(Name = "mastery")]
public int Mastery { get; set; }
[DataMember(Name = "recharge")]
public int Recharge { get; set; }
[DataMember(Name = "heal")]
public int Healing { get; set; }
[DataMember(Name = "element")]
public string ElementType { get; set; } = default!;
[DataMember(Name = "dmg")]
public int ElementHurt { get; set; }
[DataMember(Name = "phy")]
public int PhysicialHurt { get; set; }
}
}

View File

@@ -0,0 +1,26 @@
<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>
</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.5.0" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
</Project>

View File

@@ -7,6 +7,15 @@
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Optimize>True</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug As Fake Elevated|x64'">
<Optimize>True</Optimize>
</PropertyGroup>
<ItemGroup>
@@ -18,11 +27,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4-beta1.22518.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" Version="3.3.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,52 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
namespace Snap.Hutao.SourceGeneration;
/// <summary>
/// 高亮TODO
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class TodoAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Descriptor =
new("SH0001", "TODO 项尚未实现", "此 TODO 项需要实现", "Standard", DiagnosticSeverity.Info, true);
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get => ImmutableArray.Create(Descriptor); }
/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxTreeAction(HandleSyntaxTree);
}
private static void HandleSyntaxTree(SyntaxTreeAnalysisContext context)
{
SyntaxNode root = context.Tree.GetCompilationUnitRoot(context.CancellationToken);
foreach (SyntaxTrivia node in root.DescendantTrivia(descendIntoTrivia: true))
{
switch (node.Kind())
{
case SyntaxKind.SingleLineCommentTrivia:
case SyntaxKind.MultiLineCommentTrivia:
string text = node.ToString().ToLowerInvariant();
if (text.Contains("todo:"))
{
string hint = node.ToString().Substring(text.IndexOf("todo:") + 6);
DiagnosticDescriptor descriptor = new("SH0001", "TODO 项尚未实现", hint, "Standard", DiagnosticSeverity.Info, true);
context.ReportDiagnostic(Diagnostic.Create(descriptor, node.GetLocation()));
}
break;
}
}
}
}

View File

@@ -0,0 +1,188 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Snap.Hutao.SourceGeneration;
/// <summary>
/// 通用分析器
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor typeInternalDescriptor = new("SH001", "Type should be internal", "Type [{0}] should be internal", "Quality", DiagnosticSeverity.Info, true);
private static readonly DiagnosticDescriptor readOnlyStructRefDescriptor = new("SH002", "ReadOnly struct should be passed with ref-like key word", "ReadOnly Struct [{0}] should be passed with ref-like key word", "Quality", DiagnosticSeverity.Info, true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
return new DiagnosticDescriptor[]
{
typeInternalDescriptor,
readOnlyStructRefDescriptor,
}.ToImmutableArray();
}
}
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(CompilationStart);
}
private void CompilationStart(CompilationStartAnalysisContext context)
{
SyntaxKind[] types = new SyntaxKind[]
{
SyntaxKind.ClassDeclaration,
SyntaxKind.InterfaceDeclaration,
SyntaxKind.StructDeclaration,
SyntaxKind.EnumDeclaration
};
context.RegisterSyntaxNodeAction(HandleTypeDeclaration, types);
context.RegisterSyntaxNodeAction(HandleMethodDeclaration, SyntaxKind.MethodDeclaration);
context.RegisterSyntaxNodeAction(HandleConstructorDeclaration, SyntaxKind.ConstructorDeclaration);
}
private void HandleTypeDeclaration(SyntaxNodeAnalysisContext context)
{
BaseTypeDeclarationSyntax syntax = (BaseTypeDeclarationSyntax)context.Node;
bool privateExists = false;
bool internalExists = false;
bool fileExists = false;
foreach (SyntaxToken token in syntax.Modifiers)
{
if (token.IsKind(SyntaxKind.PrivateKeyword))
{
privateExists = true;
}
if (token.IsKind(SyntaxKind.InternalKeyword))
{
internalExists = true;
}
if (token.IsKind(SyntaxKind.FileKeyword))
{
fileExists = true;
}
}
if (!privateExists && !internalExists && !fileExists)
{
Location location = syntax.Identifier.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(typeInternalDescriptor, location, syntax.Identifier);
context.ReportDiagnostic(diagnostic);
}
}
private void HandleMethodDeclaration(SyntaxNodeAnalysisContext context)
{
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
// 跳过异步方法,因为异步方法无法使用 ref in out
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.AsyncKeyword)))
{
return;
}
// 跳过重载方法
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.OverrideKeyword)))
{
return;
}
// 跳过方法定义 如 接口
if (methodSyntax.Body == null)
{
return;
}
foreach (ParameterSyntax parameter in methodSyntax.ParameterList.Parameters)
{
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
{
if (IsBuiltInType(symbol.Type))
{
continue;
}
// 跳过 CancellationToken
if (symbol.Type.ToDisplayString() == "System.Threading.CancellationToken")
{
continue;
}
if (symbol.Type.IsReadOnly && symbol.RefKind == RefKind.None)
{
Location location = parameter.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(readOnlyStructRefDescriptor, location, symbol.Type);
context.ReportDiagnostic(diagnostic);
}
}
}
}
private void HandleConstructorDeclaration(SyntaxNodeAnalysisContext context)
{
ConstructorDeclarationSyntax constructorSyntax = (ConstructorDeclarationSyntax)context.Node;
foreach (ParameterSyntax parameter in constructorSyntax.ParameterList.Parameters)
{
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
{
if (IsBuiltInType(symbol.Type))
{
continue;
}
// 跳过 CancellationToken
if (symbol.Type.ToDisplayString() == "System.Threading.CancellationToken")
{
continue;
}
if (symbol.Type.IsReadOnly && symbol.RefKind == RefKind.None)
{
Location location = parameter.GetLocation();
Diagnostic diagnostic = Diagnostic.Create(readOnlyStructRefDescriptor, location, symbol.Type);
context.ReportDiagnostic(diagnostic);
}
}
}
}
private bool IsBuiltInType(ITypeSymbol symbol)
{
return symbol.SpecialType switch
{
SpecialType.System_Boolean => true,
SpecialType.System_Char => true,
SpecialType.System_SByte => true,
SpecialType.System_Byte => true,
SpecialType.System_Int16 => true,
SpecialType.System_UInt16 => true,
SpecialType.System_Int32 => true,
SpecialType.System_UInt32 => true,
SpecialType.System_Int64 => true,
SpecialType.System_UInt64 => true,
SpecialType.System_Decimal => true,
SpecialType.System_Single => true,
SpecialType.System_Double => true,
SpecialType.System_IntPtr => true,
SpecialType.System_UIntPtr => true,
_ => false,
};
}
}

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
namespace Snap.Hutao.Test;
[TestClass]
public class CSharpLanguageFeatureTest
{
[TestMethod]
public unsafe void NullStringFixedAlsoNullPointer()
{
string testStr = null!;
fixed(char* pStr = testStr)
{
Assert.IsTrue(pStr == null);
}
}
[TestMethod]
public unsafe void EmptyStringFixedIsNullTerminator()
{
string testStr = string.Empty;
fixed (char* pStr = testStr)
{
Assert.IsTrue(*pStr == '\0');
}
}
[TestMethod]
public unsafe void EmptyStringAsSpanIsZeroLength()
{
string testStr = string.Empty;
ReadOnlySpan<char> testSpan = testStr;
Assert.IsTrue(testSpan.Length == 0);
}
[TestMethod]
public void EnumParseCanNotHandleEmptyString()
{
bool caught = false;
try
{
Enum.Parse<EnumA>(string.Empty);
}
catch (ArgumentException)
{
caught = true;
}
Assert.IsTrue(caught);
}
[TestMethod]
public void EnumParseCanHandleNumberString()
{
EnumA a = Enum.Parse<EnumA>("2");
Assert.AreEqual(a, EnumA.ValueB);
}
[TestMethod]
public void EnumToStringDecimal()
{
Assert.AreEqual("2", EnumA.ValueB.ToString("D"));
}
private enum EnumA
{
None = 0,
ValueA = 1,
ValueB = 2,
ValueC = 3,
}
[TestMethod]
public void GetTwiceOnPropertyResultsNotSame()
{
Assert.AreNotEqual(UUID, UUID);
}
[TestMethod]
public void ListOfStringCanEnumerateAsReadOnlySpanOfChar()
{
List<string> strings = new()
{
"a", "b", "c"
};
int count = 0;
foreach (ReadOnlySpan<char> chars in strings)
{
Assert.IsTrue(chars.Length == 1);
++count;
}
Assert.AreEqual(3, count);
}
[TestMethod]
public void RangeTrimLastOne()
{
int[] array = { 1, 2, 3, 4 };
Assert.AreEqual(3, array[..^1].Length);
}
public static Guid UUID { get => Guid.NewGuid(); }
}

View File

@@ -0,0 +1,87 @@
using Microsoft.Extensions.DependencyInjection;
using System;
namespace Snap.Hutao.Test;
[TestClass]
public class DependencyInjectionTest
{
[TestMethod]
public void OriginalTypeNotDiscoverable()
{
IServiceProvider services = new ServiceCollection()
.AddSingleton<IService, ServiceA>()
.AddSingleton<IService, ServiceB>()
.BuildServiceProvider();
Assert.IsNull(services.GetService<ServiceA>());
Assert.IsNull(services.GetService<ServiceB>());
}
[TestMethod]
public void ScopedServiceInitializeMultipleTimesInScope()
{
IServiceProvider services = new ServiceCollection()
.AddScoped<IService, ServiceA>()
.BuildServiceProvider();
IServiceScopeFactory scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
using (IServiceScope scope = scopeFactory.CreateScope())
{
IService service1 = scope.ServiceProvider.GetRequiredService<IService>();
IService service2 = scope.ServiceProvider.GetRequiredService<IService>();
Assert.AreNotEqual(service1.Id, service2.Id);
}
}
[TestMethod]
public void GenericServicesCanBeResolved()
{
IServiceProvider services = new ServiceCollection()
.AddTransient(typeof(IGenericService<>),typeof(GenericService<>))
.BuildServiceProvider();
Assert.IsNotNull(services.GetService<IGenericService<int>>());
}
private interface IService
{
Guid Id { get; }
}
private sealed class ServiceA : IService
{
public Guid Id
{
get => Guid.NewGuid();
}
}
private sealed class ServiceB : IService
{
public Guid Id
{
get => throw new NotImplementedException();
}
}
private interface IGenericService<T>
{
}
private sealed class GenericService<T> : IGenericService<T>
{
}
private sealed class NonInjectedServiceA
{
}
private sealed class NonInjectedServiceB
{
[ActivatorUtilitiesConstructor]
public NonInjectedServiceB(NonInjectedServiceA? serviceA)
{
}
}
}

View File

@@ -0,0 +1 @@
global using Microsoft.VisualStudio.TestTools.UnitTesting;

View File

@@ -0,0 +1,47 @@
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 SmapleNumberObjectJson = """
{
"A" : ""
}
""";
[TestMethod]
public void DelegatePropertyCanSerialize()
{
Sample sample = JsonSerializer.Deserialize<Sample>(SmapleObjectJson)!;
Assert.AreEqual(sample.B, 1);
}
[TestMethod]
public void EmptyStringCanSerializeAsNumber()
{
// Throw
StringNumberSample sample = JsonSerializer.Deserialize<StringNumberSample>(SmapleNumberObjectJson)!;
Assert.AreEqual(sample.A, 0);
}
private class Sample
{
public int A { get => B; set => B = value; }
public int B { get; set; }
}
private class StringNumberSample
{
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
public int A { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -12,10 +12,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Installer", "Snap.Hutao.Installer\Snap.Hutao.Installer.csproj", "{CEC01691-F65E-4874-9AE2-F571369A7631}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug As Fake Elevated|Any CPU = Debug As Fake Elevated|Any CPU
Debug As Fake Elevated|arm64 = Debug As Fake Elevated|arm64
Debug As Fake Elevated|x64 = Debug As Fake Elevated|x64
Debug As Fake Elevated|x86 = Debug As Fake Elevated|x86
Debug|Any CPU = Debug|Any CPU
Debug|arm64 = Debug|arm64
Debug|x64 = Debug|x64
@@ -26,6 +30,18 @@ Global
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|Any CPU.ActiveCfg = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|Any CPU.Build.0 = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|Any CPU.Deploy.0 = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|arm64.ActiveCfg = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|arm64.Build.0 = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|arm64.Deploy.0 = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x64.ActiveCfg = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x64.Build.0 = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x64.Deploy.0 = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x86.ActiveCfg = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x86.Build.0 = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug As Fake Elevated|x86.Deploy.0 = Debug As Fake Elevated|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|Any CPU.ActiveCfg = Debug|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|Any CPU.Build.0 = Debug|x64
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Debug|Any CPU.Deploy.0 = Debug|x64
@@ -50,8 +66,16 @@ 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|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|Any CPU.ActiveCfg = Debug As Fake Elevated|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|Any CPU.Build.0 = Debug As Fake Elevated|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|arm64.ActiveCfg = Debug As Fake Elevated|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|arm64.Build.0 = Debug As Fake Elevated|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|x64.ActiveCfg = Debug As Fake Elevated|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|x64.Build.0 = Debug As Fake Elevated|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|x86.ActiveCfg = Debug As Fake Elevated|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug As Fake Elevated|x86.Build.0 = Debug As Fake Elevated|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.ActiveCfg = Debug|x64
@@ -66,22 +90,30 @@ Global
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.ActiveCfg = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.Build.0 = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x86.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x86.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.Build.0 = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|Any CPU.ActiveCfg = Debug As Fake Elevated|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|Any CPU.Build.0 = Debug As Fake Elevated|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|arm64.ActiveCfg = Debug As Fake Elevated|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|arm64.Build.0 = Debug As Fake Elevated|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|x64.ActiveCfg = Debug As Fake Elevated|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|x64.Build.0 = Debug As Fake Elevated|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|x86.ActiveCfg = Debug As Fake Elevated|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug As Fake Elevated|x86.Build.0 = Debug As Fake Elevated|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|arm64.ActiveCfg = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|arm64.Build.0 = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x64.ActiveCfg = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x64.Build.0 = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x86.ActiveCfg = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x86.Build.0 = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|Any CPU.Build.0 = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|arm64.ActiveCfg = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|arm64.Build.0 = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.ActiveCfg = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.Build.0 = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.ActiveCfg = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -9,7 +9,7 @@
},
"pathSegment": {
"add": {
".*": [ ".cs" ]
".*": [ ".cs", ".resx" ]
}
},
"fileSuffixToExtension": {
@@ -19,11 +19,12 @@
},
"fileToFile": {
"add": {
".filenesting.json": [ "App.xaml.cs" ],
"app.manifest": [ "App.xaml.cs" ],
"Package.appxmanifest": [ "App.xaml.cs" ],
"GlobalUsing.cs": [ "Program.cs" ],
".filenesting.json": [ "Program.cs" ],
".editorconfig": [ "Program.cs" ]
"Package.appxmanifest": [ "App.xaml" ],
"Package.StoreAssociation.xml": [ "App.xaml" ],
".editorconfig": [ "Program.cs" ],
"GlobalUsing.cs": [ "Program.cs" ]
}
}
}

View File

@@ -10,28 +10,28 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<muxc:XamlControlsResources/>
<ResourceDictionary Source="ms-appx:///SettingsUI/Themes/Generic.xaml"/>
<ResourceDictionary Source="Control/Theme/FontStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<Color x:Key="AvatarPropertyAddValueColor">#FF74BF00</Color>
<Color x:Key="CompatBackgroundColor">#FFF4F4F4</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<Color x:Key="AvatarPropertyAddValueColor">#FF90E800</Color>
<Color x:Key="CompatBackgroundColor">#FF242424</Color>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<!-- Modify Window title bar color -->
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush"/>
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush"/>
<!-- Page Transparent Background -->
<StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush"/>
<!-- IconFont -->
<FontFamily x:Key="SymbolThemeFontFamily">ms-appx:///Resource/Font/Segoe Fluent Icons.ttf#Segoe Fluent Icons</FontFamily>
<!-- InfoBar Resource -->
<Thickness x:Key="InfoBarIconMargin">6,16,16,16</Thickness>
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
<Thickness x:Key="InfoBarIconMargin">19,16,19,16</Thickness>
<Thickness x:Key="InfoBarContentRootPadding">0,0,0,0</Thickness>
<x:Double x:Key="InfoBarIconFontSize">20</x:Double>
<!-- Pivot Resource -->
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
<Thickness x:Key="PivotHeaderItemMargin">16,0,0,0</Thickness>
@@ -42,15 +42,54 @@
<CornerRadius x:Key="CompatCornerRadiusRight">0,6,6,0</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusBottom">0,0,6,6</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
<!-- OpenPaneLength -->
<x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double>
<x:Double x:Key="CompatSplitViewOpenPaneLength2">268</x:Double>
<GridLength x:Key="CompatGridLength2">268</GridLength>
<x:Double x:Key="HomeAdaptiveCardHeight">180</x:Double>
<!-- Brushes -->
<SolidColorBrush x:Key="AvatarPropertyAddValueBrush" Color="{ThemeResource AvatarPropertyAddValueColor}"/>
<!-- Settings -->
<x:Double x:Key="SettingsCardSpacing">3</x:Double>
<x:Double x:Key="SettingsCardMinHeight">0</x:Double>
<x:Double x:Key="SettingsCardWrapThreshold">0</x:Double>
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
<Style
x:Key="SettingsSectionHeaderTextBlockStyle"
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
TargetType="TextBlock">
<Style.Setters>
<Setter Property="Margin" Value="1,29,0,5"/>
</Style.Setters>
</Style>
<!-- Uris -->
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String>
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
<x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String>
<x:String x:Key="DocumentLink_Home">https://hut.ao</x:String>
<x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String>
<x:String x:Key="Sponsor_Afadian">https://afdian.net/a/DismissedLight</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_ImgSign_ItemIcon">https://static.snapgenshin.com/Bg/UI_ImgSign_ItemIcon.png</x:String>
<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_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>
<!-- Converters -->
<cwuc:BoolNegationConverter x:Key="BoolNegationConverter"/>
<cwuc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<cwuc:FileSizeToFriendlyStringConverter x:Key="FileSizeToFriendlyStringConverter"/>
<shmmc:AchievementIconConverter x:Key="AchievementIconConverter"/>
<shmmc:AvatarCardConverter x:Key="AvatarCardConverter"/>
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
<shmmc:DescParamDescriptor x:Key="DescParamDescriptor"/>
<shmmc:DescriptionsParametersDescriptor x:Key="DescParamDescriptor"/>
<shmmc:ElementNameIconConverter x:Key="ElementNameIconConverter"/>
<shmmc:EmotionIconConverter x:Key="EmotionIconConverter"/>
<shmmc:EquipIconConverter x:Key="EquipIconConverter"/>
@@ -58,19 +97,20 @@
<shmmc:GachaAvatarIconConverter x:Key="GachaAvatarIconConverter"/>
<shmmc:GachaEquipIconConverter x:Key="GachaEquipIconConverter"/>
<shmmc:ItemIconConverter x:Key="ItemIconConverter"/>
<shmmc:PropertyInfoDescriptor x:Key="PropertyDescriptor"/>
<shmmc:MonsterIconConverter x:Key="MonsterIconConverter"/>
<shmmc:PropertiesParametersDescriptor x:Key="PropertyDescriptor"/>
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
<shvc:EmptyCollectionToBoolConverter x:Key="EmptyCollectionToBoolConverter"/>
<shvc:EmptyCollectionToBoolRevertConverter x:Key="EmptyCollectionToBoolRevertConverter"/>
<shvc:EmptyCollectionToVisibilityConverter x:Key="EmptyCollectionToVisibilityConverter"/>
<shvc:EmptyCollectionToVisibilityRevertConverter x:Key="EmptyCollectionToVisibilityRevertConverter"/>
<shvc:EmptyObjectToBoolConverter x:Key="EmptyObjectToBoolConverter"/>
<shvc:EmptyObjectToBoolRevertConverter x:Key="EmptyObjectToBoolRevertConverter"/>
<shvc:EmptyObjectToVisibilityConverter x:Key="EmptyObjectToVisibilityConverter"/>
<shvc:EmptyObjectToVisibilityRevertConverter x:Key="EmptyObjectToVisibilityRevertConverter"/>
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
<shvc:StringBoolConverter x:Key="StringBoolConverter"/>
<!-- Styles -->
<Style
x:Key="LargeGridViewItemStyle"
BasedOn="{StaticResource DefaultGridViewItemStyle}"
@@ -93,6 +133,328 @@
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{StaticResource CompatCornerRadius}"/>
</Style>
<Style x:Key="WebView2ContentDialogStyle" TargetType="ContentDialog">
<Setter Property="Foreground" Value="{ThemeResource ContentDialogForeground}"/>
<Setter Property="Background" Value="{ThemeResource ContentDialogBackground}"/>
<Setter Property="BorderThickness" Value="{ThemeResource ContentDialogBorderWidth}"/>
<Setter Property="BorderBrush" Value="{ThemeResource ContentDialogBorderBrush}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentDialog">
<Border x:Name="Container">
<Grid x:Name="LayoutRoot" Visibility="Collapsed">
<Rectangle x:Name="SmokeLayerBackground" Fill="{ThemeResource ContentDialogSmokeFill}"/>
<Border
x:Name="BackgroundElement"
MinWidth="{ThemeResource ContentDialogMinWidth}"
MinHeight="{ThemeResource ContentDialogMinHeight}"
MaxWidth="{ThemeResource ContentDialogMaxWidth}"
MaxHeight="{ThemeResource ContentDialogMaxHeight}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{TemplateBinding Background}"
BackgroundSizing="InnerBorderEdge"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
FlowDirection="{TemplateBinding FlowDirection}"
RenderTransformOrigin="0.5,0.5">
<Border.RenderTransform>
<ScaleTransform x:Name="ScaleTransform"/>
</Border.RenderTransform>
<Grid x:Name="DialogSpace" CornerRadius="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollViewer
x:Name="ContentScrollViewer"
HorizontalScrollBarVisibility="Disabled"
IsTabStop="False"
VerticalScrollBarVisibility="Disabled"
ZoomMode="Disabled">
<Grid
Padding="0"
BorderBrush="{ThemeResource ContentDialogSeparatorBorderBrush}"
BorderThickness="{ThemeResource ContentDialogSeparatorThickness}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ContentControl
x:Name="Title"
Margin="{ThemeResource ContentDialogTitleMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="{TemplateBinding Title}"
ContentTemplate="{TemplateBinding TitleTemplate}"
FontFamily="{StaticResource ContentControlThemeFontFamily}"
FontSize="20"
FontWeight="SemiBold"
Foreground="{TemplateBinding Foreground}"
IsTabStop="False">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<ContentPresenter
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
MaxLines="2"
TextWrapping="Wrap"/>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
<ContentPresenter
x:Name="Content"
Grid.Row="1"
Margin="0,0,0,8"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
FontFamily="{StaticResource ContentControlThemeFontFamily}"
FontSize="{StaticResource ControlContentThemeFontSize}"
Foreground="{TemplateBinding Foreground}"
TextWrapping="Wrap"/>
</Grid>
</ScrollViewer>
<Grid
x:Name="CommandSpace"
Grid.Row="1"
Padding="8,0,8,8"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
XYFocusKeyboardNavigation="Enabled">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="PrimaryColumn" Width="*"/>
<ColumnDefinition x:Name="FirstSpacer" Width="0"/>
<ColumnDefinition x:Name="SecondaryColumn" Width="0"/>
<ColumnDefinition x:Name="SecondSpacer" Width="{ThemeResource ContentDialogButtonSpacing}"/>
<ColumnDefinition x:Name="CloseColumn" Width="*"/>
</Grid.ColumnDefinitions>
<Button
x:Name="PrimaryButton"
HorizontalAlignment="Stretch"
Content="{TemplateBinding PrimaryButtonText}"
ElementSoundMode="FocusOnly"
IsEnabled="{TemplateBinding IsPrimaryButtonEnabled}"
IsTabStop="False"
Style="{TemplateBinding PrimaryButtonStyle}"/>
<Button
x:Name="SecondaryButton"
HorizontalAlignment="Stretch"
Content="{TemplateBinding SecondaryButtonText}"
ElementSoundMode="FocusOnly"
IsEnabled="{TemplateBinding IsSecondaryButtonEnabled}"
IsTabStop="False"
Style="{TemplateBinding SecondaryButtonStyle}"/>
<Button
x:Name="CloseButton"
Grid.Column="4"
HorizontalAlignment="Stretch"
Content="{TemplateBinding CloseButtonText}"
ElementSoundMode="FocusOnly"
IsTabStop="False"
Style="{TemplateBinding CloseButtonStyle}"/>
</Grid>
</Grid>
</Border>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="DialogShowingStates">
<VisualStateGroup.Transitions>
<VisualTransition To="DialogHidden">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="False"/>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleX">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.0"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFastAnimationDuration}"
Value="1.05"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleY">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.0"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFastAnimationDuration}"
Value="1.05"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Opacity">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.0"/>
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0.0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition To="DialogShowing">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleX">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.05"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScaleTransform" Storyboard.TargetProperty="ScaleY">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="1.05"/>
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Opacity">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0.0"/>
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="DialogHidden"/>
<VisualState x:Name="DialogShowing">
<VisualState.Setters>
<Setter Target="PrimaryButton.IsTabStop" Value="True"/>
<Setter Target="SecondaryButton.IsTabStop" Value="True"/>
<Setter Target="CloseButton.IsTabStop" Value="True"/>
<Setter Target="LayoutRoot.Visibility" Value="Visible"/>
<Setter Target="BackgroundElement.TabFocusNavigation" Value="Cycle"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DialogShowingWithoutSmokeLayer">
<VisualState.Setters>
<Setter Target="PrimaryButton.IsTabStop" Value="True"/>
<Setter Target="SecondaryButton.IsTabStop" Value="True"/>
<Setter Target="CloseButton.IsTabStop" Value="True"/>
<Setter Target="LayoutRoot.Visibility" Value="Visible"/>
<Setter Target="LayoutRoot.Background" Value="{x:Null}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DialogSizingStates">
<VisualState x:Name="DefaultDialogSizing"/>
<VisualState x:Name="FullDialogSizing">
<VisualState.Setters>
<Setter Target="BackgroundElement.VerticalAlignment" Value="Stretch"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ButtonsVisibilityStates">
<VisualState x:Name="AllVisible">
<VisualState.Setters>
<Setter Target="FirstSpacer.Width" Value="{ThemeResource ContentDialogButtonSpacing}"/>
<Setter Target="SecondaryColumn.Width" Value="*"/>
<Setter Target="SecondaryButton.(Grid.Column)" Value="2"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NoneVisible">
<VisualState.Setters>
<Setter Target="CommandSpace.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PrimaryVisible">
<VisualState.Setters>
<Setter Target="PrimaryButton.(Grid.Column)" Value="4"/>
<Setter Target="SecondaryButton.Visibility" Value="Collapsed"/>
<Setter Target="CloseButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SecondaryVisible">
<VisualState.Setters>
<Setter Target="SecondaryButton.(Grid.Column)" Value="4"/>
<Setter Target="PrimaryButton.Visibility" Value="Collapsed"/>
<Setter Target="CloseButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="CloseVisible">
<VisualState.Setters>
<Setter Target="PrimaryButton.Visibility" Value="Collapsed"/>
<Setter Target="SecondaryButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PrimaryAndSecondaryVisible">
<VisualState.Setters>
<Setter Target="SecondaryButton.(Grid.Column)" Value="4"/>
<Setter Target="CloseButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PrimaryAndCloseVisible">
<VisualState.Setters>
<Setter Target="SecondaryButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SecondaryAndCloseVisible">
<VisualState.Setters>
<Setter Target="PrimaryButton.Visibility" Value="Collapsed"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DefaultButtonStates">
<VisualState x:Name="NoDefaultButton"/>
<VisualState x:Name="PrimaryAsDefaultButton">
<VisualState.Setters>
<Setter Target="PrimaryButton.Style" Value="{StaticResource AccentButtonStyle}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SecondaryAsDefaultButton">
<VisualState.Setters>
<Setter Target="SecondaryButton.Style" Value="{StaticResource AccentButtonStyle}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="CloseAsDefaultButton">
<VisualState.Setters>
<Setter Target="CloseButton.Style" Value="{StaticResource AccentButtonStyle}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DialogBorderStates">
<VisualState x:Name="NoBorder"/>
<VisualState x:Name="AccentColorBorder">
<VisualState.Setters>
<Setter Target="BackgroundElement.BorderBrush" Value="{ThemeResource SystemControlForegroundAccentBrush}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ItemsPanelTemplate -->
<ItemsPanelTemplate x:Key="ItemsStackPanelTemplate">
<ItemsStackPanel/>
@@ -102,4 +464,4 @@
</ItemsPanelTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>
</Application>

View File

@@ -1,38 +1,44 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Notifications;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Exception;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Shell;
using System.Diagnostics;
using Windows.Storage;
namespace Snap.Hutao;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// This class must be public
/// </summary>
[HighQuality]
[Injection(InjectAs.Singleton)]
public partial class App : Application
[SuppressMessage("", "SH001")]
public sealed partial class App : Application
{
private const string AppInstanceKey = "main";
private readonly IServiceProvider serviceProvider;
private readonly IActivation activation;
private readonly ILogger<App> logger;
/// <summary>
/// Initializes the singleton application object.
/// </summary>
/// <param name="logger">日志器</param>
/// <param name="appCenter">App Center</param>
public App(ILogger<App> logger)
/// <param name="serviceProvider">服务提供器</param>
public App(IServiceProvider serviceProvider)
{
// load app resource
// Load app resource
InitializeComponent();
this.logger = logger;
_ = new ExceptionRecorder(this, logger);
activation = serviceProvider.GetRequiredService<IActivation>();
logger = serviceProvider.GetRequiredService<ILogger<App>>();
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
this.serviceProvider = serviceProvider;
}
/// <inheritdoc/>
@@ -41,19 +47,16 @@ public partial class App : Application
try
{
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
AppInstance firstInstance = AppInstance.FindOrRegisterForKey("main");
AppInstance firstInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey);
if (firstInstance.IsCurrent)
{
// manually invoke
Activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
firstInstance.Activated += Activation.Activate;
ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate;
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
activation.InitializeWith(firstInstance);
logger.LogInformation(EventIds.CommonLog, "Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version);
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.TemporaryFolder.Path);
JumpListHelper.ConfigureAsync().SafeForget(logger);
LogDiagnosticInformation();
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
}
else
{
@@ -62,10 +65,19 @@ public partial class App : Application
Process.GetCurrentProcess().Kill();
}
}
catch (Exception)
catch
{
// AppInstance.GetCurrent() calls failed
Process.GetCurrentProcess().Kill();
}
}
private void LogDiagnosticInformation()
{
HutaoOptions hutaoOptions = serviceProvider.GetRequiredService<HutaoOptions>();
logger.LogInformation("FamilyName: {name}", hutaoOptions.FamilyName);
logger.LogInformation("Version: {version}", hutaoOptions.Version);
logger.LogInformation("LocalCache: {folder}", hutaoOptions.LocalCache);
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao;
/// <summary>
/// 应用程序资源提供器
/// </summary>
[Injection(InjectAs.Transient, typeof(IAppResourceProvider))]
internal sealed class AppResourceProvider : IAppResourceProvider
{
private readonly App app;
/// <summary>
/// 构造一个新的应用程序资源提供器
/// </summary>
/// <param name="app">应用</param>
public AppResourceProvider(App app)
{
this.app = app;
}
/// <inheritdoc/>
public T GetResource<T>(string name)
{
return (T)app.Resources[name];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

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