mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
460 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6ec0f5d7e | ||
|
|
7e73710103 | ||
|
|
4ba9f32ac3 | ||
|
|
6cb2818161 | ||
|
|
34d291001b | ||
|
|
157f64fb47 | ||
|
|
6b9479d27e | ||
|
|
ea6cbb8d6b | ||
|
|
9f94f36cf8 | ||
|
|
c50dd0737c | ||
|
|
6f63ed55d7 | ||
|
|
4cc16153c2 | ||
|
|
a214dd331a | ||
|
|
e6c83390eb | ||
|
|
aa21e3e5e9 | ||
|
|
58c992e278 | ||
|
|
bd19bfdd52 | ||
|
|
7a9c1a7b3c | ||
|
|
c5a24ae0d7 | ||
|
|
6ee2899692 | ||
|
|
9b3ea4496a | ||
|
|
96ac468a24 | ||
|
|
0a26297446 | ||
|
|
de2cafd9e5 | ||
|
|
3f8674ad9a | ||
|
|
8e1ea87993 | ||
|
|
cdbe013310 | ||
|
|
a972ba0035 | ||
|
|
a285dbb9b9 | ||
|
|
39182ce44c | ||
|
|
95ad0ca574 | ||
|
|
3ec905f5ca | ||
|
|
4306461ba9 | ||
|
|
133e767be3 | ||
|
|
949cf59177 | ||
|
|
42e693ba8a | ||
|
|
09d00f25f6 | ||
|
|
4f9828f358 | ||
|
|
4f7cf08285 | ||
|
|
46a36a4d17 | ||
|
|
ef1c31732c | ||
|
|
26a392e376 | ||
|
|
2731abc815 | ||
|
|
c8fb7fe82a | ||
|
|
d4f6cf8e02 | ||
|
|
a3c4965932 | ||
|
|
e30a010931 | ||
|
|
8af117ea11 | ||
|
|
5f6d279dba | ||
|
|
aed1171933 | ||
|
|
dbc81f0a93 | ||
|
|
1c7b926d76 | ||
|
|
ab2d38b142 | ||
|
|
87f7e22022 | ||
|
|
7af57118bc | ||
|
|
5d03f5d0b5 | ||
|
|
84f629c113 | ||
|
|
ecd535f9a0 | ||
|
|
d3ea7cf8ac | ||
|
|
12afac8d1a | ||
|
|
5763f56ab2 | ||
|
|
a8b679acbf | ||
|
|
25987ba5b3 | ||
|
|
b0626a0d60 | ||
|
|
fd16521c94 | ||
|
|
eb077cc8d6 | ||
|
|
28813444bf | ||
|
|
c24cca58bf | ||
|
|
4da0258f7b | ||
|
|
0c98d4c2a1 | ||
|
|
85e718947c | ||
|
|
328bca597f | ||
|
|
94b957ed20 | ||
|
|
490df104f0 | ||
|
|
b8a7ecdd40 | ||
|
|
14f1b43e08 | ||
|
|
c93215320c | ||
|
|
da6534d36c | ||
|
|
b9306bc213 | ||
|
|
4ad031015b | ||
|
|
2080df1dc0 | ||
|
|
cc41037a2b | ||
|
|
2316a5beaf | ||
|
|
73d173613a | ||
|
|
ad10a758bd | ||
|
|
bf66cf96d8 | ||
|
|
2a439664c4 | ||
|
|
04e6f36111 | ||
|
|
f254f2d437 | ||
|
|
1c9cbc2082 | ||
|
|
2cc8435115 | ||
|
|
167dc906d4 | ||
|
|
945a53a737 | ||
|
|
38747298b5 | ||
|
|
1594edc16c | ||
|
|
7a5dec4291 | ||
|
|
300e99a9ae | ||
|
|
97bd2b5af9 | ||
|
|
a464cc1f33 | ||
|
|
8a447813d7 | ||
|
|
ec0cbc614d | ||
|
|
4c5e6984d7 | ||
|
|
c5bbbbdfe3 | ||
|
|
9f9f2a9d14 | ||
|
|
a7640fdcb3 | ||
|
|
2bec008381 | ||
|
|
3ada42c927 | ||
|
|
bbed07b2ea | ||
|
|
d90ce0afe0 | ||
|
|
4e0d83726e | ||
|
|
2b53ffd4d2 | ||
|
|
84c1fbd13c | ||
|
|
4077c85be4 | ||
|
|
948987e31a | ||
|
|
686d2bd3b8 | ||
|
|
552fb131ed | ||
|
|
cb1fb96cf8 | ||
|
|
7e37d0a008 | ||
|
|
3abe1d25cd | ||
|
|
00c13ec333 | ||
|
|
c3246da5dd | ||
|
|
79ade22f94 | ||
|
|
9d196c7c14 | ||
|
|
3949e32324 | ||
|
|
c04b473494 | ||
|
|
a8437b90fc | ||
|
|
c667623c06 | ||
|
|
e62a386ac8 | ||
|
|
7407bd44ce | ||
|
|
aaea81022f | ||
|
|
d99b7b46c7 | ||
|
|
a000b3a10e | ||
|
|
d06cd53ac4 | ||
|
|
0b8c3fd225 | ||
|
|
6c267b51d3 | ||
|
|
0140c85d07 | ||
|
|
9129ff88fd | ||
|
|
a123325c71 | ||
|
|
cb9e4c5c04 | ||
|
|
24a186e254 | ||
|
|
604c51e064 | ||
|
|
088f915a54 | ||
|
|
5fdbcfc2e8 | ||
|
|
bcaeb86fe7 | ||
|
|
c83db8695f | ||
|
|
c2319cac68 | ||
|
|
cd3ce6d338 | ||
|
|
0bdc5d6c54 | ||
|
|
f007f9b193 | ||
|
|
90ea0f7276 | ||
|
|
af5c4c258d | ||
|
|
eaa50df837 | ||
|
|
04ea760fba | ||
|
|
57c0016839 | ||
|
|
3f019e0d23 | ||
|
|
09e94f5ccc | ||
|
|
e5f295fae1 | ||
|
|
ae8da285ab | ||
|
|
50b244f76c | ||
|
|
171add072c | ||
|
|
16ea8ea5fb | ||
|
|
18bce39e2a | ||
|
|
d8712c45b4 | ||
|
|
325800602c | ||
|
|
71bbf20c12 | ||
|
|
a55ef96a79 | ||
|
|
f27308c12d | ||
|
|
a8877d93b8 | ||
|
|
4605df8f03 | ||
|
|
3a26c7a3ed | ||
|
|
e648080d8b | ||
|
|
60585f90ed | ||
|
|
f104823d04 | ||
|
|
29c6e20738 | ||
|
|
f922bd6c2f | ||
|
|
76b2921847 | ||
|
|
6a41ace38d | ||
|
|
40cd084304 | ||
|
|
f84e5f2f1b | ||
|
|
b9d7987605 | ||
|
|
ddf272d8f4 | ||
|
|
7c7b959916 | ||
|
|
260aa8797f | ||
|
|
e551072e52 | ||
|
|
f3a271dd48 | ||
|
|
f8d804eb4e | ||
|
|
4271bdb23b | ||
|
|
0f96a6d8da | ||
|
|
f9185b519e | ||
|
|
59f5095432 | ||
|
|
19d4b8f25d | ||
|
|
0026add54b | ||
|
|
0c6789c71d | ||
|
|
1d45c75584 | ||
|
|
b4d58405e0 | ||
|
|
85ee37926e | ||
|
|
3b411c6949 | ||
|
|
361cd850da | ||
|
|
3ea59a0352 | ||
|
|
9abe39e806 | ||
|
|
47fcd6f851 | ||
|
|
8f83853a8f | ||
|
|
f5300745b0 | ||
|
|
43f65db1b6 | ||
|
|
d2d1e73891 | ||
|
|
4bf1d8513d | ||
|
|
6006e79923 | ||
|
|
2cd5d0c622 | ||
|
|
f2d6a2627c | ||
|
|
7b6c340348 | ||
|
|
5e5f5398fb | ||
|
|
bb042a1498 | ||
|
|
9db1fc360b | ||
|
|
4a3676d549 | ||
|
|
fdbf7ecf90 | ||
|
|
a9712fa6df | ||
|
|
a3e105db7b | ||
|
|
94fe2016f8 | ||
|
|
baab021a42 | ||
|
|
5c3365a7c6 | ||
|
|
a5a85ac0fc | ||
|
|
967f01108c | ||
|
|
605a48f8f2 | ||
|
|
058484459b | ||
|
|
32a780f460 | ||
|
|
d50117af3d | ||
|
|
a8406cf5a4 | ||
|
|
67052b5f24 | ||
|
|
beff98c8c5 | ||
|
|
7756e348a8 | ||
|
|
b3fabf92e8 | ||
|
|
3ced5ee962 | ||
|
|
fc3424e59e | ||
|
|
5e7e310bf5 | ||
|
|
92afa4b23c | ||
|
|
82b00ba458 | ||
|
|
3f798b86a4 | ||
|
|
49fa5846f8 | ||
|
|
1e73c1ba96 | ||
|
|
f37ed74238 | ||
|
|
b57faac904 | ||
|
|
4f7be56376 | ||
|
|
81574f678c | ||
|
|
188fd831e4 | ||
|
|
8ff369bd11 | ||
|
|
51871f9795 | ||
|
|
8ca6251d33 | ||
|
|
00f083608e | ||
|
|
14c75d7c04 | ||
|
|
eb557afd18 | ||
|
|
2ac428d848 | ||
|
|
1d8ee4cee5 | ||
|
|
335dbc9dc7 | ||
|
|
c87555e86f | ||
|
|
809ea5768c | ||
|
|
7b035053a1 | ||
|
|
d872639c19 | ||
|
|
e969267c1e | ||
|
|
a874ddc078 | ||
|
|
853473ebf7 | ||
|
|
08a1363a13 | ||
|
|
d8fdcbcf2f | ||
|
|
0cdcb1c444 | ||
|
|
548ecea28b | ||
|
|
f14933a797 | ||
|
|
8b53bb6a89 | ||
|
|
99a91166ee | ||
|
|
0de4aff03a | ||
|
|
7ef88d390c | ||
|
|
fe8648e241 | ||
|
|
5f000629e8 | ||
|
|
604708298a | ||
|
|
cc529df60c | ||
|
|
195ddb20a0 | ||
|
|
020c8c38a8 | ||
|
|
800ecb07d7 | ||
|
|
486e7fffd2 | ||
|
|
119bacea7d | ||
|
|
900730935e | ||
|
|
cd37fc761f | ||
|
|
79d8a63016 | ||
|
|
0fd018f42e | ||
|
|
c1cc4961a0 | ||
|
|
243217d1fa | ||
|
|
c4e3e993fd | ||
|
|
4e4770dc23 | ||
|
|
a74d9e5c4c | ||
|
|
b7642847cd | ||
|
|
5f37b6bd6d | ||
|
|
9158aca48d | ||
|
|
591be8473c | ||
|
|
409ac022ea | ||
|
|
ff6f3c50c4 | ||
|
|
b3de8b11a1 | ||
|
|
5a952d8b41 | ||
|
|
39db32a48b | ||
|
|
67d17cf23f | ||
|
|
2df292f1b8 | ||
|
|
b09a16079c | ||
|
|
8bd831a16d | ||
|
|
097d5ab9ef | ||
|
|
73071e6e81 | ||
|
|
7da2b21db7 | ||
|
|
7b78b501a7 | ||
|
|
55d5847980 | ||
|
|
9259f173b5 | ||
|
|
b6080c45c2 | ||
|
|
704113cacd | ||
|
|
f4f3242546 | ||
|
|
23feb78f05 | ||
|
|
e347694a0b | ||
|
|
3748f721bf | ||
|
|
96f458b42e | ||
|
|
2b313e6c0a | ||
|
|
ed90664170 | ||
|
|
eb75ce75ce | ||
|
|
4188dd1f9e | ||
|
|
08b82dda07 | ||
|
|
f2fea5a0e6 | ||
|
|
368d0bfbd7 | ||
|
|
baee72a2fa | ||
|
|
6f1c7b250e | ||
|
|
75938c3ede | ||
|
|
79cf2839e6 | ||
|
|
cb06949e60 | ||
|
|
282eb228d9 | ||
|
|
674002ee8d | ||
|
|
faa00d7c5f | ||
|
|
638c9ff732 | ||
|
|
dabe80900d | ||
|
|
a8b7a7f850 | ||
|
|
7e5a536119 | ||
|
|
0ea85ae26b | ||
|
|
d3beccc304 | ||
|
|
facfec4831 | ||
|
|
d6eea91f2c | ||
|
|
a5f3821d43 | ||
|
|
6d5485d4ca | ||
|
|
1e92edca63 | ||
|
|
055e040238 | ||
|
|
96f6921120 | ||
|
|
ddbb2611d7 | ||
|
|
617961d0d2 | ||
|
|
cf1c3eb056 | ||
|
|
882d9fc655 | ||
|
|
6bfad4571a | ||
|
|
bc3aea5515 | ||
|
|
c1c7cda390 | ||
|
|
0bd8c01fdd | ||
|
|
c4e4ffebd6 | ||
|
|
d08d2b406f | ||
|
|
4c50479e96 | ||
|
|
f1cea8e5da | ||
|
|
3225f8105a | ||
|
|
565817c14a | ||
|
|
ade6613fe1 | ||
|
|
71fcbc367c | ||
|
|
a23043fb6d | ||
|
|
d9169df3b8 | ||
|
|
420bf6ff41 | ||
|
|
5b269c7b3d | ||
|
|
9387b955ee | ||
|
|
f8173208f4 | ||
|
|
3368ae1b25 | ||
|
|
5706e877e7 | ||
|
|
3cf0cd1c9a | ||
|
|
312aa5f71a | ||
|
|
4dca174019 | ||
|
|
fd52334c13 | ||
|
|
0ee875d28d | ||
|
|
1c46412324 | ||
|
|
a69ae12e4f | ||
|
|
d738c60d3c | ||
|
|
4f6c2905d2 | ||
|
|
4c337a79b9 | ||
|
|
a661530025 | ||
|
|
d3b72ecb98 | ||
|
|
69dc8355ad | ||
|
|
2c45274cd3 | ||
|
|
93ee1a3386 | ||
|
|
c5ab707b66 | ||
|
|
4226598442 | ||
|
|
5f38c370c1 | ||
|
|
de9abcfad4 | ||
|
|
53044b0dda | ||
|
|
e843c84374 | ||
|
|
8525aeafac | ||
|
|
d665ba22e5 | ||
|
|
d03f8185b8 | ||
|
|
2ad87d25df | ||
|
|
01fdcda729 | ||
|
|
f531684e6a | ||
|
|
cd066e1462 | ||
|
|
e9ee31a604 | ||
|
|
f2fc5e443c | ||
|
|
4d2b54d49d | ||
|
|
bea9d5caf2 | ||
|
|
2fbf904987 | ||
|
|
6cd7e14ac9 | ||
|
|
7826e019d7 | ||
|
|
e3e8400978 | ||
|
|
500003f9c2 | ||
|
|
147312c289 | ||
|
|
e90f76ead8 | ||
|
|
8631933ef4 | ||
|
|
c5dada3f72 | ||
|
|
9aaeb327b6 | ||
|
|
75e771c75e | ||
|
|
d4515936bd | ||
|
|
d9f2261129 | ||
|
|
f0d4ea9a10 | ||
|
|
bdcbba3237 | ||
|
|
a931a661cd | ||
|
|
0fb2991085 | ||
|
|
cda04c6aa7 | ||
|
|
b23659bf80 | ||
|
|
2a3f119fb0 | ||
|
|
30e888ffb2 | ||
|
|
27b79659a4 | ||
|
|
4cef096cb5 | ||
|
|
e065ba0964 | ||
|
|
4c0d86fd13 | ||
|
|
70ab81bb0f | ||
|
|
72be37834d | ||
|
|
f1a2a828c8 | ||
|
|
3eaaf0e4e2 | ||
|
|
41d7441eb3 | ||
|
|
173a1343c1 | ||
|
|
0fc0413612 | ||
|
|
1fc1f9b08d | ||
|
|
133a1a532e | ||
|
|
f4fa08a939 | ||
|
|
5020621c46 | ||
|
|
4d526bb363 | ||
|
|
b37d5331a2 | ||
|
|
451486e3fc | ||
|
|
bbacb038cb | ||
|
|
7aba05d9c8 | ||
|
|
fbcd43c0af | ||
|
|
51314da123 | ||
|
|
8ed2e8cc72 | ||
|
|
7ab10d7824 | ||
|
|
ca5f84911b | ||
|
|
500efa60ed | ||
|
|
650e5d8a6e | ||
|
|
79a9128434 | ||
|
|
c7c883bb11 | ||
|
|
3ee98810f1 | ||
|
|
f09c924a61 | ||
|
|
86d9a381c4 | ||
|
|
b2d141af11 | ||
|
|
15865eb746 | ||
|
|
242b9aa036 | ||
|
|
7b81cc7a9c | ||
|
|
2060f20707 | ||
|
|
27a6c4f1fb | ||
|
|
6427fe23ef | ||
|
|
6cf516f883 | ||
|
|
080674f648 | ||
|
|
09834ada6b |
3
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,6 +1,7 @@
|
||||
name: 问题反馈
|
||||
name: BUG Report 问题反馈
|
||||
description: 告诉我们你的问题
|
||||
title: "[Bug]: 在这里填写一个合适的标题"
|
||||
labels: ["BUG"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
15
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: 功能请求
|
||||
description: 告诉我们你的想法
|
||||
title: "[Feat]: 在这里填写一个合适的标题"
|
||||
name: Feature Request 功能请求
|
||||
description: Tell us about your thought 告诉我们你的想法
|
||||
title: "[Feat]: Place your title here 在这里填写一个合适的标题"
|
||||
labels: ["功能"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
@@ -8,20 +8,21 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please fill the form below
|
||||
请按下方的要求填写完整的问题表单。
|
||||
|
||||
- type: textarea
|
||||
id: back
|
||||
attributes:
|
||||
label: 背景与动机
|
||||
description: 添加此功能的理由,如果你想要实现多个功能,请分别发起多个单独的 Issue
|
||||
label: Background & Motivation 背景与动机
|
||||
description: Reason why this feature is needed. If multiple features is requested, please open multiple issues for each of them. 添加此功能的理由,如果你想要实现多个功能,请分别发起多个单独的 Issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: req
|
||||
attributes:
|
||||
label: 想要实现或优化的功能
|
||||
description: 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高
|
||||
label: Detail of the Feature 想要实现或优化的功能
|
||||
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted. 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高
|
||||
validations:
|
||||
required: true
|
||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -5,7 +5,12 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "nuget" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
- package-ecosystem: "nuget"
|
||||
directory: "/src/Snap.Hutao" # Snap.Hutao.csproj
|
||||
target-branch: "develop"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
packages:
|
||||
patterns:
|
||||
- "*"
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,8 +2,11 @@ desktop.ini
|
||||
|
||||
*.csproj.user
|
||||
*.pubxml
|
||||
*.DotSettings.user
|
||||
|
||||
.vs/
|
||||
.idea/
|
||||
src/Snap.Hutao/_ReSharper.Caches
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao/bin/
|
||||
src/Snap.Hutao/Snap.Hutao/obj/
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<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" />
|
||||
<add key="CommunityToolkit-MainLatest" value="https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-MainLatest/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" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
胡桃工具箱是一个 Windows 平台的开源的原神工具箱,旨在帮助玩家获得更好的游戏体验; 它是对官方移动端工具的一种非破坏性功能扩展,为不习惯在移动端进行原神游戏的 PC 玩家提供一个在 Windows 平台下获得接近移动端功能权利的途径
|
||||
|
||||
@@ -29,13 +29,12 @@ Snap Hutao is an open-source Genshin Impact toolbox on Windows platform, aim to
|
||||
|
||||
* [CommunityToolkit/dotnet](https://github.com/CommunityToolkit/dotnet)
|
||||
* [CommunityToolkit/Labs-Windows](https://github.com/CommunityToolkit/Labs-Windows)
|
||||
* [CommunityToolkit/WindowsCommunityToolkit](https://github.com/CommunityToolkit/WindowsCommunityToolkit)
|
||||
* [CommunityToolkit/Windows](https://github.com/CommunityToolkit/Windows)
|
||||
* [dahall/taskscheduler](https://github.com/dahall/taskscheduler)
|
||||
* [dotnet/efcore](https://github.com/dotnet/efcore)
|
||||
* [dotnet/runtime](https://github.com/dotnet/runtime)
|
||||
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
|
||||
* [microsoft/CsWin32](https://github.com/microsoft/CsWin32)
|
||||
* [microsoft/vs-threading](https://github.com/microsoft/vs-threading)
|
||||
* [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)
|
||||
@@ -45,5 +44,5 @@ Snap Hutao is an open-source Genshin Impact toolbox on Windows platform, aim to
|
||||
* [Snap.Hutao.Server](https://github.com/DGP-Studio/Snap.Hutao.Server)
|
||||
* [Snap.Metadata](https://github.com/DGP-Studio/Snap.Metadata)
|
||||
|
||||
## 近期活跃数据 / Active Stat
|
||||

|
||||
## 开发 / Development
|
||||

|
||||
@@ -11,6 +11,7 @@ trigger:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
exclude:
|
||||
- README.md
|
||||
|
||||
BIN
res/HutaoRepoBanner2.png
Normal file
BIN
res/HutaoRepoBanner2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 661 KiB |
BIN
res/HutaoRepoBanner2.psd
Normal file
BIN
res/HutaoRepoBanner2.psd
Normal file
Binary file not shown.
@@ -1,3 +1,3 @@
|
||||
本文件夹中的所有图片,均由 [DGP Studio](https://github.com/DGP-Studio) 委托 [Bilibili 画画的芦苇](https://space.bilibili.com/274422134) 绘制
|
||||
|
||||
Copyright ©2023 DGP Studio, All Rights Reserved.
|
||||
Copyright © 2023 DGP Studio, All Rights Reserved.
|
||||
@@ -57,6 +57,7 @@ dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||
dotnet_style_namespace_match_folder = true:suggestion
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
|
||||
dotnet_style_predefined_type_for_member_access = true:silent
|
||||
dotnet_diagnostic.CA1000.severity = suggestion
|
||||
[*.cs]
|
||||
#### 命名样式 ####
|
||||
|
||||
@@ -162,11 +163,166 @@ dotnet_diagnostic.CA1309.severity = suggestion
|
||||
dotnet_diagnostic.CA1805.severity = suggestion
|
||||
|
||||
# VSTHRD111: Use ConfigureAwait(bool)
|
||||
dotnet_diagnostic.VSTHRD111.severity = suggestion
|
||||
dotnet_diagnostic.VSTHRD111.severity = silent
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_prefer_readonly_struct = true:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
|
||||
# SA1600: Elements should be documented
|
||||
dotnet_diagnostic.SA1600.severity = none
|
||||
dotnet_diagnostic.SA1601.severity = silent
|
||||
dotnet_diagnostic.SA1602.severity = silent
|
||||
|
||||
# CA1008: 枚举应具有零值
|
||||
dotnet_diagnostic.CA1008.severity = suggestion
|
||||
|
||||
# CA1010: 还应实现泛型接口
|
||||
dotnet_diagnostic.CA1010.severity = suggestion
|
||||
|
||||
# CA1012: 抽象类型不应具有公共构造函数
|
||||
dotnet_diagnostic.CA1012.severity = suggestion
|
||||
|
||||
# CA1024: 在适用处使用属性
|
||||
dotnet_diagnostic.CA1024.severity = suggestion
|
||||
|
||||
# CA1034: 嵌套类型应不可见
|
||||
dotnet_diagnostic.CA1034.severity = suggestion
|
||||
|
||||
# CA1036: 重写可比较类型中的方法
|
||||
dotnet_diagnostic.CA1036.severity = suggestion
|
||||
|
||||
# CA1040: 避免使用空接口
|
||||
dotnet_diagnostic.CA1040.severity = suggestion
|
||||
|
||||
# CA1044: 属性不应是只写的
|
||||
dotnet_diagnostic.CA1044.severity = suggestion
|
||||
|
||||
# CA1043: 将整型或字符串参数用于索引器
|
||||
dotnet_diagnostic.CA1043.severity = suggestion
|
||||
|
||||
# CA1046: 不要对引用类型重载相等运算符
|
||||
dotnet_diagnostic.CA1046.severity = suggestion
|
||||
|
||||
# CA1051: 不要声明可见实例字段
|
||||
dotnet_diagnostic.CA1051.severity = suggestion
|
||||
|
||||
# CA1052: 静态容器类型应为 Static 或 NotInheritable
|
||||
dotnet_diagnostic.CA1052.severity = suggestion
|
||||
|
||||
# CA1058: 类型不应扩展某些基类型
|
||||
dotnet_diagnostic.CA1058.severity = suggestion
|
||||
|
||||
# CA1063: 正确实现 IDisposable
|
||||
dotnet_diagnostic.CA1063.severity = suggestion
|
||||
|
||||
# CA1065: 不要在意外的位置引发异常
|
||||
dotnet_diagnostic.CA1065.severity = suggestion
|
||||
|
||||
# CA1066: 重写 Object.Equals 时实现 IEquatable
|
||||
dotnet_diagnostic.CA1066.severity = suggestion
|
||||
|
||||
# CA1304: 指定 CultureInfo
|
||||
dotnet_diagnostic.CA1304.severity = suggestion
|
||||
|
||||
# CA1305: 指定 IFormatProvider
|
||||
dotnet_diagnostic.CA1305.severity = suggestion
|
||||
|
||||
# CA1307: 为了清晰起见,请指定 StringComparison
|
||||
dotnet_diagnostic.CA1307.severity = suggestion
|
||||
|
||||
# CA1310: 为了确保正确,请指定 StringComparison
|
||||
dotnet_diagnostic.CA1310.severity = suggestion
|
||||
|
||||
# CA1308: 将字符串规范化为大写
|
||||
dotnet_diagnostic.CA1308.severity = suggestion
|
||||
|
||||
# CA1501: 避免过度继承
|
||||
dotnet_diagnostic.CA1501.severity = suggestion
|
||||
|
||||
# CA1502: 避免过度复杂性
|
||||
dotnet_diagnostic.CA1502.severity = suggestion
|
||||
|
||||
# CA1505: 避免使用无法维护的代码
|
||||
dotnet_diagnostic.CA1505.severity = suggestion
|
||||
|
||||
# CA1506: 避免过度的类耦合
|
||||
dotnet_diagnostic.CA1506.severity = suggestion
|
||||
|
||||
# CA1508: 避免死条件代码
|
||||
dotnet_diagnostic.CA1508.severity = suggestion
|
||||
|
||||
# CA1810: 以内联方式初始化引用类型的静态字段
|
||||
dotnet_diagnostic.CA1810.severity = suggestion
|
||||
|
||||
# CA1813: 避免使用非密封特性
|
||||
dotnet_diagnostic.CA1813.severity = suggestion
|
||||
|
||||
# CA1814: 与多维数组相比,首选使用交错数组
|
||||
dotnet_diagnostic.CA1814.severity = suggestion
|
||||
|
||||
# CA1819: 属性不应返回数组
|
||||
dotnet_diagnostic.CA1819.severity = suggestion
|
||||
|
||||
# CA1820: 使用字符串长度测试是否有空字符串
|
||||
dotnet_diagnostic.CA1820.severity = suggestion
|
||||
|
||||
# CA1823: 避免未使用的私有字段
|
||||
dotnet_diagnostic.CA1823.severity = suggestion
|
||||
|
||||
# CA1849: 当在异步方法中时,调用异步方法
|
||||
dotnet_diagnostic.CA1849.severity = suggestion
|
||||
|
||||
# CA1852: 密封内部类型
|
||||
dotnet_diagnostic.CA1852.severity = suggestion
|
||||
|
||||
# CA2000: 丢失范围之前释放对象
|
||||
dotnet_diagnostic.CA2000.severity = suggestion
|
||||
|
||||
# CA2002: 不要锁定具有弱标识的对象
|
||||
dotnet_diagnostic.CA2002.severity = suggestion
|
||||
|
||||
# CA2007: 考虑对等待的任务调用 ConfigureAwait
|
||||
dotnet_diagnostic.CA2007.severity = suggestion
|
||||
|
||||
# CA2008: 不要在未传递 TaskScheduler 的情况下创建任务
|
||||
dotnet_diagnostic.CA2008.severity = suggestion
|
||||
|
||||
# CA2100: 检查 SQL 查询是否存在安全漏洞
|
||||
dotnet_diagnostic.CA2100.severity = suggestion
|
||||
|
||||
# CA2109: 检查可见的事件处理程序
|
||||
dotnet_diagnostic.CA2109.severity = suggestion
|
||||
|
||||
# CA2119: 密封满足私有接口的方法
|
||||
dotnet_diagnostic.CA2119.severity = suggestion
|
||||
|
||||
# CA2153: 不要捕获损坏状态异常
|
||||
dotnet_diagnostic.CA2153.severity = suggestion
|
||||
|
||||
# CA2201: 不要引发保留的异常类型
|
||||
dotnet_diagnostic.CA2201.severity = suggestion
|
||||
|
||||
# CA2207: 以内联方式初始化值类型的静态字段
|
||||
dotnet_diagnostic.CA2207.severity = suggestion
|
||||
|
||||
# CA2213: 应释放可释放的字段
|
||||
dotnet_diagnostic.CA2213.severity = suggestion
|
||||
|
||||
# CA2214: 不要在构造函数中调用可重写的方法
|
||||
dotnet_diagnostic.CA2214.severity = suggestion
|
||||
|
||||
# CA2215: Dispose 方法应调用基类释放
|
||||
dotnet_diagnostic.CA2215.severity = suggestion
|
||||
|
||||
# CA2216: 可释放类型应声明终结器
|
||||
dotnet_diagnostic.CA2216.severity = suggestion
|
||||
|
||||
# CA2227: 集合属性应为只读
|
||||
dotnet_diagnostic.CA2227.severity = suggestion
|
||||
|
||||
# CA2251: 使用 “string.Equals”
|
||||
dotnet_diagnostic.CA2251.severity = suggestion
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
// 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;
|
||||
@@ -16,7 +13,7 @@ namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class CommandGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.CommandAttribute";
|
||||
public const string AttributeName = "Snap.Hutao.Core.Annotation.CommandAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
@@ -79,7 +76,6 @@ internal sealed class CommandGenerator : IIncrementalGenerator
|
||||
|
||||
string className = classSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
|
||||
// TODO: 支持嵌套类
|
||||
string code = $$"""
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
@@ -99,4 +95,4 @@ internal sealed class CommandGenerator : IIncrementalGenerator
|
||||
string normalizedClassName = classSymbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.{commandName}.g.cs", code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// 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;
|
||||
@@ -17,6 +17,9 @@ namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
internal sealed class ConstructorGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.ConstructorGeneratedAttribute";
|
||||
private const string CompilerGenerated = "System.Runtime.CompilerServices.CompilerGeneratedAttribute";
|
||||
|
||||
//private static readonly DiagnosticDescriptor genericTypeNotSupportedDescriptor = new("SH102", "Generic type is not supported to generate .ctor", "Type [{0}] is not supported", "Quality", DiagnosticSeverity.Error, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
@@ -62,17 +65,18 @@ internal sealed class ConstructorGenerator : IIncrementalGenerator
|
||||
AttributeData constructorInfo = context2.SingleAttribute(AttributeName);
|
||||
|
||||
bool resolveHttpClient = constructorInfo.HasNamedArgumentWith<bool>("ResolveHttpClient", value => value);
|
||||
bool callBaseConstructor = constructorInfo.HasNamedArgumentWith<bool>("CallBaseConstructor", value => value);
|
||||
string httpclient = resolveHttpClient ? ", System.Net.Http.HttpClient httpClient" : string.Empty;
|
||||
|
||||
FieldValueAssignmentOptions options = new(resolveHttpClient);
|
||||
FieldValueAssignmentOptions options = new(resolveHttpClient, callBaseConstructor);
|
||||
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(ConstructorGenerator)}}", "1.0.0.0")]
|
||||
partial class {{context2.Symbol.Name}}
|
||||
partial class {{context2.Symbol.ToDisplayString(SymbolDisplayFormats.QualifiedNonNullableFormat)}}
|
||||
{
|
||||
public {{context2.Symbol.Name}}(System.IServiceProvider serviceProvider{{httpclient}})
|
||||
public {{context2.Symbol.Name}}(System.IServiceProvider serviceProvider{{httpclient}}){{(options.CallBaseConstructor ? " : base(serviceProvider)" : string.Empty)}}
|
||||
{
|
||||
|
||||
""");
|
||||
@@ -83,8 +87,9 @@ internal sealed class ConstructorGenerator : IIncrementalGenerator
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
production.AddSource($"{context2.Symbol.ToDisplayString()}.ctor.g.cs", sourceBuilder.ToString());
|
||||
|
||||
string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.ctor.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithFieldValueAssignment(StringBuilder builder, GeneratorSyntaxContext2 context2, FieldValueAssignmentOptions options)
|
||||
@@ -95,6 +100,31 @@ internal sealed class ConstructorGenerator : IIncrementalGenerator
|
||||
|
||||
foreach (IFieldSymbol fieldSymbol in fields)
|
||||
{
|
||||
if (fieldSymbol.Name.AsSpan()[0] is '<')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool shoudSkip = false;
|
||||
foreach (SyntaxReference syntaxReference in fieldSymbol.DeclaringSyntaxReferences)
|
||||
{
|
||||
if (syntaxReference.GetSyntax() is VariableDeclaratorSyntax declarator)
|
||||
{
|
||||
if (declarator.Initializer is not null)
|
||||
{
|
||||
// Skip field with initializer
|
||||
builder.Append(" // Skip field with initializer: ").AppendLine(fieldSymbol.Name);
|
||||
shoudSkip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shoudSkip)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldSymbol.IsReadOnly && !fieldSymbol.IsStatic)
|
||||
{
|
||||
switch (fieldSymbol.Type.ToDisplayString())
|
||||
@@ -137,7 +167,7 @@ internal sealed class ConstructorGenerator : IIncrementalGenerator
|
||||
}
|
||||
}
|
||||
|
||||
foreach(INamedTypeSymbol interfaceSymbol in context2.Symbol.Interfaces)
|
||||
foreach (INamedTypeSymbol interfaceSymbol in context2.Symbol.Interfaces)
|
||||
{
|
||||
if (interfaceSymbol.Name == "IRecipient")
|
||||
{
|
||||
@@ -152,10 +182,12 @@ internal sealed class ConstructorGenerator : IIncrementalGenerator
|
||||
private readonly struct FieldValueAssignmentOptions
|
||||
{
|
||||
public readonly bool ResolveHttpClient;
|
||||
public readonly bool CallBaseConstructor;
|
||||
|
||||
public FieldValueAssignmentOptions(bool resolveHttpClient)
|
||||
public FieldValueAssignmentOptions(bool resolveHttpClient, bool callBaseConstructor)
|
||||
{
|
||||
ResolveHttpClient = resolveHttpClient;
|
||||
ResolveHttpClient = resolveHttpClient;
|
||||
CallBaseConstructor = callBaseConstructor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class DependencyPropertyGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.DependencyPropertyAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> commands =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, CommandMethod)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(commands, GenerateDependencyPropertyImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 CommandMethod(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? methodSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, methodSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateDependencyPropertyImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2 context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateDependencyPropertyImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateDependencyPropertyImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
foreach (AttributeData propertyInfo in context2.Attributes.Where(attr => attr.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
string owner = context2.Symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
Dictionary<string, TypedConstant> namedArguments = propertyInfo.NamedArguments.ToDictionary();
|
||||
bool isAttached = namedArguments.TryGetValue("IsAttached", out TypedConstant constant) && (bool)constant.Value!;
|
||||
string register = isAttached ? "RegisterAttached" : "Register";
|
||||
|
||||
ImmutableArray<TypedConstant> arguments = propertyInfo.ConstructorArguments;
|
||||
|
||||
string propertyName = (string)arguments[0].Value!;
|
||||
string propertyType = arguments[1].Value!.ToString();
|
||||
string defaultValue = GetLiteralString(arguments.ElementAtOrDefault(2)) ?? "default";
|
||||
string propertyChangedCallback = arguments.ElementAtOrDefault(3) is { IsNull: false } arg3 ? $", {arg3.Value}" : string.Empty;
|
||||
|
||||
string code;
|
||||
if (isAttached)
|
||||
{
|
||||
string objType = namedArguments.TryGetValue("AttachedType", out TypedConstant attachedType)
|
||||
? attachedType.Value!.ToString()
|
||||
: "object";
|
||||
|
||||
code = $$"""
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
partial class {{owner}}
|
||||
{
|
||||
private static readonly DependencyProperty {{propertyName}}Property =
|
||||
DependencyProperty.RegisterAttached("{{propertyName}}", typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
|
||||
|
||||
public static {{propertyType}} Get{{propertyName}}({{objType}} obj)
|
||||
{
|
||||
return ({{propertyType}})obj?.GetValue({{propertyName}}Property);
|
||||
}
|
||||
|
||||
public static void Set{{propertyName}}({{objType}} obj, {{propertyType}} value)
|
||||
{
|
||||
obj.SetValue({{propertyName}}Property, value);
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
else
|
||||
{
|
||||
code = $$"""
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
partial class {{owner}}
|
||||
{
|
||||
private static readonly DependencyProperty {{propertyName}}Property =
|
||||
DependencyProperty.Register(nameof({{propertyName}}), typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
|
||||
|
||||
public {{propertyType}} {{propertyName}}
|
||||
{
|
||||
get => ({{propertyType}})GetValue({{propertyName}}Property);
|
||||
set => SetValue({{propertyName}}Property, value);
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.{propertyName}.g.cs", code);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetLiteralString(TypedConstant typedConstant)
|
||||
{
|
||||
if (typedConstant.IsNull)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (typedConstant.Value is bool boolValue)
|
||||
{
|
||||
return boolValue ? "true" : "false";
|
||||
}
|
||||
|
||||
string result = typedConstant.Value!.ToString();
|
||||
if (string.IsNullOrEmpty(result))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -24,6 +23,8 @@ internal sealed class HttpClientGenerator : IIncrementalGenerator
|
||||
private const string UseDynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
|
||||
private const string CRLF = "\r\n";
|
||||
|
||||
private static readonly DiagnosticDescriptor injectionShouldOmitDescriptor = new("SH201", "Injection 特性可以省略", "HttpClient 特性已将 {0} 注册为 Transient 服务", "Quality", DiagnosticSeverity.Warning, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
|
||||
@@ -91,6 +92,17 @@ internal sealed class HttpClientGenerator : IIncrementalGenerator
|
||||
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
if (context.SingleOrDefaultAttribute(InjectionGenerator.AttributeName) is AttributeData injectionData)
|
||||
{
|
||||
if (injectionData.ConstructorArguments[0].ToCSharpString() == InjectionGenerator.InjectAsTransientName)
|
||||
{
|
||||
if (injectionData.ConstructorArguments.Length < 2)
|
||||
{
|
||||
production.ReportDiagnostic(Diagnostic.Create(injectionShouldOmitDescriptor, context.Context.Node.GetLocation(), context.Context.Node));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lineBuilder.Clear().Append(CRLF);
|
||||
lineBuilder.Append(@" services.AddHttpClient<");
|
||||
|
||||
|
||||
@@ -16,11 +16,10 @@ 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";
|
||||
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
|
||||
public const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
|
||||
public const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
|
||||
public const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
|
||||
|
||||
private static readonly DiagnosticDescriptor invalidInjectionDescriptor = new("SH101", "无效的 InjectAs 枚举值", "尚未支持生成 {0} 配置", "Quality", DiagnosticSeverity.Error, true);
|
||||
|
||||
@@ -87,7 +86,7 @@ internal sealed class InjectionGenerator : IIncrementalGenerator
|
||||
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
lineBuilder.Clear().Append(CRLF);
|
||||
lineBuilder.Clear().AppendLine();
|
||||
|
||||
AttributeData injectionInfo = context.SingleAttribute(AttributeName);
|
||||
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
|
||||
@@ -105,7 +104,7 @@ internal sealed class InjectionGenerator : IIncrementalGenerator
|
||||
lineBuilder.Append(" services.AddScoped<");
|
||||
break;
|
||||
default:
|
||||
production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, null, injectAsName));
|
||||
production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, context.Context.Node.GetLocation(), injectAsName));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
internal class ServiceAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor NonSingletonUseServiceProviderDescriptor = new("SH301", "Non Singleton service should avoid direct use of IServiceProvider", "Non Singleton service should avoid direct use of IServiceProvider", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor SingletonServiceCaptureNonSingletonServiceDescriptor = new("SH302", "Singleton service should avoid keep reference of non singleton service", "Singleton service should avoid keep reference of non singleton service", "Quality", DiagnosticSeverity.Info, true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
|
||||
{
|
||||
get => new DiagnosticDescriptor[]
|
||||
{
|
||||
NonSingletonUseServiceProviderDescriptor,
|
||||
SingletonServiceCaptureNonSingletonServiceDescriptor,
|
||||
}.ToImmutableArray();
|
||||
}
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterCompilationStartAction(CompilationStart);
|
||||
}
|
||||
|
||||
private static void CompilationStart(CompilationStartAnalysisContext context)
|
||||
{
|
||||
context.RegisterSyntaxNodeAction(HandleNonSingletonUseServiceProvider, SyntaxKind.ClassDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleSingletonServiceCaptureNonSingletonService, SyntaxKind.ClassDeclaration);
|
||||
}
|
||||
|
||||
private static void HandleNonSingletonUseServiceProvider(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
ClassDeclarationSyntax classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
|
||||
if (classDeclarationSyntax.HasAttributeLists())
|
||||
{
|
||||
INamedTypeSymbol? classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
|
||||
if (classSymbol is not null)
|
||||
{
|
||||
foreach (AttributeData attributeData in classSymbol.GetAttributes())
|
||||
{
|
||||
if (attributeData.AttributeClass!.ToDisplayString() is InjectionGenerator.AttributeName)
|
||||
{
|
||||
string serviceType = attributeData.ConstructorArguments[0].ToCSharpString();
|
||||
if (serviceType is InjectionGenerator.InjectAsTransientName or InjectionGenerator.InjectAsScopedName)
|
||||
{
|
||||
HandleNonSingletonUseServiceProviderActual(context, classSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleNonSingletonUseServiceProviderActual(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
ISymbol? symbol = classSymbol.GetMembers().Where(m => m is IFieldSymbol f && f.Type.ToDisplayString() == "System.IServiceProvider").SingleOrDefault();
|
||||
|
||||
if (symbol is not null)
|
||||
{
|
||||
Diagnostic diagnostic = Diagnostic.Create(NonSingletonUseServiceProviderDescriptor, symbol.Locations.FirstOrDefault());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleSingletonServiceCaptureNonSingletonService(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
//classSymbol.GetMembers().Where(m => m is IFieldSymbol { IsReadOnly: true, DeclaredAccessibility: Accessibility.Private } f);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ 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
|
||||
@@ -116,7 +116,7 @@ internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
.Where(m => m.Kind == SymbolKind.Field)
|
||||
.Cast<IFieldSymbol>();
|
||||
|
||||
foreach(IFieldSymbol fieldSymbol in fields)
|
||||
foreach (IFieldSymbol fieldSymbol in fields)
|
||||
{
|
||||
AttributeData? localizationKeyInfo = fieldSymbol.GetAttributes()
|
||||
.SingleOrDefault(data => data.AttributeClass!.ToDisplayString() == LocalizationKeyName);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
@@ -32,4 +33,9 @@ internal static class EnumerableExtension
|
||||
while (enumerator.MoveNext());
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> source)
|
||||
{
|
||||
return source.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ 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)
|
||||
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;
|
||||
|
||||
@@ -12,4 +12,10 @@ internal static class SymbolDisplayFormats
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
||||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
|
||||
|
||||
public static SymbolDisplayFormat QualifiedNonNullableFormat { get; } = new(
|
||||
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
||||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
</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>
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
@@ -10,14 +10,6 @@
|
||||
<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>
|
||||
<None Remove="stylecop.json" />
|
||||
</ItemGroup>
|
||||
@@ -28,7 +20,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" Version="3.3.4" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
@@ -16,16 +17,33 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor typeInternalDescriptor = new("SH001", "Type should be internal", "Type [{0}] should be internal", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor readOnlyStructRefDescriptor = new("SH002", "ReadOnly struct should be passed with ref-like key word", "ReadOnly Struct [{0}] should be passed with ref-like key word", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useValueTaskIfPossibleDescriptor = new("SH003", "Use ValueTask instead of Task whenever possible", "Use ValueTask instead of Task", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsNotNullPatternMatchingDescriptor = new("SH004", "Use \"is not null\" instead of \"!= null\" whenever possible", "Use \"is not null\" instead of \"!= null\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsNullPatternMatchingDescriptor = new("SH005", "Use \"is null\" instead of \"== null\" whenever possible", "Use \"is null\" instead of \"== null\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsPatternRecursiveMatchingDescriptor = new("SH006", "Use \"is { } obj\" whenever possible", "Use \"is {{ }} {0}\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useArgumentNullExceptionThrowIfNullDescriptor = new("SH007", "Use \"ArgumentNullException.ThrowIfNull()\" instead of \"!\"", "Use \"ArgumentNullException.ThrowIfNull()\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
|
||||
|
||||
private static readonly ImmutableHashSet<string> RefLikeKeySkipTypes = new HashSet<string>()
|
||||
{
|
||||
"System.Threading.CancellationToken",
|
||||
"System.Guid"
|
||||
}.ToImmutableHashSet();
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
|
||||
{
|
||||
get
|
||||
{
|
||||
return new DiagnosticDescriptor[]
|
||||
{
|
||||
typeInternalDescriptor,
|
||||
readOnlyStructRefDescriptor,
|
||||
}.ToImmutableArray();
|
||||
{
|
||||
typeInternalDescriptor,
|
||||
readOnlyStructRefDescriptor,
|
||||
useValueTaskIfPossibleDescriptor,
|
||||
useIsNotNullPatternMatchingDescriptor,
|
||||
useIsNullPatternMatchingDescriptor,
|
||||
useIsPatternRecursiveMatchingDescriptor,
|
||||
useArgumentNullExceptionThrowIfNullDescriptor
|
||||
}.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,23 +55,34 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
context.RegisterCompilationStartAction(CompilationStart);
|
||||
}
|
||||
|
||||
private void CompilationStart(CompilationStartAnalysisContext context)
|
||||
private static void CompilationStart(CompilationStartAnalysisContext context)
|
||||
{
|
||||
SyntaxKind[] types = new SyntaxKind[]
|
||||
SyntaxKind[] types =
|
||||
{
|
||||
SyntaxKind.ClassDeclaration,
|
||||
SyntaxKind.InterfaceDeclaration,
|
||||
SyntaxKind.StructDeclaration,
|
||||
SyntaxKind.EnumDeclaration
|
||||
SyntaxKind.EnumDeclaration,
|
||||
};
|
||||
context.RegisterSyntaxNodeAction(HandleTypeShouldBeInternal, types);
|
||||
context.RegisterSyntaxNodeAction(HandleMethodParameterShouldUseRefLikeKeyword, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleConstructorParameterShouldUseRefLikeKeyword, SyntaxKind.ConstructorDeclaration);
|
||||
|
||||
context.RegisterSyntaxNodeAction(HandleTypeDeclaration, types);
|
||||
SyntaxKind[] expressions =
|
||||
{
|
||||
SyntaxKind.EqualsExpression,
|
||||
SyntaxKind.NotEqualsExpression,
|
||||
};
|
||||
context.RegisterSyntaxNodeAction(HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching, expressions);
|
||||
context.RegisterSyntaxNodeAction(HandleIsPatternShouldUseRecursivePattern, SyntaxKind.IsPatternExpression);
|
||||
context.RegisterSyntaxNodeAction(HandleArgumentNullExceptionThrowIfNull, SyntaxKind.SuppressNullableWarningExpression);
|
||||
|
||||
context.RegisterSyntaxNodeAction(HandleMethodDeclaration, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleConstructorDeclaration, SyntaxKind.ConstructorDeclaration);
|
||||
// TODO add analyzer for unnecessary IServiceProvider registration
|
||||
// TODO add analyzer for Singlton service use Scoped or Transient services
|
||||
}
|
||||
|
||||
private void HandleTypeDeclaration(SyntaxNodeAnalysisContext context)
|
||||
private static void HandleTypeShouldBeInternal(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
BaseTypeDeclarationSyntax syntax = (BaseTypeDeclarationSyntax)context.Node;
|
||||
|
||||
@@ -87,15 +116,10 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleMethodDeclaration(SyntaxNodeAnalysisContext context)
|
||||
private static void HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
|
||||
|
||||
// 跳过异步方法,因为异步方法无法使用 ref in out
|
||||
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.AsyncKeyword)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
|
||||
|
||||
// 跳过重载方法
|
||||
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.OverrideKeyword)))
|
||||
@@ -103,12 +127,57 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
return;
|
||||
}
|
||||
|
||||
// ICommand can only use Task or Task<T>
|
||||
if (methodSymbol.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == Automation.CommandGenerator.AttributeName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
|
||||
{
|
||||
Location location = methodSyntax.ReturnType.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useValueTaskIfPossibleDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleMethodParameterShouldUseRefLikeKeyword(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
|
||||
|
||||
// 跳过方法定义 如 接口
|
||||
if (methodSyntax.Body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.ValueTask"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (SyntaxToken token in methodSyntax.Modifiers)
|
||||
{
|
||||
// 跳过异步方法,因为异步方法无法使用 ref/in/out
|
||||
if (token.IsKind(SyntaxKind.AsyncKeyword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳过重载方法
|
||||
if (token.IsKind(SyntaxKind.OverrideKeyword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ParameterSyntax parameter in methodSyntax.ParameterList.Parameters)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
|
||||
@@ -118,8 +187,7 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过 CancellationToken
|
||||
if (symbol.Type.ToDisplayString() == "System.Threading.CancellationToken")
|
||||
if (RefLikeKeySkipTypes.Contains(symbol.Type.ToDisplayString()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -134,7 +202,7 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConstructorDeclaration(SyntaxNodeAnalysisContext context)
|
||||
private static void HandleConstructorParameterShouldUseRefLikeKeyword(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
ConstructorDeclarationSyntax constructorSyntax = (ConstructorDeclarationSyntax)context.Node;
|
||||
|
||||
@@ -163,7 +231,63 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsBuiltInType(ITypeSymbol symbol)
|
||||
public static void HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
BinaryExpressionSyntax syntax = (BinaryExpressionSyntax)context.Node;
|
||||
if (syntax.IsKind(SyntaxKind.NotEqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
|
||||
{
|
||||
Location location = syntax.OperatorToken.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsNotNullPatternMatchingDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
else if (syntax.IsKind(SyntaxKind.EqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
|
||||
{
|
||||
Location location = syntax.OperatorToken.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsNullPatternMatchingDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleIsPatternShouldUseRecursivePattern(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
IsPatternExpressionSyntax syntax = (IsPatternExpressionSyntax)context.Node;
|
||||
if (syntax.Pattern is DeclarationPatternSyntax declaration)
|
||||
{
|
||||
ITypeSymbol? leftType = context.SemanticModel.GetTypeInfo(syntax.Expression).ConvertedType;
|
||||
ITypeSymbol? rightType = context.SemanticModel.GetTypeInfo(declaration).ConvertedType;
|
||||
if (SymbolEqualityComparer.Default.Equals(leftType, rightType))
|
||||
{
|
||||
Location location = declaration.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsPatternRecursiveMatchingDescriptor, location, declaration.Designation);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleArgumentNullExceptionThrowIfNull(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
PostfixUnaryExpressionSyntax syntax = (PostfixUnaryExpressionSyntax)context.Node;
|
||||
|
||||
if (syntax.Operand is LiteralExpressionSyntax literal)
|
||||
{
|
||||
if (literal.IsKind(SyntaxKind.DefaultLiteralExpression))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (syntax.Operand is DefaultExpressionSyntax expression)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Location location = syntax.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useArgumentNullExceptionThrowIfNullDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
|
||||
private static bool IsBuiltInType(ITypeSymbol symbol)
|
||||
{
|
||||
return symbol.SpecialType switch
|
||||
{
|
||||
|
||||
@@ -1,61 +1,57 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test;
|
||||
|
||||
[TestClass]
|
||||
public class DependencyInjectionTest
|
||||
public sealed class DependencyInjectionTest
|
||||
{
|
||||
[ClassInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
|
||||
}
|
||||
private readonly IServiceProvider services = new ServiceCollection()
|
||||
.AddSingleton<IService, ServiceA>()
|
||||
.AddSingleton<IService, ServiceB>()
|
||||
.AddScoped<IScopedService, ServiceA>()
|
||||
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
|
||||
.BuildServiceProvider();
|
||||
|
||||
[TestMethod]
|
||||
public void OriginalTypeNotDiscoverable()
|
||||
public void OriginalTypeCannotResolved()
|
||||
{
|
||||
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<>))
|
||||
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
|
||||
.BuildServiceProvider();
|
||||
|
||||
Assert.IsNotNull(services.GetService<IGenericService<int>>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ScopedServiceInitializeMultipleTimesInScope()
|
||||
{
|
||||
using (IServiceScope scope = services.CreateScope())
|
||||
{
|
||||
IScopedService service1 = scope.ServiceProvider.GetRequiredService<IScopedService>();
|
||||
IScopedService service2 = scope.ServiceProvider.GetRequiredService<IScopedService>();
|
||||
Assert.AreNotEqual(service1.Id, service2.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private interface IService
|
||||
{
|
||||
Guid Id { get; }
|
||||
}
|
||||
|
||||
private sealed class ServiceA : IService
|
||||
private interface IScopedService
|
||||
{
|
||||
Guid Id { get; }
|
||||
}
|
||||
|
||||
private sealed class ServiceA : IService, IScopedService
|
||||
{
|
||||
public Guid Id
|
||||
{
|
||||
|
||||
@@ -13,13 +13,13 @@ public class JsonSerializeTest
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SmapleNumberObjectJson = """
|
||||
private const string SmapleEmptyStringObjectJson = """
|
||||
{
|
||||
"A" : ""
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SmapleNumberDictionaryJson = """
|
||||
private const string SmapleNumberKeyDictionaryJson = """
|
||||
{
|
||||
"111" : "12",
|
||||
"222" : "34"
|
||||
@@ -34,21 +34,11 @@ public class JsonSerializeTest
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(JsonException))]
|
||||
public void EmptyStringCannotSerializeAsNumber()
|
||||
{
|
||||
bool caught = false;
|
||||
try
|
||||
{
|
||||
// Throw
|
||||
StringNumberSample sample = JsonSerializer.Deserialize<StringNumberSample>(SmapleNumberObjectJson)!;
|
||||
Assert.AreEqual(sample.A, 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
caught = true;
|
||||
}
|
||||
|
||||
Assert.IsTrue(caught);
|
||||
StringNumberSample sample = JsonSerializer.Deserialize<StringNumberSample>(SmapleEmptyStringObjectJson)!;
|
||||
Assert.AreEqual(sample.A, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -59,17 +49,17 @@ public class JsonSerializeTest
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
Dictionary<int,string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberDictionaryJson, options)!;
|
||||
Dictionary<int,string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, options)!;
|
||||
Assert.AreEqual(sample[111], "12");
|
||||
}
|
||||
|
||||
private class Sample
|
||||
private sealed class Sample
|
||||
{
|
||||
public int A { get => B; set => B = value; }
|
||||
public int B { get; set; }
|
||||
}
|
||||
|
||||
private class StringNumberSample
|
||||
private sealed class StringNumberSample
|
||||
{
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||
public int A { get; set; }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
internal sealed class EnumRuntimeBehaviorTest
|
||||
public sealed class EnumRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
@@ -20,7 +20,6 @@ internal sealed class EnumRuntimeBehaviorTest
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(Exception), AllowDerivedTypes = true)]
|
||||
public void EnumToStringDecimal()
|
||||
{
|
||||
Assert.AreEqual("2", EnumA.ValueB.ToString("D"));
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Collections.Generic;
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
public class ForEachRuntimeBehaviorTest
|
||||
public sealed class ForEachRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void ListOfStringCanEnumerateAsReadOnlySpanOfChar()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
internal sealed class PropertyRuntimeBehaviorTest
|
||||
public sealed class PropertyRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void GetTwiceOnPropertyResultsNotSame()
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
public class RangeRuntimeBehaviorTest
|
||||
[TestClass]
|
||||
public sealed class RangeRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void RangeTrimLastOne()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
internal sealed class StringRuntimeBehaviorTest
|
||||
public sealed class StringRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public unsafe void NullStringFixedIsNullPointer()
|
||||
@@ -32,4 +32,4 @@ internal sealed class StringRuntimeBehaviorTest
|
||||
ReadOnlySpan<char> testSpan = testStr;
|
||||
Assert.IsTrue(testSpan.Length == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
public sealed class UnsafeRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public unsafe void UInt32AllSetIs()
|
||||
{
|
||||
byte[] bytes = { 0xFF, 0xFF, 0xFF, 0xFF, };
|
||||
|
||||
fixed (byte* pBytes = bytes)
|
||||
{
|
||||
Assert.AreEqual(uint.MaxValue, *(uint*)pBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -119,6 +119,11 @@ Global
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
|
||||
RESX_ShowErrorsInErrorList = False
|
||||
RESX_SortFileContentOnSave = True
|
||||
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
||||
RESX_NeutralResourcesLanguage = zh-CN
|
||||
RESX_AutoApplyExistingTranslations = False
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
x:Class="Snap.Hutao.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cwcw="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||
xmlns:shci="using:Snap.Hutao.Control.Image"
|
||||
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
|
||||
xmlns:shvc="using:Snap.Hutao.View.Converter">
|
||||
<Application.Resources>
|
||||
@@ -42,16 +44,25 @@
|
||||
<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">304</x:Double>
|
||||
<!-- Length -->
|
||||
<GridLength x:Key="CompatGridLength2">288</GridLength>
|
||||
|
||||
<x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double>
|
||||
<x:Double x:Key="CompatSplitViewOpenPaneLength2">304</x:Double>
|
||||
<x:Double x:Key="CompatSplitViewOpenPaneLength3">320</x:Double>
|
||||
<x:Double x:Key="HomeAdaptiveCardHeight">180</x:Double>
|
||||
<x:Double x:Key="ContentDialogMinHeight">64</x:Double>
|
||||
<x:Double x:Key="LargeAppBarButtonWidth">100</x:Double>
|
||||
<!-- ProgressBar -->
|
||||
<x:Double x:Key="LargeBackgroundProgressBarOpacity">0.2</x:Double>
|
||||
|
||||
<ThemeShadow x:Key="CompatShadow"/>
|
||||
<!-- Brushes -->
|
||||
<SolidColorBrush x:Key="AvatarPropertyAddValueBrush" Color="{ThemeResource AvatarPropertyAddValueColor}"/>
|
||||
<SolidColorBrush x:Key="BlueBrush" Color="#FF5180CB"/>
|
||||
<SolidColorBrush x:Key="PurpleBrush" Color="#FFA156E0"/>
|
||||
<SolidColorBrush x:Key="OrangeBrush" Color="#FFBC6932"/>
|
||||
<SolidColorBrush x:Key="GuaranteePullBrush" Color="#FF0063FF"/>
|
||||
<SolidColorBrush x:Key="UpPullBrush" Color="#FFFFA400"/>
|
||||
<!-- Settings -->
|
||||
<x:Double x:Key="SettingsCardSpacing">3</x:Double>
|
||||
<x:Double x:Key="SettingsCardMinHeight">0</x:Double>
|
||||
@@ -65,6 +76,12 @@
|
||||
<Setter Property="Margin" Value="1,29,0,5"/>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="SettingsContentComboBoxStyle"
|
||||
BasedOn="{StaticResource DefaultComboBoxStyle}"
|
||||
TargetType="ComboBox">
|
||||
<Setter Property="MinWidth" Value="120"/>
|
||||
</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>
|
||||
@@ -73,7 +90,15 @@
|
||||
<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>
|
||||
|
||||
<!-- Images -->
|
||||
<x:String x:Key="UI_ItemIcon_None">https://static.snapgenshin.com/Bg/UI_ItemIcon_None.png</x:String>
|
||||
<x:String x:Key="UI_MarkTower">https://static.snapgenshin.com/Bg/UI_MarkTower.png</x:String>
|
||||
<x:String x:Key="UI_Icon_Intee_Explore_1">https://static.snapgenshin.com/Bg/UI_Icon_Intee_Explore_1.png</x:String>
|
||||
<x:String x:Key="UI_MarkQuest_Events_Proce">https://static.snapgenshin.com/Bg/UI_MarkQuest_Events_Proce.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_201">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_201.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_204">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_204.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_210">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_210.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_220021">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_220021.png</x:String>
|
||||
<x:String x:Key="UI_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>
|
||||
@@ -81,6 +106,16 @@
|
||||
<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>
|
||||
|
||||
<!-- FontIcon Content -->
|
||||
<x:String x:Key="FontIconContentAdd"></x:String>
|
||||
<x:String x:Key="FontIconContentSetting"></x:String>
|
||||
<x:String x:Key="FontIconContentRefresh"></x:String>
|
||||
<x:String x:Key="FontIconContentDelete"></x:String>
|
||||
<x:String x:Key="FontIconContentFolder"></x:String>
|
||||
<x:String x:Key="FontIconContentCheckList"></x:String>
|
||||
<x:String x:Key="FontIconContentAsteriskBadge12"></x:String>
|
||||
<x:String x:Key="FontIconContentZipFolder"></x:String>
|
||||
<!-- Converters -->
|
||||
<cwuc:BoolNegationConverter x:Key="BoolNegationConverter"/>
|
||||
<cwuc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
@@ -110,8 +145,8 @@
|
||||
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
|
||||
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
|
||||
<shvc:StringBoolConverter x:Key="StringBoolConverter"/>
|
||||
<!-- Styles -->
|
||||
|
||||
<!-- Styles -->
|
||||
<Style
|
||||
x:Key="LargeGridViewItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
@@ -134,324 +169,85 @@
|
||||
<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}"/>
|
||||
<Style x:Key="BorderGridStyle" TargetType="Grid">
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="{StaticResource CompatCornerRadius}"/>
|
||||
</Style>
|
||||
<Style TargetType="shci:CachedImage">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}"/>
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="CornerRadius" Value="0"/>
|
||||
<Setter Property="LazyLoadingThreshold" Value="300"/>
|
||||
<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>
|
||||
|
||||
<ControlTemplate TargetType="shci:CachedImage">
|
||||
<Grid
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Image
|
||||
Name="PlaceholderImage"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
Opacity="1.0"
|
||||
Source="{TemplateBinding PlaceholderSource}"
|
||||
Stretch="{TemplateBinding PlaceholderStretch}"/>
|
||||
<Image
|
||||
Name="Image"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
NineGrid="{TemplateBinding NineGrid}"
|
||||
Opacity="0.0"
|
||||
Stretch="{TemplateBinding Stretch}"/>
|
||||
<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>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Failed">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Image" Storyboard.TargetProperty="Opacity">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="0"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PlaceholderImage" Storyboard.TargetProperty="Opacity">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</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 x:Name="Loading">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Image" Storyboard.TargetProperty="Opacity">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="0"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PlaceholderImage" Storyboard.TargetProperty="Opacity">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</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 x:Name="Loaded">
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
AutoReverse="False"
|
||||
BeginTime="0"
|
||||
Storyboard.TargetName="Image"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="0"
|
||||
To="1"
|
||||
Duration="0:0:0.5"/>
|
||||
<DoubleAnimation
|
||||
AutoReverse="False"
|
||||
BeginTime="0"
|
||||
Storyboard.TargetName="PlaceholderImage"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="1"
|
||||
To="0"
|
||||
Duration="0:0:0.5"/>
|
||||
</Storyboard>
|
||||
</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>
|
||||
|
||||
<VisualState x:Name="Unloaded"/>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Border>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
@@ -460,9 +256,16 @@
|
||||
<ItemsPanelTemplate x:Key="ItemsStackPanelTemplate">
|
||||
<ItemsStackPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="WrapPanelTemplate">
|
||||
<cwcw:WrapPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelTemplate">
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
<!-- Transitions -->
|
||||
<TransitionCollection x:Key="ReorderThemeTransitions">
|
||||
<ReorderThemeTransition/>
|
||||
</TransitionCollection>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -74,10 +74,10 @@ public sealed partial class App : Application
|
||||
|
||||
private void LogDiagnosticInformation()
|
||||
{
|
||||
HutaoOptions hutaoOptions = serviceProvider.GetRequiredService<HutaoOptions>();
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
|
||||
logger.LogInformation("FamilyName: {name}", hutaoOptions.FamilyName);
|
||||
logger.LogInformation("Version: {version}", hutaoOptions.Version);
|
||||
logger.LogInformation("LocalCache: {folder}", hutaoOptions.LocalCache);
|
||||
logger.LogInformation("FamilyName: {name}", runtimeOptions.FamilyName);
|
||||
logger.LogInformation("Version: {version}", runtimeOptions.Version);
|
||||
logger.LogInformation("LocalCache: {folder}", runtimeOptions.LocalCache);
|
||||
}
|
||||
}
|
||||
1
src/Snap.Hutao/Snap.Hutao/CodeMetricsConfig.txt
Normal file
1
src/Snap.Hutao/Snap.Hutao/CodeMetricsConfig.txt
Normal file
@@ -0,0 +1 @@
|
||||
CA1501: 8
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Snap.Hutao.Control.Alternating;
|
||||
|
||||
[DependencyProperty("ItemAlternateBackground", typeof(Brush))]
|
||||
internal sealed partial class AlternatingItemsControl : ItemsControl
|
||||
{
|
||||
private readonly VectorChangedEventHandler<object> itemsVectorChangedEventHandler;
|
||||
|
||||
public AlternatingItemsControl()
|
||||
{
|
||||
itemsVectorChangedEventHandler = OnItemsVectorChanged;
|
||||
Items.VectorChanged += itemsVectorChangedEventHandler;
|
||||
}
|
||||
|
||||
private void OnItemsVectorChanged(IObservableVector<object> items, IVectorChangedEventArgs args)
|
||||
{
|
||||
if (args.CollectionChange is CollectionChange.Reset)
|
||||
{
|
||||
int index = (int)args.Index;
|
||||
for (int i = index; i < items.Count; i++)
|
||||
{
|
||||
if (items[i] is IAlternatingItem item)
|
||||
{
|
||||
item.Background = i % 2 is 0 ? default : ItemAlternateBackground;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace Snap.Hutao.Control.Alternating;
|
||||
|
||||
internal interface IAlternatingItem
|
||||
{
|
||||
public Brush? Background { get; set; }
|
||||
}
|
||||
@@ -13,4 +13,14 @@ internal static class AnimationDurations
|
||||
/// 图片缩放动画
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageZoom = TimeSpan.FromSeconds(0.5);
|
||||
|
||||
/// <summary>
|
||||
/// 图像淡入
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageFadeIn = TimeSpan.FromSeconds(0.3);
|
||||
|
||||
/// <summary>
|
||||
/// 图像淡出
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageFadeOut = TimeSpan.FromSeconds(0.2);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI;
|
||||
using CommunityToolkit.WinUI.UI.Animations;
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using Microsoft.UI.Composition;
|
||||
using System.Numerics;
|
||||
|
||||
@@ -21,7 +21,7 @@ internal sealed class ImageZoomInAnimation : ImplicitAnimation<string, Vector3>
|
||||
{
|
||||
Duration = AnimationDurations.ImageZoom;
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
|
||||
EasingType = CommunityToolkit.WinUI.UI.Animations.EasingType.Circle;
|
||||
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
|
||||
To = Core.StringLiterals.OnePointOne;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI;
|
||||
using CommunityToolkit.WinUI.UI.Animations;
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using Microsoft.UI.Composition;
|
||||
using System.Numerics;
|
||||
|
||||
@@ -21,7 +21,7 @@ internal sealed class ImageZoomOutAnimation : ImplicitAnimation<string, Vector3>
|
||||
{
|
||||
Duration = AnimationDurations.ImageZoom;
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
|
||||
EasingType = CommunityToolkit.WinUI.UI.Animations.EasingType.Circle;
|
||||
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
|
||||
To = Core.StringLiterals.One;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Behaviors;
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
@@ -10,41 +10,30 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<AutoHeightBehavior>.DependBoxed<double>(nameof(TargetWidth), BoxedValues.DoubleOne);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<AutoHeightBehavior>.DependBoxed<double>(nameof(TargetHeight), BoxedValues.DoubleOne);
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
/// </summary>
|
||||
public double TargetWidth
|
||||
public AutoHeightBehavior()
|
||||
{
|
||||
get => (double)GetValue(TargetWidthProperty);
|
||||
set => SetValue(TargetWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标高度
|
||||
/// </summary>
|
||||
public double TargetHeight
|
||||
{
|
||||
get => (double)GetValue(TargetHeightProperty);
|
||||
set => SetValue(TargetHeightProperty, value);
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
protected override bool Initialize()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += OnSizeChanged;
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDetaching()
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= OnSizeChanged;
|
||||
base.OnDetaching();
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
@@ -54,6 +43,6 @@ internal sealed class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Height = (double)AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
|
||||
AssociatedObject.Height = AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Behaviors;
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
@@ -10,41 +10,30 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<AutoWidthBehavior>.DependBoxed<double>(nameof(TargetWidth), BoxedValues.DoubleOne);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<AutoWidthBehavior>.DependBoxed<double>(nameof(TargetHeight), BoxedValues.DoubleOne);
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
/// </summary>
|
||||
public double TargetWidth
|
||||
public AutoWidthBehavior()
|
||||
{
|
||||
get => (double)GetValue(TargetWidthProperty);
|
||||
set => SetValue(TargetWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标高度
|
||||
/// </summary>
|
||||
public double TargetHeight
|
||||
{
|
||||
get => (double)GetValue(TargetHeightProperty);
|
||||
set => SetValue(TargetHeightProperty, value);
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
protected override bool Initialize()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += OnSizeChanged;
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDetaching()
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= OnSizeChanged;
|
||||
base.OnDetaching();
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
@@ -54,6 +43,6 @@ internal sealed class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Width = (double)AssociatedObject.Height * (TargetWidth / TargetHeight);
|
||||
AssociatedObject.Width = AssociatedObject.Height * (TargetWidth / TargetHeight);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI.UI.Behaviors;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
/// <summary>
|
||||
/// AppTitleBar Workaround
|
||||
/// https://github.com/microsoft/microsoft-ui-xaml/issues/7756
|
||||
/// </summary>
|
||||
internal sealed class ComboBoxExtendsContentIntoTitleBarWorkaroundBehavior : BehaviorBase<ComboBox>
|
||||
{
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
/// <summary>
|
||||
/// AppTitleBar Workaround
|
||||
/// </summary>
|
||||
public ComboBoxExtendsContentIntoTitleBarWorkaroundBehavior()
|
||||
{
|
||||
messenger = Ioc.Default.GetRequiredService<IMessenger>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
{
|
||||
AssociatedObject.DropDownOpened += OnDropDownOpened;
|
||||
AssociatedObject.DropDownClosed += OnDropDownClosed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
AssociatedObject.DropDownOpened -= OnDropDownOpened;
|
||||
AssociatedObject.DropDownClosed -= OnDropDownClosed;
|
||||
|
||||
base.OnDetaching();
|
||||
}
|
||||
|
||||
private void OnDropDownOpened(object? sender, object e)
|
||||
{
|
||||
messenger.Send(new Message.FlyoutOpenCloseMessage(true));
|
||||
}
|
||||
|
||||
private void OnDropDownClosed(object? sender, object e)
|
||||
{
|
||||
messenger.Send(new Message.FlyoutOpenCloseMessage(false));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Behaviors;
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
@@ -10,34 +10,14 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
/// 在元素加载完成后执行命令的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class InvokeCommandOnLoadedBehavior : BehaviorBase<UIElement>
|
||||
[DependencyProperty("Command", typeof(ICommand))]
|
||||
[DependencyProperty("CommandParameter", typeof(object))]
|
||||
internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIElement>
|
||||
{
|
||||
private static readonly DependencyProperty CommandProperty = Property<InvokeCommandOnLoadedBehavior>.Depend<ICommand>(nameof(Command));
|
||||
private static readonly DependencyProperty CommandParameterProperty = Property<InvokeCommandOnLoadedBehavior>.Depend<object>(nameof(CommandParameter));
|
||||
|
||||
/// <summary>
|
||||
/// 待执行的命令
|
||||
/// </summary>
|
||||
public ICommand Command
|
||||
{
|
||||
get => (ICommand)GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 命令参数
|
||||
/// </summary>
|
||||
[MaybeNull]
|
||||
public object CommandParameter
|
||||
{
|
||||
get => GetValue(CommandParameterProperty);
|
||||
set => SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
{
|
||||
if (Command != null && Command.CanExecute(CommandParameter))
|
||||
if (Command is not null && Command.CanExecute(CommandParameter))
|
||||
{
|
||||
Command.Execute(CommandParameter);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@ internal sealed class OpenAttachedFlyoutAction : DependencyObject, IAction
|
||||
public object Execute(object sender, object parameter)
|
||||
{
|
||||
FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement);
|
||||
return null!;
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
internal sealed class SelectedItemInViewBehavior : BehaviorBase<ListViewBase>
|
||||
{
|
||||
protected override bool Initialize()
|
||||
{
|
||||
if (AssociatedObject.SelectedItem is { } item)
|
||||
{
|
||||
AssociatedObject.ScrollIntoView(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -12,16 +12,7 @@ namespace Snap.Hutao.Control;
|
||||
/// when object is not used anymore.
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class BindingProxy : DependencyObject
|
||||
[DependencyProperty("DataContext", typeof(object))]
|
||||
internal sealed partial class BindingProxy : DependencyObject
|
||||
{
|
||||
private static readonly DependencyProperty DataContextProperty = Property<BindingProxy>.Depend<object>(nameof(DataContext));
|
||||
|
||||
/// <summary>
|
||||
/// 数据上下文
|
||||
/// </summary>
|
||||
public object? DataContext
|
||||
{
|
||||
get => GetValue(DataContextProperty);
|
||||
set => SetValue(DataContextProperty, value);
|
||||
}
|
||||
}
|
||||
@@ -9,21 +9,6 @@ namespace Snap.Hutao.Control;
|
||||
[HighQuality]
|
||||
internal static class BoxedValues
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="double"/> 0
|
||||
/// </summary>
|
||||
public static readonly object DoubleZero = 0D;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="double"/> 0
|
||||
/// </summary>
|
||||
public static readonly object DoubleOne = 1D;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="int"/> 0
|
||||
/// </summary>
|
||||
public static readonly object Int32Zero = 0;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="true"/>
|
||||
/// </summary>
|
||||
|
||||
19
src/Snap.Hutao/Snap.Hutao/Control/ColorSegment.cs
Normal file
19
src/Snap.Hutao/Snap.Hutao/Control/ColorSegment.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
internal sealed class ColorSegment : IColorSegment
|
||||
{
|
||||
public ColorSegment(Color color, double value)
|
||||
{
|
||||
Color = color;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public Color Color { get; set; }
|
||||
|
||||
public double Value { get; set; }
|
||||
}
|
||||
@@ -17,34 +17,13 @@ internal static class ContentDialogExtension
|
||||
/// <param name="contentDialog">对话框</param>
|
||||
/// <param name="taskContext">任务上下文</param>
|
||||
/// <returns>用于恢复用户交互</returns>
|
||||
public static async ValueTask<IDisposable> BlockAsync(this ContentDialog contentDialog, ITaskContext taskContext)
|
||||
public static async ValueTask<ContentDialogHideToken> BlockAsync(this ContentDialog contentDialog, ITaskContext taskContext)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
contentDialog.ShowAsync().AsTask().SafeForget();
|
||||
|
||||
// E_ASYNC_OPERATION_NOT_STARTED 0x80000019
|
||||
// Only a single ContentDialog can be open at any time.
|
||||
return new ContentDialogHider(contentDialog, taskContext);
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SA1201")]
|
||||
[SuppressMessage("", "SA1400")]
|
||||
[SuppressMessage("", "SA1600")]
|
||||
file readonly struct ContentDialogHider : IDisposable
|
||||
{
|
||||
private readonly ContentDialog contentDialog;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
public ContentDialogHider(ContentDialog contentDialog, ITaskContext taskContext)
|
||||
{
|
||||
this.contentDialog = contentDialog;
|
||||
this.taskContext = taskContext;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Hide() must be called on main thread.
|
||||
taskContext.InvokeOnMainThread(contentDialog.Hide);
|
||||
contentDialog.ShowAsync().AsTask().SafeForget();
|
||||
return new ContentDialogHideToken(contentDialog, taskContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Extension;
|
||||
|
||||
internal struct ContentDialogHideToken : IDisposable, IAsyncDisposable
|
||||
{
|
||||
private readonly ContentDialog contentDialog;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
private bool disposed = false;
|
||||
private bool disposing = false;
|
||||
|
||||
public ContentDialogHideToken(ContentDialog contentDialog, ITaskContext taskContext)
|
||||
{
|
||||
this.contentDialog = contentDialog;
|
||||
this.taskContext = taskContext;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!disposed && !disposing)
|
||||
{
|
||||
disposing = true;
|
||||
taskContext.InvokeOnMainThread(contentDialog.Hide); // Hide() must be called on main thread.
|
||||
disposing = false;
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!disposed && !disposing)
|
||||
{
|
||||
disposing = true;
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
contentDialog.Hide();
|
||||
disposing = false;
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Extension;
|
||||
|
||||
internal static class DependencyObjectExtension
|
||||
{
|
||||
public static IServiceProvider ServiceProvider(this DependencyObject obj)
|
||||
{
|
||||
return Ioc.Default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Extension;
|
||||
|
||||
internal static class FrameworkElementExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Make properties below false:
|
||||
/// <code>
|
||||
/// * AllowFocusOnInteraction
|
||||
/// * IsDoubleTapEnabled
|
||||
/// * IsHitTestVisible
|
||||
/// * IsHoldingEnabled
|
||||
/// * IsRightTapEnabled
|
||||
/// * IsTabStop
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="frameworkElement">元素</param>
|
||||
public static void DisableInteraction(this FrameworkElement frameworkElement)
|
||||
{
|
||||
frameworkElement.AllowFocusOnInteraction = false;
|
||||
frameworkElement.IsDoubleTapEnabled = false;
|
||||
frameworkElement.IsHitTestVisible = false;
|
||||
frameworkElement.IsHoldingEnabled = false;
|
||||
frameworkElement.IsRightTapEnabled = false;
|
||||
frameworkElement.IsTabStop = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Helper;
|
||||
|
||||
[SuppressMessage("", "SH001")]
|
||||
[DependencyProperty("SquareLength", typeof(double), 0D, nameof(OnSquareLengthChanged), IsAttached = true, AttachedType = typeof(FrameworkElement))]
|
||||
public sealed partial class FrameworkElementHelper
|
||||
{
|
||||
private static void OnSquareLengthChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
Microsoft.UI.Xaml.Controls.Control control = (Microsoft.UI.Xaml.Controls.Control)dp;
|
||||
control.Width = (double)e.NewValue;
|
||||
control.Height = (double)e.NewValue;
|
||||
}
|
||||
}
|
||||
13
src/Snap.Hutao/Snap.Hutao/Control/IColorSegment.cs
Normal file
13
src/Snap.Hutao/Snap.Hutao/Control/IColorSegment.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
internal interface IColorSegment
|
||||
{
|
||||
Color Color { get; }
|
||||
|
||||
double Value { get; }
|
||||
}
|
||||
@@ -32,16 +32,10 @@ internal sealed class CachedImage : ImageEx
|
||||
|
||||
try
|
||||
{
|
||||
Verify.Operation(imageUri.Host != string.Empty, SH.ControlImageCachedImageInvalidResourceUri);
|
||||
|
||||
// BitmapImage need to be created by main thread.
|
||||
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
|
||||
|
||||
// check token state to determine whether the operation should be canceled.
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// BitmapImage initialize with a uri will increase image quality and loading speed.
|
||||
return new BitmapImage(new(file));
|
||||
Verify.Operation(!string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
|
||||
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
|
||||
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
|
||||
return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
@@ -54,9 +48,5 @@ internal sealed class CachedImage : ImageEx
|
||||
// task was explicitly canceled
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +52,15 @@ internal static class CompositionExtension
|
||||
Mode = blendEffectMode,
|
||||
};
|
||||
|
||||
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
|
||||
using (effect)
|
||||
{
|
||||
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
|
||||
|
||||
brush.SetSourceParameter(Background, background);
|
||||
brush.SetSourceParameter(Foreground, foreground);
|
||||
brush.SetSourceParameter(Background, background);
|
||||
brush.SetSourceParameter(Foreground, foreground);
|
||||
|
||||
return brush;
|
||||
return brush;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -75,11 +78,14 @@ internal static class CompositionExtension
|
||||
Source = new CompositionEffectSourceParameter(Source),
|
||||
};
|
||||
|
||||
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
|
||||
using (effect)
|
||||
{
|
||||
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
|
||||
|
||||
brush.SetSourceParameter(Source, source);
|
||||
brush.SetSourceParameter(Source, source);
|
||||
|
||||
return brush;
|
||||
return brush;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -97,11 +103,14 @@ internal static class CompositionExtension
|
||||
Source = new CompositionEffectSourceParameter(Source),
|
||||
};
|
||||
|
||||
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
|
||||
using (effect)
|
||||
{
|
||||
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
|
||||
|
||||
brush.SetSourceParameter(Source, sourceBrush);
|
||||
brush.SetSourceParameter(Source, sourceBrush);
|
||||
|
||||
return brush;
|
||||
return brush;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -116,18 +125,21 @@ internal static class CompositionExtension
|
||||
CompositionBrush sourceBrush,
|
||||
CompositionBrush alphaMask)
|
||||
{
|
||||
AlphaMaskEffect maskEffect = new()
|
||||
AlphaMaskEffect effect = new()
|
||||
{
|
||||
AlphaMask = new CompositionEffectSourceParameter(AlphaMask),
|
||||
Source = new CompositionEffectSourceParameter(Source),
|
||||
};
|
||||
|
||||
CompositionEffectBrush brush = compositor.CreateEffectFactory(maskEffect).CreateBrush();
|
||||
using (effect)
|
||||
{
|
||||
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
|
||||
|
||||
brush.SetSourceParameter(AlphaMask, alphaMask);
|
||||
brush.SetSourceParameter(Source, sourceBrush);
|
||||
brush.SetSourceParameter(AlphaMask, alphaMask);
|
||||
brush.SetSourceParameter(Source, sourceBrush);
|
||||
|
||||
return brush;
|
||||
return brush;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Animations;
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Control.Animation;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
|
||||
@@ -19,50 +22,37 @@ namespace Snap.Hutao.Control.Image;
|
||||
/// 为其他图像类控件提供基类
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
[DependencyProperty("EnableLazyLoading", typeof(bool), true, nameof(OnSourceChanged))]
|
||||
[DependencyProperty("Source", typeof(Uri), default!, nameof(OnSourceChanged))]
|
||||
internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
{
|
||||
private static readonly DependencyProperty SourceProperty = Property<CompositionImage>.Depend(nameof(Source), default(Uri), OnSourceChanged);
|
||||
private static readonly DependencyProperty EnableLazyLoadingProperty = Property<CompositionImage>.DependBoxed<bool>(nameof(EnableLazyLoading), BoxedValues.True);
|
||||
private static readonly ConcurrentCancellationTokenSource<CompositionImage> LoadingTokenSource = new();
|
||||
private readonly ConcurrentCancellationTokenSource loadingTokenSource = new();
|
||||
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private readonly RoutedEventHandler unloadEventHandler;
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
private readonly TypedEventHandler<LoadedImageSurface, LoadedImageSourceLoadCompletedEventArgs> loadedImageSourceLoadCompletedEventHandler;
|
||||
|
||||
private TaskCompletionSource? surfaceLoadTaskCompletionSource;
|
||||
private SpriteVisual? spriteVisual;
|
||||
private bool isShow = true;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的单色图像
|
||||
/// </summary>
|
||||
public CompositionImage()
|
||||
protected CompositionImage()
|
||||
{
|
||||
serviceProvider = Ioc.Default;
|
||||
serviceProvider = this.ServiceProvider();
|
||||
this.DisableInteraction();
|
||||
|
||||
AllowFocusOnInteraction = false;
|
||||
IsDoubleTapEnabled = false;
|
||||
IsHitTestVisible = false;
|
||||
IsHoldingEnabled = false;
|
||||
IsRightTapEnabled = false;
|
||||
IsTabStop = false;
|
||||
unloadEventHandler = OnUnload;
|
||||
Unloaded += unloadEventHandler;
|
||||
|
||||
SizeChanged += OnSizeChanged;
|
||||
}
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
SizeChanged += sizeChangedEventHandler;
|
||||
|
||||
/// <summary>
|
||||
/// 源
|
||||
/// </summary>
|
||||
public Uri Source
|
||||
{
|
||||
get => (Uri)GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用延迟加载
|
||||
/// </summary>
|
||||
public bool EnableLazyLoading
|
||||
{
|
||||
get => (bool)GetValue(EnableLazyLoadingProperty);
|
||||
set => SetValue(EnableLazyLoadingProperty, value);
|
||||
loadedImageSourceLoadCompletedEventHandler = OnLoadImageSurfaceLoadCompleted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -73,26 +63,19 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
/// <returns>拼合视觉</returns>
|
||||
protected abstract SpriteVisual CompositeSpriteVisual(Compositor compositor, LoadedImageSurface imageSurface);
|
||||
|
||||
/// <summary>
|
||||
/// 异步加载图像表面
|
||||
/// </summary>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>加载的图像表面</returns>
|
||||
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
|
||||
protected virtual void LoadImageSurfaceCompleted(LoadedImageSurface surface)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void Unloading()
|
||||
{
|
||||
TaskCompletionSource loadCompleteTaskSource = new();
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file));
|
||||
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
|
||||
await loadCompleteTaskSource.Task.ConfigureAwait(true);
|
||||
return surface;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新视觉对象
|
||||
/// </summary>
|
||||
/// <param name="spriteVisual">拼合视觉</param>
|
||||
protected virtual void OnUpdateVisual(SpriteVisual spriteVisual)
|
||||
protected virtual void UpdateVisual(SpriteVisual spriteVisual)
|
||||
{
|
||||
spriteVisual.Size = ActualSize;
|
||||
}
|
||||
@@ -100,7 +83,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
|
||||
{
|
||||
CompositionImage image = (CompositionImage)sender;
|
||||
CancellationToken token = LoadingTokenSource.Register(image);
|
||||
CancellationToken token = image.loadingTokenSource.Register();
|
||||
IServiceProvider serviceProvider = image.serviceProvider;
|
||||
ILogger<CompositionImage> logger = serviceProvider.GetRequiredService<ILogger<CompositionImage>>();
|
||||
|
||||
@@ -127,7 +110,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
if (exception is HttpRequestException httpRequestException)
|
||||
{
|
||||
infoBarService.Error(httpRequestException, string.Format(SH.ControlImageCompositionImageHttpRequest, uri));
|
||||
infoBarService.Error(httpRequestException, SH.ControlImageCompositionImageHttpRequest.Format(uri));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -139,11 +122,11 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyImageAsync(Uri? uri, CancellationToken token)
|
||||
private async ValueTask ApplyImageAsync(Uri? uri, CancellationToken token)
|
||||
{
|
||||
await HideAsync(token).ConfigureAwait(true);
|
||||
|
||||
if (uri != null)
|
||||
if (uri is not null)
|
||||
{
|
||||
LoadedImageSurface? imageSurface = null;
|
||||
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
|
||||
@@ -164,18 +147,31 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
imageCache.Remove(uri);
|
||||
}
|
||||
|
||||
if (imageSurface != null)
|
||||
if (imageSurface is not null)
|
||||
{
|
||||
spriteVisual = CompositeSpriteVisual(compositor, imageSurface);
|
||||
OnUpdateVisual(spriteVisual);
|
||||
using (imageSurface)
|
||||
{
|
||||
spriteVisual = CompositeSpriteVisual(compositor, imageSurface);
|
||||
UpdateVisual(spriteVisual);
|
||||
|
||||
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
|
||||
await ShowAsync(token).ConfigureAwait(true);
|
||||
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
|
||||
await ShowAsync(token).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowAsync(CancellationToken token)
|
||||
private async ValueTask<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
|
||||
{
|
||||
surfaceLoadTaskCompletionSource = new();
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(file.ToUri());
|
||||
surface.LoadCompleted += loadedImageSourceLoadCompletedEventHandler;
|
||||
await surfaceLoadTaskCompletionSource.Task.ConfigureAwait(true);
|
||||
LoadImageSurfaceCompleted(surface);
|
||||
return surface;
|
||||
}
|
||||
|
||||
private async ValueTask ShowAsync(CancellationToken token)
|
||||
{
|
||||
if (!isShow)
|
||||
{
|
||||
@@ -183,7 +179,11 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
if (EnableLazyLoading)
|
||||
{
|
||||
await AnimationBuilder.Create().Opacity(from: 0D, to: 1D).StartAsync(this, token).ConfigureAwait(true);
|
||||
await AnimationBuilder
|
||||
.Create()
|
||||
.Opacity(from: 0D, to: 1D, duration: AnimationDurations.ImageFadeIn)
|
||||
.StartAsync(this, token)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -192,7 +192,7 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HideAsync(CancellationToken token)
|
||||
private async ValueTask HideAsync(CancellationToken token)
|
||||
{
|
||||
if (isShow)
|
||||
{
|
||||
@@ -200,7 +200,11 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
if (EnableLazyLoading)
|
||||
{
|
||||
await AnimationBuilder.Create().Opacity(from: 1D, to: 0D).StartAsync(this, token).ConfigureAwait(true);
|
||||
await AnimationBuilder
|
||||
.Create()
|
||||
.Opacity(from: 1D, to: 0D, duration: AnimationDurations.ImageFadeOut)
|
||||
.StartAsync(this, token)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -209,11 +213,27 @@ internal abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoadImageSurfaceLoadCompleted(LoadedImageSurface surface, LoadedImageSourceLoadCompletedEventArgs e)
|
||||
{
|
||||
surfaceLoadTaskCompletionSource?.TrySetResult();
|
||||
surface.LoadCompleted -= loadedImageSourceLoadCompletedEventHandler;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (e.NewSize != e.PreviousSize && spriteVisual != null)
|
||||
if (e.NewSize != e.PreviousSize && spriteVisual is not null)
|
||||
{
|
||||
OnUpdateVisual(spriteVisual);
|
||||
UpdateVisual(spriteVisual);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnload(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Unloading();
|
||||
spriteVisual?.Dispose();
|
||||
spriteVisual = null;
|
||||
|
||||
SizeChanged -= sizeChangedEventHandler;
|
||||
Unloaded -= unloadEventHandler;
|
||||
}
|
||||
}
|
||||
@@ -13,50 +13,27 @@ namespace Snap.Hutao.Control.Image;
|
||||
/// 渐变图像
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class Gradient : CompositionImage
|
||||
[DependencyProperty("BackgroundDirection", typeof(GradientDirection), GradientDirection.TopToBottom)]
|
||||
[DependencyProperty("ForegroundDirection", typeof(GradientDirection), GradientDirection.TopToBottom)]
|
||||
internal sealed partial class Gradient : CompositionImage
|
||||
{
|
||||
private static readonly DependencyProperty BackgroundDirectionProperty = Property<Gradient>.Depend(nameof(BackgroundDirection), GradientDirection.TopToBottom);
|
||||
private static readonly DependencyProperty ForegroundDirectionProperty = Property<Gradient>.Depend(nameof(ForegroundDirection), GradientDirection.TopToBottom);
|
||||
|
||||
private double imageAspectRatio;
|
||||
|
||||
/// <summary>
|
||||
/// 背景方向
|
||||
/// </summary>
|
||||
public GradientDirection BackgroundDirection
|
||||
{
|
||||
get => (GradientDirection)GetValue(BackgroundDirectionProperty);
|
||||
set => SetValue(BackgroundDirectionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 前景方向
|
||||
/// </summary>
|
||||
public GradientDirection ForegroundDirection
|
||||
{
|
||||
get => (GradientDirection)GetValue(ForegroundDirectionProperty);
|
||||
set => SetValue(ForegroundDirectionProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnUpdateVisual(SpriteVisual spriteVisual)
|
||||
protected override void UpdateVisual(SpriteVisual? spriteVisual)
|
||||
{
|
||||
if (spriteVisual is not null)
|
||||
if (spriteVisual is null)
|
||||
{
|
||||
Height = Math.Clamp(ActualWidth / imageAspectRatio, 0D, MaxHeight);
|
||||
spriteVisual.Size = ActualSize;
|
||||
return;
|
||||
}
|
||||
|
||||
Height = Math.Clamp(ActualWidth / imageAspectRatio, 0D, MaxHeight);
|
||||
spriteVisual.Size = ActualSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
|
||||
protected override void LoadImageSurfaceCompleted(LoadedImageSurface surface)
|
||||
{
|
||||
TaskCompletionSource loadCompleteTaskSource = new();
|
||||
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file));
|
||||
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
|
||||
await loadCompleteTaskSource.Task.ConfigureAwait(true);
|
||||
imageAspectRatio = surface.NaturalSize.AspectRatio();
|
||||
return surface;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -6,8 +6,6 @@ namespace Snap.Hutao.Control.Image;
|
||||
/// <summary>
|
||||
/// 渐变锚点
|
||||
/// </summary>
|
||||
/// <param name="Offset">便宜</param>
|
||||
/// <param name="Color">颜色</param>
|
||||
[HighQuality]
|
||||
internal readonly struct GradientStop
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Control.Theme;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
|
||||
@@ -16,6 +17,8 @@ namespace Snap.Hutao.Control.Image;
|
||||
[HighQuality]
|
||||
internal sealed class MonoChrome : CompositionImage
|
||||
{
|
||||
private readonly TypedEventHandler<FrameworkElement, object> actualThemeChangedEventHandler;
|
||||
|
||||
private CompositionColorBrush? backgroundBrush;
|
||||
|
||||
/// <summary>
|
||||
@@ -23,15 +26,16 @@ internal sealed class MonoChrome : CompositionImage
|
||||
/// </summary>
|
||||
public MonoChrome()
|
||||
{
|
||||
ActualThemeChanged += OnActualThemeChanged;
|
||||
actualThemeChangedEventHandler = OnActualThemeChanged;
|
||||
ActualThemeChanged += actualThemeChangedEventHandler;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override SpriteVisual CompositeSpriteVisual(Compositor compositor, LoadedImageSurface imageSurface)
|
||||
{
|
||||
CompositionColorBrush blackLayerBursh = compositor.CreateColorBrush(Colors.Black);
|
||||
CompositionColorBrush blackLayerBrush = compositor.CreateColorBrush(Colors.Black);
|
||||
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.Uniform, vRatio: 0f);
|
||||
CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBursh, imageSurfaceBrush, BlendEffectMode.Overlay);
|
||||
CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBrush, imageSurfaceBrush, BlendEffectMode.Overlay);
|
||||
CompositionEffectBrush opacityBrush = compositor.CompositeLuminanceToAlphaEffectBrush(overlayBrush);
|
||||
|
||||
backgroundBrush = compositor.CreateColorBrush();
|
||||
@@ -41,9 +45,19 @@ internal sealed class MonoChrome : CompositionImage
|
||||
return compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
|
||||
}
|
||||
|
||||
protected override void Unloading()
|
||||
{
|
||||
ActualThemeChanged -= actualThemeChangedEventHandler;
|
||||
|
||||
backgroundBrush?.Dispose();
|
||||
backgroundBrush = null;
|
||||
|
||||
base.Unloading();
|
||||
}
|
||||
|
||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
if (backgroundBrush != null)
|
||||
if (backgroundBrush is not null)
|
||||
{
|
||||
SetBackgroundColor(backgroundBrush);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ internal sealed class BitmapIconExtension : MarkupExtension
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to display the icon as monochrome.
|
||||
/// </summary>
|
||||
public bool ShowAsMonochrome { get; set; } = true;
|
||||
public bool ShowAsMonochrome { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override object ProvideValue()
|
||||
|
||||
18
src/Snap.Hutao/Snap.Hutao/Control/Markup/Int32Extension.cs
Normal file
18
src/Snap.Hutao/Snap.Hutao/Control/Markup/Int32Extension.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
|
||||
namespace Snap.Hutao.Control.Markup;
|
||||
|
||||
[MarkupExtensionReturnType(ReturnType = typeof(int))]
|
||||
internal sealed class Int32Extension : MarkupExtension
|
||||
{
|
||||
public string Value { get; set; } = default!;
|
||||
|
||||
protected override object ProvideValue()
|
||||
{
|
||||
_ = int.TryParse(Value, out int result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Snap.Hutao.Control.Markup;
|
||||
|
||||
@@ -20,6 +21,6 @@ internal sealed class ResourceStringExtension : MarkupExtension
|
||||
/// <inheritdoc/>
|
||||
protected override object ProvideValue()
|
||||
{
|
||||
return SH.ResourceManager.GetString(Name ?? string.Empty) ?? Name ?? string.Empty;
|
||||
return SH.ResourceManager.GetString(Name ?? string.Empty, CultureInfo.CurrentCulture) ?? Name ?? string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
|
||||
namespace Snap.Hutao.Control.Markup;
|
||||
|
||||
/// <summary>
|
||||
/// 类型拓展
|
||||
/// </summary>
|
||||
[MarkupExtensionReturnType(ReturnType = typeof(Type))]
|
||||
internal sealed class TypeExtension : MarkupExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 类型
|
||||
/// </summary>
|
||||
public Type Type { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override object ProvideValue()
|
||||
{
|
||||
return Type;
|
||||
}
|
||||
}
|
||||
18
src/Snap.Hutao/Snap.Hutao/Control/Markup/UInt32Extension.cs
Normal file
18
src/Snap.Hutao/Snap.Hutao/Control/Markup/UInt32Extension.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
|
||||
namespace Snap.Hutao.Control.Markup;
|
||||
|
||||
[MarkupExtensionReturnType(ReturnType = typeof(uint))]
|
||||
internal sealed class UInt32Extension : MarkupExtension
|
||||
{
|
||||
public string Value { get; set; } = default!;
|
||||
|
||||
protected override object ProvideValue()
|
||||
{
|
||||
_ = uint.TryParse(Value, out uint result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.UI;
|
||||
@@ -38,7 +39,7 @@ internal struct Bgra32
|
||||
/// </summary>
|
||||
/// <param name="color">颜色</param>
|
||||
/// <returns>新的 BGRA8 结构</returns>
|
||||
public static unsafe Bgra32 FromColor(Color color)
|
||||
public static unsafe implicit operator Bgra32(Color color)
|
||||
{
|
||||
Unsafe.SkipInit(out Bgra32 bgra8);
|
||||
*(uint*)&bgra8 = BinaryPrimitives.ReverseEndianness(*(uint*)&color);
|
||||
|
||||
32
src/Snap.Hutao/Snap.Hutao/Control/Media/Hsl32.cs
Normal file
32
src/Snap.Hutao/Snap.Hutao/Control/Media/Hsl32.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
// Some part of this file came from:
|
||||
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
|
||||
/// </summary>
|
||||
internal struct Hsl32
|
||||
{
|
||||
/// <summary>
|
||||
/// The Hue in 0..360 range.
|
||||
/// </summary>
|
||||
public double H;
|
||||
|
||||
/// <summary>
|
||||
/// The Saturation in 0..1 range.
|
||||
/// </summary>
|
||||
public double S;
|
||||
|
||||
/// <summary>
|
||||
/// The Lightness in 0..1 range.
|
||||
/// </summary>
|
||||
public double L;
|
||||
|
||||
/// <summary>
|
||||
/// The Alpha/opacity in 0..1 range.
|
||||
/// </summary>
|
||||
public double A;
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
// Some part of this file came from:
|
||||
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
@@ -40,7 +38,7 @@ internal struct Rgba32
|
||||
/// 构造一个新的 RGBA8 颜色
|
||||
/// </summary>
|
||||
/// <param name="hex">色值字符串</param>
|
||||
public unsafe Rgba32(string hex)
|
||||
public Rgba32(string hex)
|
||||
: this(Convert.ToUInt32(hex, 16))
|
||||
{
|
||||
}
|
||||
@@ -86,7 +84,7 @@ internal struct Rgba32
|
||||
/// </summary>
|
||||
/// <param name="hsl">HSL 颜色</param>
|
||||
/// <returns>RGBA8颜色</returns>
|
||||
public static Rgba32 FromHsl(HslColor hsl)
|
||||
public static Rgba32 FromHsl(Hsl32 hsl)
|
||||
{
|
||||
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
|
||||
double h1 = hsl.H / 60;
|
||||
@@ -143,7 +141,7 @@ internal struct Rgba32
|
||||
/// 转换到 HSL 颜色
|
||||
/// </summary>
|
||||
/// <returns>HSL 颜色</returns>
|
||||
public HslColor ToHsl()
|
||||
public readonly Hsl32 ToHsl()
|
||||
{
|
||||
const double toDouble = 1.0 / 255;
|
||||
double r = toDouble * R;
|
||||
@@ -176,7 +174,7 @@ internal struct Rgba32
|
||||
double lightness = 0.5 * (max + min);
|
||||
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
|
||||
|
||||
HslColor ret;
|
||||
Hsl32 ret;
|
||||
ret.H = 60 * h1;
|
||||
ret.S = saturation;
|
||||
ret.L = lightness;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
@@ -10,31 +9,12 @@ namespace Snap.Hutao.Control.Panel;
|
||||
/// 纵横比控件
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal class AspectRatio : Microsoft.UI.Xaml.Controls.Control
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AspectRatio : Microsoft.UI.Xaml.Controls.Control
|
||||
{
|
||||
private const double Epsilon = 2.2204460492503131e-016;
|
||||
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<AspectRatio>.DependBoxed<double>(nameof(TargetWidth), BoxedValues.DoubleOne);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<AspectRatio>.DependBoxed<double>(nameof(TargetHeight), BoxedValues.DoubleOne);
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
/// </summary>
|
||||
public double TargetWidth
|
||||
{
|
||||
get => (double)GetValue(TargetWidthProperty);
|
||||
set => SetValue(TargetWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标高度
|
||||
/// </summary>
|
||||
public double TargetHeight
|
||||
{
|
||||
get => (double)GetValue(TargetHeightProperty);
|
||||
set => SetValue(TargetHeightProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
@@ -54,7 +34,7 @@ internal class AspectRatio : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
|
||||
// 更高
|
||||
else if (ratioAvailable < ratio)
|
||||
if (ratioAvailable < ratio)
|
||||
{
|
||||
double newHeight = availableSize.Width / ratio;
|
||||
return new Size(availableSize.Width, newHeight);
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
<SplitButton
|
||||
<cwc:Segmented
|
||||
x:Class="Snap.Hutao.Control.Panel.PanelSelector"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
Name="RootSplitButton"
|
||||
Padding="0,6"
|
||||
Click="SplitButtonClick"
|
||||
Loaded="OnRootControlLoaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<SplitButton.Content>
|
||||
<FontIcon Name="IconPresenter" Glyph=""/>
|
||||
</SplitButton.Content>
|
||||
<SplitButton.Flyout>
|
||||
<MenuFlyout>
|
||||
<RadioMenuFlyoutItem
|
||||
Click="RadioMenuFlyoutItemClick"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Tag="List"
|
||||
Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
|
||||
<RadioMenuFlyoutItem
|
||||
Click="RadioMenuFlyoutItemClick"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Tag="Grid"
|
||||
Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
|
||||
</MenuFlyout>
|
||||
</SplitButton.Flyout>
|
||||
</SplitButton>
|
||||
<cwc:SegmentedItem
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Tag="List"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
|
||||
<cwc:SegmentedItem
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Tag="Grid"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
|
||||
|
||||
</cwc:Segmented>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
@@ -10,11 +10,21 @@ namespace Snap.Hutao.Control.Panel;
|
||||
/// 面板选择器
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed partial class PanelSelector : SplitButton
|
||||
[DependencyProperty("Current", typeof(string), List)]
|
||||
internal sealed partial class PanelSelector : Segmented
|
||||
{
|
||||
private const string List = nameof(List);
|
||||
public const string List = nameof(List);
|
||||
public const string Grid = nameof(Grid);
|
||||
|
||||
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), List, OnCurrentChanged);
|
||||
private static readonly Dictionary<int, string> IndexTypeMap = new()
|
||||
{
|
||||
[0] = List,
|
||||
[1] = Grid,
|
||||
};
|
||||
|
||||
private readonly RoutedEventHandler loadedEventHandler;
|
||||
private readonly RoutedEventHandler unloadedEventHandler;
|
||||
private readonly long selectedIndexChangedCallbackToken;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的面板选择器
|
||||
@@ -22,71 +32,31 @@ internal sealed partial class PanelSelector : SplitButton
|
||||
public PanelSelector()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
loadedEventHandler = OnRootLoaded;
|
||||
Loaded += loadedEventHandler;
|
||||
|
||||
unloadedEventHandler = OnRootUnload;
|
||||
Unloaded += unloadedEventHandler;
|
||||
|
||||
selectedIndexChangedCallbackToken = RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前选择
|
||||
/// </summary>
|
||||
public string Current
|
||||
private void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp)
|
||||
{
|
||||
get => (string)GetValue(CurrentProperty);
|
||||
set => SetValue(CurrentProperty, value);
|
||||
Current = IndexTypeMap[(int)GetValue(dp)];
|
||||
}
|
||||
|
||||
private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
private void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
OnCurrentChanged((PanelSelector)obj, (string)args.NewValue);
|
||||
}
|
||||
|
||||
private static void OnCurrentChanged(PanelSelector sender, string current)
|
||||
{
|
||||
MenuFlyout menuFlyout = (MenuFlyout)sender.RootSplitButton.Flyout;
|
||||
RadioMenuFlyoutItem targetItem = menuFlyout.Items
|
||||
.Cast<RadioMenuFlyoutItem>()
|
||||
.Single(i => (string)i.Tag == current);
|
||||
targetItem.IsChecked = true;
|
||||
sender.IconPresenter.Glyph = ((FontIcon)targetItem.Icon).Glyph;
|
||||
}
|
||||
|
||||
private void OnRootControlLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// because the GroupName shares in global
|
||||
// we have to implement a control scoped GroupName.
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
MenuFlyout menuFlyout = (MenuFlyout)selector.RootSplitButton.Flyout;
|
||||
int hash = GetHashCode();
|
||||
foreach (RadioMenuFlyoutItem item in menuFlyout.Items.Cast<RadioMenuFlyoutItem>())
|
||||
{
|
||||
item.GroupName = $"{nameof(PanelSelector)}GroupOf@{hash}";
|
||||
}
|
||||
|
||||
OnCurrentChanged(selector, Current);
|
||||
selector.SelectedItem = selector.Items.Cast<SegmentedItem>().Single(item => (string)item.Tag == Current);
|
||||
}
|
||||
|
||||
private void SplitButtonClick(SplitButton sender, SplitButtonClickEventArgs args)
|
||||
private void OnRootUnload(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MenuFlyout menuFlyout = (MenuFlyout)sender.Flyout;
|
||||
|
||||
int i = 0;
|
||||
for (; i < menuFlyout.Items.Count; i++)
|
||||
{
|
||||
if ((string)menuFlyout.Items[i].Tag == Current)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
++i;
|
||||
i %= menuFlyout.Items.Count; // move the count index to 0
|
||||
|
||||
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)menuFlyout.Items[i];
|
||||
item.IsChecked = true;
|
||||
Current = (string)item.Tag;
|
||||
}
|
||||
|
||||
private void RadioMenuFlyoutItemClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)sender;
|
||||
Current = (string)item.Tag;
|
||||
UnregisterPropertyChangedCallback(SelectedIndexProperty, selectedIndexChangedCallbackToken);
|
||||
Loaded -= loadedEventHandler;
|
||||
Unloaded -= unloadedEventHandler;
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
/// <summary>
|
||||
/// 快速创建 <see cref="TOwner"/> 的 <see cref="DependencyProperty"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="TOwner">所有者的类型</typeparam>
|
||||
[HighQuality]
|
||||
internal static class Property<TOwner>
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册依赖属性
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">属性的类型</typeparam>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <returns>注册的依赖属性</returns>
|
||||
public static DependencyProperty Depend<TProperty>(string name)
|
||||
{
|
||||
return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(default(TProperty)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册依赖属性
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">属性的类型</typeparam>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>注册的依赖属性</returns>
|
||||
public static DependencyProperty Depend<TProperty>(string name, TProperty defaultValue)
|
||||
{
|
||||
return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(defaultValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册依赖属性
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">属性的类型</typeparam>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <param name="defaultValue">封装的默认值</param>
|
||||
/// <returns>注册的依赖属性</returns>
|
||||
public static DependencyProperty DependBoxed<TProperty>(string name, object defaultValue)
|
||||
{
|
||||
return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(defaultValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册依赖属性
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">属性的类型</typeparam>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <param name="defaultValue">封装的默认值</param>
|
||||
/// <param name="callback">属性更改回调</param>
|
||||
/// <returns>注册的依赖属性</returns>
|
||||
public static DependencyProperty DependBoxed<TProperty>(string name, object defaultValue, Action<DependencyObject, DependencyPropertyChangedEventArgs> callback)
|
||||
{
|
||||
return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(defaultValue, new(callback)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册依赖属性
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">属性的类型</typeparam>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <param name="callback">属性更改回调</param>
|
||||
/// <returns>注册的依赖属性</returns>
|
||||
public static DependencyProperty Depend<TProperty>(
|
||||
string name,
|
||||
TProperty defaultValue,
|
||||
Action<DependencyObject, DependencyPropertyChangedEventArgs> callback)
|
||||
{
|
||||
return DependencyProperty.Register(name, typeof(TProperty), typeof(TOwner), new(defaultValue, new(callback)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册附加属性
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">属性的类型</typeparam>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <returns>注册的附加属性</returns>
|
||||
public static DependencyProperty Attach<TProperty>(string name)
|
||||
{
|
||||
return DependencyProperty.RegisterAttached(name, typeof(TProperty), typeof(TOwner), new(default(TProperty)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册附加属性
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">属性的类型</typeparam>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>注册的附加属性</returns>
|
||||
public static DependencyProperty Attach<TProperty>(string name, TProperty defaultValue)
|
||||
{
|
||||
return DependencyProperty.RegisterAttached(name, typeof(TProperty), typeof(TOwner), new(defaultValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册附加属性
|
||||
/// </summary>
|
||||
/// <typeparam name="TProperty">属性的类型</typeparam>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <param name="callback">属性更改回调</param>
|
||||
/// <returns>注册的附加属性</returns>
|
||||
public static DependencyProperty Attach<TProperty>(
|
||||
string name,
|
||||
TProperty defaultValue,
|
||||
Action<DependencyObject, DependencyPropertyChangedEventArgs> callback)
|
||||
{
|
||||
return DependencyProperty.RegisterAttached(name, typeof(TProperty), typeof(TOwner), new(defaultValue, new(callback)));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
@@ -17,17 +18,19 @@ namespace Snap.Hutao.Control;
|
||||
internal class ScopedPage : Page
|
||||
{
|
||||
// Allow GC to Collect the IServiceScope
|
||||
private static readonly WeakReference<IServiceScope> PreviousScopeReference = new(null!);
|
||||
private static readonly WeakReference<IServiceScope> PreviousScopeReference = new(default!);
|
||||
|
||||
private readonly RoutedEventHandler unloadEventHandler;
|
||||
private readonly CancellationTokenSource viewCancellationTokenSource = new();
|
||||
private readonly IServiceScope currentScope;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的页面
|
||||
/// </summary>
|
||||
public ScopedPage()
|
||||
protected ScopedPage()
|
||||
{
|
||||
Unloaded += OnScopedPageUnloaded;
|
||||
unloadEventHandler = OnUnloaded;
|
||||
Unloaded += unloadEventHandler;
|
||||
currentScope = Ioc.Default.CreateScope();
|
||||
DisposePreviousScope();
|
||||
|
||||
@@ -46,27 +49,14 @@ internal class ScopedPage : Page
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// 应当在 InitializeComponent() 前调用
|
||||
/// </summary>
|
||||
/// <typeparam name="TViewModel">视图模型类型</typeparam>
|
||||
public void InitializeWith<TViewModel>()
|
||||
where TViewModel : class, IViewModel
|
||||
{
|
||||
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步通知接收器
|
||||
/// </summary>
|
||||
/// <param name="extra">额外内容</param>
|
||||
/// <returns>任务</returns>
|
||||
public async Task NotifyRecipentAsync(INavigationData extra)
|
||||
public async ValueTask NotifyRecipientAsync(INavigationData extra)
|
||||
{
|
||||
if (extra.Data != null && DataContext is INavigationRecipient recipient)
|
||||
if (extra.Data is not null && DataContext is INavigationRecipient recipient)
|
||||
{
|
||||
await recipient.ReceiveAsync(extra).ConfigureAwait(false);
|
||||
}
|
||||
@@ -74,6 +64,19 @@ internal class ScopedPage : Page
|
||||
extra.NotifyNavigationCompleted();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// 应当在 InitializeComponent() 前调用
|
||||
/// </summary>
|
||||
/// <typeparam name="TViewModel">视图模型类型</typeparam>
|
||||
protected void InitializeWith<TViewModel>()
|
||||
where TViewModel : class, IViewModel
|
||||
{
|
||||
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
@@ -100,13 +103,13 @@ internal class ScopedPage : Page
|
||||
{
|
||||
if (e.Parameter is INavigationData extra)
|
||||
{
|
||||
NotifyRecipentAsync(extra).SafeForget();
|
||||
NotifyRecipientAsync(extra).SafeForget();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScopedPageUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DataContext = null;
|
||||
Unloaded -= OnScopedPageUnloaded;
|
||||
Unloaded -= unloadEventHandler;
|
||||
}
|
||||
}
|
||||
44
src/Snap.Hutao/Snap.Hutao/Control/SegmentedBar.cs
Normal file
44
src/Snap.Hutao/Snap.Hutao/Control/SegmentedBar.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Shapes;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
[DependencyProperty("Source", typeof(List<IColorSegment>), default!, nameof(OnSourceChanged))]
|
||||
internal sealed partial class SegmentedBar : ContentControl
|
||||
{
|
||||
private readonly LinearGradientBrush brush = new() { StartPoint = new(0, 0), EndPoint = new(1, 0), };
|
||||
|
||||
public SegmentedBar()
|
||||
{
|
||||
Content = new Rectangle()
|
||||
{
|
||||
Fill = brush,
|
||||
};
|
||||
}
|
||||
|
||||
private static void OnSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
SegmentedBar segmentedBar = (SegmentedBar)obj;
|
||||
|
||||
GradientStopCollection collection = segmentedBar.brush.GradientStops;
|
||||
collection.Clear();
|
||||
|
||||
if (args.NewValue as List<IColorSegment> is [_, ..] list)
|
||||
{
|
||||
double total = list.Sum(seg => seg.Value);
|
||||
double offset = 0;
|
||||
foreach (ref readonly IColorSegment segment in CollectionsMarshal.AsSpan(list))
|
||||
{
|
||||
collection.Add(new GradientStop() { Color = segment.Color, Offset = offset, });
|
||||
offset += segment.Value / total;
|
||||
collection.Add(new GradientStop() { Color = segment.Color, Offset = offset, });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Documents;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Control.Media;
|
||||
using Snap.Hutao.Control.Theme;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Text;
|
||||
@@ -19,83 +19,76 @@ namespace Snap.Hutao.Control.Text;
|
||||
/// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class DescriptionTextBlock : ContentControl
|
||||
[DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))]
|
||||
internal sealed partial class DescriptionTextBlock : ContentControl
|
||||
{
|
||||
private static readonly DependencyProperty DescriptionProperty = Property<DescriptionTextBlock>.Depend(nameof(Description), string.Empty, OnDescriptionChanged);
|
||||
|
||||
private static readonly int ColorTagFullLength = "<color=#FFFFFFFF></color>".Length;
|
||||
private static readonly int ColorTagLeftLength = "<color=#FFFFFFFF>".Length;
|
||||
|
||||
private static readonly int ItalicTagFullLength = "<i></i>".Length;
|
||||
private static readonly int ItalicTagLeftLength = "<i>".Length;
|
||||
|
||||
private readonly TypedEventHandler<FrameworkElement, object> actualThemeChangedEventHandler;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的呈现描述文本的文本块
|
||||
/// </summary>
|
||||
public DescriptionTextBlock()
|
||||
{
|
||||
IsTabStop = false;
|
||||
this.DisableInteraction();
|
||||
|
||||
Content = new TextBlock()
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
|
||||
ActualThemeChanged += OnActualThemeChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可绑定的描述文本
|
||||
/// </summary>
|
||||
public string Description
|
||||
{
|
||||
get => (string)GetValue(DescriptionProperty);
|
||||
set => SetValue(DescriptionProperty, value);
|
||||
actualThemeChangedEventHandler = OnActualThemeChanged;
|
||||
ActualThemeChanged += actualThemeChangedEventHandler;
|
||||
}
|
||||
|
||||
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
TextBlock text = (TextBlock)((DescriptionTextBlock)d).Content;
|
||||
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
|
||||
ReadOnlySpan<char> description = (string)e.NewValue;
|
||||
|
||||
UpdateDescription(text, description);
|
||||
UpdateDescription(textBlock, description);
|
||||
}
|
||||
|
||||
private static void UpdateDescription(TextBlock text, in ReadOnlySpan<char> description)
|
||||
private static void UpdateDescription(TextBlock textBlock, in ReadOnlySpan<char> description)
|
||||
{
|
||||
text.Inlines.Clear();
|
||||
textBlock.Inlines.Clear();
|
||||
|
||||
int last = 0;
|
||||
for (int i = 0; i < description.Length;)
|
||||
{
|
||||
// newline
|
||||
if (description.Slice(i, 2).SequenceEqual(@"\n"))
|
||||
if (description[i..].StartsWith(@"\n"))
|
||||
{
|
||||
AppendText(text, description[last..i]);
|
||||
AppendLineBreak(text);
|
||||
AppendText(textBlock, description[last..i]);
|
||||
AppendLineBreak(textBlock);
|
||||
i += 1;
|
||||
last = i;
|
||||
}
|
||||
|
||||
// color tag
|
||||
else if (description.Slice(i, 2).SequenceEqual("<c"))
|
||||
else if (description[i..].StartsWith("<c"))
|
||||
{
|
||||
AppendText(text, description[last..i]);
|
||||
AppendText(textBlock, description[last..i]);
|
||||
Rgba32 color = new(description.Slice(i + 8, 8).ToString());
|
||||
int length = description[(i + ColorTagLeftLength)..].IndexOf('<');
|
||||
AppendColorText(text, description.Slice(i + ColorTagLeftLength, length), color);
|
||||
AppendColorText(textBlock, description.Slice(i + ColorTagLeftLength, length), color);
|
||||
|
||||
i += length + ColorTagFullLength;
|
||||
last = i;
|
||||
}
|
||||
|
||||
// italic
|
||||
else if (description.Slice(i, 2).SequenceEqual("<i"))
|
||||
else if (description[i..].StartsWith("<i"))
|
||||
{
|
||||
AppendText(text, description[last..i]);
|
||||
AppendText(textBlock, description[last..i]);
|
||||
|
||||
int length = description[(i + ItalicTagLeftLength)..].IndexOf('<');
|
||||
AppendItalicText(text, description.Slice(i + ItalicTagLeftLength, length));
|
||||
AppendItalicText(textBlock, description.Slice(i + ItalicTagLeftLength, length));
|
||||
|
||||
i += length + ItalicTagFullLength;
|
||||
last = i;
|
||||
@@ -107,7 +100,7 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
|
||||
if (i == description.Length - 1)
|
||||
{
|
||||
AppendText(text, description[last..(i + 1)]);
|
||||
AppendText(textBlock, description[last..(i + 1)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,7 +119,8 @@ internal sealed class DescriptionTextBlock : ContentControl
|
||||
}
|
||||
else
|
||||
{
|
||||
HslColor hsl = color.ToHsl();
|
||||
// Make lighter in light mode
|
||||
Hsl32 hsl = color.ToHsl();
|
||||
hsl.L *= 0.3;
|
||||
targetColor = Rgba32.FromHsl(hsl);
|
||||
}
|
||||
|
||||
@@ -97,8 +97,273 @@
|
||||
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
<!-- TODO: When will DefaultInfoBarStyle added -->
|
||||
<Style TargetType="InfoBar">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="CloseButtonStyle" Value="{StaticResource InfoBarCloseButtonStyle}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource InfoBarBorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource InfoBarBorderThickness}"/>
|
||||
<Setter Property="AutomationProperties.LandmarkType" Value="Custom"/>
|
||||
<Setter Property="AutomationProperties.IsDialog" Value="True"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="InfoBar">
|
||||
<Border
|
||||
x:Name="ContentRoot"
|
||||
VerticalAlignment="Top"
|
||||
Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<!-- Background is used here so that it overrides the severity status color if set. -->
|
||||
<Grid
|
||||
MinHeight="{ThemeResource InfoBarMinHeight}"
|
||||
Padding="{StaticResource InfoBarContentRootPadding}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<!-- Icon -->
|
||||
<ColumnDefinition Width="*"/>
|
||||
<!-- Title, message, and action -->
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<!-- Close button -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid x:Name="StandardIconArea" Visibility="Collapsed">
|
||||
<TextBlock
|
||||
x:Name="IconBackground"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource InfoBarIconMargin}"
|
||||
VerticalAlignment="Top"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="{StaticResource InfoBarIconFontSize}"
|
||||
Foreground="{ThemeResource InfoBarInformationalSeverityIconBackground}"
|
||||
Text="{StaticResource InfoBarIconBackgroundGlyph}"/>
|
||||
<TextBlock
|
||||
x:Name="StandardIcon"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource InfoBarIconMargin}"
|
||||
VerticalAlignment="Top"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="{StaticResource InfoBarIconFontSize}"
|
||||
Foreground="{ThemeResource InfoBarInformationalSeverityIconForeground}"
|
||||
Text="{StaticResource InfoBarInformationalIconGlyph}"/>
|
||||
</Grid>
|
||||
<Viewbox
|
||||
x:Name="UserIconBox"
|
||||
Grid.Column="0"
|
||||
MaxWidth="{ThemeResource InfoBarIconFontSize}"
|
||||
MaxHeight="{ThemeResource InfoBarIconFontSize}"
|
||||
Margin="{ThemeResource InfoBarIconMargin}"
|
||||
VerticalAlignment="Top"
|
||||
Child="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.IconElement}"
|
||||
Visibility="Collapsed"/>
|
||||
<InfoBarPanel
|
||||
Grid.Column="1"
|
||||
Margin="{StaticResource InfoBarPanelMargin}"
|
||||
HorizontalOrientationPadding="{StaticResource InfoBarPanelHorizontalOrientationPadding}"
|
||||
VerticalOrientationPadding="{StaticResource InfoBarPanelVerticalOrientationPadding}">
|
||||
<TextBlock
|
||||
x:Name="Title"
|
||||
FontSize="{StaticResource InfoBarTitleFontSize}"
|
||||
FontWeight="{StaticResource InfoBarTitleFontWeight}"
|
||||
Foreground="{ThemeResource InfoBarTitleForeground}"
|
||||
InfoBarPanel.HorizontalOrientationMargin="{StaticResource InfoBarTitleHorizontalOrientationMargin}"
|
||||
InfoBarPanel.VerticalOrientationMargin="{StaticResource InfoBarTitleVerticalOrientationMargin}"
|
||||
Text="{TemplateBinding Title}"
|
||||
TextWrapping="WrapWholeWords"/>
|
||||
<TextBlock
|
||||
x:Name="Message"
|
||||
FontSize="{StaticResource InfoBarMessageFontSize}"
|
||||
FontWeight="{StaticResource InfoBarMessageFontWeight}"
|
||||
Foreground="{ThemeResource InfoBarMessageForeground}"
|
||||
InfoBarPanel.HorizontalOrientationMargin="{StaticResource InfoBarMessageHorizontalOrientationMargin}"
|
||||
InfoBarPanel.VerticalOrientationMargin="{StaticResource InfoBarMessageVerticalOrientationMargin}"
|
||||
Text="{TemplateBinding Message}"
|
||||
TextWrapping="WrapWholeWords"/>
|
||||
<ContentPresenter
|
||||
VerticalAlignment="Top"
|
||||
Content="{TemplateBinding ActionButton}"
|
||||
InfoBarPanel.HorizontalOrientationMargin="{StaticResource InfoBarActionHorizontalOrientationMargin}"
|
||||
InfoBarPanel.VerticalOrientationMargin="{StaticResource InfoBarActionVerticalOrientationMargin}">
|
||||
<ContentPresenter.Resources>
|
||||
<Style BasedOn="{StaticResource DefaultHyperlinkButtonStyle}" TargetType="HyperlinkButton">
|
||||
<Style.Setters>
|
||||
<Setter Property="Margin" Value="{StaticResource InfoBarHyperlinkButtonMargin}"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource InfoBarHyperlinkButtonForeground}"/>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
</ContentPresenter.Resources>
|
||||
</ContentPresenter>
|
||||
</InfoBarPanel>
|
||||
<ContentPresenter
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"/>
|
||||
<Button
|
||||
Name="CloseButton"
|
||||
Grid.Column="2"
|
||||
Command="{TemplateBinding CloseButtonCommand}"
|
||||
CommandParameter="{TemplateBinding CloseButtonCommandParameter}"
|
||||
Style="{TemplateBinding CloseButtonStyle}">
|
||||
<Button.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<StaticResource x:Key="ButtonBackground" ResourceKey="AppBarButtonBackground"/>
|
||||
<StaticResource x:Key="ButtonBackgroundPointerOver" ResourceKey="AppBarButtonBackgroundPointerOver"/>
|
||||
<StaticResource x:Key="ButtonBackgroundPressed" ResourceKey="AppBarButtonBackgroundPressed"/>
|
||||
<StaticResource x:Key="ButtonBackgroundDisabled" ResourceKey="AppBarButtonBackgroundDisabled"/>
|
||||
<StaticResource x:Key="ButtonForeground" ResourceKey="AppBarButtonForeground"/>
|
||||
<StaticResource x:Key="ButtonForegroundPointerOver" ResourceKey="AppBarButtonForegroundPointerOver"/>
|
||||
<StaticResource x:Key="ButtonForegroundPressed" ResourceKey="AppBarButtonForegroundPressed"/>
|
||||
<StaticResource x:Key="ButtonForegroundDisabled" ResourceKey="AppBarButtonForegroundDisabled"/>
|
||||
<StaticResource x:Key="ButtonBorderBrush" ResourceKey="AppBarButtonBorderBrush"/>
|
||||
<StaticResource x:Key="ButtonBorderBrushPointerOver" ResourceKey="AppBarButtonBorderBrushPointerOver"/>
|
||||
<StaticResource x:Key="ButtonBorderBrushPressed" ResourceKey="AppBarButtonBorderBrushPressed"/>
|
||||
<StaticResource x:Key="ButtonBorderBrushDisabled" ResourceKey="AppBarButtonBorderBrushDisabled"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<StaticResource x:Key="ButtonBackground" ResourceKey="AppBarButtonBackground"/>
|
||||
<StaticResource x:Key="ButtonBackgroundPointerOver" ResourceKey="AppBarButtonBackgroundPointerOver"/>
|
||||
<StaticResource x:Key="ButtonBackgroundPressed" ResourceKey="AppBarButtonBackgroundPressed"/>
|
||||
<StaticResource x:Key="ButtonBackgroundDisabled" ResourceKey="AppBarButtonBackgroundDisabled"/>
|
||||
<StaticResource x:Key="ButtonForeground" ResourceKey="AppBarButtonForeground"/>
|
||||
<StaticResource x:Key="ButtonForegroundPointerOver" ResourceKey="AppBarButtonForegroundPointerOver"/>
|
||||
<StaticResource x:Key="ButtonForegroundPressed" ResourceKey="AppBarButtonForegroundPressed"/>
|
||||
<StaticResource x:Key="ButtonForegroundDisabled" ResourceKey="AppBarButtonForegroundDisabled"/>
|
||||
<StaticResource x:Key="ButtonBorderBrush" ResourceKey="AppBarButtonBorderBrush"/>
|
||||
<StaticResource x:Key="ButtonBorderBrushPointerOver" ResourceKey="AppBarButtonBorderBrushPointerOver"/>
|
||||
<StaticResource x:Key="ButtonBorderBrushPressed" ResourceKey="AppBarButtonBorderBrushPressed"/>
|
||||
<StaticResource x:Key="ButtonBorderBrushDisabled" ResourceKey="AppBarButtonBorderBrushDisabled"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<StaticResource x:Key="ButtonBackground" ResourceKey="AppBarButtonBackground"/>
|
||||
<StaticResource x:Key="ButtonBackgroundPointerOver" ResourceKey="AppBarButtonBackgroundPointerOver"/>
|
||||
<StaticResource x:Key="ButtonBackgroundPressed" ResourceKey="AppBarButtonBackgroundPressed"/>
|
||||
<StaticResource x:Key="ButtonBackgroundDisabled" ResourceKey="AppBarButtonBackgroundDisabled"/>
|
||||
<StaticResource x:Key="ButtonForeground" ResourceKey="AppBarButtonForeground"/>
|
||||
<StaticResource x:Key="ButtonForegroundPointerOver" ResourceKey="AppBarButtonForegroundPointerOver"/>
|
||||
<StaticResource x:Key="ButtonForegroundPressed" ResourceKey="AppBarButtonForegroundPressed"/>
|
||||
<StaticResource x:Key="ButtonForegroundDisabled" ResourceKey="AppBarButtonForegroundDisabled"/>
|
||||
<StaticResource x:Key="ButtonBorderBrush" ResourceKey="AppBarButtonBorderBrush"/>
|
||||
<StaticResource x:Key="ButtonBorderBrushPointerOver" ResourceKey="AppBarButtonBorderBrushPointerOver"/>
|
||||
<StaticResource x:Key="ButtonBorderBrushPressed" ResourceKey="AppBarButtonBorderBrushPressed"/>
|
||||
<StaticResource x:Key="ButtonBorderBrushDisabled" ResourceKey="AppBarButtonBorderBrushDisabled"/>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Button.Resources>
|
||||
<Viewbox
|
||||
Width="{StaticResource InfoBarCloseButtonGlyphSize}"
|
||||
Height="{StaticResource InfoBarCloseButtonGlyphSize}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<SymbolIcon Symbol="{StaticResource InfoBarCloseButtonSymbol}"/>
|
||||
</Viewbox>
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="SeverityLevels">
|
||||
<VisualState x:Name="Informational"/>
|
||||
<VisualState x:Name="Error">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentRoot.Background" Value="{ThemeResource InfoBarErrorSeverityBackgroundBrush}"/>
|
||||
<Setter Target="IconBackground.Foreground" Value="{ThemeResource InfoBarErrorSeverityIconBackground}"/>
|
||||
<Setter Target="StandardIcon.Text" Value="{StaticResource InfoBarErrorIconGlyph}"/>
|
||||
<Setter Target="StandardIcon.Foreground" Value="{ThemeResource InfoBarErrorSeverityIconForeground}"/>
|
||||
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Warning">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentRoot.Background" Value="{ThemeResource InfoBarWarningSeverityBackgroundBrush}"/>
|
||||
<Setter Target="IconBackground.Foreground" Value="{ThemeResource InfoBarWarningSeverityIconBackground}"/>
|
||||
<Setter Target="StandardIcon.Text" Value="{StaticResource InfoBarWarningIconGlyph}"/>
|
||||
<Setter Target="StandardIcon.Foreground" Value="{ThemeResource InfoBarWarningSeverityIconForeground}"/>
|
||||
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Success">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentRoot.Background" Value="{ThemeResource InfoBarSuccessSeverityBackgroundBrush}"/>
|
||||
<Setter Target="IconBackground.Foreground" Value="{ThemeResource InfoBarSuccessSeverityIconBackground}"/>
|
||||
<Setter Target="StandardIcon.Text" Value="{StaticResource InfoBarSuccessIconGlyph}"/>
|
||||
<Setter Target="StandardIcon.Foreground" Value="{ThemeResource InfoBarSuccessSeverityIconForeground}"/>
|
||||
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="IconStates">
|
||||
<VisualState x:Name="StandardIconVisible">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="UserIconBox.Visibility" Value="Collapsed"/>
|
||||
<Setter Target="StandardIconArea.Visibility" Value="Visible"/>
|
||||
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="UserIconVisible">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="UserIconBox.Visibility" Value="Visible"/>
|
||||
<Setter Target="StandardIconArea.Visibility" Value="Collapsed"/>
|
||||
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="NoIconVisible"/>
|
||||
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup>
|
||||
<VisualState x:Name="CloseButtonVisible"/>
|
||||
<VisualState x:Name="CloseButtonCollapsed">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CloseButton.Visibility" Value="Collapsed"/>
|
||||
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="InfoBarVisibility">
|
||||
<VisualState x:Name="InfoBarVisible"/>
|
||||
<VisualState x:Name="InfoBarCollapsed">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentRoot.Visibility" Value="Collapsed"/>
|
||||
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup>
|
||||
<VisualState x:Name="ForegroundNotSet"/>
|
||||
<VisualState x:Name="ForegroundSet">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="Title.Foreground" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Foreground}"/>
|
||||
<Setter Target="Message.Foreground" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Foreground}"/>
|
||||
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
|
||||
</VisualStateGroup>
|
||||
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Border>
|
||||
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -25,9 +25,9 @@ internal abstract class ValueConverter<TFrom, TTo> : IValueConverter
|
||||
Ioc.Default
|
||||
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
|
||||
.LogError(ex, "值转换器异常");
|
||||
}
|
||||
|
||||
return null;
|
||||
throw;
|
||||
}
|
||||
#else
|
||||
return Convert((TFrom)value);
|
||||
#endif
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Abstraction;
|
||||
/// </summary>
|
||||
/// <typeparam name="TSelf">自身类型</typeparam>
|
||||
[HighQuality]
|
||||
internal interface ICloneable<TSelf>
|
||||
internal interface ICloneable<out TSelf>
|
||||
{
|
||||
/// <summary>
|
||||
/// 克隆
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Abstraction;
|
||||
/// <typeparam name="T1">元组的第一个类型</typeparam>
|
||||
/// <typeparam name="T2">元组的第二个类型</typeparam>
|
||||
[HighQuality]
|
||||
internal interface IDeconstructable<T1, T2>
|
||||
internal interface IDeconstruct<T1, T2>
|
||||
{
|
||||
/// <summary>
|
||||
/// 解构
|
||||
27
src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IMappingFrom.cs
Normal file
27
src/Snap.Hutao/Snap.Hutao/Core/Abstraction/IMappingFrom.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics.Contracts;
|
||||
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
internal interface IMappingFrom<TSelf, TFrom>
|
||||
where TSelf : IMappingFrom<TSelf, TFrom>
|
||||
{
|
||||
[Pure]
|
||||
static abstract TSelf From(TFrom source);
|
||||
}
|
||||
|
||||
internal interface IMappingFrom<TSelf, T1, T2>
|
||||
where TSelf : IMappingFrom<TSelf, T1, T2>
|
||||
{
|
||||
[Pure]
|
||||
static abstract TSelf From(T1 t1, T2 t2);
|
||||
}
|
||||
|
||||
internal interface IMappingFrom<TSelf, T1, T2, T3>
|
||||
where TSelf : IMappingFrom<TSelf, T1, T2, T3>
|
||||
{
|
||||
[Pure]
|
||||
static abstract TSelf From(T1 t1, T2 t2, T3 t3);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Core.Annotation;
|
||||
/// <summary>
|
||||
/// 指示此方法为命令的调用方法
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
|
||||
internal sealed class CommandAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Core.Annotation;
|
||||
/// <summary>
|
||||
/// 指示此类自动生成构造器
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class ConstructorGeneratedAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
@@ -16,6 +16,11 @@ internal sealed class ConstructorGeneratedAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否调用基类构造函数
|
||||
/// </summary>
|
||||
public bool CallBaseConstructor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 在构造函数中插入 HttpClient
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Annotation;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
internal sealed class DependencyPropertyAttribute : Attribute
|
||||
{
|
||||
public DependencyPropertyAttribute(string name, Type type)
|
||||
{
|
||||
}
|
||||
|
||||
public DependencyPropertyAttribute(string name, Type type, object defaultValue)
|
||||
{
|
||||
}
|
||||
|
||||
public DependencyPropertyAttribute(string name, Type type, object defaultValue, string valueChangedCallbackName)
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsAttached { get; set; }
|
||||
|
||||
public Type AttachedType { get; set; } = default!;
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Core.Annotation;
|
||||
|
||||
/// <summary>
|
||||
/// 高质量代码
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
internal class HighQualityAttribute : Attribute
|
||||
[Conditional("DEBUG")]
|
||||
internal sealed class HighQualityAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@@ -2,22 +2,22 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
using Snap.Hutao.Core.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// 为图像缓存提供抽象
|
||||
/// </summary>
|
||||
/// <typeparam name="T">缓存类型</typeparam>
|
||||
[HighQuality]
|
||||
internal interface IImageCache : ICastableService
|
||||
internal interface IImageCache : ICastService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the file path containing cached item for given Uri
|
||||
/// </summary>
|
||||
/// <param name="uri">Uri of the item.</param>
|
||||
/// <returns>a string path</returns>
|
||||
Task<string> GetFileFromCacheAsync(Uri uri);
|
||||
ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri);
|
||||
|
||||
/// <summary>
|
||||
/// Removed items based on uri list passed
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
|
||||
/// <summary>
|
||||
@@ -15,5 +17,5 @@ internal interface IImageCacheFilePathOperation
|
||||
/// <param name="category">分类</param>
|
||||
/// <param name="fileName">文件名</param>
|
||||
/// <returns>文件路径</returns>
|
||||
string GetFilePathFromCategoryAndFileName(string category, string fileName);
|
||||
ValueFile GetFileFromCategoryAndName(string category, string fileName);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
@@ -23,6 +24,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
{
|
||||
private const string CacheFolderName = nameof(ImageCache);
|
||||
|
||||
// TODO: use FrozenDictionary
|
||||
private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new()
|
||||
{
|
||||
[0] = TimeSpan.FromSeconds(4),
|
||||
@@ -34,7 +36,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
};
|
||||
|
||||
private readonly ILogger logger;
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
@@ -46,12 +48,10 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// Initializes a new instance of the <see cref="ImageCache"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="httpClientFactory">http客户端工厂</param>
|
||||
public ImageCache(IServiceProvider serviceProvider)
|
||||
{
|
||||
logger = serviceProvider.GetRequiredService<ILogger<ImageCache>>();
|
||||
httpClient = serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ImageCache));
|
||||
httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
@@ -59,20 +59,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// <inheritdoc/>
|
||||
public void RemoveInvalid()
|
||||
{
|
||||
string folder = GetCacheFolder();
|
||||
string[] files = Directory.GetFiles(folder);
|
||||
|
||||
List<string> filesToDelete = new();
|
||||
|
||||
foreach (string file in files)
|
||||
{
|
||||
if (IsFileInvalid(file, false))
|
||||
{
|
||||
filesToDelete.Add(file);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveInternal(filesToDelete);
|
||||
RemoveInternal(Directory.GetFiles(GetCacheFolder()).Where(file => IsFileInvalid(file, false)));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -84,7 +71,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// <inheritdoc/>
|
||||
public void Remove(in ReadOnlySpan<Uri> uriForCachedItems)
|
||||
{
|
||||
if (uriForCachedItems == null || uriForCachedItems.Length <= 0)
|
||||
if (uriForCachedItems.Length <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -93,11 +80,10 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
string[] files = Directory.GetFiles(folder);
|
||||
|
||||
List<string> filesToDelete = new();
|
||||
|
||||
foreach (Uri uri in uriForCachedItems)
|
||||
foreach (ref readonly Uri uri in uriForCachedItems)
|
||||
{
|
||||
string filePath = Path.Combine(folder, GetCacheFileName(uri));
|
||||
if (Array.IndexOf(files, filePath) >= 0)
|
||||
if (files.Contains(filePath))
|
||||
{
|
||||
filesToDelete.Add(filePath);
|
||||
}
|
||||
@@ -107,38 +93,40 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<string> GetFileFromCacheAsync(Uri uri)
|
||||
public async ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri)
|
||||
{
|
||||
string fileName = GetCacheFileName(uri);
|
||||
string filePath = Path.Combine(GetCacheFolder(), fileName);
|
||||
|
||||
if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0)
|
||||
if (File.Exists(filePath) && new FileInfo(filePath).Length != 0)
|
||||
{
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
try
|
||||
{
|
||||
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
|
||||
{
|
||||
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
|
||||
}
|
||||
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
concurrentTasks.TryRemove(fileName, out _);
|
||||
}
|
||||
finally
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
try
|
||||
{
|
||||
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
|
||||
{
|
||||
taskCompletionSource.TrySetResult();
|
||||
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
|
||||
}
|
||||
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
concurrentTasks.TryRemove(fileName, out _);
|
||||
}
|
||||
finally
|
||||
{
|
||||
taskCompletionSource.TrySetResult();
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetFilePathFromCategoryAndFileName(string category, string fileName)
|
||||
public ValueFile GetFileFromCategoryAndName(string category, string fileName)
|
||||
{
|
||||
Uri dummyUri = Web.HutaoEndpoints.StaticFile(category, fileName).ToUri();
|
||||
return Path.Combine(GetCacheFolder(), GetCacheFileName(dummyUri));
|
||||
@@ -173,16 +161,18 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Remove Cache Image Failed:{file}", filePath);
|
||||
logger.LogWarning(ex, "Remove Cache Image Failed:{File}", filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH003")]
|
||||
private async Task DownloadFileAsync(Uri uri, string baseFile)
|
||||
{
|
||||
logger.LogInformation("Begin downloading for {uri}", uri);
|
||||
logger.LogInformation("Begin downloading for {Uri}", uri);
|
||||
|
||||
int retryCount = 0;
|
||||
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
|
||||
while (retryCount < 6)
|
||||
{
|
||||
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
@@ -198,21 +188,24 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (message.StatusCode == HttpStatusCode.NotFound)
|
||||
|
||||
switch (message.StatusCode)
|
||||
{
|
||||
// directly goto https://static.hut.ao
|
||||
retryCount += 3;
|
||||
}
|
||||
else if (message.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry {uri} after {delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
case HttpStatusCode.NotFound:
|
||||
// directly goto https://static.hut.ao
|
||||
retryCount += 3;
|
||||
break;
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry {Uri} after {Delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,13 +218,15 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
|
||||
private string GetCacheFolder()
|
||||
{
|
||||
if (cacheFolder == null)
|
||||
if (cacheFolder is not null)
|
||||
{
|
||||
baseFolder ??= serviceProvider.GetRequiredService<HutaoOptions>().LocalCache;
|
||||
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
|
||||
cacheFolder = info.FullName;
|
||||
return cacheFolder;
|
||||
}
|
||||
|
||||
return cacheFolder!;
|
||||
baseFolder ??= serviceProvider.GetRequiredService<RuntimeOptions>().LocalCache;
|
||||
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
|
||||
cacheFolder = info.FullName;
|
||||
|
||||
return cacheFolder;
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ internal sealed class CommandLineBuilder
|
||||
/// <returns>命令行建造器</returns>
|
||||
public CommandLineBuilder AppendIfNotNull(string name, object? value = null)
|
||||
{
|
||||
return AppendIf(name, value != null, value);
|
||||
return AppendIf(name, value is not null, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -58,11 +58,13 @@ internal sealed class CommandLineBuilder
|
||||
s.Append(WhiteSpace);
|
||||
s.Append(key);
|
||||
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
s.Append(WhiteSpace);
|
||||
s.Append(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
s.Append(WhiteSpace);
|
||||
s.Append(value);
|
||||
}
|
||||
|
||||
return s.ToString();
|
||||
|
||||
9
src/Snap.Hutao/Snap.Hutao/Core/Database/IReorderable.cs
Normal file
9
src/Snap.Hutao/Snap.Hutao/Core/Database/IReorderable.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
internal interface IReorderable
|
||||
{
|
||||
int Index { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
internal sealed class ObservableReorderableDbCollection<T> : ObservableCollection<T>
|
||||
where T : class, IReorderable
|
||||
{
|
||||
private readonly DbContext dbContext;
|
||||
private bool previousChangeIsRemoved;
|
||||
|
||||
public ObservableReorderableDbCollection(List<T> items, DbContext dbContext)
|
||||
: base(AdjustIndex(items))
|
||||
{
|
||||
this.dbContext = dbContext;
|
||||
}
|
||||
|
||||
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
base.OnCollectionChanged(e);
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
previousChangeIsRemoved = true;
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
if (!previousChangeIsRemoved)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnReorder();
|
||||
previousChangeIsRemoved = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<T> AdjustIndex(List<T> list)
|
||||
{
|
||||
Span<T> span = CollectionsMarshal.AsSpan(list);
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
ref readonly T item = ref span[i];
|
||||
item.Index = i;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private void OnReorder()
|
||||
{
|
||||
AdjustIndex((List<T>)Items);
|
||||
|
||||
DbSet<T> dbSet = dbContext.Set<T>();
|
||||
foreach (ref readonly T item in CollectionsMarshal.AsSpan((List<T>)Items))
|
||||
{
|
||||
dbSet.UpdateAndSave(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,8 +35,8 @@ internal static class QueryableExtension
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>SQL返回个数</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Task<int> ExecuteDeleteWhereAsync<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate, CancellationToken token = default)
|
||||
public static ValueTask<int> ExecuteDeleteWhereAsync<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate, CancellationToken token = default)
|
||||
{
|
||||
return source.Where(predicate).ExecuteDeleteAsync(token);
|
||||
return source.Where(predicate).ExecuteDeleteAsync(token).AsValueTask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
@@ -13,7 +14,8 @@ namespace Snap.Hutao.Core.Database;
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体的类型</typeparam>
|
||||
/// <typeparam name="TMessage">消息的类型</typeparam>
|
||||
internal sealed class ScopedDbCurrent<TEntity, TMessage>
|
||||
[ConstructorGenerated]
|
||||
internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
|
||||
where TEntity : class, ISelectable
|
||||
where TMessage : Message.ValueChangedMessage<TEntity>, new()
|
||||
{
|
||||
@@ -22,16 +24,6 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
|
||||
|
||||
private TEntity? current;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的数据库当前项
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public ScopedDbCurrent(IServiceProvider serviceProvider)
|
||||
{
|
||||
messenger = serviceProvider.GetRequiredService<IMessenger>();
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的项
|
||||
/// </summary>
|
||||
@@ -46,31 +38,86 @@ internal sealed class ScopedDbCurrent<TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Troubeshooting why the serviceProvider will NRE
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||
|
||||
// only update when not processing a deletion
|
||||
if (value != null)
|
||||
if (value is not null && current is not null)
|
||||
{
|
||||
if (current != null)
|
||||
{
|
||||
current.IsSelected = false;
|
||||
dbSet.UpdateAndSave(current);
|
||||
}
|
||||
current.IsSelected = false;
|
||||
dbSet.UpdateAndSave(current);
|
||||
}
|
||||
|
||||
TMessage message = new() { OldValue = current, NewValue = value };
|
||||
|
||||
current = value;
|
||||
|
||||
if (current != null)
|
||||
if (current is not null)
|
||||
{
|
||||
current.IsSelected = true;
|
||||
dbSet.UpdateAndSave(current);
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ConstructorGenerated]
|
||||
internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
|
||||
where TEntityOnly : class, IEntityOnly<TEntity>
|
||||
where TEntity : class, ISelectable
|
||||
where TMessage : Message.ValueChangedMessage<TEntityOnly>, new()
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
private TEntityOnly? current;
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的项
|
||||
/// </summary>
|
||||
public TEntityOnly? Current
|
||||
{
|
||||
get => current;
|
||||
set
|
||||
{
|
||||
// prevent useless sets
|
||||
if (current == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Troubeshooting why the serviceProvider will NRE
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||
|
||||
// only update when not processing a deletion
|
||||
if (value is not null)
|
||||
{
|
||||
if (current is not null)
|
||||
{
|
||||
current.Entity.IsSelected = false;
|
||||
dbSet.UpdateAndSave(current.Entity);
|
||||
}
|
||||
}
|
||||
|
||||
TMessage message = new() { OldValue = current, NewValue = value };
|
||||
|
||||
current = value;
|
||||
|
||||
if (current is not null)
|
||||
{
|
||||
current.Entity.IsSelected = true;
|
||||
dbSet.UpdateAndSave(current.Entity);
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ internal static class SelectableExtension
|
||||
/// <typeparam name="TSource">源类型</typeparam>
|
||||
/// <param name="source">源</param>
|
||||
/// <returns>选中的值或默认值</returns>
|
||||
/// <exception cref="InvalidOperationException">存在多个选中的值</exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static TSource? SelectedOrDefault<TSource>(this IEnumerable<TSource> source)
|
||||
where TSource : ISelectable
|
||||
|
||||
@@ -6,6 +6,6 @@ namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
/// <summary>
|
||||
/// 可转换类型服务
|
||||
/// </summary>
|
||||
internal interface ICastableService
|
||||
internal interface ICastService
|
||||
{
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
/// 有名称的对象
|
||||
/// 指示该对象可通过名称区分
|
||||
/// </summary>
|
||||
[Obsolete("无意义的接口")]
|
||||
[HighQuality]
|
||||
internal interface INamedService
|
||||
{
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 海外服/Hoyolab 可区分
|
||||
/// 海外服/HoYoLAB 可区分
|
||||
/// </summary>
|
||||
[Obsolete("Use IOverseaSupportFactory instead")]
|
||||
internal interface IOverseaSupport
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否为 海外服/Hoyolab
|
||||
/// 是否为 海外服/HoYoLAB
|
||||
/// </summary>
|
||||
public bool IsOversea { get; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.ViewModel.User;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
internal interface IOverseaSupportFactory<TClient>
|
||||
{
|
||||
TClient Create(bool isOversea);
|
||||
|
||||
TClient CreateFor(UserAndUid userAndUid)
|
||||
{
|
||||
return Create(userAndUid.User.IsOversea);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user