mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
299 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9993082b86 | ||
|
|
f835178b10 | ||
|
|
0b8b10e2f7 | ||
|
|
97130156f0 | ||
|
|
06def00e2c | ||
|
|
2679a68785 | ||
|
|
57e8bc8bdf | ||
|
|
b6ad96c0cb | ||
|
|
b6769b63e3 | ||
|
|
1c67da607c | ||
|
|
70cb4b8285 | ||
|
|
56fc4dcbcd | ||
|
|
626418680a | ||
|
|
8f3e166773 | ||
|
|
013639f57e | ||
|
|
21ad6be9da | ||
|
|
ed556c8539 | ||
|
|
42e11ec94a | ||
|
|
6cdfac6e09 | ||
|
|
b0c23e329b | ||
|
|
3ca46d3836 | ||
|
|
5df2d7210b | ||
|
|
7dece546a5 | ||
|
|
2b851a5459 | ||
|
|
98a711da70 | ||
|
|
176c26df51 | ||
|
|
af87891a5f | ||
|
|
b523a2bb2a | ||
|
|
b3f8093c09 | ||
|
|
b4eb97a6ea | ||
|
|
77217d2fc3 | ||
|
|
8e386c1457 | ||
|
|
330154c9ec | ||
|
|
f97385089a | ||
|
|
8982fcd427 | ||
|
|
aaa2e09dde | ||
|
|
58fdcc7804 | ||
|
|
ca352a5262 | ||
|
|
0f9e34ffb8 | ||
|
|
76a60e30fc | ||
|
|
693566812b | ||
|
|
8fbe2ee831 | ||
|
|
639a86d11b | ||
|
|
3f21a5d8fc | ||
|
|
8a62327e30 | ||
|
|
69b9a0c9dc | ||
|
|
af56f3ac30 | ||
|
|
ffc899efe5 | ||
|
|
80b958d9d3 | ||
|
|
b60f7c215e | ||
|
|
7128dddb57 | ||
|
|
9960b6f6b1 | ||
|
|
7099ca43b6 | ||
|
|
c9627e19e7 | ||
|
|
6999103aaa | ||
|
|
e54eef3aa7 | ||
|
|
f2ef6ff8ec | ||
|
|
35ddaaeb35 | ||
|
|
c833655231 | ||
|
|
8724784803 | ||
|
|
d1ceac0fe9 | ||
|
|
43415ebd0d | ||
|
|
a8b697e782 | ||
|
|
602b31c52d | ||
|
|
3b43389049 | ||
|
|
3a1fc839eb | ||
|
|
e0f967341e | ||
|
|
74ac738236 | ||
|
|
0189c4824b | ||
|
|
1a43841833 | ||
|
|
29ad939498 | ||
|
|
60c8e948e8 | ||
|
|
a17f9ca543 | ||
|
|
ee86f12168 | ||
|
|
9fb79a9fbd | ||
|
|
467eb13c87 | ||
|
|
40b055d310 | ||
|
|
126d19e96a | ||
|
|
485ac1e682 | ||
|
|
1117e322a6 | ||
|
|
85b40b71e5 | ||
|
|
66bbf4335a | ||
|
|
cb20bd0df4 | ||
|
|
cb9c9a0af2 | ||
|
|
28ea71cf77 | ||
|
|
94744c4bc1 | ||
|
|
58af255485 | ||
|
|
c45e6560b8 | ||
|
|
29d7d36b66 | ||
|
|
2bbf6f192e | ||
|
|
cc8565428b | ||
|
|
d84e0bf199 | ||
|
|
9d8c981f5a | ||
|
|
1f412b289a | ||
|
|
07b3e98ac0 | ||
|
|
8a9f4ced34 | ||
|
|
0a0aa3e404 | ||
|
|
84861c5e6b | ||
|
|
a23e6f55d4 | ||
|
|
c1cf5aa499 | ||
|
|
91b6e6793c | ||
|
|
ac1ef44801 | ||
|
|
e7b23c8a5a | ||
|
|
68a3851595 | ||
|
|
7861ebf998 | ||
|
|
0cc75ab245 | ||
|
|
ef09c87835 | ||
|
|
b13ec8c12a | ||
|
|
29e14111f0 | ||
|
|
0dd66288cd | ||
|
|
bb835ee20e | ||
|
|
a1037e1827 | ||
|
|
ba6ee9bb85 | ||
|
|
f1c50dc6c3 | ||
|
|
8e841b1295 | ||
|
|
ea2000b03d | ||
|
|
d186dd6f98 | ||
|
|
5c46c92a1a | ||
|
|
98f2dd13d1 | ||
|
|
3e0493be31 | ||
|
|
7025074170 | ||
|
|
0eade5e81a | ||
|
|
163d076ed5 | ||
|
|
fa5aac3366 | ||
|
|
f98a40d080 | ||
|
|
cf5b377532 | ||
|
|
afbc91abe8 | ||
|
|
be572fa327 | ||
|
|
fe594864f2 | ||
|
|
d8b6315e7e | ||
|
|
d4e9c2aa9c | ||
|
|
800c948c38 | ||
|
|
32589bc994 | ||
|
|
afc91b1a29 | ||
|
|
40db69825f | ||
|
|
407dd7bac8 | ||
|
|
4708cd5629 | ||
|
|
2b93d31788 | ||
|
|
39831b0ae1 | ||
|
|
51c9936018 | ||
|
|
3466a98ffb | ||
|
|
a064cc10ee | ||
|
|
3479a19164 | ||
|
|
d4549581c1 | ||
|
|
f97bc344d0 | ||
|
|
26d23fec7f | ||
|
|
7442f7f1ec | ||
|
|
3eb2556393 | ||
|
|
cfff6f39fc | ||
|
|
3005031b39 | ||
|
|
71363f4d8d | ||
|
|
e833578334 | ||
|
|
d529b3cea6 | ||
|
|
1c0ce62885 | ||
|
|
acdf2baa9a | ||
|
|
ec007d5d81 | ||
|
|
5e734ac689 | ||
|
|
b0ecd048b6 | ||
|
|
91010d0d8b | ||
|
|
fc771eb90a | ||
|
|
80f2fed722 | ||
|
|
bdb406c451 | ||
|
|
5bc957c6a5 | ||
|
|
416c6f15a6 | ||
|
|
9eed633e05 | ||
|
|
7e30173990 | ||
|
|
2200e2e58e | ||
|
|
b8886c5cd3 | ||
|
|
43007d8fb4 | ||
|
|
88684bff00 | ||
|
|
0c7ce7a72f | ||
|
|
075d92f754 | ||
|
|
a0cba171cc | ||
|
|
f41185310b | ||
|
|
2a4c93d241 | ||
|
|
c0980fabe8 | ||
|
|
f2ba316059 | ||
|
|
e4e9dd91f1 | ||
|
|
749ef0e138 | ||
|
|
24086ee4d0 | ||
|
|
aeb6962ae4 | ||
|
|
87e5ede91f | ||
|
|
91de6d170e | ||
|
|
3057673cdb | ||
|
|
c3ace405ac | ||
|
|
0b48581e65 | ||
|
|
4ab129e4a2 | ||
|
|
13ad36f5b4 | ||
|
|
f026321aa8 | ||
|
|
d1dfdf107b | ||
|
|
59f8895675 | ||
|
|
4cb3d5f03f | ||
|
|
067c7d7c4d | ||
|
|
1cc072ba28 | ||
|
|
0e7afa8efb | ||
|
|
b753728b7e | ||
|
|
df019da891 | ||
|
|
c6435f30eb | ||
|
|
3ac0be4220 | ||
|
|
24f6a33256 | ||
|
|
dc9278eb4f | ||
|
|
4b2c82db62 | ||
|
|
70f30edd7c | ||
|
|
c8e8213df6 | ||
|
|
7cad996902 | ||
|
|
eec47b72c7 | ||
|
|
5943b1a1fb | ||
|
|
9f9a5670bc | ||
|
|
10ba927136 | ||
|
|
07d42cedd1 | ||
|
|
07c52019f4 | ||
|
|
77cb2fc603 | ||
|
|
b5c16e2dae | ||
|
|
29c954b032 | ||
|
|
8df5d5d6eb | ||
|
|
827d944987 | ||
|
|
4c47f3c08b | ||
|
|
6540cc4577 | ||
|
|
df22d30a96 | ||
|
|
5ea9dd533f | ||
|
|
744c1079e1 | ||
|
|
1539863415 | ||
|
|
3527e43118 | ||
|
|
8388def548 | ||
|
|
d0bfbfa505 | ||
|
|
fa640d27f0 | ||
|
|
389c1417f7 | ||
|
|
6dcacb1bf4 | ||
|
|
4ffd09cce8 | ||
|
|
0dcbac3ee1 | ||
|
|
b1ea5332fc | ||
|
|
b34dab0f99 | ||
|
|
791e517e39 | ||
|
|
4408e3994e | ||
|
|
32cbbefe1a | ||
|
|
d754c0d117 | ||
|
|
4c75295f2c | ||
|
|
34e5312d75 | ||
|
|
465d6b631e | ||
|
|
0d2d1b8115 | ||
|
|
981949651e | ||
|
|
874dac1119 | ||
|
|
3f0694b28e | ||
|
|
6b166b6aed | ||
|
|
b351231c84 | ||
|
|
0603b24466 | ||
|
|
f97ad4eac0 | ||
|
|
28fc4558be | ||
|
|
ea3391b112 | ||
|
|
ec95e42d7d | ||
|
|
e9f12aeb09 | ||
|
|
04850dd136 | ||
|
|
4782d61ed0 | ||
|
|
28ade90926 | ||
|
|
dde97b6489 | ||
|
|
44fe729e1a | ||
|
|
2a1d814cc5 | ||
|
|
c1ee37bd8f | ||
|
|
65b81f0ad8 | ||
|
|
91fea88623 | ||
|
|
026c68229a | ||
|
|
91b2db886f | ||
|
|
101d316525 | ||
|
|
59d62f931d | ||
|
|
f0bb19bc07 | ||
|
|
c0b05e2c2f | ||
|
|
fc02f833a0 | ||
|
|
7b8ebd86b1 | ||
|
|
47b24286b1 | ||
|
|
0e3e3b9e4a | ||
|
|
85d7b22e11 | ||
|
|
7caeb17788 | ||
|
|
b11b90e9f1 | ||
|
|
58643a60b5 | ||
|
|
a29b487c26 | ||
|
|
1bd6023e0a | ||
|
|
579173d464 | ||
|
|
9ed53e8c34 | ||
|
|
830556a043 | ||
|
|
7ba27e184f | ||
|
|
9aa6a2b57b | ||
|
|
5773902f4a | ||
|
|
06c5bcad3e | ||
|
|
c0165c57fd | ||
|
|
4e57520115 | ||
|
|
17c3480dae | ||
|
|
7d5faadbb5 | ||
|
|
b7df968ea7 | ||
|
|
5f6cc46774 | ||
|
|
a34b5a5101 | ||
|
|
457e3c4af2 | ||
|
|
26143079b3 | ||
|
|
20ad9aec60 | ||
|
|
61dd098d95 | ||
|
|
e7233fbf2a | ||
|
|
8de526274a | ||
|
|
5b1fb6e1dd | ||
|
|
1584fd1428 | ||
|
|
65179a340f |
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,8 +1,8 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: [DGP-Studio]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
open_collective: snaphutao
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 问题反馈
|
||||
description: 告诉我们你的问题
|
||||
description: 通过这个议题向开发团队反馈你发现的程序中的问题
|
||||
title: "[Bug]: 在这里填写一个合适的标题"
|
||||
labels: ["BUG"]
|
||||
labels: ["BUG", "priority:none"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: 检查清单
|
||||
description: |-
|
||||
请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略
|
||||
请确保你已完整执行检查清单,否则你的议题可能会被忽略
|
||||
options:
|
||||
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
|
||||
required: true
|
||||
@@ -51,6 +51,7 @@ body:
|
||||
description: |
|
||||
在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID
|
||||
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
|
||||
如果你的程序已经无法启动,请下载并运行[此PowerShell脚本](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1),它将显示你的设备 ID
|
||||
validations:
|
||||
required: false
|
||||
|
||||
27
.github/ISSUE_TEMPLATE/CHS-feature-request.yml
vendored
Normal file
27
.github/ISSUE_TEMPLATE/CHS-feature-request.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: 功能请求
|
||||
description: 通过这个议题来向开发团队分享你的想法
|
||||
title: "[Feat]: 在这里填写一个合适的标题"
|
||||
labels: ["功能", "needs-triage", "priority:none"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
请按下方的要求填写完整的问题表单。
|
||||
|
||||
- type: textarea
|
||||
id: back
|
||||
attributes:
|
||||
label: 背景与动机
|
||||
description: 添加此功能的理由,如果你想要实现多个功能,请分别发起多个单独的议题
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: req
|
||||
attributes:
|
||||
label: 想要实现或优化的功能
|
||||
description: 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,5 +1,5 @@
|
||||
name: 网络问题
|
||||
description: 当网络问题影响到你的程序使用时
|
||||
description: 通过这个议题来反馈网络问题
|
||||
title: "[Network]: 在这里填写一个合适的标题"
|
||||
labels: ["area-Network"]
|
||||
assignees:
|
||||
@@ -19,10 +19,10 @@ body:
|
||||
description: |
|
||||
停下!
|
||||
**在填写下面的问题之前请先使用我们的网络诊断工具**
|
||||
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
|
||||
**这个工具将会生成一份报告并加密压缩,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
|
||||
- 你可以点击下面的链接以下载网络诊断工具:
|
||||
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-hutao.exe)
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/network-diagnosis-hutao.exe)
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/SH-Network-Diagnosis.exe)
|
||||
- [极狐 GitLab](https://jihulab.com/DGP-Studio/network-diagnosis-tool/-/jobs/11144011/artifacts/raw/SH-Network-Diagnosis.exe?inline=false)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -60,7 +60,6 @@ body:
|
||||
- 完全无法连接服务器
|
||||
- 连接速度慢
|
||||
- 获取到了不正确的页面或数据
|
||||
- 客户端提示 429 Error
|
||||
- 客户端图片下载错误
|
||||
- 客户端图片预下载错误
|
||||
- 其它
|
||||
@@ -74,5 +73,12 @@ body:
|
||||
description: 如果你在上一项中选择了`其它`或者你有更多信息需要提供,请在这里写下来
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: 最后一步
|
||||
description: 检查你提交的议题
|
||||
options:
|
||||
- label: 我已经在该议题中上传了包含网络诊断报告的加密压缩包
|
||||
required: true
|
||||
@@ -1,7 +1,7 @@
|
||||
name: BUG Report [English Form]
|
||||
description: Tell us what issue you get
|
||||
title: "[ENG][Bug]: Place your Issue Title Here"
|
||||
labels: ["BUG"]
|
||||
labels: ["BUG", "priority:none"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -50,7 +50,8 @@ body:
|
||||
label: Device ID
|
||||
description: |
|
||||
In Snap Hutao's settings page, you can find and copy your device ID
|
||||
If your issue is about program crash, please fill this so we can dump the log and locate the source easier
|
||||
If your issue is about program crash, please fill this so we can dump the log and locate the source easier
|
||||
If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
27
.github/ISSUE_TEMPLATE/ENG-feature-request.yml
vendored
Normal file
27
.github/ISSUE_TEMPLATE/ENG-feature-request.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Feature Request [English Form]
|
||||
description: Tell us about your thought
|
||||
title: "[Feat]: Place your title here"
|
||||
labels: ["功能", "needs-triage", "priority:none"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please fill the form below
|
||||
|
||||
- type: textarea
|
||||
id: back
|
||||
attributes:
|
||||
label: Background & Motivation
|
||||
description: Reason why this feature is needed. If multiple features is requested, please open multiple issues for each of them.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: req
|
||||
attributes:
|
||||
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
|
||||
79
.github/ISSUE_TEMPLATE/ENG-network-issue.yml
vendored
Normal file
79
.github/ISSUE_TEMPLATE/ENG-network-issue.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: Network Issue [English Form]
|
||||
description: Submit this issue form when network issue affect your client experience
|
||||
title: "[Network]: Place your title here"
|
||||
labels: ["area-Network"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
- Masterain98
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Please use one sentence to briefly describe your issue as title above**
|
||||
**Please follow the instruction below to fill the form, so we can locate the issue quickly**
|
||||
|
||||
- type: textarea
|
||||
id: network-diagnosis-report
|
||||
attributes:
|
||||
label: Submit Your Network Diagnosis Report
|
||||
description: |
|
||||
STOP HERE!
|
||||
**Please run our network diagnosis tool before filling this form**
|
||||
**The diagnosis tool will generate a report and add it into a password-protected archive. Drag the `.zip` archive to the box below so it can be uploaded.**
|
||||
- Use the following link to download the Network Diagnosis Tool:
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/SH-Network-Diagnosis.exe)
|
||||
- [JIHu GitLab](https://jihulab.com/DGP-Studio/network-diagnosis-tool/-/jobs/11144011/artifacts/raw/SH-Network-Diagnosis.exe?inline=false)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: user-geo-location
|
||||
attributes:
|
||||
label: Your Geographical Location
|
||||
description: |
|
||||
Description accurate to country
|
||||
placeholder: USA
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: user-isp
|
||||
attributes:
|
||||
label: Your ISP Name
|
||||
description: |
|
||||
Name of your Internet service provider
|
||||
placeholder: AT&T
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: user-issue-category
|
||||
attributes:
|
||||
label: Issue Category
|
||||
description: Select an issue category
|
||||
options:
|
||||
- Cannot connect to server completely
|
||||
- Slow spped
|
||||
- Fetched wrong page or data
|
||||
- Image download error in the client
|
||||
- Image set pre-download error (client welcome wizard process)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Your Issue (cont.)
|
||||
description: If you selected `Other` in previous dropdown, please explain your issue in detail here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: One Last Step
|
||||
description: Check your issue form
|
||||
options:
|
||||
- label: I confirm I have attached the network diagnosis report archive in the issue
|
||||
required: true
|
||||
61
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
Normal file
61
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Publish Process
|
||||
description: FOR ADMIN USE ONLY. WILL CAUSE A BAN IF NO PERMISSION.
|
||||
title: "[Publish]: Version 1.9.98"
|
||||
labels: ["Publish"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: textarea
|
||||
id: main-body
|
||||
attributes:
|
||||
label: Publish Process
|
||||
value: |
|
||||
|
||||
## 创建版本
|
||||
|
||||
- [ ] 同步一次 [Crowdin](https://crowdin.com/project/snap-hutao) 翻译
|
||||
- [ ] 发布 RC 版本(Optional)
|
||||
- [ ] 合并入主分支
|
||||
- [ ] 整理更新内容,等待翻译
|
||||
- [ ] 打包
|
||||
- [ ] 提交微软商店
|
||||
- [ ] 包含更新日志
|
||||
- [ ] 在 [Snap.Hutao.Docs@next-patch](https://github.com/DGP-Studio/Snap.Hutao.Docs/tree/next-patch) 分支更新文档并直接开 PR
|
||||
- [ ] 更新日志
|
||||
- [ ] 功能文档更新
|
||||
|
||||
## 发布版本 [半自动]
|
||||
|
||||
- [ ] 在 GitHub 个人设置中更新 [Publish-Automate Beta PAT](https://github.com/settings/tokens?type=beta),有效期需小于预计发版需要天数
|
||||
- [ ] 将更新的 PAT 更新至 Publish-Automate 库的 [Actions Secrets](https://github.com/DGP-Studio/Publish-Automate/settings/secrets/actions) 中
|
||||
|
||||
***
|
||||
|
||||
- [ ] 运行 [Auto Publish Action](https://github.com/DGP-Studio/Publish-Automate/actions/workflows/auto-publish.yml)
|
||||
- [ ] 在 https://store.rg-adguard.net/ 下载新版本安装包
|
||||
- [ ] Store URL: https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52
|
||||
- [ ] 命名格式为 `Snap.Hutao x.x.x.msix`
|
||||
- [ ] Merge 文档 PR
|
||||
- [ ] 发布 Release
|
||||
- [ ] 更新日志格式(以 1.6.2 版本为例)
|
||||
|
||||
```jsx
|
||||
## Update log
|
||||
https://hut.ao/en/statements/update-log.html#_1-6-2
|
||||
|
||||
## 更新日志
|
||||
[此处从文档复制]
|
||||
|
||||
## What's Changed
|
||||
**Full Changelog**: https://github.com/DGP-Studio/Snap.Hutao/compare/1.6.0...1.6.2
|
||||
```
|
||||
|
||||
- [ ] 通知用户
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: Final Check
|
||||
description: Understand what you are doing
|
||||
options:
|
||||
- label: I understand that I will get banned from repository if I don't have permission to use this template
|
||||
required: true
|
||||
65
.github/ISSUE_TEMPLATE/artifact-rating-rules.yml
vendored
65
.github/ISSUE_TEMPLATE/artifact-rating-rules.yml
vendored
@@ -1,65 +0,0 @@
|
||||
name: 圣遗物评分细则建议
|
||||
description: 为圣遗物评分规则提供你的想法
|
||||
title: "[Artifact Rating] 请在这里填写角色名称"
|
||||
labels: area-AvatarInfo
|
||||
assignees: Lightczx
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
请按下方的要求填写完整的问题表单
|
||||
|
||||
- type: textarea
|
||||
id: your-suggested-rule
|
||||
attributes:
|
||||
label: 评分细则
|
||||
description: |
|
||||
请修改下方表格中的**角色名称**和**各属性权重**,并在表格后添加合适的说明
|
||||
你可以点击预览按钮(preview)来查看表格最终会显示出的内容
|
||||
value: |
|
||||
|项目|评分权重(0-100)|
|
||||
|-----|-----|
|
||||
|角色名称| 旅行者 |
|
||||
|生命值| 10 |
|
||||
|攻击力| 10 |
|
||||
|防御力| 10 |
|
||||
|暴击率| 10 |
|
||||
|暴击伤害| 10 |
|
||||
|元素精通| 10 |
|
||||
|充能效率| 10 |
|
||||
|治疗加成| 10 |
|
||||
|元素伤害| 10 |
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: no-duplicated-dropdown
|
||||
attributes:
|
||||
label: 我确认当前没有其它的该角色的圣遗物评分细则建议
|
||||
description: 如果有,你应该在已有的工单内回复以提出你的建议
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: title-filled-dropdown
|
||||
attributes:
|
||||
label: 我确认已设置合适的标题
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: all-filled-dropdown
|
||||
attributes:
|
||||
label: 我确认已完整填写表格
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
28
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
28
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Feature Request 功能请求
|
||||
description: Tell us about your thought 告诉我们你的想法
|
||||
title: "[Feat]: Place your title here 在这里填写一个合适的标题"
|
||||
labels: ["功能"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please fill the form below
|
||||
请按下方的要求填写完整的问题表单。
|
||||
|
||||
- type: textarea
|
||||
id: back
|
||||
attributes:
|
||||
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: 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
|
||||
33
.github/workflows/PublishDistribution.yml
vendored
33
.github/workflows/PublishDistribution.yml
vendored
@@ -12,40 +12,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Download Assets
|
||||
- name: Download Release
|
||||
timeout-minutes: 5
|
||||
uses: robinraju/release-downloader@v1.7
|
||||
with:
|
||||
repository: "DGP-Studio/Snap.Hutao"
|
||||
latest: true
|
||||
fileName: "*.msix"
|
||||
out-file-path: ./release-download
|
||||
|
||||
# Upload to Drive
|
||||
- name: Upload Drive
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
RCCONF: ${{ secrets.RCCONF }}
|
||||
run: |
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
mkdir -p ~/.config/rclone/
|
||||
cat << EOF > ~/.config/rclone/rclone.conf
|
||||
$RCCONF
|
||||
EOF
|
||||
|
||||
rclone copy ./release-download/* dgpODCN:/releases/
|
||||
|
||||
# Purge Patch System Cache
|
||||
- name: Purge Patch
|
||||
env:
|
||||
PATCH_HOSTS: ${{ secrets.PATCH_HOSTS }}
|
||||
PURGE_TOKEN: ${{ secrets.PURGE_TOKEN }}
|
||||
PURGE_URL: ${{ secrets.PURGE_URL }}
|
||||
run: |
|
||||
sudo echo "$PATCH_HOSTS" | sudo tee -a /etc/hosts
|
||||
curl --header "Authorization: token $PURGE_TOKEN" $PURGE_URL
|
||||
curl -X PATCH $PURGE_URL
|
||||
|
||||
26
.github/workflows/qodana_code_quality.yml
vendored
26
.github/workflows/qodana_code_quality.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Qodana
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.yml'
|
||||
- '**.resx'
|
||||
|
||||
jobs:
|
||||
qodana:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Qodana Scan'
|
||||
uses: JetBrains/qodana-action@v2023.2
|
||||
with:
|
||||
pr-mode: false
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
desktop.ini
|
||||
|
||||
*.csproj.user
|
||||
*.pubxml
|
||||
*.DotSettings.user
|
||||
|
||||
.vs/
|
||||
@@ -10,13 +9,13 @@ src/Snap.Hutao/_ReSharper.Caches
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao/bin/
|
||||
src/Snap.Hutao/Snap.Hutao/obj/
|
||||
src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs
|
||||
src/Snap.Hutao/Snap.Hutao/Snap.Hutao_TemporaryKey.pfx
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao.Win32/bin/
|
||||
src/Snap.Hutao/Snap.Hutao.Win32/obj/
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao.SourceGeneration/bin/
|
||||
src/Snap.Hutao/Snap.Hutao.SourceGeneration/obj/
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao.Test/bin/
|
||||
src/Snap.Hutao/Snap.Hutao.Test/obj/
|
||||
src/Snap.Hutao/Snap.Hutao.Test/obj/
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao/Properties/PublishProfiles/FolderProfile.pubxml.user
|
||||
@@ -1,8 +1,9 @@
|
||||

|
||||

|
||||
|
||||
胡桃工具箱是一个 Windows 平台的开源的原神工具箱,旨在帮助玩家获得更好的游戏体验; 它是对官方移动端工具的一种非破坏性功能扩展,为不习惯在移动端进行原神游戏的 PC 玩家提供一个在 Windows 平台下获得接近移动端功能权利的途径
|
||||
|
||||
Snap Hutao is an open-source Genshin Impact toolbox on Windows platform, aim to provide a better gaming experience for players. It's an nondestructive feature extension from Genshin Impact's official mobile application, to provide similar feature on desktop, to allow PC gamers gain deserved benefits from mobile platforms.
|
||||
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新 功能相结合,它提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
|
||||
|
||||
Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed for modern Windows platform to improve the gaming experience for desktop players. By combining existing official resources with new features designed by the development team, it provides a complete and useful set of tools without the need to rely on mobile devices. Snap Hutao does not take any destructive modification to the game client to ensure the security of the toolkit.
|
||||
|
||||
## 下载使用 / Download
|
||||
|
||||
@@ -47,4 +48,4 @@ Snap Hutao is an open-source Genshin Impact toolbox on Windows platform, aim to
|
||||
## 开发 / Development
|
||||

|
||||
|
||||
[](https://star-history.com/#DGP-Studio/Snap.Hutao&Date)
|
||||
[](https://star-history.com/#DGP-Studio/Snap.Hutao&Date)
|
||||
|
||||
@@ -55,7 +55,7 @@ steps:
|
||||
displayName: Install dotNet
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '7.x'
|
||||
version: '8.x'
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: NuGetToolInstaller@1
|
||||
@@ -86,7 +86,7 @@ steps:
|
||||
|
||||
- task: MagicChunks@2
|
||||
inputs:
|
||||
sourcePath: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\AppxManifest.xml'
|
||||
sourcePath: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\AppxManifest.xml'
|
||||
fileType: 'Xml'
|
||||
targetPathType: 'source'
|
||||
transformationType: 'json'
|
||||
@@ -107,7 +107,7 @@ steps:
|
||||
mkdir Assets
|
||||
|
||||
mkdir Resource
|
||||
workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64'
|
||||
workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64'
|
||||
|
||||
|
||||
- task: CopyFiles@2
|
||||
@@ -115,19 +115,19 @@ steps:
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Assets'
|
||||
Contents: '**'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\Assets'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\Assets'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy Resource Folder
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Resource'
|
||||
Contents: '**'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\Resource'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\Resource'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: Build MSIX
|
||||
inputs:
|
||||
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
|
||||
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
|
||||
|
||||
- task: MsixSigning@1
|
||||
name: signMsix
|
||||
|
||||
33
qodana.yaml
33
qodana.yaml
@@ -1,33 +0,0 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
exclude:
|
||||
- name: Test
|
||||
paths:
|
||||
- Snap.Hutao.Test
|
||||
- Snap.Hutao.SourceGeneration
|
||||
- name: All
|
||||
paths:
|
||||
- Snap.Hutao.SourceGeneration
|
||||
- Snap.Hutao.Test
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-dotnet:2023.2-eap
|
||||
@@ -108,7 +108,9 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_diagnostic.SA1629.severity = none
|
||||
dotnet_diagnostic.SA1642.severity = none
|
||||
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
dotnet_diagnostic.IDE0060.severity = none
|
||||
dotnet_diagnostic.IDE0290.severity = none
|
||||
|
||||
# SA1208: System using directives should be placed before other using directives
|
||||
dotnet_diagnostic.SA1208.severity = none
|
||||
@@ -322,6 +324,8 @@ dotnet_diagnostic.CA2227.severity = suggestion
|
||||
|
||||
# CA2251: 使用 “string.Equals”
|
||||
dotnet_diagnostic.CA2251.severity = suggestion
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
dotnet_diagnostic.SA1010.severity = none
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class AttributeGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(GenerateAllAttributes);
|
||||
}
|
||||
|
||||
public static void GenerateAllAttributes(IncrementalGeneratorPostInitializationContext context)
|
||||
{
|
||||
string coreAnnotations = """
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Core.Annotation;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
|
||||
internal sealed class CommandAttribute : Attribute
|
||||
{
|
||||
public CommandAttribute(string name)
|
||||
{
|
||||
}
|
||||
|
||||
public bool AllowConcurrentExecutions { get; set; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class ConstructorGeneratedAttribute : Attribute
|
||||
{
|
||||
public ConstructorGeneratedAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
public bool CallBaseConstructor { get; set; }
|
||||
public bool ResolveHttpClient { get; set; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
internal sealed class DependencyPropertyAttribute : Attribute
|
||||
{
|
||||
public DependencyPropertyAttribute(string name, Type type)
|
||||
{
|
||||
}
|
||||
|
||||
public DependencyPropertyAttribute(string name, Type type, object defaultValue)
|
||||
{
|
||||
}
|
||||
|
||||
public DependencyPropertyAttribute(string name, Type type, object defaultValue, string valueChangedCallbackName)
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsAttached { get; set; }
|
||||
public Type AttachedType { get; set; } = default;
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
[Conditional("DEBUG")]
|
||||
internal sealed class HighQualityAttribute : Attribute
|
||||
{
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Core.Annotation.Attributes.g.cs", coreAnnotations);
|
||||
|
||||
string coreDependencyInjectionAnnotationHttpClients = """
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class HttpClientAttribute : Attribute
|
||||
{
|
||||
public HttpClientAttribute(HttpClientConfiguration configuration)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpClientAttribute(HttpClientConfiguration configuration, Type interfaceType)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal enum HttpClientConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认配置
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// 米游社请求配置
|
||||
/// </summary>
|
||||
XRpc,
|
||||
|
||||
/// <summary>
|
||||
/// 米游社登录请求配置
|
||||
/// </summary>
|
||||
XRpc2,
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab app
|
||||
/// </summary>
|
||||
XRpc3,
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class PrimaryHttpMessageHandlerAttribute : Attribute
|
||||
{
|
||||
/// <inheritdoc cref="System.Net.Http.HttpClientHandler.MaxConnectionsPerServer"/>
|
||||
public int MaxConnectionsPerServer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="System.Net.Http.HttpClientHandler.UseCookies"/>
|
||||
/// </summary>
|
||||
public bool UseCookies { get; set; }
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.Attributes.g.cs", coreDependencyInjectionAnnotationHttpClients);
|
||||
|
||||
string coreDependencyInjectionAnnotations = """
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Annotation;
|
||||
|
||||
internal enum InjectAs
|
||||
{
|
||||
Singleton,
|
||||
Transient,
|
||||
Scoped,
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
internal sealed class InjectionAttribute : Attribute
|
||||
{
|
||||
public InjectionAttribute(InjectAs injectAs)
|
||||
{
|
||||
}
|
||||
|
||||
public InjectionAttribute(InjectAs injectAs, Type interfaceType)
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.Attributes.g.cs", coreDependencyInjectionAnnotations);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class CommandGenerator : IIncrementalGenerator
|
||||
{
|
||||
public const string AttributeName = "Snap.Hutao.Core.Annotation.CommandAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2<IMethodSymbol>>> commands =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedMethods, CommandMethod)
|
||||
.Where(GeneratorSyntaxContext2<IMethodSymbol>.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(commands, GenerateCommandImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedMethods(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is MethodDeclarationSyntax methodDeclarationSyntax
|
||||
&& methodDeclarationSyntax.Parent is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& methodDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2<IMethodSymbol> CommandMethod(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out IMethodSymbol? methodSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, methodSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateCommandImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2<IMethodSymbol>> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2<IMethodSymbol> context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateCommandImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateCommandImplementation(SourceProductionContext production, GeneratorSyntaxContext2<IMethodSymbol> context2)
|
||||
{
|
||||
INamedTypeSymbol classSymbol = context2.Symbol.ContainingType;
|
||||
|
||||
AttributeData commandInfo = context2.SingleAttribute(AttributeName);
|
||||
string commandName = (string)commandInfo.ConstructorArguments[0].Value!;
|
||||
|
||||
string commandType = context2.Symbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task")
|
||||
? "AsyncRelayCommand"
|
||||
: "RelayCommand";
|
||||
|
||||
string genericParameter = context2.Symbol.Parameters.ElementAtOrDefault(0) is IParameterSymbol parameter
|
||||
? $"<{parameter.Type.ToDisplayString(SymbolDisplayFormats.FullyQualifiedNonNullableFormat)}>"
|
||||
: string.Empty;
|
||||
|
||||
string concurrentExecution = commandInfo.HasNamedArgumentWith<bool>("AllowConcurrentExecutions", value => value)
|
||||
? ", AsyncRelayCommandOptions.AllowConcurrentExecutions"
|
||||
: string.Empty;
|
||||
|
||||
string className = classSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
|
||||
string code = $$"""
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace {{classSymbol.ContainingNamespace}};
|
||||
|
||||
partial class {{className}}
|
||||
{
|
||||
private ICommand _{{commandName}};
|
||||
|
||||
public ICommand {{commandName}}
|
||||
{
|
||||
get => _{{commandName}} ??= new {{commandType}}{{genericParameter}}({{context2.Symbol.Name}}{{concurrentExecution}});
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
string normalizedClassName = classSymbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.{commandName}.g.cs", code);
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class ConstructorGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.ConstructorGeneratedAttribute";
|
||||
private const string CompilerGenerated = "System.Runtime.CompilerServices.CompilerGeneratedAttribute";
|
||||
|
||||
//private static readonly DiagnosticDescriptor genericTypeNotSupportedDescriptor = new("SH102", "Generic type is not supported to generate .ctor", "Type [{0}] is not supported", "Quality", DiagnosticSeverity.Error, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, ConstructorGeneratedClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(injectionClasses, GenerateConstructorImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 ConstructorGeneratedClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateConstructorImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2 context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateConstructorImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateConstructorImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
AttributeData constructorInfo = context2.SingleAttribute(AttributeName);
|
||||
|
||||
bool resolveHttpClient = constructorInfo.HasNamedArgumentWith<bool>("ResolveHttpClient", value => value);
|
||||
bool callBaseConstructor = constructorInfo.HasNamedArgumentWith<bool>("CallBaseConstructor", value => value);
|
||||
string httpclient = resolveHttpClient ? ", System.Net.Http.HttpClient httpClient" : string.Empty;
|
||||
|
||||
FieldValueAssignmentOptions options = new(resolveHttpClient, callBaseConstructor);
|
||||
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(ConstructorGenerator)}}", "1.0.0.0")]
|
||||
partial class {{context2.Symbol.ToDisplayString(SymbolDisplayFormats.QualifiedNonNullableFormat)}}
|
||||
{
|
||||
public {{context2.Symbol.Name}}(System.IServiceProvider serviceProvider{{httpclient}}){{(options.CallBaseConstructor ? " : base(serviceProvider)" : string.Empty)}}
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithFieldValueAssignment(sourceBuilder, context2, options);
|
||||
|
||||
sourceBuilder.Append("""
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.ctor.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithFieldValueAssignment(StringBuilder builder, GeneratorSyntaxContext2 context2, FieldValueAssignmentOptions options)
|
||||
{
|
||||
IEnumerable<IFieldSymbol> fields = context2.Symbol.GetMembers()
|
||||
.Where(m => m.Kind == SymbolKind.Field)
|
||||
.OfType<IFieldSymbol>();
|
||||
|
||||
foreach (IFieldSymbol fieldSymbol in fields)
|
||||
{
|
||||
if (fieldSymbol.Name.AsSpan()[0] is '<')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool shoudSkip = false;
|
||||
foreach (SyntaxReference syntaxReference in fieldSymbol.DeclaringSyntaxReferences)
|
||||
{
|
||||
if (syntaxReference.GetSyntax() is VariableDeclaratorSyntax declarator)
|
||||
{
|
||||
if (declarator.Initializer is not null)
|
||||
{
|
||||
// Skip field with initializer
|
||||
builder.Append(" // Skip field with initializer: ").AppendLine(fieldSymbol.Name);
|
||||
shoudSkip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shoudSkip)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldSymbol.IsReadOnly && !fieldSymbol.IsStatic)
|
||||
{
|
||||
switch (fieldSymbol.Type.ToDisplayString())
|
||||
{
|
||||
case "System.IServiceProvider":
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.AppendLine(" = serviceProvider;");
|
||||
break;
|
||||
|
||||
case "System.Net.Http.HttpClient":
|
||||
if (options.ResolveHttpClient)
|
||||
{
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.AppendLine(" = httpClient;");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.Append(" = serviceProvider.GetRequiredService<System.Net.Http.IHttpClientFactory>().CreateClient(nameof(")
|
||||
.Append(context2.Symbol.Name)
|
||||
.AppendLine("));");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.Append(" = serviceProvider.GetRequiredService<")
|
||||
.Append(fieldSymbol.Type)
|
||||
.AppendLine(">();");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (INamedTypeSymbol interfaceSymbol in context2.Symbol.Interfaces)
|
||||
{
|
||||
if (interfaceSymbol.Name == "IRecipient")
|
||||
{
|
||||
builder
|
||||
.Append(" CommunityToolkit.Mvvm.Messaging.IMessengerExtensions.Register<")
|
||||
.Append(interfaceSymbol.TypeArguments[0])
|
||||
.AppendLine(">(serviceProvider.GetRequiredService<CommunityToolkit.Mvvm.Messaging.IMessenger>(), this);");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct FieldValueAssignmentOptions
|
||||
{
|
||||
public readonly bool ResolveHttpClient;
|
||||
public readonly bool CallBaseConstructor;
|
||||
|
||||
public FieldValueAssignmentOptions(bool resolveHttpClient, bool callBaseConstructor)
|
||||
{
|
||||
ResolveHttpClient = resolveHttpClient;
|
||||
CallBaseConstructor = callBaseConstructor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class DependencyPropertyGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.DependencyPropertyAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> commands =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, CommandMethod)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(commands, GenerateDependencyPropertyImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 CommandMethod(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? methodSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, methodSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateDependencyPropertyImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2 context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateDependencyPropertyImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateDependencyPropertyImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
foreach (AttributeData propertyInfo in context2.Attributes.Where(attr => attr.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
string owner = context2.Symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
Dictionary<string, TypedConstant> namedArguments = propertyInfo.NamedArguments.ToDictionary();
|
||||
bool isAttached = namedArguments.TryGetValue("IsAttached", out TypedConstant constant) && (bool)constant.Value!;
|
||||
string register = isAttached ? "RegisterAttached" : "Register";
|
||||
|
||||
ImmutableArray<TypedConstant> arguments = propertyInfo.ConstructorArguments;
|
||||
|
||||
string propertyName = (string)arguments[0].Value!;
|
||||
string propertyType = arguments[1].Value!.ToString();
|
||||
string defaultValue = GetLiteralString(arguments.ElementAtOrDefault(2)) ?? "default";
|
||||
string propertyChangedCallback = arguments.ElementAtOrDefault(3) is { IsNull: false } arg3 ? $", {arg3.Value}" : string.Empty;
|
||||
|
||||
string code;
|
||||
if (isAttached)
|
||||
{
|
||||
string objType = namedArguments.TryGetValue("AttachedType", out TypedConstant attachedType)
|
||||
? attachedType.Value!.ToString()
|
||||
: "object";
|
||||
|
||||
code = $$"""
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
partial class {{owner}}
|
||||
{
|
||||
private static readonly DependencyProperty {{propertyName}}Property =
|
||||
DependencyProperty.RegisterAttached("{{propertyName}}", typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
|
||||
|
||||
public static {{propertyType}} Get{{propertyName}}({{objType}} obj)
|
||||
{
|
||||
return ({{propertyType}})obj?.GetValue({{propertyName}}Property);
|
||||
}
|
||||
|
||||
public static void Set{{propertyName}}({{objType}} obj, {{propertyType}} value)
|
||||
{
|
||||
obj.SetValue({{propertyName}}Property, value);
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
else
|
||||
{
|
||||
code = $$"""
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
partial class {{owner}}
|
||||
{
|
||||
private static readonly DependencyProperty {{propertyName}}Property =
|
||||
DependencyProperty.Register(nameof({{propertyName}}), typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
|
||||
|
||||
public {{propertyType}} {{propertyName}}
|
||||
{
|
||||
get => ({{propertyType}})GetValue({{propertyName}}Property);
|
||||
set => SetValue({{propertyName}}Property, value);
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.{propertyName}.g.cs", code);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetLiteralString(TypedConstant typedConstant)
|
||||
{
|
||||
if (typedConstant.IsNull)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (typedConstant.Value is bool boolValue)
|
||||
{
|
||||
return boolValue ? "true" : "false";
|
||||
}
|
||||
|
||||
string result = typedConstant.Value!.ToString();
|
||||
if (string.IsNullOrEmpty(result))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class SaltConstantGenerator : IIncrementalGenerator
|
||||
{
|
||||
private static readonly HttpClient httpClient = new();
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(GenerateSaltContstants);
|
||||
}
|
||||
|
||||
private static void GenerateSaltContstants(IncrementalGeneratorPostInitializationContext context)
|
||||
{
|
||||
string body = httpClient.GetStringAsync("https://internal.snapgenshin.cn/Archive/Salt/Latest").GetAwaiter().GetResult();
|
||||
Response<SaltLatest> saltInfo = JsonParser.FromJson<Response<SaltLatest>>(body)!;
|
||||
string code = $$"""
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
internal sealed class SaltConstants
|
||||
{
|
||||
public const string CNVersion = "{{saltInfo.Data.CNVersion}}";
|
||||
public const string CNK2 = "{{saltInfo.Data.CNK2}}";
|
||||
public const string CNLK2 = "{{saltInfo.Data.CNLK2}}";
|
||||
|
||||
public const string OSVersion = "{{saltInfo.Data.OSVersion}}";
|
||||
public const string OSK2 = "{{saltInfo.Data.OSK2}}";
|
||||
public const string OSLK2 = "{{saltInfo.Data.OSLK2}}";
|
||||
}
|
||||
""";
|
||||
context.AddSource("SaltConstants.g.cs", code);
|
||||
}
|
||||
|
||||
private sealed class Response<T>
|
||||
{
|
||||
[DataMember(Name = "data")]
|
||||
public T Data { get; set; } = default!;
|
||||
}
|
||||
|
||||
internal sealed class SaltLatest
|
||||
{
|
||||
public string CNVersion { get; set; } = default!;
|
||||
|
||||
public string CNK2 { get; set; } = default!;
|
||||
|
||||
public string CNLK2 { get; set; } = default!;
|
||||
|
||||
public string OSVersion { get; set; } = default!;
|
||||
|
||||
public string OSK2 { get; set; } = default!;
|
||||
|
||||
public string OSLK2 { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace System.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
internal sealed class NotNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
public NotNullWhenAttribute(bool returnValue)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
}
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class HttpClientGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
|
||||
|
||||
private const string HttpClientConfiguration = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.";
|
||||
private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute";
|
||||
private const string CRLF = "\r\n";
|
||||
|
||||
private static readonly DiagnosticDescriptor injectionShouldOmitDescriptor = new("SH201", "Injection 特性可以省略", "HttpClient 特性已将 {0} 注册为 Transient 服务", "Quality", DiagnosticSeverity.Warning, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddHttpClientsImplementation);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 HttpClientClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateAddHttpClientsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(HttpClientGenerator)}}", "1.0.0.0")]
|
||||
public static partial IServiceCollection AddHttpClients(this IServiceCollection services)
|
||||
{
|
||||
""");
|
||||
|
||||
FillUpWithAddHttpClient(sourceBuilder, context, context2s);
|
||||
|
||||
sourceBuilder.Append("""
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource("IocHttpClientConfiguration.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithAddHttpClient(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
|
||||
{
|
||||
List<string> lines = new();
|
||||
StringBuilder lineBuilder = new();
|
||||
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
if (context.SingleOrDefaultAttribute(InjectionGenerator.AttributeName) is AttributeData injectionData)
|
||||
{
|
||||
if (injectionData.ConstructorArguments[0].ToCSharpString() == InjectionGenerator.InjectAsTransientName)
|
||||
{
|
||||
if (injectionData.ConstructorArguments.Length < 2)
|
||||
{
|
||||
production.ReportDiagnostic(Diagnostic.Create(injectionShouldOmitDescriptor, context.Context.Node.GetLocation(), context.Context.Node));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lineBuilder.Clear().Append(CRLF);
|
||||
lineBuilder.Append(@" services.AddHttpClient<");
|
||||
|
||||
AttributeData httpClientData = context.SingleAttribute(AttributeName);
|
||||
ImmutableArray<TypedConstant> arguments = httpClientData.ConstructorArguments;
|
||||
|
||||
if (arguments.Length == 2)
|
||||
{
|
||||
lineBuilder.Append($"{arguments[1].Value}, ");
|
||||
}
|
||||
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>(");
|
||||
lineBuilder.Append(arguments[0].ToCSharpString().Substring(HttpClientConfiguration.Length)).Append("Configuration)");
|
||||
|
||||
if (context.SingleOrDefaultAttribute(PrimaryHttpMessageHandlerAttributeName) is AttributeData handlerData)
|
||||
{
|
||||
ImmutableArray<KeyValuePair<string, TypedConstant>> properties = handlerData.NamedArguments;
|
||||
lineBuilder.Append(@".ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() {");
|
||||
|
||||
foreach (KeyValuePair<string, TypedConstant> property in properties)
|
||||
{
|
||||
lineBuilder.Append(' ');
|
||||
lineBuilder.Append(property.Key);
|
||||
lineBuilder.Append(" = ");
|
||||
lineBuilder.Append(property.Value.ToCSharpString());
|
||||
lineBuilder.Append(',');
|
||||
}
|
||||
|
||||
lineBuilder.Append(" })");
|
||||
}
|
||||
|
||||
lineBuilder.Append(';');
|
||||
|
||||
lines.Add(lineBuilder.ToString());
|
||||
}
|
||||
|
||||
foreach (string line in lines.OrderBy(x => x))
|
||||
{
|
||||
sourceBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class InjectionGenerator : IIncrementalGenerator
|
||||
{
|
||||
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
|
||||
public const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
|
||||
public const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
|
||||
public const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
|
||||
|
||||
private static readonly DiagnosticDescriptor invalidInjectionDescriptor = new("SH101", "无效的 InjectAs 枚举值", "尚未支持生成 {0} 配置", "Quality", DiagnosticSeverity.Error, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddInjectionsImplementation);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 HttpClientClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateAddInjectionsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
internal static partial class ServiceCollectionExtension
|
||||
{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(InjectionGenerator)}}", "1.0.0.0")]
|
||||
public static partial IServiceCollection AddInjections(this IServiceCollection services)
|
||||
{
|
||||
""");
|
||||
|
||||
FillUpWithAddServices(sourceBuilder, context, context2s);
|
||||
sourceBuilder.Append("""
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource("ServiceCollectionExtension.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithAddServices(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
|
||||
{
|
||||
List<string> lines = new();
|
||||
StringBuilder lineBuilder = new();
|
||||
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
lineBuilder.Clear().AppendLine();
|
||||
|
||||
AttributeData injectionInfo = context.SingleAttribute(AttributeName);
|
||||
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
|
||||
|
||||
string injectAsName = arguments[0].ToCSharpString();
|
||||
switch (injectAsName)
|
||||
{
|
||||
case InjectAsSingletonName:
|
||||
lineBuilder.Append(" services.AddSingleton<");
|
||||
break;
|
||||
case InjectAsTransientName:
|
||||
lineBuilder.Append(" services.AddTransient<");
|
||||
break;
|
||||
case InjectAsScopedName:
|
||||
lineBuilder.Append(" services.AddScoped<");
|
||||
break;
|
||||
default:
|
||||
production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, context.Context.Node.GetLocation(), injectAsName));
|
||||
break;
|
||||
}
|
||||
|
||||
if (arguments.Length == 2)
|
||||
{
|
||||
lineBuilder.Append($"{arguments[1].Value}, ");
|
||||
}
|
||||
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>();");
|
||||
|
||||
lines.Add(lineBuilder.ToString());
|
||||
}
|
||||
|
||||
foreach (string line in lines.OrderBy(x => x))
|
||||
{
|
||||
sourceBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
internal class ServiceAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor NonSingletonUseServiceProviderDescriptor = new("SH301", "Non Singleton service should avoid direct use of IServiceProvider", "Non Singleton service should avoid direct use of IServiceProvider", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor SingletonServiceCaptureNonSingletonServiceDescriptor = new("SH302", "Singleton service should avoid keep reference of non singleton service", "Singleton service should avoid keep reference of non singleton service", "Quality", DiagnosticSeverity.Info, true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
|
||||
{
|
||||
get => new DiagnosticDescriptor[]
|
||||
{
|
||||
NonSingletonUseServiceProviderDescriptor,
|
||||
SingletonServiceCaptureNonSingletonServiceDescriptor,
|
||||
}.ToImmutableArray();
|
||||
}
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterCompilationStartAction(CompilationStart);
|
||||
}
|
||||
|
||||
private static void CompilationStart(CompilationStartAnalysisContext context)
|
||||
{
|
||||
context.RegisterSyntaxNodeAction(HandleNonSingletonUseServiceProvider, SyntaxKind.ClassDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleSingletonServiceCaptureNonSingletonService, SyntaxKind.ClassDeclaration);
|
||||
}
|
||||
|
||||
private static void HandleNonSingletonUseServiceProvider(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
ClassDeclarationSyntax classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
|
||||
if (classDeclarationSyntax.HasAttributeLists())
|
||||
{
|
||||
INamedTypeSymbol? classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
|
||||
if (classSymbol is not null)
|
||||
{
|
||||
foreach (AttributeData attributeData in classSymbol.GetAttributes())
|
||||
{
|
||||
if (attributeData.AttributeClass!.ToDisplayString() is InjectionGenerator.AttributeName)
|
||||
{
|
||||
string serviceType = attributeData.ConstructorArguments[0].ToCSharpString();
|
||||
if (serviceType is InjectionGenerator.InjectAsTransientName or InjectionGenerator.InjectAsScopedName)
|
||||
{
|
||||
HandleNonSingletonUseServiceProviderActual(context, classSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleNonSingletonUseServiceProviderActual(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
ISymbol? symbol = classSymbol.GetMembers().Where(m => m is IFieldSymbol f && f.Type.ToDisplayString() == "System.IServiceProvider").SingleOrDefault();
|
||||
|
||||
if (symbol is not null)
|
||||
{
|
||||
Diagnostic diagnostic = Diagnostic.Create(NonSingletonUseServiceProviderDescriptor, symbol.Locations.FirstOrDefault());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleSingletonServiceCaptureNonSingletonService(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
//classSymbol.GetMembers().Where(m => m is IFieldSymbol { IsReadOnly: true, DeclaredAccessibility: Accessibility.Private } f);
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Enum;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Resource.Localization.LocalizationAttribute";
|
||||
private const string LocalizationKeyName = "Snap.Hutao.Resource.Localization.LocalizationKeyAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValuesProvider<GeneratorSyntaxContext2> localizationEnums = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedEnums, LocalizationEnum)
|
||||
.Where(GeneratorSyntaxContext2.NotNull);
|
||||
|
||||
context.RegisterSourceOutput(localizationEnums, GenerateGetLocalizedDescriptionImplementation);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedEnums(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is EnumDeclarationSyntax enumDeclarationSyntax
|
||||
&& enumDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 LocalizationEnum(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(context.Node, token) is INamedTypeSymbol enumSymbol)
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = enumSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, enumSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateGetLocalizedDescriptionImplementation(SourceProductionContext context, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Resource.Localization;
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(LocalizedEnumGenerator)}}", "1.0.0.0")]
|
||||
internal static class {{context2.Symbol.Name}}Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <param name="value">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
public static string GetLocalizedDescription(this {{context2.Symbol}} value)
|
||||
{
|
||||
string key = value switch
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithSwitchBranches(sourceBuilder, context2);
|
||||
|
||||
sourceBuilder.Append($$"""
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return Enum.GetName(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return SH.ResourceManager.GetString(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <param name="value">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
[return:MaybeNull]
|
||||
public static string GetLocalizedDescriptionOrDefault(this {{context2.Symbol}} value)
|
||||
{
|
||||
string key = value switch
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithSwitchBranches(sourceBuilder, context2);
|
||||
|
||||
sourceBuilder.Append($$"""
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
return SH.ResourceManager.GetString(key);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource($"{context2.Symbol.Name}Extension.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithSwitchBranches(StringBuilder sourceBuilder, GeneratorSyntaxContext2 context)
|
||||
{
|
||||
IEnumerable<IFieldSymbol> fields = context.Symbol.GetMembers()
|
||||
.Where(m => m.Kind == SymbolKind.Field)
|
||||
.Cast<IFieldSymbol>();
|
||||
|
||||
foreach (IFieldSymbol fieldSymbol in fields)
|
||||
{
|
||||
AttributeData? localizationKeyInfo = fieldSymbol.GetAttributes()
|
||||
.SingleOrDefault(data => data.AttributeClass!.ToDisplayString() == LocalizationKeyName);
|
||||
if (localizationKeyInfo != null)
|
||||
{
|
||||
sourceBuilder
|
||||
.Append(" ")
|
||||
.Append(fieldSymbol)
|
||||
.Append(" => \"")
|
||||
.Append(localizationKeyInfo.ConstructorArguments[0].Value)
|
||||
.AppendLine("\",");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("", "RS2008")]
|
||||
@@ -1,202 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Identity;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class IdentityGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string FileName = "IdentityStructs.json";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<AdditionalText>> provider = context.AdditionalTextsProvider.Where(MatchFileName).Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(provider, GenerateIdentityStructs);
|
||||
}
|
||||
|
||||
private static bool MatchFileName(AdditionalText text)
|
||||
{
|
||||
return Path.GetFileName(text.Path) == FileName;
|
||||
}
|
||||
|
||||
private static void GenerateIdentityStructs(SourceProductionContext context, ImmutableArray<AdditionalText> texts)
|
||||
{
|
||||
AdditionalText jsonFile = texts.Single();
|
||||
|
||||
string identityJson = jsonFile.GetText(context.CancellationToken)!.ToString();
|
||||
List<IdentityStructMetadata> identities = identityJson.FromJson<List<IdentityStructMetadata>>()!;
|
||||
|
||||
if (identities.Any())
|
||||
{
|
||||
foreach (IdentityStructMetadata identityStruct in identities)
|
||||
{
|
||||
GenerateIdentityStruct(context, identityStruct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateIdentityStruct(SourceProductionContext context, IdentityStructMetadata metadata)
|
||||
{
|
||||
string name = metadata.Name;
|
||||
|
||||
StringBuilder sourceBuilder = new StringBuilder().AppendLine($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive.Converter;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Snap.Hutao.Model.Primitive;
|
||||
|
||||
/// <summary>
|
||||
/// {{metadata.Documentation}}
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(IdentityConverter<{{name}}>))]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(IdentityGenerator)}}","1.0.0.0")]
|
||||
internal readonly partial struct {{name}}
|
||||
{
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public readonly uint Value;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="{{name}}"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="value">value</param>
|
||||
public {{name}}(uint value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator uint({{name}} value)
|
||||
{
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
public static implicit operator {{name}}(uint value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return Value.ToString();
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
if (metadata.Equatable)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IEquatable<{{name}}>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is {{name}} other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals({{name}} other)
|
||||
{
|
||||
return Value == other.Value;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.EqualityOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IEqualityOperators<{{name}}, {{name}}, bool>, IEqualityOperators<{{name}}, uint, bool>
|
||||
{
|
||||
public static bool operator ==({{name}} left, {{name}} right)
|
||||
{
|
||||
return left.Value == right.Value;
|
||||
}
|
||||
|
||||
public static bool operator ==({{name}} left, uint right)
|
||||
{
|
||||
return left.Value == right;
|
||||
}
|
||||
|
||||
public static bool operator !=({{name}} left, {{name}} right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public static bool operator !=({{name}} left, uint right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.AdditionOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IAdditionOperators<{{name}}, {{name}}, {{name}}>, IAdditionOperators<{{name}}, uint, {{name}}>
|
||||
{
|
||||
public static {{name}} operator +({{name}} left, {{name}} right)
|
||||
{
|
||||
return left.Value + right.Value;
|
||||
}
|
||||
|
||||
public static {{name}} operator +({{name}} left, uint right)
|
||||
{
|
||||
return left.Value + right;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.IncrementOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IIncrementOperators<{{name}}>
|
||||
{
|
||||
public static unsafe {{name}} operator ++({{name}} value)
|
||||
{
|
||||
++*(uint*)&value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
context.AddSource($"{name}.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private sealed class IdentityStructMetadata
|
||||
{
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
public string? Documentation { get; set; }
|
||||
|
||||
public bool Equatable { get; set; }
|
||||
|
||||
public bool EqualityOperators { get; set; }
|
||||
|
||||
public bool AdditionOperators { get; set; }
|
||||
|
||||
public bool IncrementOperators { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration;
|
||||
|
||||
// Really simple JSON parser in ~300 lines
|
||||
// - Attempts to parse JSON files with minimal GC allocation
|
||||
// - Nice and simple "[1,2,3]".FromJson<List<int>>() API
|
||||
// - Classes and structs can be parsed too!
|
||||
// class Foo { public int Value; }
|
||||
// "{\"Value\":10}".FromJson<Foo>()
|
||||
// - Can parse JSON without type information into Dictionary<string,object> and List<object> e.g.
|
||||
// "[1,2,3]".FromJson<object>().GetType() == typeof(List<object>)
|
||||
// "{\"Value\":10}".FromJson<object>().GetType() == typeof(Dictionary<string,object>)
|
||||
// - No JIT Emit support to support AOT compilation on iOS
|
||||
// - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead.
|
||||
// - Only public fields and property setters on classes/structs will be written to
|
||||
//
|
||||
// Limitations:
|
||||
// - No JIT Emit support to parse structures quickly
|
||||
// - Limited to parsing <2GB JSON files (due to int.MaxValue)
|
||||
// - Parsing of abstract classes or interfaces is NOT supported and will throw an exception.
|
||||
public static class JsonParser
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static Stack<List<string>>? splitArrayPool;
|
||||
|
||||
[ThreadStatic]
|
||||
private static StringBuilder? stringBuilder;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, Dictionary<string, FieldInfo>>? fieldInfoCache;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, Dictionary<string, PropertyInfo>>? propertyInfoCache;
|
||||
|
||||
public static T? FromJson<T>(this string json)
|
||||
{
|
||||
// Initialize, if needed, the ThreadStatic variables
|
||||
propertyInfoCache ??= new Dictionary<Type, Dictionary<string, PropertyInfo>>();
|
||||
fieldInfoCache ??= new Dictionary<Type, Dictionary<string, FieldInfo>>();
|
||||
stringBuilder ??= new StringBuilder();
|
||||
splitArrayPool ??= new Stack<List<string>>();
|
||||
|
||||
// Remove all whitespace not within strings to make parsing simpler
|
||||
stringBuilder.Length = 0;
|
||||
for (int i = 0; i < json.Length; i++)
|
||||
{
|
||||
char c = json[i];
|
||||
if (c == '"')
|
||||
{
|
||||
i = AppendUntilStringEnd(true, i, json);
|
||||
continue;
|
||||
}
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
// Parse the thing!
|
||||
return (T?)ParseValue(typeof(T), stringBuilder.ToString());
|
||||
}
|
||||
|
||||
private static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json)
|
||||
{
|
||||
stringBuilder!.Append(json[startIdx]);
|
||||
for (int i = startIdx + 1; i < json.Length; i++)
|
||||
{
|
||||
if (json[i] == '\\')
|
||||
{
|
||||
if (appendEscapeCharacter)
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
|
||||
stringBuilder.Append(json[i + 1]);
|
||||
i++;//Skip next character as it is escaped
|
||||
}
|
||||
else if (json[i] == '"')
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
return i;
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
}
|
||||
return json.Length - 1;
|
||||
}
|
||||
|
||||
// Splits { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
|
||||
private static List<string> Split(string json)
|
||||
{
|
||||
List<string> splitArray = splitArrayPool!.Count > 0 ? splitArrayPool.Pop() : new List<string>();
|
||||
splitArray.Clear();
|
||||
if (json.Length == 2)
|
||||
{
|
||||
return splitArray;
|
||||
}
|
||||
|
||||
int parseDepth = 0;
|
||||
stringBuilder!.Length = 0;
|
||||
for (int i = 1; i < json.Length - 1; i++)
|
||||
{
|
||||
switch (json[i])
|
||||
{
|
||||
case '[':
|
||||
case '{':
|
||||
parseDepth++;
|
||||
break;
|
||||
case ']':
|
||||
case '}':
|
||||
parseDepth--;
|
||||
break;
|
||||
case '"':
|
||||
i = AppendUntilStringEnd(true, i, json);
|
||||
continue;
|
||||
case ',':
|
||||
case ':':
|
||||
if (parseDepth == 0)
|
||||
{
|
||||
splitArray.Add(stringBuilder.ToString());
|
||||
stringBuilder.Length = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
|
||||
splitArray.Add(stringBuilder.ToString());
|
||||
|
||||
return splitArray;
|
||||
}
|
||||
|
||||
internal static object? ParseValue(Type type, string json)
|
||||
{
|
||||
if (type == typeof(string))
|
||||
{
|
||||
if (json.Length <= 2)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
StringBuilder parseStringBuilder = new(json.Length);
|
||||
for (int i = 1; i < json.Length - 1; ++i)
|
||||
{
|
||||
if (json[i] == '\\' && i + 1 < json.Length - 1)
|
||||
{
|
||||
int j = "\"\\nrtbf/".IndexOf(json[i + 1]);
|
||||
if (j >= 0)
|
||||
{
|
||||
parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (json[i + 1] == 'u' && i + 5 < json.Length - 1)
|
||||
{
|
||||
if (uint.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out uint c))
|
||||
{
|
||||
parseStringBuilder.Append((char)c);
|
||||
i += 5;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
parseStringBuilder.Append(json[i]);
|
||||
}
|
||||
return parseStringBuilder.ToString();
|
||||
}
|
||||
if (type.IsPrimitive)
|
||||
{
|
||||
object result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture);
|
||||
return result;
|
||||
}
|
||||
if (type == typeof(decimal))
|
||||
{
|
||||
decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out decimal result);
|
||||
return result;
|
||||
}
|
||||
if (type == typeof(DateTime))
|
||||
{
|
||||
DateTime.TryParse(json.Replace("\"", ""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime result);
|
||||
return result;
|
||||
}
|
||||
if (json == "null")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (type.IsEnum)
|
||||
{
|
||||
if (json[0] == '"')
|
||||
{
|
||||
json = json.Substring(1, json.Length - 2);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return System.Enum.Parse(type, json, false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (type.IsArray)
|
||||
{
|
||||
Type arrayType = type.GetElementType();
|
||||
if (json[0] != '[' || json[json.Length - 1] != ']')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<string> elems = Split(json);
|
||||
Array newArray = Array.CreateInstance(arrayType, elems.Count);
|
||||
for (int i = 0; i < elems.Count; i++)
|
||||
{
|
||||
newArray.SetValue(ParseValue(arrayType, elems[i]), i);
|
||||
}
|
||||
|
||||
splitArrayPool!.Push(elems);
|
||||
return newArray;
|
||||
}
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
Type listType = type.GetGenericArguments()[0];
|
||||
if (json[0] != '[' || json[json.Length - 1] != ']')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<string> elems = Split(json);
|
||||
IList list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count });
|
||||
for (int i = 0; i < elems.Count; i++)
|
||||
{
|
||||
list.Add(ParseValue(listType, elems[i]));
|
||||
}
|
||||
|
||||
splitArrayPool!.Push(elems);
|
||||
return list;
|
||||
}
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
Type keyType, valueType;
|
||||
{
|
||||
Type[] args = type.GetGenericArguments();
|
||||
keyType = args[0];
|
||||
valueType = args[1];
|
||||
}
|
||||
|
||||
//Refuse to parse dictionary keys that aren't of type string
|
||||
if (keyType != typeof(string))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
//Must be a valid dictionary element
|
||||
if (json[0] != '{' || json[json.Length - 1] != '}')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
//The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IDictionary dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 });
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
if (elems[i].Length <= 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string keyValue = elems[i].Substring(1, elems[i].Length - 2);
|
||||
object? val = ParseValue(valueType, elems[i + 1]);
|
||||
dictionary[keyValue] = val;
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
if (type == typeof(object))
|
||||
{
|
||||
return ParseAnonymousValue(json);
|
||||
}
|
||||
if (json[0] == '{' && json[json.Length - 1] == '}')
|
||||
{
|
||||
return ParseObject(type, json);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object? ParseAnonymousValue(string json)
|
||||
{
|
||||
if (json.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (json[0] == '{' && json[json.Length - 1] == '}')
|
||||
{
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Dictionary<string, object?> dict = new(elems.Count / 2);
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
if (json[0] == '[' && json[json.Length - 1] == ']')
|
||||
{
|
||||
List<string> items = Split(json);
|
||||
List<object?> finalList = new(items.Count);
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
finalList.Add(ParseAnonymousValue(items[i]));
|
||||
}
|
||||
|
||||
return finalList;
|
||||
}
|
||||
if (json[0] == '"' && json[json.Length - 1] == '"')
|
||||
{
|
||||
string str = json.Substring(1, json.Length - 2);
|
||||
return str.Replace("\\", string.Empty);
|
||||
}
|
||||
if (char.IsDigit(json[0]) || json[0] == '-')
|
||||
{
|
||||
if (json.Contains("."))
|
||||
{
|
||||
double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double result);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
int.TryParse(json, out int result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (json == "true")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (json == "false")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// handles json == "null" as well as invalid JSON
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, T> CreateMemberNameDictionary<T>(T[] members) where T : MemberInfo
|
||||
{
|
||||
Dictionary<string, T> nameToMember = new(StringComparer.OrdinalIgnoreCase);
|
||||
for (int i = 0; i < members.Length; i++)
|
||||
{
|
||||
T member = members[i];
|
||||
if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = member.Name;
|
||||
if (member.IsDefined(typeof(DataMemberAttribute), true))
|
||||
{
|
||||
DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true);
|
||||
if (!string.IsNullOrEmpty(dataMemberAttribute.Name))
|
||||
{
|
||||
name = dataMemberAttribute.Name;
|
||||
}
|
||||
}
|
||||
|
||||
nameToMember.Add(name, member);
|
||||
}
|
||||
|
||||
return nameToMember;
|
||||
}
|
||||
|
||||
private static object ParseObject(Type type, string json)
|
||||
{
|
||||
object instance = FormatterServices.GetUninitializedObject(type);
|
||||
|
||||
// The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
if (!fieldInfoCache!.TryGetValue(type, out Dictionary<string, FieldInfo> nameToField))
|
||||
{
|
||||
nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
|
||||
fieldInfoCache.Add(type, nameToField);
|
||||
}
|
||||
if (!propertyInfoCache!.TryGetValue(type, out Dictionary<string, PropertyInfo> nameToProperty))
|
||||
{
|
||||
nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
|
||||
propertyInfoCache.Add(type, nameToProperty);
|
||||
}
|
||||
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
if (elems[i].Length <= 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = elems[i].Substring(1, elems[i].Length - 2);
|
||||
string value = elems[i + 1];
|
||||
|
||||
if (nameToField.TryGetValue(key, out FieldInfo fieldInfo))
|
||||
{
|
||||
fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value));
|
||||
}
|
||||
else if (nameToProperty.TryGetValue(key, out PropertyInfo propertyInfo))
|
||||
{
|
||||
propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null);
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class AttributeDataExtension
|
||||
{
|
||||
public static bool HasNamedArgumentWith<TValue>(this AttributeData data, string key, Func<TValue, bool> predicate)
|
||||
{
|
||||
return data.NamedArguments.Any(a => a.Key == key && predicate((TValue)a.Value.Value!));
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class EnumerableExtension
|
||||
{
|
||||
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
{
|
||||
return DistinctByIterator(source, keySelector);
|
||||
}
|
||||
|
||||
private static IEnumerable<TSource> DistinctByIterator<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
{
|
||||
using IEnumerator<TSource> enumerator = source.GetEnumerator();
|
||||
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
HashSet<TKey> set = new();
|
||||
|
||||
do
|
||||
{
|
||||
TSource element = enumerator.Current;
|
||||
if (set.Add(keySelector(element)))
|
||||
{
|
||||
yield return element;
|
||||
}
|
||||
}
|
||||
while (enumerator.MoveNext());
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> source)
|
||||
{
|
||||
return source.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal readonly struct GeneratorSyntaxContext2
|
||||
{
|
||||
public readonly GeneratorSyntaxContext Context;
|
||||
public readonly INamedTypeSymbol Symbol;
|
||||
public readonly ImmutableArray<AttributeData> Attributes;
|
||||
public readonly bool HasValue = false;
|
||||
|
||||
public GeneratorSyntaxContext2(GeneratorSyntaxContext context, INamedTypeSymbol symbol, ImmutableArray<AttributeData> attributes)
|
||||
{
|
||||
Context = context;
|
||||
Symbol = symbol;
|
||||
Attributes = attributes;
|
||||
HasValue = true;
|
||||
}
|
||||
|
||||
public static bool NotNull(GeneratorSyntaxContext2 context)
|
||||
{
|
||||
return context.HasValue;
|
||||
}
|
||||
|
||||
public bool HasAttributeWithName(string name)
|
||||
{
|
||||
return Attributes.Any(attr => attr.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public AttributeData SingleAttribute(string name)
|
||||
{
|
||||
return Attributes.Single(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public AttributeData? SingleOrDefaultAttribute(string name)
|
||||
{
|
||||
return Attributes.SingleOrDefault(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public TSyntaxNode Node<TSyntaxNode>()
|
||||
where TSyntaxNode : SyntaxNode
|
||||
{
|
||||
return (TSyntaxNode)Context.Node;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct GeneratorSyntaxContext2<TSymbol>
|
||||
where TSymbol : ISymbol
|
||||
{
|
||||
public readonly GeneratorSyntaxContext Context;
|
||||
public readonly TSymbol Symbol;
|
||||
public readonly ImmutableArray<AttributeData> Attributes;
|
||||
public readonly bool HasValue = false;
|
||||
|
||||
public GeneratorSyntaxContext2(GeneratorSyntaxContext context, TSymbol symbol, ImmutableArray<AttributeData> attributes)
|
||||
{
|
||||
Context = context;
|
||||
Symbol = symbol;
|
||||
Attributes = attributes;
|
||||
HasValue = true;
|
||||
}
|
||||
|
||||
public static bool NotNull(GeneratorSyntaxContext2<TSymbol> context)
|
||||
{
|
||||
return context.HasValue;
|
||||
}
|
||||
|
||||
public AttributeData SingleAttribute(string name)
|
||||
{
|
||||
return Attributes.Single(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class GeneratorSyntaxContextExtension
|
||||
{
|
||||
public static bool TryGetDeclaredSymbol<TSymbol>(this GeneratorSyntaxContext context, System.Threading.CancellationToken token, [NotNullWhen(true)] out TSymbol? symbol)
|
||||
where TSymbol : class, ISymbol
|
||||
{
|
||||
symbol = context.SemanticModel.GetDeclaredSymbol(context.Node, token) as TSymbol;
|
||||
return symbol != null;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class SymbolDisplayFormats
|
||||
{
|
||||
public static SymbolDisplayFormat FullyQualifiedNonNullableFormat { get; } = new(
|
||||
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
||||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
|
||||
|
||||
public static SymbolDisplayFormat QualifiedNonNullableFormat { get; } = new(
|
||||
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
||||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class SyntaxExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether a given <see cref="MemberDeclarationSyntax"/> has or could potentially have any attribute lists.
|
||||
/// </summary>
|
||||
/// <param name="declaration">The input <see cref="MemberDeclarationSyntax"/> to check.</param>
|
||||
/// <returns>Whether <paramref name="declaration"/> has or potentially has any attribute lists.</returns>
|
||||
public static bool HasAttributeLists<TSyntax>(this TSyntax declaration)
|
||||
where TSyntax : MemberDeclarationSyntax
|
||||
{
|
||||
return declaration.AttributeLists.Count > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class TypeSymbolExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether or not a given <see cref="ITypeSymbol"/> has or inherits from a specified type.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
|
||||
/// <param name="name">The full name of the type to check for inheritance.</param>
|
||||
/// <returns>Whether or not <paramref name="typeSymbol"/> is or inherits from <paramref name="name"/>.</returns>
|
||||
public static bool IsOrInheritsFrom(this ITypeSymbol typeSymbol, string name)
|
||||
{
|
||||
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
|
||||
{
|
||||
if (currentType.ToDisplayString() == name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" Version="3.3.4" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,312 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// 通用分析器
|
||||
/// </summary>
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor typeInternalDescriptor = new("SH001", "Type should be internal", "Type [{0}] should be internal", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor readOnlyStructRefDescriptor = new("SH002", "ReadOnly struct should be passed with ref-like key word", "ReadOnly Struct [{0}] should be passed with ref-like key word", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useValueTaskIfPossibleDescriptor = new("SH003", "Use ValueTask instead of Task whenever possible", "Use ValueTask instead of Task", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsNotNullPatternMatchingDescriptor = new("SH004", "Use \"is not null\" instead of \"!= null\" whenever possible", "Use \"is not null\" instead of \"!= null\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsNullPatternMatchingDescriptor = new("SH005", "Use \"is null\" instead of \"== null\" whenever possible", "Use \"is null\" instead of \"== null\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsPatternRecursiveMatchingDescriptor = new("SH006", "Use \"is { } obj\" whenever possible", "Use \"is {{ }} {0}\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useArgumentNullExceptionThrowIfNullDescriptor = new("SH007", "Use \"ArgumentNullException.ThrowIfNull()\" instead of \"!\"", "Use \"ArgumentNullException.ThrowIfNull()\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
|
||||
|
||||
private static readonly ImmutableHashSet<string> RefLikeKeySkipTypes = new HashSet<string>()
|
||||
{
|
||||
"System.Threading.CancellationToken",
|
||||
"System.Guid"
|
||||
}.ToImmutableHashSet();
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
|
||||
{
|
||||
get
|
||||
{
|
||||
return new DiagnosticDescriptor[]
|
||||
{
|
||||
typeInternalDescriptor,
|
||||
readOnlyStructRefDescriptor,
|
||||
useValueTaskIfPossibleDescriptor,
|
||||
useIsNotNullPatternMatchingDescriptor,
|
||||
useIsNullPatternMatchingDescriptor,
|
||||
useIsPatternRecursiveMatchingDescriptor,
|
||||
useArgumentNullExceptionThrowIfNullDescriptor
|
||||
}.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterCompilationStartAction(CompilationStart);
|
||||
}
|
||||
|
||||
private static void CompilationStart(CompilationStartAnalysisContext context)
|
||||
{
|
||||
SyntaxKind[] types =
|
||||
{
|
||||
SyntaxKind.ClassDeclaration,
|
||||
SyntaxKind.InterfaceDeclaration,
|
||||
SyntaxKind.StructDeclaration,
|
||||
SyntaxKind.EnumDeclaration,
|
||||
};
|
||||
context.RegisterSyntaxNodeAction(HandleTypeShouldBeInternal, types);
|
||||
context.RegisterSyntaxNodeAction(HandleMethodParameterShouldUseRefLikeKeyword, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleConstructorParameterShouldUseRefLikeKeyword, SyntaxKind.ConstructorDeclaration);
|
||||
|
||||
SyntaxKind[] expressions =
|
||||
{
|
||||
SyntaxKind.EqualsExpression,
|
||||
SyntaxKind.NotEqualsExpression,
|
||||
};
|
||||
context.RegisterSyntaxNodeAction(HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching, expressions);
|
||||
context.RegisterSyntaxNodeAction(HandleIsPatternShouldUseRecursivePattern, SyntaxKind.IsPatternExpression);
|
||||
context.RegisterSyntaxNodeAction(HandleArgumentNullExceptionThrowIfNull, SyntaxKind.SuppressNullableWarningExpression);
|
||||
|
||||
// TODO add analyzer for unnecessary IServiceProvider registration
|
||||
// TODO add analyzer for Singlton service use Scoped or Transient services
|
||||
}
|
||||
|
||||
private static void HandleTypeShouldBeInternal(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
BaseTypeDeclarationSyntax syntax = (BaseTypeDeclarationSyntax)context.Node;
|
||||
|
||||
bool privateExists = false;
|
||||
bool internalExists = false;
|
||||
bool fileExists = false;
|
||||
|
||||
foreach (SyntaxToken token in syntax.Modifiers)
|
||||
{
|
||||
if (token.IsKind(SyntaxKind.PrivateKeyword))
|
||||
{
|
||||
privateExists = true;
|
||||
}
|
||||
|
||||
if (token.IsKind(SyntaxKind.InternalKeyword))
|
||||
{
|
||||
internalExists = true;
|
||||
}
|
||||
|
||||
if (token.IsKind(SyntaxKind.FileKeyword))
|
||||
{
|
||||
fileExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!privateExists && !internalExists && !fileExists)
|
||||
{
|
||||
Location location = syntax.Identifier.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(typeInternalDescriptor, location, syntax.Identifier);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
|
||||
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
|
||||
|
||||
// 跳过重载方法
|
||||
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.OverrideKeyword)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ICommand can only use Task or Task<T>
|
||||
if (methodSymbol.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == Automation.CommandGenerator.AttributeName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
|
||||
{
|
||||
Location location = methodSyntax.ReturnType.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useValueTaskIfPossibleDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleMethodParameterShouldUseRefLikeKeyword(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
|
||||
|
||||
// 跳过方法定义 如 接口
|
||||
if (methodSyntax.Body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.ValueTask"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (SyntaxToken token in methodSyntax.Modifiers)
|
||||
{
|
||||
// 跳过异步方法,因为异步方法无法使用 ref/in/out
|
||||
if (token.IsKind(SyntaxKind.AsyncKeyword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳过重载方法
|
||||
if (token.IsKind(SyntaxKind.OverrideKeyword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ParameterSyntax parameter in methodSyntax.ParameterList.Parameters)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
|
||||
{
|
||||
if (IsBuiltInType(symbol.Type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RefLikeKeySkipTypes.Contains(symbol.Type.ToDisplayString()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (symbol.Type.IsReadOnly && symbol.RefKind == RefKind.None)
|
||||
{
|
||||
Location location = parameter.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(readOnlyStructRefDescriptor, location, symbol.Type);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleConstructorParameterShouldUseRefLikeKeyword(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
ConstructorDeclarationSyntax constructorSyntax = (ConstructorDeclarationSyntax)context.Node;
|
||||
|
||||
foreach (ParameterSyntax parameter in constructorSyntax.ParameterList.Parameters)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
|
||||
{
|
||||
if (IsBuiltInType(symbol.Type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过 CancellationToken
|
||||
if (symbol.Type.ToDisplayString() == "System.Threading.CancellationToken")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (symbol.Type.IsReadOnly && symbol.RefKind == RefKind.None)
|
||||
{
|
||||
Location location = parameter.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(readOnlyStructRefDescriptor, location, symbol.Type);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
BinaryExpressionSyntax syntax = (BinaryExpressionSyntax)context.Node;
|
||||
if (syntax.IsKind(SyntaxKind.NotEqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
|
||||
{
|
||||
Location location = syntax.OperatorToken.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsNotNullPatternMatchingDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
else if (syntax.IsKind(SyntaxKind.EqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
|
||||
{
|
||||
Location location = syntax.OperatorToken.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsNullPatternMatchingDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleIsPatternShouldUseRecursivePattern(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
IsPatternExpressionSyntax syntax = (IsPatternExpressionSyntax)context.Node;
|
||||
if (syntax.Pattern is DeclarationPatternSyntax declaration)
|
||||
{
|
||||
ITypeSymbol? leftType = context.SemanticModel.GetTypeInfo(syntax.Expression).ConvertedType;
|
||||
ITypeSymbol? rightType = context.SemanticModel.GetTypeInfo(declaration).ConvertedType;
|
||||
if (SymbolEqualityComparer.Default.Equals(leftType, rightType))
|
||||
{
|
||||
Location location = declaration.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsPatternRecursiveMatchingDescriptor, location, declaration.Designation);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleArgumentNullExceptionThrowIfNull(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
PostfixUnaryExpressionSyntax syntax = (PostfixUnaryExpressionSyntax)context.Node;
|
||||
|
||||
if (syntax.Operand is LiteralExpressionSyntax literal)
|
||||
{
|
||||
if (literal.IsKind(SyntaxKind.DefaultLiteralExpression))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (syntax.Operand is DefaultExpressionSyntax expression)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Location location = syntax.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useArgumentNullExceptionThrowIfNullDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
|
||||
private static bool IsBuiltInType(ITypeSymbol symbol)
|
||||
{
|
||||
return symbol.SpecialType switch
|
||||
{
|
||||
SpecialType.System_Boolean => true,
|
||||
SpecialType.System_Char => true,
|
||||
SpecialType.System_SByte => true,
|
||||
SpecialType.System_Byte => true,
|
||||
SpecialType.System_Int16 => true,
|
||||
SpecialType.System_UInt16 => true,
|
||||
SpecialType.System_Int32 => true,
|
||||
SpecialType.System_UInt32 => true,
|
||||
SpecialType.System_Int64 => true,
|
||||
SpecialType.System_UInt64 => true,
|
||||
SpecialType.System_Decimal => true,
|
||||
SpecialType.System_Single => true,
|
||||
SpecialType.System_Double => true,
|
||||
SpecialType.System_IntPtr => true,
|
||||
SpecialType.System_UIntPtr => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
|
||||
"settings": {
|
||||
"documentationRules": {
|
||||
"companyName": "DGP Studio",
|
||||
"copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license.",
|
||||
"xmlHeader": false,
|
||||
"variables": {
|
||||
"licenseName": "MIT"
|
||||
}
|
||||
},
|
||||
"orderingRules": {
|
||||
"elementOrder": [
|
||||
"kind",
|
||||
"accessibility",
|
||||
"constant",
|
||||
"static",
|
||||
"readonly"
|
||||
],
|
||||
"usingDirectivesPlacement": "outsideNamespace"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public class CollectionsMarshalTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void DictionaryMarshalGetValueRefOrNullRefIsNullRef()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
|
||||
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
|
||||
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
|
||||
Dictionary<string, string> dictionaryRefKeyRefValue = [];
|
||||
#else
|
||||
Dictionary<uint, string> dictionaryValueKeyRefValue = new();
|
||||
Dictionary<uint, uint> dictionaryValueKeyValueValue = new();
|
||||
Dictionary<string, uint> dictionaryRefKeyValueValue = new();
|
||||
Dictionary<string, string> dictionaryRefKeyRefValue = new();
|
||||
#endif
|
||||
|
||||
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyRefValue, 1U)));
|
||||
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyValueValue, 1U)));
|
||||
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyValueValue, "no such key")));
|
||||
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyRefValue, "no such key")));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DictionaryMarshalGetValueRefOrAddDefaultIsDefault()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
|
||||
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
|
||||
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
|
||||
Dictionary<string, string> dictionaryRefKeyRefValue = [];
|
||||
#else
|
||||
Dictionary<uint, string> dictionaryValueKeyRefValue = new();
|
||||
Dictionary<uint, uint> dictionaryValueKeyValueValue = new();
|
||||
Dictionary<string, uint> dictionaryRefKeyValueValue = new();
|
||||
Dictionary<string, string> dictionaryRefKeyRefValue = new();
|
||||
#endif
|
||||
|
||||
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyRefValue, 1U, out _) == default);
|
||||
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyValueValue, 1U, out _) == default);
|
||||
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyValueValue, "no such key", out _) == default);
|
||||
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyRefValue, "no such key", out _) == default);
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,18 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Snap.Hutao.Test;
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public class JsonSerializeTest
|
||||
public sealed class JsonSerializeTest
|
||||
{
|
||||
public TestContext? TestContext { get; set; }
|
||||
|
||||
private readonly JsonSerializerOptions AlowStringNumberOptions = new()
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
private const string SmapleObjectJson = """
|
||||
{
|
||||
"A" :1
|
||||
@@ -44,13 +51,29 @@ public class JsonSerializeTest
|
||||
[TestMethod]
|
||||
public void NumberStringKeyCanSerializeAsKey()
|
||||
{
|
||||
JsonSerializerOptions options = new()
|
||||
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, AlowStringNumberOptions)!;
|
||||
Assert.AreEqual(sample[111], "12");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ByteArraySerializeAsBase64()
|
||||
{
|
||||
byte[] array =
|
||||
#if NET8_0_OR_GREATER
|
||||
[1, 2, 3, 4, 5];
|
||||
#else
|
||||
{ 1, 2, 3, 4, 5 };
|
||||
#endif
|
||||
ByteArraySample sample = new()
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
Array = array,
|
||||
};
|
||||
|
||||
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, options)!;
|
||||
Assert.AreEqual(sample[111], "12");
|
||||
string result = JsonSerializer.Serialize(sample);
|
||||
TestContext!.WriteLine($"ByteArray Serialize Result: {result}");
|
||||
Assert.AreEqual(result, """
|
||||
{"Array":"AQIDBAU="}
|
||||
""");
|
||||
}
|
||||
|
||||
private sealed class Sample
|
||||
@@ -64,4 +87,9 @@ public class JsonSerializeTest
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||
public int A { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ByteArraySample
|
||||
{
|
||||
public byte[]? Array { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public sealed class TypeReflectionTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void TypeCodeOfEnumIsUserlyingTypeTypeCode()
|
||||
{
|
||||
Assert.AreEqual(Type.GetTypeCode(typeof(TestEnum)), TypeCode.Int32);
|
||||
}
|
||||
|
||||
private enum TestEnum
|
||||
{
|
||||
A,
|
||||
B,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Test.IncomingFeature;
|
||||
|
||||
[TestClass]
|
||||
public sealed class GeniusInvokationDecoding
|
||||
{
|
||||
public TestContext? TestContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// https://www.bilibili.com/video/av278125720
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public unsafe void GeniusInvokationShareCodeDecoding()
|
||||
{
|
||||
// 51 bytes obfuscated data
|
||||
byte[] bytes = Convert.FromBase64String("BCHBwxQNAYERyVANCJGBynkOCZER2pgOCrFx8poQChGR9bYQDEGB9rkQDFKRD7oRDeEB");
|
||||
|
||||
// ---------------------------------------------
|
||||
// | Data | Caesar Cipher Key |
|
||||
// |----------|-------------------|
|
||||
// | 50 Bytes | 1 Byte |
|
||||
// ---------------------------------------------
|
||||
// Data:
|
||||
// 00000100 00100001 11000001 11000011 00010100
|
||||
// 00001101 00000001 10000001 00010001 11001001
|
||||
// 01010000 00001101 00001000 10010001 10000001
|
||||
// 11001010 01111001 00001110 00001001 10010001
|
||||
// 00010001 11011010 10011000 00001110 00001010
|
||||
// 10110001 01110001 11110010 10011010 00010000
|
||||
// 00001010 00010001 10010001 11110101 10110110
|
||||
// 00010000 00001100 01000001 10000001 11110110
|
||||
// 10111001 00010000 00001100 01010010 10010001
|
||||
// 00001111 10111010 00010001 00001101 11100001
|
||||
// ---------------------------------------------
|
||||
// Caesar Cipher Key:
|
||||
// 00000001
|
||||
// ---------------------------------------------
|
||||
fixed (byte* ptr = bytes)
|
||||
{
|
||||
// Reinterpret as 50 byte actual data and 1 deobfuscate key byte
|
||||
EncryptedDataAndKey* data = (EncryptedDataAndKey*)ptr;
|
||||
byte* dataPtr = data->Data;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// | First | Second | Padding |
|
||||
// |-----------|----------|---------|
|
||||
// | 25 Bytes | 25 Bytes | 1 Byte |
|
||||
// ----------------------------------------------------------
|
||||
// We are doing two things here:
|
||||
// 1. Retrieve actual data by subtracting key
|
||||
// 2. Store data into two halves by alternating between them
|
||||
// ----------------------------------------------------------
|
||||
// What we will get after this step:
|
||||
// ----------------------------------------------------------
|
||||
// First:
|
||||
// 00000011 11000000 00010011 00000000 00010000
|
||||
// 01001111 00000111 10000000 01111000 00001000
|
||||
// 00010000 10010111 00001001 01110000 10011001
|
||||
// 00001001 10010000 10110101 00001011 10000000
|
||||
// 10111000 00001011 10010000 10111001 00001100
|
||||
// ----------------------------------------------------------
|
||||
// Second:
|
||||
// 00100000 11000010 00001100 10000000 11001000
|
||||
// 00001100 10010000 11001001 00001101 10010000
|
||||
// 11011001 00001101 10110000 11110001 00001111
|
||||
// 00010000 11110100 00001111 01000000 11110101
|
||||
// 00001111 01010001 00001110 00010000 11100000
|
||||
// ----------------------------------------------------------
|
||||
RearrangeBuffer rearranged = default;
|
||||
byte* pFirst = rearranged.First;
|
||||
byte* pSecond = rearranged.Second;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
// Determine which half are we going to insert
|
||||
byte** ppTarget = i % 2 == 0 ? &pFirst : &pSecond;
|
||||
|
||||
// (actual data = data - key) and store it directly to the target half
|
||||
**ppTarget = unchecked((byte)(dataPtr[i] - data->Key));
|
||||
|
||||
(*ppTarget)++;
|
||||
}
|
||||
|
||||
// Prepare decoded data result storage
|
||||
DecryptedData decoded = default;
|
||||
ushort* pDecoded = decoded.Data;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// | Data |
|
||||
// |----------| x 17 = 51 Bytes
|
||||
// | 3 Bytes |
|
||||
// ----------------------------------------------------------
|
||||
// Grouping each 3 bytes and read out as 2 ushort with
|
||||
// 12 bits each (Big Endian)
|
||||
// ----------------------------------------------------------
|
||||
// 00000011 1100·0000 00010011|
|
||||
// 00000000 0001·0000 01001111|
|
||||
// 00000111 1000·0000 01111000|
|
||||
// 00001000 0001·0000 10010111|
|
||||
// 00001001 0111·0000 10011001|
|
||||
// 00001001 1001·0000 10110101|
|
||||
// 00001011 1000·0000 10111000|
|
||||
// 00001011 1001·0000 10111001|
|
||||
// 00001100 0010·0000 11000010|
|
||||
// 00001100 1000·0000 11001000|
|
||||
// 00001100 1001·0000 11001001|
|
||||
// 00001101 1001·0000 11011001|
|
||||
// 00001101 1011·0000 11110001|
|
||||
// 00001111 0001·0000 11110100|
|
||||
// 00001111 0100·0000 11110101|
|
||||
// 00001111 0101·0001 00001110|
|
||||
// 00010000 1110·0000 -padding|[padding32]
|
||||
// ----------------------------------------------------------
|
||||
// reinterpret as DecodeGroupingHelper for each 3 bytes
|
||||
DecodeGroupingHelper* pGroup = (DecodeGroupingHelper*)&rearranged;
|
||||
for (int i = 0; i < 17; i++)
|
||||
{
|
||||
(ushort first, ushort second) = pGroup->GetData();
|
||||
|
||||
*pDecoded = first;
|
||||
*(pDecoded + 1) = second;
|
||||
|
||||
pDecoded += 2;
|
||||
pGroup++;
|
||||
}
|
||||
|
||||
// Now we get
|
||||
// 60, 19, 1,
|
||||
// 79,120,120,
|
||||
// 129,151,151,
|
||||
// 153,153,181,
|
||||
// 184,184,185,
|
||||
// 185,194,194,
|
||||
// 200,200,201,
|
||||
// 201,217,217,
|
||||
// 219,241,241,
|
||||
// 244,244,245,
|
||||
// 245,270,270,
|
||||
StringBuilder stringBuilder = new();
|
||||
for (int i = 0; i < 33; i++)
|
||||
{
|
||||
stringBuilder
|
||||
.AppendFormat(CultureInfo.InvariantCulture, "{0,3}", decoded.Data[i])
|
||||
.Append(',');
|
||||
|
||||
if (i % 11 == 10)
|
||||
{
|
||||
stringBuilder.Append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
TestContext?.WriteLine(stringBuilder.ToString(0, stringBuilder.Length - 1));
|
||||
|
||||
ushort[] resultArray = new ushort[33];
|
||||
Span<ushort> result = new((ushort*)&decoded, 33);
|
||||
result.CopyTo(resultArray);
|
||||
|
||||
ushort[] testKnownResult =
|
||||
#if NET8_0_OR_GREATER
|
||||
[
|
||||
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
|
||||
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
|
||||
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
|
||||
];
|
||||
#else
|
||||
{
|
||||
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
|
||||
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
|
||||
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
|
||||
};
|
||||
#endif
|
||||
|
||||
CollectionAssert.AreEqual(resultArray, testKnownResult);
|
||||
}
|
||||
}
|
||||
|
||||
private struct EncryptedDataAndKey
|
||||
{
|
||||
public unsafe fixed byte Data[50];
|
||||
public byte Key;
|
||||
}
|
||||
|
||||
private struct RearrangeBuffer
|
||||
{
|
||||
public unsafe fixed byte First[25];
|
||||
public unsafe fixed byte Second[25];
|
||||
|
||||
// Make it 51 bytes
|
||||
// allow to be group as 17 DecodeGroupingHelper later
|
||||
public byte padding;
|
||||
|
||||
// prevent accidently int32 cast access violation
|
||||
public byte paddingTo32;
|
||||
}
|
||||
|
||||
private struct DecodeGroupingHelper
|
||||
{
|
||||
public unsafe fixed byte Data[3];
|
||||
|
||||
public unsafe (ushort First, ushort Second) GetData()
|
||||
{
|
||||
fixed (byte* ptr = Data)
|
||||
{
|
||||
uint value = BinaryPrimitives.ReverseEndianness((*(uint*)ptr) & 0x00FFFFFF) >> 8; // keep low 24 bits only
|
||||
return ((ushort)((value >> 12) & 0x0FFF), (ushort)(value & 0x0FFF));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DecryptedData
|
||||
{
|
||||
public unsafe fixed ushort Data[33];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test.IncomingFeature;
|
||||
|
||||
[TestClass]
|
||||
public class SpiralAbyssScheduleIdTest
|
||||
{
|
||||
private static readonly TimeSpan Utc8 = new(8, 0, 0);
|
||||
|
||||
[TestMethod]
|
||||
public void Test()
|
||||
{
|
||||
Console.WriteLine($"当前第 {GetForDateTimeOffset(DateTimeOffset.Now)} 期");
|
||||
|
||||
DateTimeOffset dateTimeOffset = new(2020, 7, 1, 4, 0, 0, Utc8);
|
||||
Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(dateTimeOffset)} 期");
|
||||
}
|
||||
public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
// Force time in UTC+08
|
||||
dateTimeOffset = dateTimeOffset.ToOffset(Utc8);
|
||||
|
||||
((int year, int mouth, int day), (int hour, _), _) = dateTimeOffset;
|
||||
|
||||
// 2020-07-01 04:00:00 为第 1 期
|
||||
int periodNum = (((year - 2020) * 12) + (mouth - 6)) * 2;
|
||||
|
||||
// 上半月:1-15 日, 以及 16 日 00:00-04:00
|
||||
if (day < 16 || (day == 16 && hour < 4))
|
||||
{
|
||||
periodNum--;
|
||||
}
|
||||
|
||||
// 上个月:1 日 00:00-04:00
|
||||
if (day is 1 && hour < 4)
|
||||
{
|
||||
periodNum--;
|
||||
}
|
||||
|
||||
return periodNum;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test;
|
||||
namespace Snap.Hutao.Test.PlatformExtensions;
|
||||
|
||||
[TestClass]
|
||||
public sealed class DependencyInjectionTest
|
||||
@@ -9,11 +9,12 @@ public sealed class ForEachRuntimeBehaviorTest
|
||||
[TestMethod]
|
||||
public void ListOfStringCanEnumerateAsReadOnlySpanOfChar()
|
||||
{
|
||||
List<string> strings = new()
|
||||
{
|
||||
"a", "b", "c"
|
||||
};
|
||||
|
||||
List<string> strings =
|
||||
#if NET8_0_OR_GREATER
|
||||
["a", "b", "c"];
|
||||
#else
|
||||
new() { "a", "b", "c" };
|
||||
#endif
|
||||
int count = 0;
|
||||
foreach (ReadOnlySpan<char> chars in strings)
|
||||
{
|
||||
|
||||
@@ -8,8 +8,13 @@ public sealed class RangeRuntimeBehaviorTest
|
||||
[TestMethod]
|
||||
public void RangeTrimLastOne()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
int[] array = [1, 2, 3, 4];
|
||||
int[] test = [1, 2, 3];
|
||||
#else
|
||||
int[] array = { 1, 2, 3, 4 };
|
||||
int[] test = { 1, 2, 3 };
|
||||
#endif
|
||||
int[] result = array[..^1];
|
||||
Assert.AreEqual(3, result.Length);
|
||||
Assert.IsTrue(MemoryExtensions.SequenceEqual<int>(test, result));
|
||||
|
||||
@@ -1,16 +1,49 @@
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
public sealed class UnsafeRuntimeBehaviorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public unsafe void UInt32AllSetIs()
|
||||
public unsafe void UInt32AllSetIsUInt32MaxValue()
|
||||
{
|
||||
byte[] bytes = { 0xFF, 0xFF, 0xFF, 0xFF, };
|
||||
byte[] bytes =
|
||||
#if NET8_0_OR_GREATER
|
||||
[0xFF, 0xFF, 0xFF, 0xFF];
|
||||
#else
|
||||
{ 0xFF, 0xFF, 0xFF, 0xFF, };
|
||||
#endif
|
||||
|
||||
fixed (byte* pBytes = bytes)
|
||||
{
|
||||
Assert.AreEqual(uint.MaxValue, *(uint*)pBytes);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void ReadOnlyStructCanBeModifiedInCtor()
|
||||
{
|
||||
TestStruct testStruct = new([4444, 7878, 5656, 1212]);
|
||||
|
||||
Assert.AreEqual(4444, testStruct.Value1);
|
||||
Assert.AreEqual(7878, testStruct.Value2);
|
||||
Assert.AreEqual(5656, testStruct.Value3);
|
||||
Assert.AreEqual(1212, testStruct.Value4);
|
||||
}
|
||||
|
||||
private readonly struct TestStruct
|
||||
{
|
||||
public readonly int Value1;
|
||||
public readonly int Value2;
|
||||
public readonly int Value3;
|
||||
public readonly int Value4;
|
||||
|
||||
public TestStruct(List<int> list)
|
||||
{
|
||||
CollectionsMarshal.AsSpan(list).CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.As<TestStruct, int>(ref this), 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
|
||||
@@ -2,5 +2,10 @@
|
||||
"$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
|
||||
"allowMarshaling": true,
|
||||
"useSafeHandles": false,
|
||||
"emitSingleFile": true
|
||||
"comInterop": {
|
||||
"preserveSigMethods": [
|
||||
"IFileOpenDialog.Show",
|
||||
"IFileSaveDialog.Show"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,12 @@ WaitForSingleObject
|
||||
WriteProcessMemory
|
||||
|
||||
// OLE32
|
||||
CoCreateInstance
|
||||
CoWaitForMultipleObjects
|
||||
|
||||
// SHELL32
|
||||
SHCreateItemFromParsingName
|
||||
|
||||
// USER32
|
||||
AttachThreadInput
|
||||
FindWindowExW
|
||||
@@ -46,6 +50,10 @@ SetForegroundWindow
|
||||
UnregisterHotKey
|
||||
|
||||
// COM
|
||||
FileOpenDialog
|
||||
FileSaveDialog
|
||||
IFileOpenDialog
|
||||
IFileSaveDialog
|
||||
IPersistFile
|
||||
IShellLinkW
|
||||
ShellLink
|
||||
@@ -56,6 +64,7 @@ IMemoryBufferByteAccess
|
||||
|
||||
// Const value
|
||||
INFINITE
|
||||
MAX_PATH
|
||||
WM_GETMINMAXINFO
|
||||
WM_HOTKEY
|
||||
WM_NCRBUTTONDOWN
|
||||
@@ -63,6 +72,8 @@ WM_NCRBUTTONUP
|
||||
WM_NULL
|
||||
|
||||
// Type & Enum definition
|
||||
HRESULT_FROM_WIN32
|
||||
SLGP_FLAGS
|
||||
|
||||
// System.Threading
|
||||
LPTHREAD_START_ROUTINE
|
||||
|
||||
17
src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs
Normal file
17
src/Snap.Hutao/Snap.Hutao.Win32/PInvoke.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Com;
|
||||
|
||||
namespace Windows.Win32;
|
||||
|
||||
internal static partial class PInvoke
|
||||
{
|
||||
/// <inheritdoc cref="CoCreateInstance(Guid*, object, CLSCTX, Guid*, out object)"/>
|
||||
internal static unsafe HRESULT CoCreateInstance<TClass, TInterface>(object? pUnkOuter, CLSCTX dwClsContext, out TInterface ppv)
|
||||
where TInterface : class
|
||||
{
|
||||
HRESULT hr = CoCreateInstance(typeof(TClass).GUID, pUnkOuter, dwClsContext, typeof(TInterface).GUID, out object o);
|
||||
ppv = (TInterface)o;
|
||||
return hr;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
@@ -16,7 +16,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -19,18 +19,6 @@ internal static class StructMarshal
|
||||
/// <returns>新的实例</returns>
|
||||
public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
|
||||
{
|
||||
return new() { length = SizeOf<WINDOWPLACEMENT>() };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取结构的大小
|
||||
/// </summary>
|
||||
/// <typeparam name="TStruct">结构类型</typeparam>
|
||||
/// <returns>结构的大小</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe uint SizeOf<TStruct>()
|
||||
where TStruct : unmanaged
|
||||
{
|
||||
return unchecked((uint)sizeof(TStruct));
|
||||
return new() { length = unchecked((uint)sizeof(WINDOWPLACEMENT)) };
|
||||
}
|
||||
}
|
||||
@@ -69,4 +69,4 @@ internal class WinRTCustomMarshaler : ICustomMarshaler
|
||||
return Marshal.GetObjectForIUnknown(pNativeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Win32", "Snap.Hutao.Win32\Snap.Hutao.Win32.csproj", "{0F7ABEB2-5107-4037-B9DC-84D288FB0801}"
|
||||
@@ -52,22 +50,6 @@ Global
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.ActiveCfg = Release|x86
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Build.0 = Release|x86
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Deploy.0 = Release|x86
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.Build.0 = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.ActiveCfg = Release|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
@@ -105,11 +87,11 @@ Global
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
|
||||
RESX_ShowErrorsInErrorList = False
|
||||
RESX_SortFileContentOnSave = True
|
||||
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
||||
RESX_NeutralResourcesLanguage = zh-CN
|
||||
RESX_AutoApplyExistingTranslations = False
|
||||
RESX_NeutralResourcesLanguage = zh-CN
|
||||
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
||||
RESX_SortFileContentOnSave = True
|
||||
RESX_ShowErrorsInErrorList = False
|
||||
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -4,12 +4,18 @@
|
||||
"add": {
|
||||
"extensionToExtension": {
|
||||
"add": {
|
||||
".json": [ ".txt" ]
|
||||
".json": [
|
||||
".txt"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pathSegment": {
|
||||
"add": {
|
||||
".*": [ ".cs", ".resx" ]
|
||||
".*": [
|
||||
".cs",
|
||||
".resx",
|
||||
".appxmanifest"
|
||||
]
|
||||
}
|
||||
},
|
||||
"fileSuffixToExtension": {
|
||||
@@ -19,12 +25,24 @@
|
||||
},
|
||||
"fileToFile": {
|
||||
"add": {
|
||||
".filenesting.json": [ "App.xaml.cs" ],
|
||||
"app.manifest": [ "App.xaml.cs" ],
|
||||
"Package.appxmanifest": [ "App.xaml" ],
|
||||
"Package.StoreAssociation.xml": [ "App.xaml" ],
|
||||
".editorconfig": [ "Program.cs" ],
|
||||
"GlobalUsing.cs": [ "Program.cs" ]
|
||||
".filenesting.json": [
|
||||
"App.xaml.cs"
|
||||
],
|
||||
"app.manifest": [
|
||||
"App.xaml.cs"
|
||||
],
|
||||
"Package.appxmanifest": [
|
||||
"App.xaml"
|
||||
],
|
||||
"Package.StoreAssociation.xml": [
|
||||
"App.xaml"
|
||||
],
|
||||
".editorconfig": [
|
||||
"Program.cs"
|
||||
],
|
||||
"GlobalUsing.cs": [
|
||||
"Program.cs"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,159 +1,39 @@
|
||||
<Application
|
||||
x:Class="Snap.Hutao.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cwcont="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:cwconv="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:cwm="using:CommunityToolkit.WinUI.Media"
|
||||
xmlns:shci="using:Snap.Hutao.Control.Image"
|
||||
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
|
||||
xmlns:shvc="using:Snap.Hutao.View.Converter">
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources/>
|
||||
<ResourceDictionary Source="Control/Theme/FontStyle.xaml"/>
|
||||
<ResourceDictionary Source="Control/Theme/InfoBarOverride.xaml"/>
|
||||
<ResourceDictionary Source="Control/Theme/PageOverride.xaml"/>
|
||||
<ResourceDictionary Source="Control/Theme/PivotOverride.xaml"/>
|
||||
<ResourceDictionary Source="Control/Theme/SettingsStyle.xaml"/>
|
||||
<ResourceDictionary Source="Control/Theme/TransitionCollection.xaml"/>
|
||||
<ResourceDictionary Source="Control/Theme/WindowOverride.xaml"/>
|
||||
<ResourceDictionary Source="Control/Loading.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Loading.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Image/CachedImage.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Card.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Color.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/ComboBox.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Converter.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/CornerRadius.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/FlyoutStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/FontStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Glyph.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/InfoBarOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/ItemsPanelTemplate.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/NumericValue.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/PageOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/SettingsStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<Color x:Key="AvatarPropertyAddValueColor">#FF74BF00</Color>
|
||||
<Color x:Key="CompatBackgroundColor">#FFF4F4F4</Color>
|
||||
<SolidColorBrush x:Key="DarkOnlyOverlayMaskBrush" Color="#00000000"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<Color x:Key="AvatarPropertyAddValueColor">#FF90E800</Color>
|
||||
<Color x:Key="CompatBackgroundColor">#FF242424</Color>
|
||||
<SolidColorBrush x:Key="DarkOnlyOverlayMaskBrush" Color="#60000000"/>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<!-- CornerRadius -->
|
||||
<CornerRadius x:Key="ControlCornerRadiusTop">4,4,0,0</CornerRadius>
|
||||
<CornerRadius x:Key="ControlCornerRadiusBottom">0,0,4,4</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadius">6</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusTop">6,6,0,0</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusRight">0,6,6,0</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusBottom">0,0,6,6</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
|
||||
<!-- 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>
|
||||
<!-- 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"/>
|
||||
|
||||
<!-- Uris -->
|
||||
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String>
|
||||
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
|
||||
<x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String>
|
||||
<x:String x:Key="DocumentLink_Home">https://hut.ao</x:String>
|
||||
<x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String>
|
||||
<x:String x:Key="Sponsor_Afadian">https://afdian.net/a/DismissedLight</x:String>
|
||||
|
||||
<!-- 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>
|
||||
<x:String x:Key="UI_EmotionIcon71">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon250">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon250.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon272">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon293">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
||||
|
||||
<!-- 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 -->
|
||||
<cwconv:BoolNegationConverter x:Key="BoolNegationConverter"/>
|
||||
<cwconv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
<cwconv:FileSizeToFriendlyStringConverter x:Key="FileSizeToFriendlyStringConverter"/>
|
||||
<shmmc:AchievementIconConverter x:Key="AchievementIconConverter"/>
|
||||
<shmmc:AvatarCardConverter x:Key="AvatarCardConverter"/>
|
||||
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
|
||||
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
|
||||
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
|
||||
<shmmc:DescriptionsParametersDescriptor x:Key="DescParamDescriptor"/>
|
||||
<shmmc:ElementNameIconConverter x:Key="ElementNameIconConverter"/>
|
||||
<shmmc:EmotionIconConverter x:Key="EmotionIconConverter"/>
|
||||
<shmmc:EquipIconConverter x:Key="EquipIconConverter"/>
|
||||
<shmmc:GachaAvatarImgConverter x:Key="GachaAvatarImgConverter"/>
|
||||
<shmmc:GachaAvatarIconConverter x:Key="GachaAvatarIconConverter"/>
|
||||
<shmmc:GachaEquipIconConverter x:Key="GachaEquipIconConverter"/>
|
||||
<shmmc:ItemIconConverter x:Key="ItemIconConverter"/>
|
||||
<shmmc:MonsterIconConverter x:Key="MonsterIconConverter"/>
|
||||
<shmmc:PropertiesParametersDescriptor x:Key="PropertyDescriptor"/>
|
||||
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
|
||||
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
|
||||
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
|
||||
<shvc:EmptyObjectToBoolConverter x:Key="EmptyObjectToBoolConverter"/>
|
||||
<shvc:EmptyObjectToBoolRevertConverter x:Key="EmptyObjectToBoolRevertConverter"/>
|
||||
<shvc:EmptyObjectToVisibilityConverter x:Key="EmptyObjectToVisibilityConverter"/>
|
||||
<shvc:EmptyObjectToVisibilityRevertConverter x:Key="EmptyObjectToVisibilityRevertConverter"/>
|
||||
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
|
||||
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
|
||||
<shvc:StringBoolConverter x:Key="StringBoolConverter"/>
|
||||
|
||||
<!-- Styles -->
|
||||
<Style
|
||||
x:Key="LargeGridViewItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
TargetType="GridViewItem">
|
||||
<Setter Property="Margin" Value="0,0,12,12"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="SettingButtonStyle"
|
||||
BasedOn="{StaticResource DefaultButtonStyle}"
|
||||
TargetType="Button">
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
<Setter Property="Padding" Value="16,6,16,6"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
<Style x:Key="BorderCardStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
</Style>
|
||||
<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="{ThemeResource ControlCornerRadius}"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Name="NoneSelectionListViewItemStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
@@ -168,108 +48,6 @@
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="0,0,2,4"/>
|
||||
</Style>
|
||||
<Style TargetType="shci:CachedImage">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}"/>
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="LazyLoadingThreshold" Value="256"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<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="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="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>
|
||||
<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>
|
||||
<VisualState x:Name="Unloaded"/>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<!-- ItemsPanelTemplate -->
|
||||
<ItemsPanelTemplate x:Key="ItemsStackPanelTemplate">
|
||||
<ItemsStackPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing0Template">
|
||||
<cwcont:WrapPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing4Template">
|
||||
<cwcont:WrapPanel HorizontalSpacing="4" VerticalSpacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelTemplate">
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="UniformGridColumns5Spacing4Template">
|
||||
<cwcont:UniformGrid
|
||||
ColumnSpacing="4"
|
||||
Columns="5"
|
||||
RowSpacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
|
||||
|
||||
<cwm:AttachedCardShadow
|
||||
x:Key="CompatCardShadow"
|
||||
Opacity="0.1"
|
||||
Offset="0,4,0"/>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -3,12 +3,18 @@
|
||||
|
||||
namespace Snap.Hutao.Control.Animation;
|
||||
|
||||
/// <summary>
|
||||
/// 动画时长
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class AnimationDurations
|
||||
internal static class ControlAnimationConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// 1
|
||||
/// </summary>
|
||||
public const string One = "1";
|
||||
|
||||
/// <summary>
|
||||
/// 1.1
|
||||
/// </summary>
|
||||
public const string OnePointOne = "1.1";
|
||||
|
||||
/// <summary>
|
||||
/// 图片缩放动画
|
||||
/// </summary>
|
||||
@@ -19,10 +19,10 @@ internal sealed class ImageZoomInAnimation : ImplicitAnimation<string, Vector3>
|
||||
/// </summary>
|
||||
public ImageZoomInAnimation()
|
||||
{
|
||||
Duration = AnimationDurations.ImageZoom;
|
||||
Duration = ControlAnimationConstants.ImageZoom;
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
|
||||
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
|
||||
To = Core.StringLiterals.OnePointOne;
|
||||
To = ControlAnimationConstants.OnePointOne;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -19,10 +19,10 @@ internal sealed class ImageZoomOutAnimation : ImplicitAnimation<string, Vector3>
|
||||
/// </summary>
|
||||
public ImageZoomOutAnimation()
|
||||
{
|
||||
Duration = AnimationDurations.ImageZoom;
|
||||
Duration = ControlAnimationConstants.ImageZoom;
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
|
||||
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
|
||||
To = Core.StringLiterals.One;
|
||||
To = ControlAnimationConstants.One;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
/// <summary>
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
public AutoHeightBehavior()
|
||||
{
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Initialize()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateElement();
|
||||
}
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Height = AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
/// <summary>
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
public AutoWidthBehavior()
|
||||
{
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Initialize()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateElement();
|
||||
}
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Width = AssociatedObject.Height * (TargetWidth / TargetHeight);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,11 @@ internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIEle
|
||||
|
||||
private void TryExecuteCommand()
|
||||
{
|
||||
if (AssociatedObject is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (executed)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Labs.WinUI.MarqueeTextRns;
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
internal sealed class MarqueeTextBehavior : BehaviorBase<MarqueeText>
|
||||
{
|
||||
private readonly PointerEventHandler pointerEnteredEventHandler;
|
||||
private readonly PointerEventHandler pointerExitedEventHandler;
|
||||
|
||||
public MarqueeTextBehavior()
|
||||
{
|
||||
pointerEnteredEventHandler = OnPointerEntered;
|
||||
pointerExitedEventHandler = OnPointerExited;
|
||||
}
|
||||
|
||||
protected override bool Initialize()
|
||||
{
|
||||
AssociatedObject.PointerEntered += pointerEnteredEventHandler;
|
||||
AssociatedObject.PointerExited += pointerExitedEventHandler;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.PointerEntered -= pointerEnteredEventHandler;
|
||||
AssociatedObject.PointerExited -= pointerExitedEventHandler;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnPointerEntered(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
AssociatedObject.StartMarquee();
|
||||
}
|
||||
|
||||
private void OnPointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
AssociatedObject.StopMarquee();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
@@ -12,7 +13,7 @@ internal sealed class SelectedItemInViewBehavior : BehaviorBase<ListViewBase>
|
||||
{
|
||||
if (AssociatedObject.SelectedItem is { } item)
|
||||
{
|
||||
AssociatedObject.ScrollIntoView(item);
|
||||
AssociatedObject.SmoothScrollIntoViewWithItemAsync(item, ScrollItemPlacement.Center).SafeForget();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -11,12 +11,17 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
/// 打开附着的浮出控件操作
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class OpenAttachedFlyoutAction : DependencyObject, IAction
|
||||
internal sealed class ShowAttachedFlyoutAction : DependencyObject, IAction
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object Execute(object sender, object parameter)
|
||||
public object? Execute(object sender, object parameter)
|
||||
{
|
||||
if (sender is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement);
|
||||
return default!;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Xaml.Interactivity;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
[DependencyProperty("Animation", typeof(AnimationSet))]
|
||||
[DependencyProperty("TargetObject", typeof(UIElement))]
|
||||
internal sealed partial class StartAnimationActionNoThrow : DependencyObject, IAction
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object Execute(object sender, object parameter)
|
||||
{
|
||||
if (Animation is not null)
|
||||
{
|
||||
if (TargetObject is not null)
|
||||
{
|
||||
Animation.Start(TargetObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Animation.Start(sender as UIElement);
|
||||
}
|
||||
}
|
||||
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,9 @@ internal sealed partial class SegmentedBar : ContentControl
|
||||
double offset = 0;
|
||||
foreach (ref readonly IColorSegment segment in CollectionsMarshal.AsSpan(list))
|
||||
{
|
||||
collection.Add(new GradientStop() { Color = segment.Color, Offset = offset, });
|
||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||
offset += segment.Value / total;
|
||||
collection.Add(new GradientStop() { Color = segment.Color, Offset = offset, });
|
||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ internal struct ContentDialogHideToken : IDisposable, IAsyncDisposable
|
||||
private readonly ContentDialog contentDialog;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
private bool disposed = false;
|
||||
private bool disposing = false;
|
||||
private bool disposed = false;
|
||||
|
||||
public ContentDialogHideToken(ContentDialog contentDialog, ITaskContext taskContext)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Control.Extension;
|
||||
|
||||
/// <summary>
|
||||
/// Bridge 拓展
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class WebView2Extension
|
||||
{
|
||||
[Conditional("RELEASE")]
|
||||
public static void DisableDevToolsForReleaseBuild(this CoreWebView2 webView)
|
||||
{
|
||||
CoreWebView2Settings settings = webView.Settings;
|
||||
settings.AreBrowserAcceleratorKeysEnabled = false;
|
||||
settings.AreDefaultContextMenusEnabled = false;
|
||||
settings.AreDevToolsEnabled = false;
|
||||
}
|
||||
|
||||
public static void DisableAutoCompletion(this CoreWebView2 webView)
|
||||
{
|
||||
CoreWebView2Settings settings = webView.Settings;
|
||||
settings.IsGeneralAutofillEnabled = false;
|
||||
settings.IsPasswordAutosaveEnabled = false;
|
||||
}
|
||||
|
||||
public static async ValueTask DeleteCookiesAsync(this CoreWebView2 webView, string url)
|
||||
{
|
||||
CoreWebView2CookieManager manager = webView.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync(url);
|
||||
foreach (CoreWebView2Cookie item in cookies)
|
||||
{
|
||||
manager.DeleteCookie(item);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsDisposed(this WebView2 webView2)
|
||||
{
|
||||
return WinRTExtension.IsDisposed(webView2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Helper;
|
||||
|
||||
[SuppressMessage("", "SH001")]
|
||||
[DependencyProperty("LeftPanelMaxWidth", typeof(double), IsAttached = true, AttachedType = typeof(ScrollViewer))]
|
||||
[DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))]
|
||||
public sealed partial class ScrollViewerHelper
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Helper;
|
||||
|
||||
[SuppressMessage("", "SH001")]
|
||||
[DependencyProperty("IsItemsEnabled", typeof(bool), true, nameof(OnIsItemsEnabledChanged), IsAttached = true, AttachedType = typeof(SettingsExpander))]
|
||||
public sealed partial class SettingsExpanderHelper
|
||||
{
|
||||
private static void OnIsItemsEnabledChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
foreach (object item in ((SettingsExpander)dp).Items)
|
||||
{
|
||||
if (item is Microsoft.UI.Xaml.Controls.Control control)
|
||||
{
|
||||
control.IsEnabled = (bool)e.NewValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
internal interface IScopedPageScopeReferenceTracker
|
||||
{
|
||||
IServiceScope CreateScope();
|
||||
}
|
||||
82
src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.xaml
Normal file
82
src/Snap.Hutao/Snap.Hutao/Control/Image/CachedImage.xaml
Normal file
@@ -0,0 +1,82 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:shci="using:Snap.Hutao.Control.Image">
|
||||
<Style TargetType="shci:CachedImage">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}"/>
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="LazyLoadingThreshold" Value="256"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<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="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="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>
|
||||
<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>
|
||||
<VisualState x:Name="Unloaded"/>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -30,7 +30,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private readonly RoutedEventHandler unloadEventHandler;
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
private readonly TypedEventHandler<LoadedImageSurface, LoadedImageSourceLoadCompletedEventArgs> loadedImageSourceLoadCompletedEventHandler;
|
||||
|
||||
@@ -46,9 +45,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
serviceProvider = this.ServiceProvider();
|
||||
this.DisableInteraction();
|
||||
|
||||
unloadEventHandler = OnUnload;
|
||||
Unloaded += unloadEventHandler;
|
||||
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
SizeChanged += sizeChangedEventHandler;
|
||||
|
||||
@@ -67,10 +63,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void Unloading()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新视觉对象
|
||||
/// </summary>
|
||||
@@ -110,7 +102,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
|
||||
if (exception is HttpRequestException httpRequestException)
|
||||
{
|
||||
infoBarService.Error(httpRequestException, SH.ControlImageCompositionImageHttpRequest.Format(uri));
|
||||
infoBarService.Error(httpRequestException, SH.FormatControlImageCompositionImageHttpRequest(uri));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -164,11 +156,26 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
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;
|
||||
LoadedImageSurface? surface = default;
|
||||
try
|
||||
{
|
||||
surface = LoadedImageSurface.StartLoadFromUri(file.ToUri());
|
||||
surface.LoadCompleted += loadedImageSourceLoadCompletedEventHandler;
|
||||
if (surface.DecodedPhysicalSize.Size() <= 0D)
|
||||
{
|
||||
await surfaceLoadTaskCompletionSource.Task.ConfigureAwait(true);
|
||||
}
|
||||
|
||||
LoadImageSurfaceCompleted(surface);
|
||||
return surface;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (surface is not null)
|
||||
{
|
||||
surface.LoadCompleted -= loadedImageSourceLoadCompletedEventHandler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask ShowAsync(CancellationToken token)
|
||||
@@ -181,7 +188,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
{
|
||||
await AnimationBuilder
|
||||
.Create()
|
||||
.Opacity(from: 0D, to: 1D, duration: AnimationDurations.ImageFadeIn)
|
||||
.Opacity(from: 0D, to: 1D, duration: ControlAnimationConstants.ImageFadeIn)
|
||||
.StartAsync(this, token)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
@@ -202,7 +209,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
{
|
||||
await AnimationBuilder
|
||||
.Create()
|
||||
.Opacity(from: 1D, to: 0D, duration: AnimationDurations.ImageFadeOut)
|
||||
.Opacity(from: 1D, to: 0D, duration: ControlAnimationConstants.ImageFadeOut)
|
||||
.StartAsync(this, token)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
@@ -216,7 +223,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
private void OnLoadImageSurfaceLoadCompleted(LoadedImageSurface surface, LoadedImageSourceLoadCompletedEventArgs e)
|
||||
{
|
||||
surfaceLoadTaskCompletionSource?.TrySetResult();
|
||||
surface.LoadCompleted -= loadedImageSourceLoadCompletedEventHandler;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
@@ -226,14 +232,4 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
UpdateVisual(spriteVisual);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnload(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Unloading();
|
||||
spriteVisual?.Dispose();
|
||||
spriteVisual = null;
|
||||
|
||||
SizeChanged -= sizeChangedEventHandler;
|
||||
Unloaded -= unloadEventHandler;
|
||||
}
|
||||
}
|
||||
@@ -45,16 +45,6 @@ internal sealed class MonoChrome : CompositionImage
|
||||
return compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
|
||||
}
|
||||
|
||||
protected override void Unloading()
|
||||
{
|
||||
ActualThemeChanged -= actualThemeChangedEventHandler;
|
||||
|
||||
backgroundBrush?.Dispose();
|
||||
backgroundBrush = null;
|
||||
|
||||
base.Unloading();
|
||||
}
|
||||
|
||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
if (backgroundBrush is not null)
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using System.Numerics;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class DefaultItemCollectionTransitionProvider : ItemCollectionTransitionProvider
|
||||
{
|
||||
private const double DefaultAnimationDurationInMs = 300.0;
|
||||
|
||||
static DefaultItemCollectionTransitionProvider()
|
||||
{
|
||||
AnimationSlowdownFactor = 1.0;
|
||||
}
|
||||
|
||||
public static double AnimationSlowdownFactor { get; set; }
|
||||
|
||||
protected override bool ShouldAnimateCore(ItemCollectionTransition transition)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void StartTransitions(IList<ItemCollectionTransition> transitions)
|
||||
{
|
||||
List<ItemCollectionTransition> addTransitions = [];
|
||||
List<ItemCollectionTransition> removeTransitions = [];
|
||||
List<ItemCollectionTransition> moveTransitions = [];
|
||||
|
||||
foreach (ItemCollectionTransition transition in addTransitions)
|
||||
{
|
||||
switch (transition.Operation)
|
||||
{
|
||||
case ItemCollectionTransitionOperation.Add:
|
||||
addTransitions.Add(transition);
|
||||
break;
|
||||
case ItemCollectionTransitionOperation.Remove:
|
||||
removeTransitions.Add(transition);
|
||||
break;
|
||||
case ItemCollectionTransitionOperation.Move:
|
||||
moveTransitions.Add(transition);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
StartAddTransitions(addTransitions, removeTransitions.Count > 0, moveTransitions.Count > 0);
|
||||
StartRemoveTransitions(removeTransitions);
|
||||
StartMoveTransitions(moveTransitions, removeTransitions.Count > 0);
|
||||
}
|
||||
|
||||
private static void StartAddTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveTransitions, bool hasMoveTransitions)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
|
||||
ScalarKeyFrameAnimation fadeInAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
fadeInAnimation.InsertKeyFrame(0.0f, 0.0f);
|
||||
|
||||
if (hasMoveTransitions && hasRemoveTransitions)
|
||||
{
|
||||
fadeInAnimation.InsertKeyFrame(0.66f, 0.0f);
|
||||
}
|
||||
else if (hasMoveTransitions || hasRemoveTransitions)
|
||||
{
|
||||
fadeInAnimation.InsertKeyFrame(0.5f, 0.0f);
|
||||
}
|
||||
|
||||
fadeInAnimation.InsertKeyFrame(1.0f, 1.0f);
|
||||
fadeInAnimation.Duration = TimeSpan.FromMilliseconds(
|
||||
DefaultAnimationDurationInMs * ((hasRemoveTransitions ? 1 : 0) + (hasMoveTransitions ? 1 : 0) + 1) * AnimationSlowdownFactor);
|
||||
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
visual.StartAnimation("Opacity", fadeInAnimation);
|
||||
batch.End();
|
||||
batch.Completed += (_, _) => progress.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private static void StartRemoveTransitions(IList<ItemCollectionTransition> transitions)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
|
||||
ScalarKeyFrameAnimation fadeOutAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
fadeOutAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue");
|
||||
fadeOutAnimation.InsertKeyFrame(1.0f, 0.0f);
|
||||
fadeOutAnimation.Duration = TimeSpan.FromMilliseconds(DefaultAnimationDurationInMs * AnimationSlowdownFactor);
|
||||
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
visual.StartAnimation(nameof(Visual.Opacity), fadeOutAnimation);
|
||||
batch.End();
|
||||
batch.Completed += (_, _) =>
|
||||
{
|
||||
visual.Opacity = 1.0f;
|
||||
progress.Complete();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void StartMoveTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveAnimations)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
|
||||
// Animate offset.
|
||||
if (transition.OldBounds.X != transition.NewBounds.X ||
|
||||
transition.OldBounds.Y != transition.NewBounds.Y)
|
||||
{
|
||||
AnimateOffset(visual, compositor, transition.OldBounds, transition.NewBounds, hasRemoveAnimations);
|
||||
}
|
||||
|
||||
batch.End();
|
||||
batch.Completed += (_, _) => progress.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AnimateOffset(Visual visual, Compositor compositor, Rect oldBounds, Rect newBounds, bool hasRemoveAnimations)
|
||||
{
|
||||
Vector2KeyFrameAnimation offsetAnimation = compositor.CreateVector2KeyFrameAnimation();
|
||||
|
||||
offsetAnimation.SetVector2Parameter("delta", new Vector2(
|
||||
(float)(oldBounds.X - newBounds.X),
|
||||
(float)(oldBounds.Y - newBounds.Y)));
|
||||
offsetAnimation.SetVector2Parameter("final", default);
|
||||
offsetAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue + delta");
|
||||
if (hasRemoveAnimations)
|
||||
{
|
||||
offsetAnimation.InsertExpressionKeyFrame(0.5f, "delta");
|
||||
}
|
||||
|
||||
offsetAnimation.InsertExpressionKeyFrame(1.0f, "final");
|
||||
offsetAnimation.Duration = TimeSpan.FromMilliseconds(
|
||||
DefaultAnimationDurationInMs * ((hasRemoveAnimations ? 1 : 0) + 1) * AnimationSlowdownFactor);
|
||||
|
||||
visual.StartAnimation("TransformMatrix._41_42", offsetAnimation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
[DebuggerDisplay("Count = {Count}, Height = {Height}")]
|
||||
internal class UniformStaggeredColumnLayout : List<UniformStaggeredItem>
|
||||
{
|
||||
public double Height { get; private set; }
|
||||
|
||||
public new void Add(UniformStaggeredItem item)
|
||||
{
|
||||
Height = item.Top + item.Height;
|
||||
base.Add(item);
|
||||
}
|
||||
|
||||
public new void Clear()
|
||||
{
|
||||
Height = 0;
|
||||
base.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class UniformStaggeredItem
|
||||
{
|
||||
public UniformStaggeredItem(int index)
|
||||
{
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public double Top { get; internal set; }
|
||||
|
||||
public double Height { get; internal set; }
|
||||
|
||||
public int Index { get; }
|
||||
|
||||
public UIElement? Element { get; internal set; }
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Collections.Specialized;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
[DependencyProperty("MinItemWidth", typeof(double), 0D, nameof(OnMinItemWidthChanged))]
|
||||
[DependencyProperty("MinColumnSpacing", typeof(double), 0D, nameof(OnSpacingChanged))]
|
||||
[DependencyProperty("MinRowSpacing", typeof(double), 0D, nameof(OnSpacingChanged))]
|
||||
internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
|
||||
{
|
||||
context.LayoutState = new UniformStaggeredLayoutState(context);
|
||||
base.InitializeForContextCore(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
|
||||
{
|
||||
context.LayoutState = null;
|
||||
base.UninitializeForContextCore(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
state.RemoveFromIndex(args.NewStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
state.RemoveFromIndex(args.NewStartingIndex);
|
||||
state.RecycleElementAt(args.NewStartingIndex); // We must recycle the element to ensure that it gets the correct context
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
|
||||
int maxIndex = Math.Max(args.NewStartingIndex, args.OldStartingIndex);
|
||||
state.RemoveRange(minIndex, maxIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
state.RemoveFromIndex(args.OldStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
state.Clear();
|
||||
break;
|
||||
}
|
||||
|
||||
base.OnItemsChangedCore(context, source, args);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
|
||||
{
|
||||
if (context.ItemCount == 0)
|
||||
{
|
||||
return new Size(availableSize.Width, 0);
|
||||
}
|
||||
|
||||
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
|
||||
{
|
||||
return new Size(availableSize.Width, 0.0f);
|
||||
}
|
||||
|
||||
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
|
||||
|
||||
double availableWidth = availableSize.Width;
|
||||
double availableHeight = availableSize.Height;
|
||||
|
||||
(int numberOfColumns, double columnWidth) = GetNumberOfColumnsAndWidth(availableWidth, MinItemWidth, MinColumnSpacing);
|
||||
|
||||
if (columnWidth != state.ColumnWidth)
|
||||
{
|
||||
// The items will need to be remeasured
|
||||
state.Clear();
|
||||
}
|
||||
|
||||
state.ColumnWidth = columnWidth;
|
||||
|
||||
// adjust for column spacing on all columns expect the first
|
||||
double totalWidth = state.ColumnWidth + ((numberOfColumns - 1) * (state.ColumnWidth + MinColumnSpacing));
|
||||
if (totalWidth > availableWidth)
|
||||
{
|
||||
numberOfColumns--;
|
||||
}
|
||||
else if (double.IsInfinity(availableWidth))
|
||||
{
|
||||
availableWidth = totalWidth;
|
||||
}
|
||||
|
||||
if (numberOfColumns != state.NumberOfColumns)
|
||||
{
|
||||
// The items will not need to be remeasured, but they will need to go into new columns
|
||||
state.ClearColumns();
|
||||
}
|
||||
|
||||
if (MinRowSpacing != state.RowSpacing)
|
||||
{
|
||||
// If the RowSpacing changes the height of the rows will be different.
|
||||
// The columns stores the height so we'll want to clear them out to
|
||||
// get the proper height
|
||||
state.ClearColumns();
|
||||
state.RowSpacing = MinRowSpacing;
|
||||
}
|
||||
|
||||
Span<double> columnHeights = new double[numberOfColumns];
|
||||
Span<int> itemsPerColumn = new int[numberOfColumns];
|
||||
HashSet<int> deadColumns = [];
|
||||
|
||||
for (int i = 0; i < context.ItemCount; i++)
|
||||
{
|
||||
int columnIndex = GetLowestColumnIndex(columnHeights);
|
||||
|
||||
bool measured = false;
|
||||
UniformStaggeredItem item = state.GetItemAt(i);
|
||||
if (item.Height == 0)
|
||||
{
|
||||
// Item has not been measured yet. Get the element and store the values
|
||||
UIElement element = context.GetOrCreateElementAt(i);
|
||||
element.Measure(new Size(state.ColumnWidth, availableHeight));
|
||||
item.Height = element.DesiredSize.Height;
|
||||
item.Element = element;
|
||||
measured = true;
|
||||
}
|
||||
|
||||
double spacing = itemsPerColumn[columnIndex] > 0 ? MinRowSpacing : 0;
|
||||
item.Top = columnHeights[columnIndex] + spacing;
|
||||
double bottom = item.Top + item.Height;
|
||||
columnHeights[columnIndex] = bottom;
|
||||
itemsPerColumn[columnIndex]++;
|
||||
state.AddItemToColumn(item, columnIndex);
|
||||
|
||||
if (bottom < context.RealizationRect.Top)
|
||||
{
|
||||
// The bottom of the element is above the realization area
|
||||
if (item.Element is not null)
|
||||
{
|
||||
context.RecycleElement(item.Element);
|
||||
item.Element = null;
|
||||
}
|
||||
}
|
||||
else if (item.Top > context.RealizationRect.Bottom)
|
||||
{
|
||||
// The top of the element is below the realization area
|
||||
if (item.Element is not null)
|
||||
{
|
||||
context.RecycleElement(item.Element);
|
||||
item.Element = null;
|
||||
}
|
||||
|
||||
deadColumns.Add(columnIndex);
|
||||
}
|
||||
else if (measured == false)
|
||||
{
|
||||
// We ALWAYS want to measure an item that will be in the bounds
|
||||
item.Element = context.GetOrCreateElementAt(i);
|
||||
item.Element.Measure(new Size(state.ColumnWidth, availableHeight));
|
||||
if (item.Height != item.Element.DesiredSize.Height)
|
||||
{
|
||||
// this item changed size; we need to recalculate layout for everything after this
|
||||
state.RemoveFromIndex(i + 1);
|
||||
item.Height = item.Element.DesiredSize.Height;
|
||||
columnHeights[columnIndex] = item.Top + item.Height;
|
||||
}
|
||||
}
|
||||
|
||||
if (deadColumns.Count == numberOfColumns)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
double desiredHeight = state.GetHeight();
|
||||
|
||||
return new Size(availableWidth, desiredHeight);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
|
||||
{
|
||||
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
|
||||
{
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
|
||||
|
||||
// Cycle through each column and arrange the items that are within the realization bounds
|
||||
for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
|
||||
{
|
||||
UniformStaggeredColumnLayout layout = state.GetColumnLayout(columnIndex);
|
||||
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(layout))
|
||||
{
|
||||
double bottom = item.Top + item.Height;
|
||||
if (bottom < context.RealizationRect.Top)
|
||||
{
|
||||
// element is above the realization bounds
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.Top <= context.RealizationRect.Bottom)
|
||||
{
|
||||
double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex);
|
||||
|
||||
Rect bounds = new(itemHorizontalOffset, item.Top, state.ColumnWidth, item.Height);
|
||||
UIElement element = context.GetOrCreateElementAt(item.Index);
|
||||
element.Arrange(bounds);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double minColumnSpacing)
|
||||
{
|
||||
// test if the width can fit in 2 items
|
||||
if ((2 * minItemWidth) + minColumnSpacing > availableWidth)
|
||||
{
|
||||
return (1, availableWidth);
|
||||
}
|
||||
|
||||
int columnCount = Math.Max(1, (int)((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing)));
|
||||
double columnWidthAddSpacing = (availableWidth + minColumnSpacing) / columnCount;
|
||||
return (columnCount, columnWidthAddSpacing - minColumnSpacing);
|
||||
}
|
||||
|
||||
private static int GetLowestColumnIndex(in ReadOnlySpan<double> columnHeights)
|
||||
{
|
||||
int columnIndex = 0;
|
||||
double height = columnHeights[0];
|
||||
for (int j = 1; j < columnHeights.Length; j++)
|
||||
{
|
||||
if (columnHeights[j] < height)
|
||||
{
|
||||
columnIndex = j;
|
||||
height = columnHeights[j];
|
||||
}
|
||||
}
|
||||
|
||||
return columnIndex;
|
||||
}
|
||||
|
||||
private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
|
||||
panel.InvalidateMeasure();
|
||||
}
|
||||
|
||||
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
|
||||
panel.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class UniformStaggeredLayoutState
|
||||
{
|
||||
private readonly List<UniformStaggeredItem> items = [];
|
||||
private readonly VirtualizingLayoutContext context;
|
||||
private readonly Dictionary<int, UniformStaggeredColumnLayout> columnLayout = [];
|
||||
private double lastAverageHeight;
|
||||
|
||||
public UniformStaggeredLayoutState(VirtualizingLayoutContext context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public double ColumnWidth { get; internal set; }
|
||||
|
||||
public int NumberOfColumns
|
||||
{
|
||||
get => columnLayout.Count;
|
||||
}
|
||||
|
||||
public double RowSpacing { get; internal set; }
|
||||
|
||||
internal void AddItemToColumn(UniformStaggeredItem item, int columnIndex)
|
||||
{
|
||||
if (!this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout))
|
||||
{
|
||||
columnLayout = [];
|
||||
this.columnLayout[columnIndex] = columnLayout;
|
||||
}
|
||||
|
||||
if (!columnLayout.Contains(item))
|
||||
{
|
||||
columnLayout.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "CA2201")]
|
||||
internal UniformStaggeredItem GetItemAt(int index)
|
||||
{
|
||||
if (index < 0)
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
if (index <= (items.Count - 1))
|
||||
{
|
||||
return items[index];
|
||||
}
|
||||
else
|
||||
{
|
||||
UniformStaggeredItem item = new(index);
|
||||
items.Add(item);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
internal UniformStaggeredColumnLayout GetColumnLayout(int columnIndex)
|
||||
{
|
||||
return columnLayout[columnIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear everything that has been calculated.
|
||||
/// </summary>
|
||||
internal void Clear()
|
||||
{
|
||||
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
|
||||
// The first element must be force refreshed otherwise
|
||||
// it will use the old one realized
|
||||
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
|
||||
// Now we need to refresh the first element of each column
|
||||
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
|
||||
// Finally we need to refresh the whole layout when we reset
|
||||
if (context.ItemCount > 0)
|
||||
{
|
||||
for (int i = 0; i < context.ItemCount; i++)
|
||||
{
|
||||
RecycleElementAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
columnLayout.Clear();
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the layout columns so they will be recalculated.
|
||||
/// </summary>
|
||||
internal void ClearColumns()
|
||||
{
|
||||
columnLayout.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the estimated height of the layout.
|
||||
/// </summary>
|
||||
/// <returns>The estimated height of the layout.</returns>
|
||||
/// <remarks>
|
||||
/// If all of the items have been calculated then the actual height will be returned.
|
||||
/// If all of the items have not been calculated then an estimated height will be calculated based on the average height of the items.
|
||||
/// </remarks>
|
||||
internal double GetHeight()
|
||||
{
|
||||
double desiredHeight = columnLayout.Values.Max(c => c.Height);
|
||||
int itemCount = columnLayout.Values.Sum(c => c.Count);
|
||||
|
||||
if (itemCount == context.ItemCount)
|
||||
{
|
||||
return desiredHeight;
|
||||
}
|
||||
|
||||
double averageHeight = 0;
|
||||
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
|
||||
{
|
||||
averageHeight += layout.Height / layout.Count;
|
||||
}
|
||||
|
||||
averageHeight /= columnLayout.Count;
|
||||
double estimatedHeight = (averageHeight * context.ItemCount) / columnLayout.Count;
|
||||
if (estimatedHeight > desiredHeight)
|
||||
{
|
||||
desiredHeight = estimatedHeight;
|
||||
}
|
||||
|
||||
if (Math.Abs(desiredHeight - lastAverageHeight) < 5)
|
||||
{
|
||||
return lastAverageHeight;
|
||||
}
|
||||
|
||||
lastAverageHeight = desiredHeight;
|
||||
return desiredHeight;
|
||||
}
|
||||
|
||||
internal void RecycleElementAt(int index)
|
||||
{
|
||||
UIElement element = context.GetOrCreateElementAt(index);
|
||||
context.RecycleElement(element);
|
||||
}
|
||||
|
||||
internal void RemoveFromIndex(int index)
|
||||
{
|
||||
if (index >= items.Count)
|
||||
{
|
||||
// Item was added/removed but we haven't realized that far yet
|
||||
return;
|
||||
}
|
||||
|
||||
int numToRemove = items.Count - index;
|
||||
items.RemoveRange(index, numToRemove);
|
||||
|
||||
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
|
||||
{
|
||||
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
|
||||
for (int i = 0; i < layoutSpan.Length; i++)
|
||||
{
|
||||
if (layoutSpan[i].Index >= index)
|
||||
{
|
||||
numToRemove = layoutSpan.Length - i;
|
||||
layout.RemoveRange(i, numToRemove);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveRange(int startIndex, int endIndex)
|
||||
{
|
||||
for (int i = startIndex; i <= endIndex; i++)
|
||||
{
|
||||
if (i > items.Count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ref readonly UniformStaggeredItem item = ref CollectionsMarshal.AsSpan(items)[i];
|
||||
item.Height = 0;
|
||||
item.Top = 0;
|
||||
|
||||
// We must recycle all elements to ensure that it gets the correct context
|
||||
RecycleElementAt(i);
|
||||
}
|
||||
|
||||
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
|
||||
{
|
||||
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
|
||||
for (int i = 0; i < layoutSpan.Length; i++)
|
||||
{
|
||||
if ((startIndex <= layoutSpan[i].Index) && (layoutSpan[i].Index <= endIndex))
|
||||
{
|
||||
int numToRemove = layoutSpan.Length - i;
|
||||
layout.RemoveRange(i, numToRemove);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
|
||||
{
|
||||
public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(Loading), new PropertyMetadata(default(bool), IsLoadingPropertyChanged));
|
||||
|
||||
[SuppressMessage("", "IDE0052")]
|
||||
private FrameworkElement? presenter;
|
||||
|
||||
public Loading()
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
mc:Ignorable="d">
|
||||
|
||||
<cwc:SegmentedItem
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentBulletedList}}"
|
||||
Tag="List"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
|
||||
<cwc:SegmentedItem
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentGridView}}"
|
||||
Tag="Grid"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
@@ -11,6 +12,8 @@ namespace Snap.Hutao.Control.Panel;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("Current", typeof(string), List)]
|
||||
[DependencyProperty("LocalSettingKeySuffixForCurrent", typeof(string))]
|
||||
[DependencyProperty("LocalSettingKeyExtraForCurrent", typeof(string), "")]
|
||||
internal sealed partial class PanelSelector : Segmented
|
||||
{
|
||||
public const string List = nameof(List);
|
||||
@@ -42,21 +45,41 @@ internal sealed partial class PanelSelector : Segmented
|
||||
selectedIndexChangedCallbackToken = RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged);
|
||||
}
|
||||
|
||||
private void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp)
|
||||
{
|
||||
Current = IndexTypeMap[(int)GetValue(dp)];
|
||||
}
|
||||
|
||||
private void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
private static void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp)
|
||||
{
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
selector.SelectedItem = selector.Items.Cast<SegmentedItem>().Single(item => (string)item.Tag == Current);
|
||||
selector.Current = IndexTypeMap[(int)selector.GetValue(dp)];
|
||||
|
||||
if (!string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent))
|
||||
{
|
||||
LocalSetting.Set(GetSettingKey(selector), selector.Current);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRootUnload(object sender, RoutedEventArgs e)
|
||||
private static void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UnregisterPropertyChangedCallback(SelectedIndexProperty, selectedIndexChangedCallbackToken);
|
||||
Loaded -= loadedEventHandler;
|
||||
Unloaded -= unloadedEventHandler;
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
|
||||
if (string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string value = LocalSetting.Get(GetSettingKey(selector), selector.Current);
|
||||
selector.Current = value;
|
||||
|
||||
selector.SelectedItem = selector.Items.Cast<SegmentedItem>().Single(item => (string)item.Tag == selector.Current);
|
||||
}
|
||||
|
||||
private static void OnRootUnload(object sender, RoutedEventArgs e)
|
||||
{
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
selector.UnregisterPropertyChangedCallback(SelectedIndexProperty, selector.selectedIndexChangedCallbackToken);
|
||||
selector.Unloaded -= selector.unloadedEventHandler;
|
||||
}
|
||||
|
||||
private static string GetSettingKey(PanelSelector selector)
|
||||
{
|
||||
return $"Control.PanelSelector.{selector.LocalSettingKeySuffixForCurrent}{selector.LocalSettingKeyExtraForCurrent}";
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,6 @@ namespace Snap.Hutao.Control;
|
||||
[SuppressMessage("", "CA1001")]
|
||||
internal class ScopedPage : Page
|
||||
{
|
||||
// Allow GC to Collect the IServiceScope
|
||||
private static readonly WeakReference<IServiceScope> PreviousScopeReference = new(default!);
|
||||
|
||||
private readonly RoutedEventHandler unloadEventHandler;
|
||||
private readonly CancellationTokenSource viewCancellationTokenSource = new();
|
||||
private readonly IServiceScope currentScope;
|
||||
@@ -31,22 +28,7 @@ internal class ScopedPage : Page
|
||||
{
|
||||
unloadEventHandler = OnUnloaded;
|
||||
Unloaded += unloadEventHandler;
|
||||
currentScope = Ioc.Default.CreateScope();
|
||||
DisposePreviousScope();
|
||||
|
||||
// track current
|
||||
PreviousScopeReference.SetTarget(currentScope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放上个范围
|
||||
/// </summary>
|
||||
public static void DisposePreviousScope()
|
||||
{
|
||||
if (PreviousScopeReference.TryGetTarget(out IServiceScope? scope))
|
||||
{
|
||||
scope.Dispose();
|
||||
}
|
||||
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
/// <summary>
|
||||
/// By injecting into services, we take dvantage of the fact that
|
||||
/// IServiceProvider disposes all injected services when it is disposed.
|
||||
/// </summary>
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IScopedPageScopeReferenceTracker))]
|
||||
internal sealed partial class ScopedPageScopeReferenceTracker : IScopedPageScopeReferenceTracker, IDisposable
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private readonly WeakReference<IServiceScope> previousScopeReference = new(default!);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposePreviousScope();
|
||||
}
|
||||
|
||||
public IServiceScope CreateScope()
|
||||
{
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
|
||||
IServiceScope currentScope = serviceProvider.CreateScope();
|
||||
|
||||
// In case previous one is not disposed.
|
||||
DisposePreviousScope();
|
||||
previousScopeReference.SetTarget(currentScope);
|
||||
return currentScope;
|
||||
}
|
||||
|
||||
private void DisposePreviousScope()
|
||||
{
|
||||
if (previousScopeReference.TryGetTarget(out IServiceScope? scope))
|
||||
{
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
[DependencyProperty("IsWidthRestricted", typeof(bool), true)]
|
||||
[DependencyProperty("IsHeightRestricted", typeof(bool), true)]
|
||||
internal sealed partial class SizeRestrictedContentControl : ContentControl
|
||||
{
|
||||
private double minContentWidth;
|
||||
private double minContentHeight;
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
if (Content is FrameworkElement element)
|
||||
{
|
||||
element.Measure(availableSize);
|
||||
Size contentDesiredSize = element.DesiredSize;
|
||||
Size contentActualOrDesiredSize = new(
|
||||
Math.Max(element.ActualWidth, contentDesiredSize.Width),
|
||||
Math.Max(element.ActualHeight, contentDesiredSize.Height));
|
||||
|
||||
if (IsWidthRestricted)
|
||||
{
|
||||
if (contentActualOrDesiredSize.Width > minContentWidth)
|
||||
{
|
||||
minContentWidth = contentActualOrDesiredSize.Width;
|
||||
}
|
||||
|
||||
element.MinWidth = minContentWidth;
|
||||
}
|
||||
|
||||
if (IsHeightRestricted)
|
||||
{
|
||||
if (contentActualOrDesiredSize.Height > minContentHeight)
|
||||
{
|
||||
minContentHeight = contentActualOrDesiredSize.Height;
|
||||
}
|
||||
|
||||
element.MinHeight = minContentHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return base.MeasureOverride(availableSize);
|
||||
}
|
||||
}
|
||||
21
src/Snap.Hutao/Snap.Hutao/Control/Theme/Card.xaml
Normal file
21
src/Snap.Hutao/Snap.Hutao/Control/Theme/Card.xaml
Normal file
@@ -0,0 +1,21 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cwm="using:CommunityToolkit.WinUI.Media">
|
||||
<Style x:Key="BorderCardStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
</Style>
|
||||
<Style x:Key="GridCardStyle" TargetType="Grid">
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
</Style>
|
||||
<cwm:AttachedCardShadow
|
||||
x:Key="CompatCardShadow"
|
||||
Opacity="0.1"
|
||||
Offset="0,4,0"/>
|
||||
</ResourceDictionary>
|
||||
31
src/Snap.Hutao/Snap.Hutao/Control/Theme/Color.xaml
Normal file
31
src/Snap.Hutao/Snap.Hutao/Control/Theme/Color.xaml
Normal file
@@ -0,0 +1,31 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<Color x:Key="AvatarPropertyAddValueColor">#FF74BF00</Color>
|
||||
<Color x:Key="CompatBackgroundColor">#FFF4F4F4</Color>
|
||||
<Color x:Key="DarkOnlyOverlayMaskColor">#00000000</Color>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<Color x:Key="AvatarPropertyAddValueColor">#FF90E800</Color>
|
||||
<Color x:Key="CompatBackgroundColor">#FF242424</Color>
|
||||
<Color x:Key="DarkOnlyOverlayMaskColor">#60000000</Color>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<Color x:Key="BlueColor">#FF5180CB</Color>
|
||||
<Color x:Key="PurpleColor">#FFA156E0</Color>
|
||||
<Color x:Key="OrangeColor">#FFBC6932</Color>
|
||||
|
||||
<Color x:Key="GuaranteePullColor">#FF0063FF</Color>
|
||||
<Color x:Key="UpPullColor">#FFFFA400</Color>
|
||||
|
||||
<SolidColorBrush x:Key="BlueColorBrush" Color="{ThemeResource BlueColor}"/>
|
||||
<SolidColorBrush x:Key="PurpleColorBrush" Color="{ThemeResource PurpleColor}"/>
|
||||
<SolidColorBrush x:Key="OrangeColorBrush" Color="{ThemeResource OrangeColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="GuaranteePullCoolorBrush" Color="{ThemeResource GuaranteePullColor}"/>
|
||||
<SolidColorBrush x:Key="UpPullColorBrush" Color="{ThemeResource UpPullColor}"/>
|
||||
|
||||
<SolidColorBrush x:Key="DarkOnlyOverlayMaskColorBrush" Color="{ThemeResource DarkOnlyOverlayMaskColor}"/>
|
||||
<SolidColorBrush x:Key="AvatarPropertyAddValueColorBrush" Color="{ThemeResource AvatarPropertyAddValueColor}"/>
|
||||
</ResourceDictionary>
|
||||
11
src/Snap.Hutao/Snap.Hutao/Control/Theme/ComboBox.xaml
Normal file
11
src/Snap.Hutao/Snap.Hutao/Control/Theme/ComboBox.xaml
Normal file
@@ -0,0 +1,11 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Style
|
||||
x:Key="CommandBarComboBoxStyle"
|
||||
BasedOn="{StaticResource DefaultComboBoxStyle}"
|
||||
TargetType="ComboBox">
|
||||
<Setter Property="Padding" Value="12,7,0,7"/>
|
||||
<Setter Property="Height" Value="36"/>
|
||||
</Style>
|
||||
<!-- https://github.com/microsoft/microsoft-ui-xaml/issues/4811 -->
|
||||
<x:Int32 x:Key="__DiscardPageOverride">0</x:Int32>
|
||||
</ResourceDictionary>
|
||||
35
src/Snap.Hutao/Snap.Hutao/Control/Theme/Converter.xaml
Normal file
35
src/Snap.Hutao/Snap.Hutao/Control/Theme/Converter.xaml
Normal file
@@ -0,0 +1,35 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cwc="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
|
||||
xmlns:shvc="using:Snap.Hutao.View.Converter">
|
||||
<cwc:BoolNegationConverter x:Key="BoolNegationConverter"/>
|
||||
<cwc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
<cwc:FileSizeToFriendlyStringConverter x:Key="FileSizeToFriendlyStringConverter"/>
|
||||
<shmmc:AchievementIconConverter x:Key="AchievementIconConverter"/>
|
||||
<shmmc:AvatarCardConverter x:Key="AvatarCardConverter"/>
|
||||
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
|
||||
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
|
||||
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
|
||||
<shmmc:DescriptionsParametersDescriptor x:Key="DescParamDescriptor"/>
|
||||
<shmmc:ElementNameIconConverter x:Key="ElementNameIconConverter"/>
|
||||
<shmmc:EmotionIconConverter x:Key="EmotionIconConverter"/>
|
||||
<shmmc:EquipIconConverter x:Key="EquipIconConverter"/>
|
||||
<shmmc:GachaAvatarImgConverter x:Key="GachaAvatarImgConverter"/>
|
||||
<shmmc:GachaAvatarIconConverter x:Key="GachaAvatarIconConverter"/>
|
||||
<shmmc:GachaEquipIconConverter x:Key="GachaEquipIconConverter"/>
|
||||
<shmmc:ItemIconConverter x:Key="ItemIconConverter"/>
|
||||
<shmmc:MonsterIconConverter x:Key="MonsterIconConverter"/>
|
||||
<shmmc:PropertiesParametersDescriptor x:Key="PropertyDescriptor"/>
|
||||
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
|
||||
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
|
||||
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
|
||||
<shvc:EmptyObjectToBoolConverter x:Key="EmptyObjectToBoolConverter"/>
|
||||
<shvc:EmptyObjectToBoolRevertConverter x:Key="EmptyObjectToBoolRevertConverter"/>
|
||||
<shvc:EmptyObjectToVisibilityConverter x:Key="EmptyObjectToVisibilityConverter"/>
|
||||
<shvc:EmptyObjectToVisibilityRevertConverter x:Key="EmptyObjectToVisibilityRevertConverter"/>
|
||||
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
|
||||
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
|
||||
<shvc:StringBoolConverter x:Key="StringBoolConverter"/>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,5 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<CornerRadius x:Key="ControlCornerRadiusTop">4,4,0,0</CornerRadius>
|
||||
<CornerRadius x:Key="ControlCornerRadiusBottom">0,0,4,4</CornerRadius>
|
||||
<CornerRadius x:Key="ControlCornerRadiusTopRightAndBottomLeft">0,4,0,4</CornerRadius>
|
||||
</ResourceDictionary>
|
||||
21
src/Snap.Hutao/Snap.Hutao/Control/Theme/FlyoutStyle.xaml
Normal file
21
src/Snap.Hutao/Snap.Hutao/Control/Theme/FlyoutStyle.xaml
Normal file
@@ -0,0 +1,21 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Style
|
||||
x:Key="WebViewerFlyoutPresenterStyle"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="FlyoutPresenterPadding0And2Style"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="0,2"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="FlyoutPresenterPadding6Style"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="6"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,7 +1,7 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:shch="using:Snap.Hutao.Control.Helper">
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<FontFamily x:Key="MiSans">ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
|
||||
<FontFamily x:Key="CascadiaMonoAndMiSans">ms-appx:///Resource/Font/CascadiaMono.ttf#Cascadia Mono, ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
|
||||
|
||||
@@ -93,4 +93,19 @@
|
||||
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
<SolidColorBrush x:Key="TextControlTextForeground" Color="{ThemeResource TextFillColorPrimary}"/>
|
||||
<Thickness x:Key="TextControlTextMargin">1,0</Thickness>
|
||||
<Style
|
||||
x:Key="TextControlTextBlockStyle"
|
||||
BasedOn="{StaticResource BodyTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextControlTextForeground}"/>
|
||||
<Setter Property="Margin" Value="{StaticResource TextControlTextMargin}"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="HorizontalTextAlignment" Value="Center"/>
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||
<Setter Property="TextWrapping" Value="NoWrap"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
20
src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml
Normal file
20
src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml
Normal file
@@ -0,0 +1,20 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<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="FontIconContentChevronRight"></x:String>
|
||||
<x:String x:Key="FontIconContentWarning"></x:String>
|
||||
<x:String x:Key="FontIconContentGame"></x:String>
|
||||
<x:String x:Key="FontIconContentOpenInNewWindow"></x:String>
|
||||
<x:String x:Key="FontIconContentFolder"></x:String>
|
||||
<x:String x:Key="FontIconContentCopy"></x:String>
|
||||
<x:String x:Key="FontIconContentBulletedList"></x:String>
|
||||
<x:String x:Key="FontIconContentCheckList"></x:String>
|
||||
<x:String x:Key="FontIconContentWebsite"></x:String>
|
||||
<x:String x:Key="FontIconContentHomeGroup"></x:String>
|
||||
<x:String x:Key="FontIconContentAsteriskBadge12"></x:String>
|
||||
<x:String x:Key="FontIconContentZipFolder"></x:String>
|
||||
<x:String x:Key="FontIconContentGridView"></x:String>
|
||||
<x:String x:Key="FontIconContentGiftboxOpen"></x:String>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,41 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cwcont="using:CommunityToolkit.WinUI.Controls">
|
||||
<ItemsPanelTemplate x:Key="ItemsStackPanelTemplate">
|
||||
<ItemsStackPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing0Template">
|
||||
<cwcont:WrapPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing4Template">
|
||||
<cwcont:WrapPanel HorizontalSpacing="4" VerticalSpacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing0Template">
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing2Template">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
|
||||
<StackPanel Spacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
|
||||
<cwcont:UniformGrid
|
||||
ColumnSpacing="2"
|
||||
Columns="2"
|
||||
RowSpacing="2"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="UniformGridColumns5Spacing4Template">
|
||||
<cwcont:UniformGrid
|
||||
ColumnSpacing="4"
|
||||
Columns="5"
|
||||
RowSpacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="UniformGridColumns5Spacing8Template">
|
||||
<cwcont:UniformGrid
|
||||
ColumnSpacing="8"
|
||||
Columns="5"
|
||||
RowSpacing="8"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ResourceDictionary>
|
||||
14
src/Snap.Hutao/Snap.Hutao/Control/Theme/NumericValue.xaml
Normal file
14
src/Snap.Hutao/Snap.Hutao/Control/Theme/NumericValue.xaml
Normal file
@@ -0,0 +1,14 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- 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>
|
||||
</ResourceDictionary>
|
||||
@@ -2,5 +2,5 @@
|
||||
<!-- Page Transparent Background -->
|
||||
<StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush"/>
|
||||
<!-- https://github.com/microsoft/microsoft-ui-xaml/issues/4811 -->
|
||||
<x:Int32 x:Key="__Discard">0</x:Int32>
|
||||
<x:Int32 x:Key="__DiscardPageOverride">0</x:Int32>
|
||||
</ResourceDictionary>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user