Compare commits

...

94 Commits

Author SHA1 Message Date
DismissedLight
bb2665b75e Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-01-16 14:27:45 +08:00
DismissedLight
d22ac39c1d fix dupe download items [skip ci] 2023-01-16 14:27:31 +08:00
Masterain
a312603d61 Update azure-pipelines.yml for Azure Pipelines 2023-01-15 22:22:34 -08:00
DismissedLight
0732ea0e06 replace font 2023-01-16 14:10:28 +08:00
Masterain
e4d2b3055c Update azure-pipelines.yml for Azure Pipelines
[skip ci]
2023-01-14 17:28:43 -08:00
Masterain
5668931230 Update PublishDistribution.yml
[skip ci]
2023-01-14 17:08:50 -08:00
DismissedLight
5126337138 code style [skip ci] 2023-01-12 19:42:45 +08:00
DismissedLight
4d634d3264 improve concurrent execution 2023-01-12 19:38:06 +08:00
Masterain
15a69fd0de Delete PrereleaseDistribution.yml 2023-01-11 20:42:42 -08:00
Masterain
c232891fe7 Update azure-pipelines.yml for Azure Pipelines 2023-01-11 20:41:03 -08:00
Masterain
c35c2a5700 Update workflows 2023-01-11 19:36:23 -08:00
Masterain
42305094f8 Update network-issue.yml 2023-01-11 16:12:16 -08:00
DismissedLight
9ef48ab05c fix crashes 2023-01-11 16:02:14 +08:00
DismissedLight
eec010870a improve web request experience 2023-01-11 13:11:09 +08:00
DismissedLight
a24fbf535d fix up oversea api style 2023-01-10 15:52:10 +08:00
DismissedLight
f7bd184a3c Merge pull request #352 from solacens/globalization/gotcha-histories
[Globalization] Read gotcha history from input of url or from cache/data_2
2023-01-10 15:22:06 +08:00
DismissedLight
267f285101 fix up oversea launcher support 2023-01-10 15:05:21 +08:00
solacens
2a1e77a9db add intl version gatcha history support 2023-01-10 17:46:29 +11:00
DismissedLight
abdc8e2e9f Merge pull request #334 from solacens/globalization/support-intl-version-game-launching
[Globalization] Support intl version game launching
2023-01-10 14:01:07 +08:00
solacens
64f1af293b merge main 2023-01-10 16:36:40 +11:00
DismissedLight
e0336d6b30 fix content dialog thread issue 2023-01-10 10:21:32 +08:00
DismissedLight
23f3e5df77 ContentDialogFactory 2023-01-09 12:15:11 +08:00
DismissedLight
4a027a8d3f remove filesystem context 2023-01-08 14:54:16 +08:00
DismissedLight
80459708a7 fix panel selector global group name 2023-01-08 12:32:43 +08:00
DismissedLight
650b67bea0 download static resource at startup 2023-01-07 18:27:45 +08:00
Masterain
18b3d23b1c Update azure-pipelines.yml for Azure Pipelines
- optimize CI logic for RPs
2023-01-03 19:17:14 -08:00
solacens
bf08ffa89e Missing changes 2023-01-03 12:20:13 +11:00
DismissedLight
915b1aae32 fix pipeline 2023-01-02 16:50:01 +08:00
DismissedLight
71a1bdc173 add empty page template view 2023-01-02 15:09:53 +08:00
solacens
af4180bdeb Add GenshinImpact.exe to check game running 2023-01-01 23:14:35 +11:00
solacens
a70593c529 Support international version game launching 2023-01-01 22:45:10 +11:00
DismissedLight
810f8704e6 use WinUICommunity.SettingsUI 2023-01-01 14:11:28 +08:00
DismissedLight
0165c03ae6 spiral abyss view 2023-01-01 13:38:27 +08:00
DismissedLight
423188c16a code style 2022-12-29 15:43:17 +08:00
DismissedLight
5fb935635b Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2022-12-29 15:03:33 +08:00
DismissedLight
761049ec17 cultivation improvement 2022-12-29 15:03:21 +08:00
Masterain
3f8c8874f3 Update azure-pipelines.yml for Azure Pipelines 2022-12-28 20:39:16 -08:00
Masterain
b8f354bbc7 Merge pull request #329 from Masterain98/main
devops improvements
2022-12-28 20:35:18 -08:00
Masterain
d45c40d4d7 Update bug-report.yml
add missing dropdown option
2022-12-28 20:19:48 -08:00
Masterain
6b309c4886 Update azure-pipelines.yml
add CI exception
2022-12-28 20:18:29 -08:00
DismissedLight
26e6d2008e add required field for API 2022-12-24 19:28:38 +08:00
Masterain
fb77bd2f6b Set up CI with Azure Pipelines
[skip ci]
2022-12-24 02:08:14 -08:00
DismissedLight
50459923f9 apply window-mode 2022-12-23 16:29:28 +08:00
DismissedLight
a97bab8a1c cultivation optimization 2022-12-22 14:20:19 +08:00
DismissedLight
bbc8324f5d fix feature request template 2022-12-20 15:29:06 +08:00
DismissedLight
2c0b32ab8b achievement progress 2022-12-20 15:11:05 +08:00
DismissedLight
0073636676 fix ci build 2022-12-18 15:23:43 +08:00
DismissedLight
7457d72e1b code style 2022-12-17 17:19:29 +08:00
DismissedLight
eed89b2ce1 cultivation for base avatar & weapon 2022-12-17 15:23:04 +08:00
DismissedLight
958fecdb77 avatar filter re-enable 2022-12-16 15:16:37 +08:00
Masterain
bcf38fbefc Update bug-report.yml 2022-12-15 00:42:34 -08:00
DismissedLight
ff146b4a2f cultivation 2022-12-15 16:14:16 +08:00
Masterain
419f8b8882 Update bug-report.yml 2022-12-13 21:06:59 -08:00
Masterain
7a5f1ded35 Update PublishDistribution.yml 2022-12-12 17:00:31 -08:00
DismissedLight
844c8b7810 Thread pool optimization 2022-12-10 13:50:42 +08:00
DismissedLight
267b34f571 xaml styles 2022-12-05 18:35:45 +08:00
Masterain
0f27ebc12c Update issue templates 2022-12-04 22:48:24 -08:00
DismissedLight
e051787584 add wiki weapon page 2022-11-29 18:16:59 +08:00
DismissedLight
8f273e69b5 support sign in bbs 2022-11-27 16:00:15 +08:00
Masterain
88037049f3 Create feature-request.yml
#265
2022-11-26 22:53:22 -08:00
DismissedLight
8357d3b971 check pooint 2 2022-11-25 16:16:52 +08:00
DismissedLight
d41acc0a77 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2022-11-25 14:52:11 +08:00
DismissedLight
0dd79d4206 web bridge check point 2022-11-25 14:51:30 +08:00
Masterain
86061e404f Update Issue Templates 2022-11-24 02:00:26 -08:00
DismissedLight
ee4197a18a fix code style 2022-11-24 13:40:14 +08:00
DismissedLight
c90e1ab8b8 obtain v2 stoken by old token #207 2022-11-24 13:19:46 +08:00
Masterain
de40947a7f Update PublishDistribution.yml
- add timeout field
2022-11-23 14:01:43 -08:00
DismissedLight
542a0a4622 fix dailynote refresh time 2022-11-20 15:13:24 +08:00
DismissedLight
a718ba16e2 introduce cache download retry and host replace 2022-11-20 14:54:04 +08:00
DismissedLight
c0d670c5b6 fix avatarInfo def value 2022-11-20 13:28:54 +08:00
DismissedLight
efcf0620d7 support ltoken requests #207 2022-11-18 21:39:45 +08:00
DismissedLight
a8bc0cf9b2 fixup ds algorithm call 2022-11-18 16:20:07 +08:00
DismissedLight
f1dda029e8 login by password 2022-11-16 19:48:07 +08:00
DismissedLight
d8b77369aa support rsa encrypt for passport api #207
Co-Authored-By: HolographicHat <58809250+HolographicHat@users.noreply.github.com>
2022-11-14 20:46:32 +08:00
DismissedLight
0be84a2585 support PROD salt for #207 2022-11-14 16:37:17 +08:00
DismissedLight
e29e12c9fe support login api for #207 2022-11-13 21:40:51 +08:00
DismissedLight
283df388bb fix game records for #207 2022-11-13 20:36:12 +08:00
DismissedLight
971f319b76 support Action Ticket for #207 2022-11-13 19:01:57 +08:00
DismissedLight
58b34ea60a support verifyLtoken for #207 2022-11-13 18:04:08 +08:00
DismissedLight
a150c4a04c support getWidgetData for #207 2022-11-13 15:38:02 +08:00
DismissedLight
9205d51cd5 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2022-11-11 18:55:08 +08:00
DismissedLight
a66a0b8a23 reduce using statements 2022-11-11 18:55:01 +08:00
Masterain
17c53dce4c Optimize Issue Template 2022-11-11 01:33:22 -08:00
Masterain
5049aa9cb6 Create artifact-rating-rules.yml
Inspired by #198
2022-11-11 01:32:47 -08:00
DismissedLight
aac1e62fd2 was 1.2.0 2022-11-11 17:20:36 +08:00
DismissedLight
5c1f861956 dailynote notification logic 2022-11-10 23:36:00 +08:00
DismissedLight
9667917559 file nesting 2022-11-09 12:08:59 +08:00
DismissedLight
9a3183e917 add launcher resource 2022-11-06 18:25:41 +08:00
DismissedLight
77db918178 update bug report template 2022-11-05 21:35:57 +08:00
DismissedLight
77158fc708 add installer 2022-11-05 20:45:38 +08:00
DismissedLight
f2d63e69ea bug fixes 2022-11-04 17:45:17 +08:00
DismissedLight
9e344f56e0 LaunchGame 2022-11-03 18:19:36 +08:00
DismissedLight
848392f8d4 prevent user service capture scoped app db context 2022-10-30 15:19:21 +08:00
DismissedLight
62d0fb5d05 launch game phase 1 2022-10-28 14:59:31 +08:00
686 changed files with 31918 additions and 10096 deletions

View File

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

109
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
name: 问题反馈
description: 告诉我们你的问题
title: "[Bug]: 在这里填写一个合适的标题"
body:
- type: markdown
attributes:
value: |
> **请在上方设置一个合适的工单标题**
> 请按下方的要求填写完整的问题表单,以便我们更快的定位问题。
- type: checkboxes
id: checklist
attributes:
label: 检查清单
description: |-
请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略
options:
- label: 我已完整阅读[胡桃工具箱文档](https://hut.ao/FAQ/most-frequent-questions.html),并认为我的问题没有在文档中得到解答
required: true
- label: 我使用的操作系统是[受支持的版本](https://hut.ao/quick-start.html#%E6%9C%80%E4%BD%8E%E7%B3%BB%E7%BB%9F%E8%A6%81%E6%B1%82)
required: true
- label: 我确认没有其他人已经提出了相同或类似的问题
required: true
- label: 我会在下方的表单中附上充足的信息以帮助开发人员确定问题
required: true
- type: input
id: winver
attributes:
label: Windows 版本
description: |
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
placeholder: 22000.556
validations:
required: true
- type: input
id: shver
attributes:
label: Snap Hutao 版本
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
placeholder: 1.1.0
validations:
required: true
- type: input
id: deviceid
attributes:
label: 设备 ID
description: |
在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
validations:
required: false
- type: dropdown
id: user-set-category
attributes:
label: 问题分类
description: 请设置一个你认为合适的分类,这将帮助我们快速定位问题
options:
- 安装和环境
- 成就管理
- 角色信息面板
- 游戏启动器
- 实时便笺
- 养成计算器
- 用户面板
- 文件缓存
- 祈愿记录
- 玩家查询
- 胡桃数据库
- 用户界面
- 签到
- Wiki
- 公告
- 其它
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 发生了什么?
description: 详细的描述问题发生前后的行为,以便我们解决问题
validations:
required: true
- type: textarea
id: what-expected
attributes:
label: 你期望发生的行为?
description: 详细的描述你期望发生的行为,突出与目前(可能不正确的)行为的不同
validations:
required: false
- type: textarea
id: logs
attributes:
label: 相关的崩溃日志
description: |
在资源管理器中直接输入`%userprofile%/Documents/Hutao`即可进入文件夹
如果应用程序崩溃了,请将`log.db` 文件上传,文件包含了敏感信息,谨慎上传
如果这个表单是关于导入祈愿记录的问题,请包含你导入的`Json`文件
> **务必不要上传`user.db`文件,该文件包含你的帐号敏感信息**

View File

@@ -1,89 +0,0 @@
name: 问题反馈
description: 告诉我们你的问题
title: "[Bug]: 在这里填写一个合适的标题"
labels: ["BUG"]
assignees:
- Lightczx
body:
- type: markdown
attributes:
value: |
请按下方的要求填写完整的问题表单,以便我们更快的定位问题。
- type: input
id: winver
attributes:
label: Windows 版本
description: |
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
placeholder: 22000.556
validations:
required: true
- type: input
id: shver
attributes:
label: Snap Hutao 版本
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
placeholder: 1.1.0
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 发生了什么?
description: 详细的描述问题发生前后的行为,以便我们解决问题
validations:
required: true
- type: textarea
id: what-expected
attributes:
label: 你期望发生的行为?
description: 详细的描述你期望发生的行为,突出与目前(可能不正确的)行为的不同
validations:
required: false
- type: textarea
id: logs
attributes:
label: 相关的崩溃日志 位于 `%HOMEPATH%/Documents/Hutao/Log.db`
description: |
在资源管理器中直接输入`%HOMEPATH%/Documents/Hutao`即可进入文件夹
如果应用程序崩溃了,请将`log.db` 文件上传,文件包含了敏感信息,谨慎上传
如果这个表单是关于导入祈愿记录的问题,请包含你导入的`Json`文件
**务必不要上传`user.db`文件,该文件包含你的帐号敏感信息**
render: shell
- type: checkboxes
id: confirm-issue
attributes:
label: 我确认已在表单中附上了充足的补充说明以帮助开发人员确定问题
description: 补充说明包括但不限于:日志文件、抛出的错误信息、截图和录屏
options:
- label:
required: true
- type: checkboxes
id: confirm-no-duplicated-issue
attributes:
label: 我确认没有他人提出相同或类似的问题
description: |
请先通过 Issue 搜索功能确认这不是相同的问题;
[BUG Issues](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aissue+is%3Aopen+label%3ABUG)
你应该在原始 Issue 中通过回复添加有助于解决问题的信息,而不是创建重复的问题;
**没有帮助的重复问题可能会被直接关闭**
options:
- label:
required: true
- type: checkboxes
id: confirm-docs
attributes:
label: 我确认该问题没有在文档中解释
description: Snap Hutao 官方文档:[https://hut.ao](https://hut.ao)
options:
- label:
required: true

View File

@@ -1,7 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 问题与操作指南文档
url: https://www.snapgenshin.com/documents/
about: |
[暂不可用]在发起 Issue 前 请查阅此文档确认文档中尚未包含你的问题的解决方案
- name: 胡桃工具箱 - 官方文档
url: https://hut.ao
about: 请在提出问题前阅读文档

View File

@@ -0,0 +1,27 @@
name: 功能请求
description: 告诉我们你的想法
title: "[Feat]: 在这里填写一个合适的标题"
labels: ["功能"]
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

View File

@@ -1,19 +0,0 @@
name: 功能请求
description: 告诉我们你的想法
title: "[Feat]: 在这里填写一个合适的标题"
labels: ["功能"]
assignees:
- Lightczx
body:
- type: markdown
attributes:
value: |
请按下方的要求填写完整的问题表单,以便我们更快的定位问题。
- type: textarea
id: req
attributes:
label: 你想要实现或优化的功能?
description: 详细的描述一下你想要的功能
validations:
required: true

View File

@@ -0,0 +1,78 @@
name: 网络问题
description: 当网络问题影响到你的程序使用时
title: "[Network]: 在这里填写一个合适的标题"
labels: ["area-Network"]
assignees:
- Lightczx
- Masterain98
body:
- type: markdown
attributes:
value: |
**请先在上方为工单设置一个合适的标题**
**请按下方的要求填写完整的问题表单,以便我们更快的定位问题。**
- type: textarea
id: network-diagnosis-report
attributes:
label: 提交你的网络诊断报告
description: |
停下!
**在填写下面的问题之前请先使用我们的网络诊断工具**
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
- 你可以点击下面的链接以下载网络诊断工具:
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-tool.exe)
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/network-diagnosis-hutao.exe)
validations:
required: true
- type: input
id: user-geo-location
attributes:
label: 你的地理位置
description: |
中国用户请精确到省级行政区
海外用户请精确到国家
placeholder: 北京
validations:
required: true
- type: dropdown
id: user-isp
attributes:
label: 你的运营商
description: 海外用户请选其它
options:
- 中国电信
- 中国联通
- 中国移动
- 中国广电
- 其它
validations:
required: true
- type: dropdown
id: user-issue-category
attributes:
label: 你的问题
description: 选择一个问题类别
options:
- 完全无法连接服务器
- 连接速度慢
- 获取到了不正确的页面或数据
- 客户端提示 429 Error
- 客户端图片下载错误
- 客户端图片预下载错误
- 其它
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 你的问题(补充)
description: 如果你在上一项中选择了`其它`或者你有更多信息需要提供,请在这里写下来
validations:
required: false

View File

@@ -16,17 +16,19 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@v3
# Download Publish.zip
# Download Assets
- name: Download Release
uses: robinraju/release-downloader@v1.5
timeout-minutes: 5
uses: robinraju/release-downloader@v1.7
with:
repository: "DGP-Studio/Snap.Hutao"
latest: true
fileName: "*.zip"
out-file-path: ./release-download
# Upload to OD21 (Testing)
- name: Upload OD21
# Upload to Drive
- name: Upload Drive
timeout-minutes: 15
env:
RCCONF: ${{ secrets.RCCONF }}
run: |
@@ -36,4 +38,14 @@ jobs:
$RCCONF
EOF
rclone copy ./release-download/* dgpODCN:/snaphutao/Releases/
rclone copy ./release-download/* dgpODCN:/releases/
# Purge Patch System Cache
- name: Purge Patch
env:
PATCH_HOSTS: ${{ secrets.PATCH_HOSTS }}
PURGE_TOKEN: ${{ secrets.PURGE_TOKEN }}
PURGE_URL: ${{ secrets.PURGE_URL }}
run: |
sudo echo "$PATCH_HOSTS" | sudo tee -a /etc/hosts
curl --header "Authorization: token $PURGE_TOKEN" $PURGE_URL

7
.gitignore vendored
View File

@@ -4,13 +4,14 @@
.vs/
src/Snap.Hutao/SettingsUI/bin
src/Snap.Hutao/SettingsUI/obj
src/Snap.Hutao/Snap.Hutao/bin/
src/Snap.Hutao/Snap.Hutao/obj/
src/Snap.Hutao/Snap.Hutao/Snap.Hutao_TemporaryKey.pfx
src/Snap.Hutao/Snap.Hutao.Installer/bin/
src/Snap.Hutao/Snap.Hutao.Installer/obj/
src/Snap.Hutao/Snap.Hutao.Installer/Properties/PublishProfiles/FolderProfile.pubxml.user
src/Snap.Hutao/Snap.Hutao.SourceGeneration/bin/
src/Snap.Hutao/Snap.Hutao.SourceGeneration/obj/

View File

@@ -1,34 +1,27 @@
# Snap.Hutao
# [Snap.Hutao](https://hut.ao)
![](https://repository-images.githubusercontent.com/482734649/5f8cf574-2ef0-43e9-aa8d-6cf094b54dd9)
> 唷,找本堂主有何贵干呀?
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)
## 项目首页(文档)
# 特别感谢
[![Deploy Docs](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml/badge.svg)](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml)
* [HolographicHat](https://github.com/HolographicHat)
* [UIGF organization](https://uigf.org)
[HUT.AO](https://hut.ao)
## 安装
* 前往 [下载页面](https://go.hut.ao/down) 下载最新版本的 `胡桃` 安装包
* (曾启用的可以跳过此步骤)在系统设置中打开 **开发者选项** 界面,勾选 `开发人员模式``允许 PowerShell 脚本`
* 完全解压后,右键使用 powershell 运行 `install.ps1` 文件
* 安装完成后可以关闭 `允许 PowerShell 脚本`
## 特别感谢
### 原神项目
### 特定的原神项目
* [biuuu/genshin-wish-export](https://github.com/biuuu/genshin-wish-export)
* [HolographicHat/YaeAchievement](https://github.com/HolographicHat/YaeAchievement)
* [xunkong/xunkong](https://github.com/xunkong/xunkong)
* [YuehaiTeam/cocogoat](https://github.com/YuehaiTeam/cocogoat)
### 技术栈
### 使用的技术栈
* [CommunityToolkit/dotnet](https://github.com/CommunityToolkit/dotnet)
* [CommunityToolkit/WindowsCommunityToolkit](https://github.com/CommunityToolkit/WindowsCommunityToolkit)
* [dahall/taskscheduler](https://github.com/dahall/taskscheduler)
* [dotnet/efcore](https://github.com/dotnet/efcore)
* [dotnet/runtime](https://github.com/dotnet/runtime)
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
@@ -37,4 +30,4 @@
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
* [MiniExcel/MiniExcel](https://github.com/MiniExcel/MiniExcel)
* [WinUICommunity/SettingsUI](https://github.com/WinUICommunity/SettingsUI)

180
azure-pipelines.yml Normal file
View File

@@ -0,0 +1,180 @@
# CI process script for Snap.Hutao
# Usage:
# 1. Append the script in Pipelines
# 2. Upload the pfx and cer certificates to Pipelines Library secrets
# 3. Permit the pfx usage
# 4. Add a `pw` variable in the script variables, which is pfx password
# 5. Connect the GitHub in project settings
# 6. Run
trigger:
branches:
include:
- main
paths:
exclude:
- README.md
- azure-pipelines.yml
- .github/ISSUE_TEMPLATE/*.yml
- .github/workflows/*.yml
pr:
branches:
include:
- main
paths:
exclude:
- README.md
- azure-pipelines.yml
- .github/ISSUE_TEMPLATE/*.yml
- .github/workflows/*.yml
pool:
vmImage: 'windows-2022'
variables:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
solution: '$(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao.sln'
project: $(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj'
buildPlatform: 'x64'
buildConfiguration: 'Release'
build_date: $[ format('{0:yyyy}.{0:M}.{0:d}', pipeline.startTime) ]
steps:
- task: GetRevision@1
displayName: get Pipelines revision number
inputs:
VariableName: 'rev_number'
- task: UseDotNet@2
displayName: Install dotNet
inputs:
packageType: 'sdk'
version: '7.x'
includePreviewVersions: true
- task: NuGetToolInstaller@1
name: 'NuGetToolInstaller'
displayName: 'NuGet Installer'
- task: NuGetCommand@2
displayName: NuGet restore
inputs:
command: 'restore'
restoreSolution: '$(solution)'
feedsToUse: 'select'
- task: MsixPackaging@1
displayName: Build binary package
inputs:
outputPath: '$(Build.ArtifactStagingDirectory)/'
solution: '$(solution)'
clean: false
generateBundle: false
buildConfiguration: 'Release'
buildPlatform: 'x64'
updateAppVersion: false
appPackageDistributionMode: 'SideloadOnly'
msbuildLocationMethod: 'location'
msbuildLocation: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Msbuild\Current\Bin\MSBuild.exe'
- task: MagicChunks@2
inputs:
sourcePath: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64\AppxManifest.xml'
fileType: 'Xml'
targetPathType: 'source'
transformationType: 'json'
transformations: |
{
"Package/Identity/@Name": "7f0db578-026f-4e0b-a75b-d5d06bb0a74c",
"Package/Identity/@Publisher": "CN=DGP Studio CI",
"Package/Identity/@Version": "$(build_date).$(rev_number)",
"Package/Properties/DisplayName": "胡桃 Alpha",
"Package/Properties/PublisherDisplayName":"DGP Studio CI",
"Package/Applications/Application/uap:VisualElements/@DisplayName": "胡桃 Alpha"
}
- task: CmdLine@2
displayName: Create resources folder
inputs:
script: |
mkdir Assets
mkdir Resource
workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64'
- task: CopyFiles@2
displayName: Copy Assets Folder
inputs:
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Assets'
Contents: '**'
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64\Assets'
- task: CopyFiles@2
displayName: Copy Resource Folder
inputs:
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Resource'
Contents: '**'
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64\Resource'
- task: CmdLine@2
displayName: Build MSIX
inputs:
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22000.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.18362.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
- task: MsixSigning@1
displayName: Sign MSIX package
inputs:
package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
certificate: 'DGP_Studio_CI.pfx'
passwordVariable: 'pw'
- task: PublishPipelineArtifact@1
displayName: 'Upload Output'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/'
artifact: 'Output'
publishLocation: 'pipeline'
- task: DownloadSecureFile@1
name: cerFile
displayName: Download Root CA
inputs:
secureFile: 'Snap.Hutao.CI.cer'
- task: GitHubRelease@1
condition: or(eq(variables['Build.Reason'], 'Manual'), eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'))
inputs:
gitHubConnection: 'github.com_Masterain'
repositoryName: 'DGP-Studio/Snap.Hutao'
action: 'create'
target: '$(Build.SourceVersion)'
tagSource: 'userSpecifiedTag'
tag: '$(build_date).$(rev_number)'
title: '$(build_date).$(rev_number)'
releaseNotesSource: 'inline'
releaseNotesInline: |
## 提示 (Hint)
该发布版本由 CI 程序自动打包生成,属于 `Alpha` 测试版,仅用于开发调试和内部测试用途。使用该版本可能存在意料之外的风险,请仅在有明确用途的情况下使用该版本。
This release is a Alpha Testing version generated by CI program automatically in a purpose of debugging and interal testing. Using this release may have unexpected risk, please only use it when you know what you are doing.
assets: |
$(Build.ArtifactStagingDirectory)/*
$(cerFile.secureFilePath)
isPreRelease: true
changeLogCompareToRelease: 'lastFullRelease'
changeLogType: 'commitBased'
- task: DownloadSecureFile@1
name: RcloneConfigFile
displayName: Download Rclone Config
inputs:
secureFile: 'rclone.conf'
- task: rclone@1
displayName: Upload CI via Rclone
inputs:
arguments: 'copy $(Build.ArtifactStagingDirectory)/* downloadDGPCN:/releases/Alpha/'
configPath: '$(RcloneConfigFile.secureFilePath)/rclone.conf'

6
desktop.ini Normal file
View File

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

View File

@@ -1,4 +1,6 @@
[*.cs]
charset = utf-8-bom
[*.cs]
# SA1101: Prefix local calls with this
dotnet_diagnostic.SA1101.severity = none
@@ -12,9 +14,9 @@ csharp_style_expression_bodied_accessors = when_on_single_line:silent
csharp_style_expression_bodied_lambdas = when_on_single_line:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_conditional_delegate_call = true:suggestion
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_when_type_is_apparent = false:warning
csharp_style_var_elsewhere = false:warning
csharp_prefer_simple_using_statement = false:suggestion
csharp_prefer_braces = true:silent
csharp_style_namespace_declarations = file_scoped:silent
@@ -22,11 +24,11 @@ csharp_prefer_static_local_function = false:suggestion
[*.{cs,vb}]
end_of_line = crlf
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
dotnet_code_quality_unused_parameters = non_public:suggestion
dotnet_style_readonly_field = true:suggestion
indent_size = 4
@@ -76,34 +78,36 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.non_field_members.required_modifiers =
# 命名样式
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_diagnostic.SA1629.severity = silent
dotnet_diagnostic.SA1642.severity = silent
dotnet_diagnostic.SA1629.severity = none
dotnet_diagnostic.SA1642.severity = none
dotnet_diagnostic.IDE0060.severity = none
# SA1208: System using directives should be placed before other using directives
dotnet_diagnostic.SA1208.severity = none
@@ -160,6 +164,8 @@ dotnet_diagnostic.CA1805.severity = suggestion
# VSTHRD111: Use ConfigureAwait(bool)
dotnet_diagnostic.VSTHRD111.severity = suggestion
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
[*.vb]
#### 命名样式 ####
@@ -182,29 +188,29 @@ dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.style = 帕斯
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.类型.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.类型.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.类型.required_modifiers =
dotnet_naming_symbols.类型.required_modifiers =
dotnet_naming_symbols.非字段成员.applicable_kinds = property, event, method
dotnet_naming_symbols.非字段成员.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.非字段成员.required_modifiers =
dotnet_naming_symbols.非字段成员.required_modifiers =
# 命名样式
dotnet_naming_style.以_i_开始.required_prefix = I
dotnet_naming_style.以_i_开始.required_suffix =
dotnet_naming_style.以_i_开始.word_separator =
dotnet_naming_style.以_i_开始.required_suffix =
dotnet_naming_style.以_i_开始.word_separator =
dotnet_naming_style.以_i_开始.capitalization = pascal_case
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case

View File

@@ -1,80 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using System.ComponentModel;
namespace SettingsUI.Controls;
public class CheckBoxWithDescriptionControl : CheckBox
{
private readonly CheckBoxWithDescriptionControl _checkBoxSubTextControl;
public CheckBoxWithDescriptionControl()
{
_checkBoxSubTextControl = this;
Loaded += CheckBoxSubTextControl_Loaded;
}
protected override void OnApplyTemplate()
{
Update();
base.OnApplyTemplate();
}
private void Update()
{
if (!string.IsNullOrEmpty(Header))
{
AutomationProperties.SetName(this, Header);
}
}
private void CheckBoxSubTextControl_Loaded(object sender, RoutedEventArgs e)
{
StackPanel panel = new() { Orientation = Orientation.Vertical };
// Add text box only if the description is not empty. Required for additional plugin options.
if (!string.IsNullOrWhiteSpace(Description))
{
panel.Children.Add(new TextBlock() { Margin = new Thickness(0, 10, 0, 0), Text = Header });
//Style = (Style)Application.Current.Resources["SecondaryIsEnabledTextBlockStyle"]
panel.Children.Add(new IsEnabledTextBlock() { Text = Description });
}
else
{
panel.Children.Add(new TextBlock() { Margin = new Thickness(0, 0, 0, 0), Text = Header });
}
_checkBoxSubTextControl.Content = panel;
}
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
"Header",
typeof(string),
typeof(CheckBoxWithDescriptionControl),
new PropertyMetadata(default(string)));
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
"Description",
typeof(object),
typeof(CheckBoxWithDescriptionControl),
new PropertyMetadata(default(string)));
[Localizable(true)]
public string Header
{
get => (string)GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
[Localizable(true)]
public string Description
{
get => (string)GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
}

View File

@@ -1,50 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.ComponentModel;
namespace SettingsUI.Controls;
[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")]
public class IsEnabledTextBlock : Control
{
public IsEnabledTextBlock()
{
DefaultStyleKey = typeof(IsEnabledTextBlock);
}
protected override void OnApplyTemplate()
{
IsEnabledChanged -= IsEnabledTextBlock_IsEnabledChanged;
SetEnabledState();
IsEnabledChanged += IsEnabledTextBlock_IsEnabledChanged;
base.OnApplyTemplate();
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text",
typeof(string),
typeof(IsEnabledTextBlock),
null);
[Localizable(true)]
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
private void IsEnabledTextBlock_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
SetEnabledState();
}
private void SetEnabledState()
{
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
}
}

View File

@@ -1,38 +0,0 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:SettingsUI.Controls">
<Style TargetType="local:IsEnabledTextBlock">
<Setter Property="Foreground" Value="{ThemeResource DefaultTextForegroundThemeBrush}" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:IsEnabledTextBlock">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="Label.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<TextBlock x:Name="Label"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
FontFamily="{TemplateBinding FontFamily}"
Foreground="{TemplateBinding Foreground}"
TextWrapping="WrapWholeWords"
Text="{TemplateBinding Text}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="local:IsEnabledTextBlock" x:Key="SecondaryIsEnabledTextBlockStyle">
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
<Setter Property="FontSize" Value="{StaticResource SecondaryTextFontSize}"/>
</Style>
</ResourceDictionary>

View File

@@ -1,156 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using System.ComponentModel;
namespace SettingsUI.Controls;
[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")]
[TemplatePart(Name = PartIconPresenter, Type = typeof(ContentPresenter))]
[TemplatePart(Name = PartDescriptionPresenter, Type = typeof(ContentPresenter))]
public class Setting : ContentControl
{
private const string PartIconPresenter = "IconPresenter";
private const string PartDescriptionPresenter = "DescriptionPresenter";
private ContentPresenter? _iconPresenter;
private ContentPresenter? _descriptionPresenter;
private Setting? _setting;
public Setting()
{
DefaultStyleKey = typeof(Setting);
}
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
"Header",
typeof(string),
typeof(Setting),
new PropertyMetadata(default(string), OnHeaderChanged));
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
"Description",
typeof(object),
typeof(Setting),
new PropertyMetadata(null, OnDescriptionChanged));
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
"Icon",
typeof(object),
typeof(Setting),
new PropertyMetadata(default(string), OnIconChanged));
public static readonly DependencyProperty ActionContentProperty = DependencyProperty.Register(
"ActionContent",
typeof(object),
typeof(Setting),
null);
[Localizable(true)]
public string Header
{
get => (string)GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
[Localizable(true)]
public object Description
{
get => GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
public object Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public object ActionContent
{
get => GetValue(ActionContentProperty);
set => SetValue(ActionContentProperty, value);
}
protected override void OnApplyTemplate()
{
IsEnabledChanged -= Setting_IsEnabledChanged;
_setting = this;
_iconPresenter = (ContentPresenter)_setting.GetTemplateChild(PartIconPresenter);
_descriptionPresenter = (ContentPresenter)_setting.GetTemplateChild(PartDescriptionPresenter);
Update();
SetEnabledState();
IsEnabledChanged += Setting_IsEnabledChanged;
base.OnApplyTemplate();
}
private static void OnHeaderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((Setting)d).Update();
}
private static void OnIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((Setting)d).Update();
}
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((Setting)d).Update();
}
private void Setting_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
SetEnabledState();
}
private void SetEnabledState()
{
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
}
private void Update()
{
if (_setting == null)
{
return;
}
if (_setting.ActionContent != null)
{
if (_setting.ActionContent.GetType() != typeof(Button))
{
// We do not want to override the default AutomationProperties.Name of a button. Its Content property already describes what it does.
if (!string.IsNullOrEmpty(_setting.Header))
{
AutomationProperties.SetName((UIElement)_setting.ActionContent, _setting.Header);
}
}
}
if (_setting._iconPresenter != null)
{
if (_setting.Icon == null)
{
_setting._iconPresenter.Visibility = Visibility.Collapsed;
}
else
{
_setting._iconPresenter.Visibility = Visibility.Visible;
}
}
if (_setting.Description == null)
{
_setting._descriptionPresenter!.Visibility = Visibility.Collapsed;
}
else
{
_setting._descriptionPresenter!.Visibility = Visibility.Visible;
}
}
}

View File

@@ -1,107 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:SettingsUI.Controls">
<Style TargetType="controls:Setting">
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="Background" Value="{ThemeResource CardBackgroundBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource CardBorderThickness}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Padding" Value="16" />
<Setter Property="Margin" Value="0,0,0,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:Setting">
<Grid x:Name="RootGrid"
CornerRadius="{TemplateBinding CornerRadius}"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
MinHeight="48">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="HeaderPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
<Setter Target="DescriptionPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<!-- Icon -->
<ColumnDefinition Width="*"/>
<!-- Header and subtitle -->
<ColumnDefinition Width="Auto"/>
<!-- Action control -->
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="IconPresenter"
Content="{TemplateBinding Icon}"
HorizontalAlignment="Center"
FontSize="20"
IsTextScaleFactorEnabled="False"
Margin="2,0,18,0"
MaxWidth="20"
AutomationProperties.AccessibilityView="Raw"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
VerticalAlignment="Center"/>
<StackPanel
VerticalAlignment="Center"
Grid.Column="1"
HorizontalAlignment="Stretch"
Margin="0,0,16,0">
<TextBlock
x:Name="HeaderPresenter"
Text="{TemplateBinding Header}"
VerticalAlignment="Center"
Foreground="{ThemeResource CardPrimaryForegroundBrush}" />
<ContentPresenter
x:Name="DescriptionPresenter"
Content="{TemplateBinding Description}"
FontSize="{StaticResource SecondaryTextFontSize}"
TextWrapping="WrapWholeWords"
Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<ContentPresenter.Resources>
<Style TargetType="TextBlock" BasedOn="{StaticResource CaptionTextBlockStyle}">
<Style.Setters>
<Setter Property="TextWrapping" Value="WrapWholeWords"/>
</Style.Setters>
</Style>
<Style TargetType="HyperlinkButton" BasedOn="{StaticResource TextButtonStyle}">
<Style.Setters>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Padding" Value="0,0,0,0"/>
</Style.Setters>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
</StackPanel>
<ContentPresenter
x:Name="ContentPresenter"
Content="{TemplateBinding ActionContent}"
Grid.Column="2"
VerticalAlignment="Center"
HorizontalAlignment="Right" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,37 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
namespace SettingsUI.Controls;
public partial class SettingExpander : Expander
{
public SettingExpander()
{
DefaultStyleKey = typeof(Expander);
Style = (Style)Application.Current.Resources["SettingExpanderStyle"];
RegisterPropertyChangedCallback(Expander.HeaderProperty, OnHeaderChanged);
}
private static void OnHeaderChanged(DependencyObject d, DependencyProperty dp)
{
SettingExpander self = (SettingExpander)d;
if (self.Header != null)
{
if (self.Header.GetType() == typeof(Setting))
{
Setting selfSetting = (Setting)self.Header;
selfSetting.Style = (Style)Application.Current.Resources["ExpanderHeaderSettingStyle"];
if (!string.IsNullOrEmpty(selfSetting.Header))
{
AutomationProperties.SetName(self, selfSetting.Header);
}
}
}
}
}

View File

@@ -1,101 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using System.ComponentModel;
namespace SettingsUI.Controls;
/// <summary>
/// Represents a control that can contain multiple settings (or other) controls
/// </summary>
[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")]
[TemplatePart(Name = PartDescriptionPresenter, Type = typeof(ContentPresenter))]
public partial class SettingsGroup : ItemsControl
{
private const string PartDescriptionPresenter = "DescriptionPresenter";
private ContentPresenter? _descriptionPresenter;
private SettingsGroup? _settingsGroup;
public SettingsGroup()
{
DefaultStyleKey = typeof(SettingsGroup);
}
[Localizable(true)]
public string Header
{
get => (string)GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
"Header",
typeof(string),
typeof(SettingsGroup),
new PropertyMetadata(default(string)));
[Localizable(true)]
public object Description
{
get => GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
"Description",
typeof(object),
typeof(SettingsGroup),
new PropertyMetadata(null, OnDescriptionChanged));
protected override void OnApplyTemplate()
{
IsEnabledChanged -= SettingsGroup_IsEnabledChanged;
_settingsGroup = this;
_descriptionPresenter = (ContentPresenter)_settingsGroup.GetTemplateChild(PartDescriptionPresenter);
SetEnabledState();
IsEnabledChanged += SettingsGroup_IsEnabledChanged;
base.OnApplyTemplate();
}
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((SettingsGroup)d).Update();
}
private void SettingsGroup_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
SetEnabledState();
}
private void SetEnabledState()
{
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
}
private void Update()
{
if (_settingsGroup == null)
{
return;
}
if (_settingsGroup.Description == null)
{
_settingsGroup._descriptionPresenter!.Visibility = Visibility.Collapsed;
}
else
{
_settingsGroup._descriptionPresenter!.Visibility = Visibility.Visible;
}
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new SettingsGroupAutomationPeer(this);
}
}

View File

@@ -1,71 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:SettingsUI.Controls">
<Style TargetType="controls:SettingsGroup">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="2"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="IsTabStop" Value="False" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:SettingsGroup">
<Grid HorizontalAlignment="Stretch">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="HeaderPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
<Setter Target="DescriptionPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock x:Name="HeaderPresenter"
Text="{TemplateBinding Header}"
Grid.Row="0"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Margin="1,32,0,0"
AutomationProperties.HeadingLevel="Level2"/>
<ContentPresenter
x:Name="DescriptionPresenter"
Content="{TemplateBinding Description}"
TextWrapping="WrapWholeWords"
Margin="1,4,0,0"
Grid.Row="1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<ContentPresenter.Resources>
<Style TargetType="TextBlock" BasedOn="{StaticResource CaptionTextBlockStyle}">
<Style.Setters>
<Setter Property="TextWrapping" Value="WrapWholeWords"/>
</Style.Setters>
</Style>
<Style TargetType="HyperlinkButton" BasedOn="{StaticResource TextButtonStyle}">
<Style.Setters>
<Setter Property="Padding" Value="0,0,0,0"/>
</Style.Setters>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
<ItemsPresenter Grid.Row="2" Margin="0,8,0,0"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,21 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml.Automation.Peers;
namespace SettingsUI.Controls;
public class SettingsGroupAutomationPeer : FrameworkElementAutomationPeer
{
public SettingsGroupAutomationPeer(SettingsGroup owner)
: base(owner)
{
}
protected override string GetNameCore()
{
SettingsGroup? selectedSettingsGroup = (SettingsGroup)Owner;
return selectedSettingsGroup.Header;
}
}

View File

@@ -1,49 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0-windows10.0.17763.0</TargetFrameworks>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>SettingsUI</RootNamespace>
<Platforms>x64</Platforms>
<RuntimeIdentifiers>win10-x64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.4" />
</ItemGroup>
<ItemGroup>
<Page Update="Controls\IsEnabledTextBlock\IsEnabledTextBlock.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\SettingsGroup\SettingsGroup.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\Setting\Setting.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\Button.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\Common.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\TextBlock.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Themes\Colors.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Themes\Generic.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Themes\SettingsExpanderStyles.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Themes\SettingsUI.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

View File

@@ -1,582 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:contract7NotPresent="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractNotPresent(Windows.Foundation.UniversalApiContract,7)"
xmlns:contract7Present="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract,7)">
<Style x:Key="SettingButtonStyle" TargetType="Button" BasedOn="{StaticResource DefaultButtonStyle}" >
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="Padding" Value="16,5,16,6" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
<Style x:Key="HyperlinkButtonStyle" TargetType="HyperlinkButton" >
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
</Style>
<Style x:Key="TextButtonStyle" TargetType="ButtonBase">
<Setter Property="Background" Value="{ThemeResource HyperlinkButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource HyperlinkButtonForeground}" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ButtonBase">
<Grid Margin="{TemplateBinding Padding}" CornerRadius="4" Background="{TemplateBinding Background}">
<ContentPresenter x:Name="Text"
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
FontWeight="SemiBold"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBorderBrushPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBorderBrushPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBackgroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Text" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource HyperlinkButtonBorderBrushDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- This style overrides the default style so that all ToggleSwitches are right aligned, with the label on the left -->
<Style x:Key="ToggleSwitchSettingStyle" TargetType="ToggleSwitch">
<Setter Property="Foreground" Value="{ThemeResource ToggleSwitchContentForeground}" />
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Right" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="System,TranslateX" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,-3,-7,-3" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleSwitch">
<Grid
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
contract7Present:CornerRadius="{TemplateBinding CornerRadius}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContainerBackground}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Width" EnableDependentAnimation="True" >
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Width" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="PointerOver">
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffPointerOver}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffPointerOver}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundPointerOver}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Width" EnableDependentAnimation="True" >
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Width" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="SwitchKnobOn.HorizontalAlignment" Value="Right" />
<Setter Target="SwitchKnobOn.Margin" Value="0,0,3,0" />
<Setter Target="SwitchKnobOff.HorizontalAlignment" Value="Left" />
<Setter Target="SwitchKnobOff.Margin" Value="3,0,0,0" />
</VisualState.Setters>
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffPressed}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffPressed}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundPressed}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Width" EnableDependentAnimation="True" >
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="17" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Width" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="17" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="14" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HeaderContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchHeaderForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OffContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContentForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OnContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContentForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffDisabled}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffDisabled}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundDisabled}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Width" EnableDependentAnimation="True" >
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlNormalAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlNormalAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Width" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlNormalAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Height" EnableDependentAnimation="True">
<SplineDoubleKeyFrame KeyTime="{StaticResource ControlNormalAnimationDuration}" KeySpline="{StaticResource ControlFastOutSlowInKeySpline}" Value="12" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ToggleStates">
<VisualStateGroup.Transitions>
<VisualTransition x:Name="DraggingToOnTransition"
From="Dragging"
To="On"
GeneratedDuration="0">
<Storyboard>
<RepositionThemeAnimation TargetName="SwitchKnob" FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOnOffset}" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OnToDraggingTransition"
From="On"
To="Dragging"
GeneratedDuration="0">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="DraggingToOffTransition"
From="Dragging"
To="Off"
GeneratedDuration="0">
<Storyboard>
<RepositionThemeAnimation TargetName="SwitchKnob" FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOffOffset}" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OnToOffTransition"
From="On"
To="Off"
GeneratedDuration="0">
<Storyboard>
<RepositionThemeAnimation TargetName="SwitchKnob" FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOnToOffOffset}" />
</Storyboard>
</VisualTransition>
<VisualTransition x:Name="OffToOnTransition"
From="Off"
To="On"
GeneratedDuration="0">
<Storyboard>
<RepositionThemeAnimation TargetName="SwitchKnob" FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOffToOnOffset}"/>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Dragging" />
<VisualState x:Name="Off" />
<VisualState x:Name="On">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="KnobTranslateTransform"
Storyboard.TargetProperty="X"
To="20"
Duration="0" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ContentStates">
<VisualState x:Name="OffContent">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="OffContentPresenter"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="IsHitTestVisible" Storyboard.TargetName="OffContentPresenter">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<x:Boolean>True</x:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="OnContent">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="OnContentPresenter"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="IsHitTestVisible" Storyboard.TargetName="OnContentPresenter">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<x:Boolean>True</x:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter x:Name="HeaderContentPresenter"
x:DeferLoadStrategy="Lazy"
Grid.Row="0"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
Foreground="{ThemeResource ToggleSwitchHeaderForeground}"
IsHitTestVisible="False"
Margin="{ThemeResource ToggleSwitchTopHeaderMargin}"
TextWrapping="Wrap"
VerticalAlignment="Top"
Visibility="Collapsed"
AutomationProperties.AccessibilityView="Raw" />
<Grid
Grid.Row="1"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource ToggleSwitchPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource ToggleSwitchPostContentMargin}" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="12" MaxWidth="12" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid x:Name="SwitchAreaGrid"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
Margin="0,5"
contract7Present:CornerRadius="{TemplateBinding CornerRadius}"
contract7NotPresent:CornerRadius="{StaticResource ControlCornerRadius}"
Control.IsTemplateFocusTarget="True"
Background="{ThemeResource ToggleSwitchContainerBackground}" />
<ContentPresenter x:Name="OffContentPresenter"
Grid.RowSpan="3"
Grid.Column="0"
Opacity="0"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Content="{TemplateBinding OffContent}"
ContentTemplate="{TemplateBinding OffContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw" />
<ContentPresenter x:Name="OnContentPresenter"
Grid.RowSpan="3"
Grid.Column="0"
Opacity="0"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Content="{TemplateBinding OnContent}"
ContentTemplate="{TemplateBinding OnContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw" />
<Rectangle x:Name="OuterBorder"
Grid.Row="1"
Grid.Column="2"
Height="20"
Width="40"
RadiusX="10"
RadiusY="10"
Fill="{ThemeResource ToggleSwitchFillOff}"
Stroke="{ThemeResource ToggleSwitchStrokeOff}"
StrokeThickness="{ThemeResource ToggleSwitchOuterBorderStrokeThickness}" />
<Rectangle x:Name="SwitchKnobBounds"
Grid.Row="1"
Height="20"
Width="40"
RadiusX="10"
RadiusY="10"
Grid.Column="2"
Fill="{ThemeResource ToggleSwitchFillOn}"
Stroke="{ThemeResource ToggleSwitchStrokeOn}"
StrokeThickness="{ThemeResource ToggleSwitchOnStrokeThickness}"
Opacity="0" />
<Grid x:Name="SwitchKnob"
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Left"
Width="20"
Height="20">
<Border x:Name="SwitchKnobOn"
Background="{ThemeResource ToggleSwitchKnobFillOn}"
BorderBrush="{ThemeResource ToggleSwitchKnobStrokeOn}"
contract7Present:BackgroundSizing="OuterBorderEdge"
Width="12"
Height="12"
CornerRadius="7"
Grid.Column="2"
Opacity="0"
HorizontalAlignment="Center"
Margin="0,0,1,0"
RenderTransformOrigin="0.5, 0.5">
<Border.RenderTransform>
<CompositeTransform/>
</Border.RenderTransform>
</Border>
<Rectangle x:Name="SwitchKnobOff"
Fill="{ThemeResource ToggleSwitchKnobFillOff}"
Width="12"
Height="12"
RadiusX="7"
Grid.Column="2"
RadiusY="7"
HorizontalAlignment="Center"
Margin="-1,0,0,0"
RenderTransformOrigin="0.5, 0.5">
<Rectangle.RenderTransform>
<CompositeTransform/>
</Rectangle.RenderTransform>
</Rectangle>
<Grid.RenderTransform>
<TranslateTransform x:Name="KnobTranslateTransform" />
</Grid.RenderTransform>
</Grid>
<Thumb x:Name="SwitchThumb"
AutomationProperties.AccessibilityView="Raw"
Grid.RowSpan="3"
Grid.ColumnSpan="3">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Rectangle Fill="Transparent" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,15 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:controls="using:SettingsUI.Controls"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="ListViewItemSettingStyle" TargetType="ListViewItem">
<Setter Property="Margin" Value="0,0,0,2" />
<Setter Property="Padding" Value="0,0,0,0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
<Style
TargetType="controls:CheckBoxWithDescriptionControl"
BasedOn="{StaticResource DefaultCheckBoxStyle}" />
</ResourceDictionary>

View File

@@ -1,22 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="OobeSubtitleStyle" TargetType="TextBlock">
<Setter Property="Margin" Value="0,16,0,0" />
<Setter Property="Foreground" Value="{ThemeResource DefaultTextForegroundThemeBrush}" />
<Setter Property="AutomationProperties.HeadingLevel" Value="Level3" />
<Setter Property="FontFamily" Value="XamlAutoFontFamily" />
<Setter Property="FontSize" Value="{StaticResource BodyTextBlockFontSize}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="LineStackingStrategy" Value="MaxHeight" />
<Setter Property="TextLineBounds" Value="Full" />
</Style>
<x:Double x:Key="SecondaryTextFontSize">12</x:Double>
<Style x:Key="SecondaryTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource SecondaryTextFontSize}"/>
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}"/>
</Style>
</ResourceDictionary>

View File

@@ -1,32 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<StaticResource x:Key="CardBackgroundBrush" ResourceKey="CardBackgroundFillColorDefaultBrush" />
<StaticResource x:Key="CardBorderBrush" ResourceKey="CardStrokeColorDefaultBrush" />
<StaticResource x:Key="CardPrimaryForegroundBrush" ResourceKey="TextFillColorPrimaryBrush" />
<SolidColorBrush x:Key="InfoBarInformationalSeverityBackgroundBrush" Color="#FF34424d"/>
<Color x:Key="InfoBarInformationalSeverityIconBackground">#FF5fb2f2</Color>
<Thickness x:Key="CardBorderThickness">1</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="CardBackgroundBrush" ResourceKey="CardBackgroundFillColorDefaultBrush" />
<StaticResource x:Key="CardBorderBrush" ResourceKey="CardStrokeColorDefaultBrush" />
<StaticResource x:Key="CardPrimaryForegroundBrush" ResourceKey="TextFillColorPrimaryBrush" />
<SolidColorBrush x:Key="InfoBarInformationalSeverityBackgroundBrush" Color="#FFd3e7f7"/>
<Color x:Key="InfoBarInformationalSeverityIconBackground">#FF0063b1</Color>
<Thickness x:Key="CardBorderThickness">1</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<StaticResource x:Key="CardBackgroundBrush" ResourceKey="SystemColorButtonFaceColorBrush" />
<StaticResource x:Key="CardBorderBrush" ResourceKey="SystemColorButtonTextColorBrush" />
<StaticResource x:Key="CardPrimaryForegroundBrush" ResourceKey="SystemColorButtonTextColorBrush" />
<SolidColorBrush x:Key="InfoBarInformationalSeverityBackgroundBrush" Color="#FF34424d"/>
<Color x:Key="InfoBarInformationalSeverityIconBackground">#FF5fb2f2</Color>
<Thickness x:Key="CardBorderThickness">2</Thickness>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

View File

@@ -1,9 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///SettingsUI/Controls/Setting/Setting.xaml" />
<ResourceDictionary Source="ms-appx:///SettingsUI/Controls/SettingsGroup/SettingsGroup.xaml" />
<ResourceDictionary Source="ms-appx:///SettingsUI/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -1,52 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:SettingsUI.Controls">
<!-- Thickness -->
<Thickness x:Key="ExpanderContentPadding">0</Thickness>
<Thickness x:Key="ExpanderSettingMargin">56, 8, 40, 8</Thickness>
<SolidColorBrush x:Key="ExpanderChevronPointerOverBackground">Transparent</SolidColorBrush>
<!-- Styles -->
<!-- Setting used in a Expander header -->
<Style x:Key="ExpanderHeaderSettingStyle"
TargetType="controls:Setting">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Padding" Value="0,14,0,14" />
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
<Thickness x:Key="ExpanderChevronMargin">0,0,8,0</Thickness>
<!-- Setting used in a Expander header -->
<Style x:Key="ExpanderContentSettingStyle"
TargetType="controls:Setting">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0,1,0,0" />
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Padding" Value="{StaticResource ExpanderSettingMargin}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
<!-- Setting expander style -->
<Style x:Key="SettingExpanderStyle"
TargetType="Expander">
<Setter Property="Background" Value="{ThemeResource CardBackgroundBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource CardBorderThickness}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
<Style x:Key="ExpanderSeparatorStyle"
TargetType="Rectangle">
<Setter Property="Height" Value="1" />
<Setter Property="Stroke" Value="{ThemeResource CardBorderBrush}" />
</Style>
</ResourceDictionary>

View File

@@ -1,14 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Styles/Common.xaml"/>
<ResourceDictionary Source="../Styles/TextBlock.xaml"/>
<ResourceDictionary Source="../Styles/Button.xaml"/>
<ResourceDictionary Source="../Themes/Colors.xaml"/>
<ResourceDictionary Source="../Themes/SettingsExpanderStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<x:Double x:Key="SettingActionControlMinWidth">240</x:Double>
</ResourceDictionary>

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
@@ -22,8 +23,10 @@ public class HttpClientGenerator : ISourceGenerator
{
private const string DefaultName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.Default";
private const string XRpcName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc";
private const string XRpc2Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc2";
private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute";
private const string DynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
@@ -44,12 +47,14 @@ public class HttpClientGenerator : ISourceGenerator
string toolName = this.GetGeneratorType().FullName;
StringBuilder sourceCodeBuilder = new();
sourceCodeBuilder.Append($@"// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// This class is generated by Snap.Hutao.SourceGeneration
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Net.Http;
namespace Snap.Hutao.Core.DependencyInjection;
@@ -77,10 +82,7 @@ internal static partial class IocHttpClientConfiguration
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
{
lineBuilder
.Clear()
.Append("\r\n");
lineBuilder.Clear().Append("\r\n");
lineBuilder.Append(@" services.AddHttpClient<");
lineBuilder.Append($"{classSymbol.ToDisplayString()}>(");
@@ -100,6 +102,9 @@ internal static partial class IocHttpClientConfiguration
case XRpcName:
lineBuilder.Append(@"XRpcConfiguration)");
break;
case XRpc2Name:
lineBuilder.Append(@"XRpc2Configuration)");
break;
default:
throw new InvalidOperationException($"非法的HttpClientConfigration值: [{injectAsName}]");
}
@@ -125,6 +130,11 @@ internal static partial class IocHttpClientConfiguration
lineBuilder.Append(" })");
}
if (classSymbol.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == DynamicSecretAttributeName))
{
lineBuilder.Append(".AddHttpMessageHandler<DynamicSecretHandler>()");
}
lineBuilder.Append(";");
lines.Add(lineBuilder.ToString());
@@ -135,4 +145,34 @@ internal static partial class IocHttpClientConfiguration
sourceCodeBuilder.Append(line);
}
}
private class HttpClientSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
}
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
/// <summary>
/// Created on demand before each generation pass
/// </summary>
public class HttpClientSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
}
}

View File

@@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
@@ -52,7 +53,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
internal static partial class ServiceCollectionExtensions
internal static partial class ServiceCollectionExtension
{{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{toolName}"",""1.0.0.0"")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
@@ -65,7 +66,7 @@ internal static partial class ServiceCollectionExtensions
}
}");
context.AddSource("ServiceCollectionExtensions.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
context.AddSource("ServiceCollectionExtension.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
}
private static void FillWithInjectionServices(InjectionSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
@@ -122,4 +123,34 @@ internal static partial class ServiceCollectionExtensions
sourceCodeBuilder.Append(line);
}
}
private class InjectionSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
}
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
/// <summary>
/// Created on demand before each generation pass
/// </summary>
public class InjectionSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
@@ -6,6 +6,7 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
@@ -17,15 +18,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4-beta1.22518.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
</ItemGroup>
</Project>

View File

@@ -10,10 +10,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SettingsUI", "SettingsUI\SettingsUI.csproj", "{DCA5678C-896E-49FB-97A7-5A504A5CFF17}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Installer", "Snap.Hutao.Installer\Snap.Hutao.Installer.csproj", "{CEC01691-F65E-4874-9AE2-F571369A7631}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -50,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
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|arm64.ActiveCfg = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|arm64.Build.0 = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|x64.ActiveCfg = Debug|x64
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|x64.Build.0 = Debug|x64
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|x86.ActiveCfg = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Debug|x86.Build.0 = Debug|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|Any CPU.Build.0 = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|arm64.ActiveCfg = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|arm64.Build.0 = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x64.ActiveCfg = Release|x64
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x64.Build.0 = Release|x64
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x86.ActiveCfg = Release|Any CPU
{DCA5678C-896E-49FB-97A7-5A504A5CFF17}.Release|x86.Build.0 = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.ActiveCfg = Debug|Any CPU
@@ -82,6 +66,22 @@ Global
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.ActiveCfg = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.Build.0 = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x86.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x86.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1,31 @@
{
"help": "https://go.microsoft.com/fwlink/?linkid=866610",
"dependentFileProviders": {
"add": {
"extensionToExtension": {
"add": {
".json": [ ".txt" ]
}
},
"pathSegment": {
"add": {
".*": [ ".cs" ]
}
},
"fileSuffixToExtension": {
"add": {
"DesignTimeFactory.cs": [".cs"]
}
},
"fileToFile": {
"add": {
"app.manifest": [ "App.xaml.cs" ],
"Package.appxmanifest": [ "App.xaml.cs" ],
"GlobalUsing.cs": [ "Program.cs" ],
".filenesting.json": [ "Program.cs" ],
".editorconfig": [ "Program.cs" ]
}
}
}
}
}

View File

@@ -2,32 +2,123 @@
x:Class="Snap.Hutao.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
xmlns:shvc="using:Snap.Hutao.View.Converter">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<muxc:XamlControlsResources/>
<ResourceDictionary Source="ms-appx:///SettingsUI/Themes/SettingsUI.xaml"/>
<ResourceDictionary Source="ms-appx:///SettingsUI/Themes/Generic.xaml"/>
<ResourceDictionary Source="Control/Theme/FontStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!--Modify Window title bar color-->
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<Color x:Key="CompatBackgroundColor">#FFF4F4F4</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<Color x:Key="CompatBackgroundColor">#FF242424</Color>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<!-- Modify Window title bar color -->
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush"/>
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush"/>
<!-- Page Transparent Background -->
<StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush"/>
<FontFamily x:Key="SymbolThemeFontFamily">ms-appx:///Resource/Font/Segoe Fluent Icons.ttf#Segoe Fluent Icons</FontFamily>
<!-- InfoBar Resource -->
<Thickness x:Key="InfoBarIconMargin">6,16,16,16</Thickness>
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
<!-- Pivot Resource -->
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
<Thickness x:Key="PivotHeaderItemMargin">16,0,0,0</Thickness>
<Thickness x:Key="PivotItemMargin">0</Thickness>
<!-- 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>
<!-- OpenPaneLength -->
<x:Double x:Key="CompatSplitViewOpenPaneLength">212</x:Double>
<x:Double x:Key="CompatSplitViewOpenPaneLength2">252</x:Double>
<GridLength x:Key="CompatGridLength2">252</GridLength>
<!-- Uris -->
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie</x:String>
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
<x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String>
<x:String x:Key="UI_ItemIcon_None">https://static.snapgenshin.com/Bg/UI_ItemIcon_None.png</x:String>
<x:String x:Key="UI_ImgSign_ItemIcon">https://static.snapgenshin.com/Bg/UI_ImgSign_ItemIcon.png</x:String>
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://static.snapgenshin.com/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
<x:String x:Key="UI_EmotionIcon25">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon25.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon272.png</x:String>
<x:String x:Key="UI_EmotionIcon293">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon293.png</x:String>
<!-- Converters -->
<cwuc:BoolNegationConverter x:Key="BoolNegationConverter"/>
<cwuc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<shmmc:AchievementIconConverter x:Key="AchievementIconConverter"/>
<shmmc:AvatarCardConverter x:Key="AvatarCardConverter"/>
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
<shmmc:DescParamDescriptor x:Key="DescParamDescriptor"/>
<shmmc: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:PropertyInfoDescriptor x:Key="PropertyDescriptor"/>
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
<shvc:EmptyCollectionToBoolConverter x:Key="EmptyCollectionToBoolConverter"/>
<shvc:EmptyCollectionToBoolRevertConverter x:Key="EmptyCollectionToBoolRevertConverter"/>
<shvc:EmptyCollectionToVisibilityConverter x:Key="EmptyCollectionToVisibilityConverter"/>
<shvc:EmptyCollectionToVisibilityRevertConverter x:Key="EmptyCollectionToVisibilityRevertConverter"/>
<shvc:EmptyObjectToBoolConverter x:Key="EmptyObjectToBoolConverter"/>
<shvc:EmptyObjectToBoolRevertConverter x:Key="EmptyObjectToBoolRevertConverter"/>
<shvc:EmptyObjectToVisibilityConverter x:Key="EmptyObjectToVisibilityConverter"/>
<shvc:EmptyObjectToVisibilityRevertConverter x:Key="EmptyObjectToVisibilityRevertConverter"/>
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
<!-- 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="{StaticResource CompatCornerRadius}"/>
</Style>
<!-- ItemsPanelTemplate -->
<ItemsPanelTemplate x:Key="ItemsStackPanelTemplate">
<ItemsStackPanel/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="HorizontalStackPanelTemplate">
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,16 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Notifications;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Exception;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.AppCenter;
using Snap.Hutao.Service.Metadata;
using System.Diagnostics;
using Windows.Storage;
@@ -29,43 +27,45 @@ public partial class App : Application
/// </summary>
/// <param name="logger">日志器</param>
/// <param name="appCenter">App Center</param>
public App(ILogger<App> logger, AppCenter appCenter)
public App(ILogger<App> logger)
{
// load app resource
InitializeComponent();
this.logger = logger;
_ = new ExceptionRecorder(this, logger, appCenter);
_ = new ExceptionRecorder(this, logger);
}
/// <inheritdoc/>
[SuppressMessage("", "VSTHRD100")]
protected override async void OnLaunched(LaunchActivatedEventArgs args)
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
AppInstance firstInstance = AppInstance.FindOrRegisterForKey("main");
if (firstInstance.IsCurrent)
try
{
// manually invoke
Activation.Activate(firstInstance, activatedEventArgs);
firstInstance.Activated += Activation.Activate;
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
AppInstance firstInstance = AppInstance.FindOrRegisterForKey("main");
logger.LogInformation(EventIds.CommonLog, "Snap Hutao : {version}", CoreEnvironment.Version);
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.TemporaryFolder.Path);
if (firstInstance.IsCurrent)
{
// manually invoke
Activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
firstInstance.Activated += Activation.Activate;
ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate;
Ioc.Default
.GetRequiredService<IMetadataService>()
.ImplictAs<IMetadataInitializer>()?
.InitializeInternalAsync()
.SafeForget(logger);
logger.LogInformation(EventIds.CommonLog, "Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version);
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.LocalCacheFolder.Path);
Ioc.Default.GetRequiredService<AppCenter>().Initialize();
JumpListHelper.ConfigureAsync().SafeForget(logger);
}
else
{
// Redirect the activation (and args) to the "main" instance, and exit.
firstInstance.RedirectActivationTo(activatedEventArgs);
Process.GetCurrentProcess().Kill();
}
}
else
catch (Exception)
{
// Redirect the activation (and args) to the "main" instance, and exit.
await firstInstance.RedirectActivationToAsync(activatedEventArgs);
// AppInstance.GetCurrent() calls failed
Process.GetCurrentProcess().Kill();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,76 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Configuration;
namespace Snap.Hutao.Context.Database;
/// <summary>
/// 应用程序数据库上下文
/// </summary>
public class AppDbContext : DbContext
{
/// <summary>
/// 构造一个新的应用程序数据库上下文
/// </summary>
/// <param name="options">选项</param>
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
/// <summary>
/// 设置
/// </summary>
public DbSet<SettingEntry> Settings { get; set; } = default!;
/// <summary>
/// 用户
/// </summary>
public DbSet<User> Users { get; set; } = default!;
/// <summary>
/// 成就
/// </summary>
public DbSet<Achievement> Achievements { get; set; } = default!;
/// <summary>
/// 成就存档
/// </summary>
public DbSet<AchievementArchive> AchievementArchives { get; set; } = default!;
/// <summary>
/// 卡池数据
/// </summary>
public DbSet<GachaItem> GachaItems { get; set; } = default!;
/// <summary>
/// 卡池存档
/// </summary>
public DbSet<GachaArchive> GachaArchives { get; set; } = default!;
/// <summary>
/// 角色信息
/// </summary>
public DbSet<AvatarInfo> AvatarInfos { get; set; } = default!;
/// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>
/// <param name="sqlConnectionString">连接字符串</param>
/// <returns>应用程序数据库上下文</returns>
public static AppDbContext Create(string sqlConnectionString)
{
return new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options);
}
/// <inheritdoc/>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.ApplyConfiguration(new AvatarInfoConfiguration())
.ApplyConfiguration(new UserConfiguration());
}
}

View File

@@ -1,174 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Context.FileSystem.Location;
using System.IO;
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 文件系统上下文
/// </summary>
/// <typeparam name="TLocation">路径位置类型</typeparam>
internal abstract class FileSystemContext
{
private readonly IFileSystemLocation location;
/// <summary>
/// 初始化文件系统上下文
/// </summary>
/// <param name="location">指定的文件系统位置</param>
public FileSystemContext(IFileSystemLocation location)
{
this.location = location;
EnsureDirectory();
}
/// <summary>
/// 创建文件,若已存在文件,则不会创建
/// </summary>
/// <param name="file">文件</param>
public void CreateFileOrIgnore(string file)
{
file = Locate(file);
if (!File.Exists(file))
{
File.Create(file).Dispose();
}
}
/// <summary>
/// 创建文件夹,若已存在文件,则不会创建
/// </summary>
/// <param name="folder">文件夹</param>
public void CreateFolderOrIgnore(string folder)
{
folder = Locate(folder);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
}
/// <summary>
/// 尝试删除文件夹
/// </summary>
/// <param name="folder">文件夹</param>
public void DeleteFolderOrIgnore(string folder)
{
folder = Locate(folder);
if (Directory.Exists(folder))
{
Directory.Delete(folder, true);
}
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="file">文件名称</param>
/// <returns>是否存在</returns>
public bool FileExists(string file)
{
return File.Exists(Locate(file));
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="folder">文件夹名称</param>
/// <param name="file">文件名称</param>
/// <returns>是否存在</returns>
public bool FileExists(string folder, string file)
{
return File.Exists(Locate(folder, file));
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="folder">文件夹名称</param>
/// <returns>是否存在</returns>
public bool FolderExists(string folder)
{
return Directory.Exists(Locate(folder));
}
/// <summary>
/// 定位根目录中的文件或文件夹
/// </summary>
/// <param name="fileOrFolder">文件或文件夹</param>
/// <returns>绝对路径</returns>
public string Locate(string fileOrFolder)
{
return Path.GetFullPath(fileOrFolder, location.GetPath());
}
/// <summary>
/// 定位根目录下子文件夹中的文件
/// </summary>
/// <param name="folder">文件夹</param>
/// <param name="file">文件</param>
/// <returns>绝对路径</returns>
public string Locate(string folder, string file)
{
return Path.GetFullPath(Path.Combine(folder, file), location.GetPath());
}
/// <summary>
/// 将文件移动到指定的子目录
/// </summary>
/// <param name="file">文件</param>
/// <param name="folder">文件夹</param>
/// <param name="overwrite">是否覆盖</param>
/// <returns>是否成功 当文件不存在时会失败</returns>
public bool MoveToFolderOrIgnore(string file, string folder, bool overwrite = true)
{
string target = Locate(folder, file);
file = Locate(file);
if (File.Exists(file))
{
File.Move(file, target, overwrite);
return true;
}
return false;
}
/// <summary>
/// 等效于 <see cref="File.OpenRead(string)"/> ,但路径经过解析
/// </summary>
/// <param name="file">文件名</param>
/// <returns>文件流</returns>
public FileStream OpenRead(string file)
{
return File.OpenRead(Locate(file));
}
/// <summary>
/// 等效于 <see cref="File.Create(string)"/> ,但路径经过解析
/// </summary>
/// <param name="file">文件名</param>
/// <returns>文件流</returns>
public FileStream Create(string file)
{
return File.Create(Locate(file));
}
/// <summary>
/// 检查根目录
/// </summary>
/// <returns>是否创建了路径</returns>
private bool EnsureDirectory()
{
string folder = location.GetPath();
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
return true;
}
return false;
}
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 我的文档上下文
/// </summary>
[Injection(InjectAs.Transient)]
internal class HutaoContext : FileSystemContext
{
/// <inheritdoc cref="FileSystemContext"/>
public HutaoContext(Location.HutaoLocation myDocument)
: base(myDocument)
{
}
}

View File

@@ -1,27 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 我的文档位置
/// </summary>
[Injection(InjectAs.Transient)]
internal class HutaoLocation : IFileSystemLocation
{
private string? path;
/// <inheritdoc/>
public string GetPath()
{
if (string.IsNullOrEmpty(path))
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
path = Path.GetFullPath(Path.Combine(myDocument, "Hutao"));
}
return path;
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 文件系统位置
/// </summary>
public interface IFileSystemLocation
{
/// <summary>
/// 获取路径
/// </summary>
/// <returns>路径</returns>
string GetPath();
}

View File

@@ -1,27 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 我的文档位置
/// </summary>
[Injection(InjectAs.Transient)]
internal class Metadata : IFileSystemLocation
{
private string? path;
/// <inheritdoc/>
public string GetPath()
{
if (string.IsNullOrEmpty(path))
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
path = Path.GetFullPath(Path.Combine(myDocument, "Hutao", "Metadata"));
}
return path;
}
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Context.FileSystem.Location;
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 元数据上下文
/// </summary>
[Injection(InjectAs.Transient)]
internal class MetadataContext : FileSystemContext
{
/// <inheritdoc cref="FileSystemContext"/>
public MetadataContext(Metadata metadata)
: base(metadata)
{
}
}

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control.Behavior;
@@ -55,4 +54,4 @@ internal class AutoHeightBehavior : BehaviorBase<FrameworkElement>
{
AssociatedObject.Height = (double)AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
}
}
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Control.Behavior;
/// <summary>
/// 按给定比例自动调整高度的行为
/// </summary>
internal class AutoWidthBehavior : BehaviorBase<FrameworkElement>
{
private static readonly DependencyProperty TargetWidthProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetWidth), 320D);
private static readonly DependencyProperty TargetHeightProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetHeight), 1024D);
/// <summary>
/// 目标宽度
/// </summary>
public double TargetWidth
{
get => (double)GetValue(TargetWidthProperty);
set => SetValue(TargetWidthProperty, value);
}
/// <summary>
/// 目标高度
/// </summary>
public double TargetHeight
{
get => (double)GetValue(TargetHeightProperty);
set => SetValue(TargetHeightProperty, value);
}
/// <inheritdoc/>
protected override void OnAssociatedObjectLoaded()
{
UpdateElementWidth();
AssociatedObject.SizeChanged += OnSizeChanged;
}
/// <inheritdoc/>
protected override void OnDetaching()
{
AssociatedObject.SizeChanged -= OnSizeChanged;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateElementWidth();
}
private void UpdateElementWidth()
{
AssociatedObject.Width = (double)AssociatedObject.Height * (TargetWidth / TargetHeight);
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Behavior;
/// <summary>
/// AppTitleBar Workaround
/// https://github.com/microsoft/microsoft-ui-xaml/issues/7756
/// </summary>
internal class ComboBoxExtendsContentIntoTitleBarWorkaroundBehavior : BehaviorBase<ComboBox>
{
private readonly IMessenger messenger;
/// <summary>
/// AppTitleBar Workaround
/// </summary>
public ComboBoxExtendsContentIntoTitleBarWorkaroundBehavior()
{
messenger = Ioc.Default.GetRequiredService<IMessenger>();
}
/// <inheritdoc/>
protected override void OnAssociatedObjectLoaded()
{
AssociatedObject.DropDownOpened += OnDropDownOpened;
AssociatedObject.DropDownClosed += OnDropDownClosed;
}
private void OnDropDownOpened(object? sender, object e)
{
messenger.Send(new Message.FlyoutOpenCloseMessage(true));
}
private void OnDropDownClosed(object? sender, object e)
{
messenger.Send(new Message.FlyoutOpenCloseMessage(false));
}
}

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control.Behavior;

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control.Behavior;
@@ -26,6 +25,7 @@ internal class InvokeCommandOnUnloadedBehavior : BehaviorBase<UIElement>
/// <inheritdoc/>
protected override void OnDetaching()
{
// 由于卸载顺序问题,必须重写此方法才能正确触发命令
if (Command != null && Command.CanExecute(null))
{
Command.Execute(null);

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control;

View File

@@ -1,9 +1,7 @@
// 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.Threading;
namespace Snap.Hutao.Control.Extension;
@@ -12,19 +10,6 @@ namespace Snap.Hutao.Control.Extension;
/// </summary>
internal static class ContentDialogExtensions
{
/// <summary>
/// 针对窗口进行初始化
/// </summary>
/// <param name="contentDialog">对话框</param>
/// <param name="window">窗口</param>
/// <returns>初始化完成的对话框</returns>
public static ContentDialog InitializeWithWindow(this ContentDialog contentDialog, Window window)
{
contentDialog.XamlRoot = window.Content.XamlRoot;
return contentDialog;
}
/// <summary>
/// 阻止用户交互
/// </summary>
@@ -34,10 +19,12 @@ internal static class ContentDialogExtensions
{
await ThreadHelper.SwitchToMainThreadAsync();
contentDialog.ShowAsync().AsTask().SafeForget();
// E_ASYNC_OPERATION_NOT_STARTED 0x80000019
return new ContentDialogHider(contentDialog);
}
private struct ContentDialogHider : IAsyncDisposable
private readonly struct ContentDialogHider : IAsyncDisposable
{
private readonly ContentDialog contentDialog;

View File

@@ -1,15 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control;
/// <summary>
/// 指示此类支持取消任务
/// </summary>
public interface ISupportCancellation
{
/// <summary>
/// 用于通知事件取消的取消令牌
/// </summary>
CancellationToken CancellationToken { get; set; }
}

View File

@@ -7,7 +7,6 @@ using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Extension;
using System.Runtime.InteropServices;
using Windows.Storage;
namespace Snap.Hutao.Control.Image;
@@ -23,6 +22,7 @@ public class CachedImage : ImageEx
{
IsCacheEnabled = true;
EnableLazyLoading = true;
LazyLoadingThreshold = 500;
}
/// <inheritdoc/>
@@ -33,18 +33,18 @@ public class CachedImage : ImageEx
try
{
Verify.Operation(imageUri.Host != string.Empty, "无效的Uri");
StorageFile file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
// check token state to determine whether the operation should be canceled.
token.ThrowIfCancellationRequested();
// BitmapImage initialize with a uri will increase image quality and loading speed.
return new BitmapImage(new(file.Path));
return new BitmapImage(new(file));
}
catch (COMException)
{
// The image is corrupted, remove it.
await imageCache.RemoveAsync(imageUri.Enumerate()).ConfigureAwait(false);
imageCache.Remove(imageUri.Enumerate());
return null;
}
catch (OperationCanceledException)

View File

@@ -152,19 +152,17 @@ internal static class CompositionExtensions
/// 创建一个线性渐变画刷
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="start">起点</param>
/// <param name="end">终点</param>
/// <param name="direction">方向</param>
/// <param name="stops">锚点</param>
/// <returns>线性渐变画刷</returns>
public static CompositionLinearGradientBrush CompositeLinearGradientBrush(
this Compositor compositor,
Vector2 start,
Vector2 end,
GradientDirection direction,
params GradientStop[] stops)
{
CompositionLinearGradientBrush brush = compositor.CreateLinearGradientBrush();
brush.StartPoint = start;
brush.EndPoint = end;
brush.StartPoint = GetStartPointOfDirection(direction);
brush.EndPoint = GetEndPointOfDirection(direction);
foreach (GradientStop stop in stops)
{
@@ -193,5 +191,31 @@ internal static class CompositionExtensions
return brush;
}
private static Vector2 GetStartPointOfDirection(GradientDirection direction)
{
return direction switch
{
GradientDirection.BottomToTop => Vector2.UnitY,
GradientDirection.LeftBottomToRightTop => Vector2.UnitY,
GradientDirection.RightBottomToLeftTop => Vector2.One,
GradientDirection.RightToLeft => Vector2.UnitX,
GradientDirection.RightTopToLeftBottom => Vector2.UnitX,
_ => Vector2.Zero,
};
}
private static Vector2 GetEndPointOfDirection(GradientDirection direction)
{
return direction switch
{
GradientDirection.LeftBottomToRightTop => Vector2.UnitX,
GradientDirection.LeftToRight => Vector2.UnitX,
GradientDirection.LeftTopToRightBottom => Vector2.One,
GradientDirection.RightTopToLeftBottom => Vector2.UnitY,
GradientDirection.TopToBottom => Vector2.UnitY,
_ => Vector2.Zero,
};
}
public record struct GradientStop(float Offset, Windows.UI.Color Color);
}

View File

@@ -6,15 +6,12 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using System.IO;
using System.Net.Http;
using System.Runtime.InteropServices;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Snap.Hutao.Control.Image;
@@ -62,15 +59,16 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
/// <summary>
/// 异步加载图像表面
/// </summary>
/// <param name="storageFile">文件</param>
/// <param name="file">文件</param>
/// <param name="token">取消令牌</param>
/// <returns>加载的图像表面</returns>
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
{
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
{
return LoadedImageSurface.StartLoadFromStream(imageStream);
}
TaskCompletionSource loadCompleteTaskSource = new();
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file));
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
await loadCompleteTaskSource.Task.ConfigureAwait(true);
return surface;
}
/// <summary>
@@ -119,7 +117,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
private async Task ApplyImageInternalAsync(Uri? uri, CancellationToken token)
{
await HideAsync(token);
await HideAsync(token).ConfigureAwait(true);
LoadedImageSurface? imageSurface = null;
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
@@ -132,15 +130,19 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
}
else
{
StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri);
string storageFile = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
try
{
imageSurface = await LoadImageSurfaceAsync(storageFile, token);
imageSurface = await LoadImageSurfaceAsync(storageFile, token).ConfigureAwait(true);
}
catch (COMException)
{
await imageCache.RemoveAsync(uri.Enumerate());
imageCache.Remove(uri.Enumerate());
}
catch (IOException)
{
imageCache.Remove(uri.Enumerate());
}
}
@@ -150,7 +152,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
OnUpdateVisual(spriteVisual);
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
await ShowAsync(token);
await ShowAsync(token).ConfigureAwait(true);
}
}
}
@@ -159,7 +161,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{
if (!isShow)
{
await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token);
await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token).ConfigureAwait(true);
isShow = true;
}
}
@@ -168,7 +170,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{
if (isShow)
{
await AnimationBuilder.Create().Opacity(0d).StartAsync(this, token);
await AnimationBuilder.Create().Opacity(0d).StartAsync(this, token).ConfigureAwait(true);
isShow = false;
}
}

View File

@@ -3,10 +3,10 @@
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using System.Numerics;
using System.IO;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Snap.Hutao.Control.Image;
@@ -16,8 +16,29 @@ namespace Snap.Hutao.Control.Image;
/// </summary>
public class Gradient : CompositionImage
{
private static readonly DependencyProperty BackgroundDirectionProperty = Property<Gradient>.Depend(nameof(BackgroundDirection), GradientDirection.TopToBottom);
private static readonly DependencyProperty ForegroundDirectionProperty = Property<Gradient>.Depend(nameof(ForegroundDirection), GradientDirection.TopToBottom);
private double imageAspectRatio;
/// <summary>
/// 背景方向
/// </summary>
public GradientDirection BackgroundDirection
{
get => (GradientDirection)GetValue(BackgroundDirectionProperty);
set => SetValue(BackgroundDirectionProperty, value);
}
/// <summary>
/// 前景方向
/// </summary>
public GradientDirection ForegroundDirection
{
get => (GradientDirection)GetValue(ForegroundDirectionProperty);
set => SetValue(ForegroundDirectionProperty, value);
}
/// <inheritdoc/>
protected override void OnUpdateVisual(SpriteVisual spriteVisual)
{
@@ -29,15 +50,22 @@ public class Gradient : CompositionImage
}
/// <inheritdoc/>
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
{
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
using (FileStream fileStream = new(file, FileMode.Open, FileAccess.Read, FileShare.Read))
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token);
imageAspectRatio = decoder.PixelWidth / (double)decoder.PixelHeight;
return LoadedImageSurface.StartLoadFromStream(imageStream);
using (IRandomAccessStream imageStream = fileStream.AsRandomAccessStream())
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream);
imageAspectRatio = decoder.PixelWidth / (double)decoder.PixelHeight;
}
}
TaskCompletionSource loadCompleteTaskSource = new();
LoadedImageSurface surface = LoadedImageSurface.StartLoadFromUri(new(file));
surface.LoadCompleted += (s, e) => loadCompleteTaskSource.TrySetResult();
await loadCompleteTaskSource.Task.ConfigureAwait(true);
return surface;
}
/// <inheritdoc/>
@@ -45,8 +73,8 @@ public class Gradient : CompositionImage
{
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.UniformToFill, vRatio: 0f);
CompositionLinearGradientBrush backgroundBrush = compositor.CompositeLinearGradientBrush(new(1f, 0), Vector2.UnitY, new(0, Colors.White), new(1, Colors.Black));
CompositionLinearGradientBrush foregroundBrush = compositor.CompositeLinearGradientBrush(Vector2.Zero, Vector2.UnitY, new(0, Colors.White), new(0.95f, Colors.Black));
CompositionLinearGradientBrush backgroundBrush = compositor.CompositeLinearGradientBrush(BackgroundDirection, new(0, Colors.White), new(1, Colors.Black));
CompositionLinearGradientBrush foregroundBrush = compositor.CompositeLinearGradientBrush(ForegroundDirection, new(0, Colors.White), new(1, Colors.Black));
CompositionEffectBrush gradientEffectBrush = compositor.CompositeBlendEffectBrush(backgroundBrush, foregroundBrush);
CompositionEffectBrush opacityMaskEffectBrush = compositor.CompositeLuminanceToAlphaEffectBrush(gradientEffectBrush);

View File

@@ -0,0 +1,50 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Image;
/// <summary>
/// 渐变方向
/// </summary>
public enum GradientDirection
{
/// <summary>
/// 下到上
/// </summary>
BottomToTop,
/// <summary>
/// 左下到右上
/// </summary>
LeftBottomToRightTop,
/// <summary>
/// 左到右
/// </summary>
LeftToRight,
/// <summary>
/// 左上到右下
/// </summary>
LeftTopToRightBottom,
/// <summary>
/// 右下到左上
/// </summary>
RightBottomToLeftTop,
/// <summary>
/// 右到左
/// </summary>
RightToLeft,
/// <summary>
/// 右上到左下
/// </summary>
RightTopToLeftBottom,
/// <summary>
/// 上到下
/// </summary>
TopToBottom,
}

View File

@@ -1,68 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Markup;
using Snap.Hutao.Extension;
using Snap.Hutao.Localization;
using System.Globalization;
namespace Snap.Hutao.Control.Markup;
/// <summary>
/// 国际化拓展
/// </summary>
[MarkupExtensionReturnType(ReturnType = typeof(string))]
internal class I18NExtension : MarkupExtension
{
private static readonly ITranslation Translation;
private static readonly Dictionary<string, Type> TranslationMap = new()
{
["zh-CN"] = typeof(LanguagezhCN),
};
static I18NExtension()
{
string currentName = CultureInfo.CurrentUICulture.Name;
Type? languageType = TranslationMap.GetValueOrDefault2(currentName, typeof(LanguagezhCN));
Translation = (ITranslation)Activator.CreateInstance(languageType!)!;
}
/// <summary>
/// 构造默认的国际化拓展
/// </summary>
public I18NExtension()
: base()
{
}
/// <summary>
/// 构造默认的国际化拓展
/// </summary>
/// <param name="key">键</param>
public I18NExtension(string key)
{
Key = key;
}
/// <summary>
/// 键名称
/// </summary>
public string Key { get; set; } = default!;
/// <summary>
/// 获取字符串
/// </summary>
/// <param name="key">键</param>
/// <returns>翻译的字符串</returns>
internal static string Get(string key)
{
return Translation[key];
}
/// <inheritdoc/>
protected override object ProvideValue()
{
return Translation[Key];
}
}

View File

@@ -1,69 +0,0 @@
// 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;
namespace Snap.Hutao.Control.Markup;
/// <summary>
/// 国际化帮助类
/// WinUI 3 目前存在部分页面无法使用 MarkupExtension 的问题
/// 使用 此帮助类 绕过限制
/// </summary>
internal class I18NHelper
{
private static readonly DependencyProperty TranslationProperty = Property<I18NHelper>.Attach("Translation", string.Empty, OnKeyChanged);
/// <summary>
/// 获取键
/// </summary>
/// <param name="obj">对象</param>
/// <returns>值</returns>
public static string GetTranslation(DependencyObject obj)
{
return (string)obj.GetValue(TranslationProperty);
}
/// <summary>
/// 设置键
/// </summary>
/// <param name="obj">对象</param>
/// <param name="value">值</param>
public static void SetTranslation(DependencyObject obj, string value)
{
string tarnslation = I18NExtension.Get(value);
obj.SetValue(TranslationProperty, tarnslation);
}
private static void OnKeyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs arg)
{
string translation = I18NExtension.Get(arg.NewValue.ToString() ?? string.Empty);
if (obj is AppBarButton appBarButton)
{
appBarButton.Label = translation;
}
else if (obj is AppBarToggleButton appBarToggleButton)
{
appBarToggleButton.Label = translation;
}
else if (obj is AutoSuggestBox autoSuggestBox)
{
autoSuggestBox.PlaceholderText = translation;
}
else if (obj is ContentControl contentControl)
{
contentControl.Content = translation;
}
else if (obj is MenuFlyoutItem menuFlyoutItem)
{
menuFlyoutItem.Text = translation;
}
else if (obj is TextBlock textBlock)
{
textBlock.Text = translation;
}
}
}

View File

@@ -1,31 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.Control.Markup;
/// <summary>
/// Uri扩展
/// </summary>
[MarkupExtensionReturnType(ReturnType = typeof(Uri))]
public sealed class UriExtension : MarkupExtension
{
/// <summary>
/// 构造一个新的Uri扩展
/// </summary>
public UriExtension()
{
}
/// <summary>
/// 地址
/// </summary>
public string? Value { get; set; }
/// <inheritdoc/>
protected override object ProvideValue()
{
return new Uri(Value ?? string.Empty);
}
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
using Windows.UI;
namespace Snap.Hutao.Control.Media;
/// <summary>
/// BGRA8 结构
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct Bgra8
{
/// <summary>
/// B
/// </summary>
[FieldOffset(0)]
public byte B;
/// <summary>
/// G
/// </summary>
[FieldOffset(1)]
public byte G;
/// <summary>
/// R
/// </summary>
[FieldOffset(2)]
public byte R;
/// <summary>
/// A
/// </summary>
[FieldOffset(3)]
public byte A;
[FieldOffset(0)]
private readonly uint data;
/// <summary>
/// 构造一个新的 BGRA8 结构
/// </summary>
/// <param name="b">B</param>
/// <param name="g">G</param>
/// <param name="r">R</param>
/// <param name="a">A</param>
public Bgra8(byte b, byte g, byte r, byte a)
{
B = b;
G = g;
R = r;
A = a;
}
/// <summary>
/// 从Color值转换
/// </summary>
/// <param name="color">颜色</param>
/// <returns>新的 BGRA8 结构</returns>
public static Bgra8 FromColor(Color color)
{
return new(color.B, color.G, color.R, color.A);
}
/// <summary>
/// 从RGB值转换
/// </summary>
/// <param name="r">R</param>
/// <param name="g">G</param>
/// <param name="b">B</param>
/// <returns>新的 BGRA8 结构</returns>
public static Bgra8 FromRgb(byte r, byte g, byte b)
{
return new(b, g, r, 0xFF);
}
}

View File

@@ -0,0 +1,175 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// Some part of this file came from:
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
using CommunityToolkit.WinUI;
using System.Runtime.InteropServices;
using Windows.UI;
namespace Snap.Hutao.Control.Media;
/// <summary>
/// RGBA 颜色
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct Rgba8
{
/// <summary>
/// R
/// </summary>
[FieldOffset(3)]
public byte R;
/// <summary>
/// G
/// </summary>
[FieldOffset(2)]
public byte G;
/// <summary>
/// B
/// </summary>
[FieldOffset(1)]
public byte B;
/// <summary>
/// A
/// </summary>
[FieldOffset(0)]
public byte A;
[FieldOffset(0)]
private readonly uint data;
/// <summary>
/// 构造一个新的 RGBA8 颜色
/// </summary>
/// <param name="hex">色值字符串</param>
public Rgba8(ReadOnlySpan<char> hex)
{
Must.Argument(hex.Length == 8, "色值长度不为8");
R = 0;
G = 0;
B = 0;
A = 0;
data = Convert.ToUInt32(hex.ToString(), 16);
}
private Rgba8(byte r, byte g, byte b, byte a)
{
data = 0;
R = r;
G = g;
B = b;
A = a;
}
public static implicit operator Color(Rgba8 hexColor)
{
return Color.FromArgb(hexColor.A, hexColor.R, hexColor.G, hexColor.B);
}
/// <summary>
/// 从 HSL 颜色转换
/// </summary>
/// <param name="hsl">HSL 颜色</param>
/// <returns>RGBA8颜色</returns>
public static Rgba8 FromHsl(HslColor hsl)
{
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
double h1 = hsl.H / 60;
double x = chroma * (1 - Math.Abs((h1 % 2) - 1));
double m = hsl.L - (0.5 * chroma);
double r1, g1, b1;
if (h1 < 1)
{
r1 = chroma;
g1 = x;
b1 = 0;
}
else if (h1 < 2)
{
r1 = x;
g1 = chroma;
b1 = 0;
}
else if (h1 < 3)
{
r1 = 0;
g1 = chroma;
b1 = x;
}
else if (h1 < 4)
{
r1 = 0;
g1 = x;
b1 = chroma;
}
else if (h1 < 5)
{
r1 = x;
g1 = 0;
b1 = chroma;
}
else
{
r1 = chroma;
g1 = 0;
b1 = x;
}
byte r = (byte)(255 * (r1 + m));
byte g = (byte)(255 * (g1 + m));
byte b = (byte)(255 * (b1 + m));
byte a = (byte)(255 * hsl.A);
return new(r, g, b, a);
}
/// <summary>
/// 转换到 HSL 颜色
/// </summary>
/// <returns>HSL 颜色</returns>
public HslColor ToHsl()
{
const double toDouble = 1.0 / 255;
double r = toDouble * R;
double g = toDouble * G;
double b = toDouble * B;
double max = Math.Max(Math.Max(r, g), b);
double min = Math.Min(Math.Min(r, g), b);
double chroma = max - min;
double h1;
if (chroma == 0)
{
h1 = 0;
}
else if (max == r)
{
// The % operator doesn't do proper modulo on negative
// numbers, so we'll add 6 before using it
h1 = (((g - b) / chroma) + 6) % 6;
}
else if (max == g)
{
h1 = 2 + ((b - r) / chroma);
}
else
{
h1 = 4 + ((r - g) / chroma);
}
double lightness = 0.5 * (max + min);
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
HslColor ret;
ret.H = 60 * h1;
ret.S = saturation;
ret.L = lightness;
ret.A = toDouble * A;
return ret;
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Win32;
using Windows.Win32.System.WinRT;
using WinRT;
namespace Snap.Hutao.Control.Media;
/// <summary>
/// 软件位图拓展
/// </summary>
public static class SoftwareBitmapExtension
{
/// <summary>
/// 混合模式 正常
/// </summary>
/// <param name="softwareBitmap">软件位图</param>
/// <param name="tint">底色</param>
public static unsafe void NormalBlend(this SoftwareBitmap softwareBitmap, Bgra8 tint)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.ReadWrite))
{
using (IMemoryBufferReference reference = buffer.CreateReference())
{
reference.As<IMemoryBufferByteAccess>().GetBuffer(out byte* data, out uint length);
for (int i = 0; i < length; i += 4)
{
Bgra8* pixel = (Bgra8*)(data + i);
byte baseAlpha = pixel->A;
pixel->B = (byte)(((pixel->B * baseAlpha) + (tint.B * (0xFF - baseAlpha))) / 0xFF);
pixel->G = (byte)(((pixel->G * baseAlpha) + (tint.G * (0xFF - baseAlpha))) / 0xFF);
pixel->R = (byte)(((pixel->R * baseAlpha) + (tint.R * (0xFF - baseAlpha))) / 0xFF);
pixel->A = 0xFF;
}
}
}
}
}

View File

@@ -1,27 +1,31 @@
<UserControl
x:Class="Snap.Hutao.Control.Panel.PanelSelector"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
Loaded="OnRootControlLoaded"
mc:Ignorable="d">
<SplitButton Padding="0,6" Click="SplitButtonClick" Loaded="SplitButtonLoaded">
<SplitButton
Name="RootSplitButton"
Padding="0,6"
Click="SplitButtonClick">
<SplitButton.Content>
<FontIcon Name="IconPresenter" Glyph="&#xE8FD;"/>
</SplitButton.Content>
<SplitButton.Flyout>
<MenuFlyout>
<RadioMenuFlyoutItem
Tag="List"
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
Tag="List"
Text="列表"/>
<RadioMenuFlyoutItem
Tag="Grid"
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Tag="Grid"
Text="网格"/>
</MenuFlyout>
</SplitButton.Flyout>

View File

@@ -3,7 +3,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control.Panel;
@@ -12,7 +11,7 @@ namespace Snap.Hutao.Control.Panel;
/// </summary>
public sealed partial class PanelSelector : UserControl
{
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List");
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List", OnCurrentChanged);
/// <summary>
/// 构造一个新的面板选择器
@@ -31,51 +30,60 @@ public sealed partial class PanelSelector : UserControl
set => SetValue(CurrentProperty, value);
}
private void SplitButtonLoaded(object sender, RoutedEventArgs e)
private static void OnCurrentChanged(PanelSelector sender, string current)
{
MenuFlyout menuFlyout = (MenuFlyout)((SplitButton)sender).Flyout;
((RadioMenuFlyoutItem)menuFlyout.Items[0]).IsChecked = true;
MenuFlyout menuFlyout = (MenuFlyout)sender.RootSplitButton.Flyout;
RadioMenuFlyoutItem targetItem = menuFlyout.Items
.Cast<RadioMenuFlyoutItem>()
.Single(i => (string)i.Tag == current);
targetItem.IsChecked = true;
sender.IconPresenter.Glyph = ((FontIcon)targetItem.Icon).Glyph;
}
private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
OnCurrentChanged((PanelSelector)obj, (string)args.NewValue);
}
private void OnRootControlLoaded(object sender, RoutedEventArgs e)
{
// because the GroupName shares in global
// we have to impl a control scoped GroupName.
PanelSelector selector = (PanelSelector)sender;
MenuFlyout menuFlyout = (MenuFlyout)selector.RootSplitButton.Flyout;
int hash = GetHashCode();
foreach (RadioMenuFlyoutItem item in menuFlyout.Items.Cast<RadioMenuFlyoutItem>())
{
item.GroupName = $"PanelSelector{hash}Group";
}
OnCurrentChanged(selector, Current);
}
private void SplitButtonClick(SplitButton sender, SplitButtonClickEventArgs args)
{
MenuFlyout menuFlyout = (MenuFlyout)sender.Flyout;
int i = 0;
for (; i < menuFlyout.Items.Count; i++)
{
RadioMenuFlyoutItem current = (RadioMenuFlyoutItem)menuFlyout.Items[i];
if (current.IsChecked)
if ((string)menuFlyout.Items[i].Tag == Current)
{
break;
}
}
i++;
if (i > menuFlyout.Items.Count)
{
i = 1;
}
if (i == menuFlyout.Items.Count)
{
i = 0;
}
++i;
i %= menuFlyout.Items.Count; // move the count index to 0
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)menuFlyout.Items[i];
item.IsChecked = true;
UpdateState(item);
Current = (string)item.Tag;
}
private void RadioMenuFlyoutItemClick(object sender, RoutedEventArgs e)
{
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)sender;
UpdateState(item);
}
private void UpdateState(RadioMenuFlyoutItem item)
{
Current = (string)item.Tag;
IconPresenter.Glyph = ((FontIcon)item.Icon).Glyph;
}
}

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Core;
namespace Snap.Hutao.Control;
/// <summary>
/// 快速创建 <see cref="TOwner"/> 的 <see cref="DependencyProperty"/>

View File

@@ -5,16 +5,15 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.ViewModel.Abstraction;
namespace Snap.Hutao.Control;
/// <summary>
/// 表示支持取消加载的异步页面
/// 在被导航到其他页面前触发取消异步通知
/// <para/>
/// InitializeWith{T}();
/// InitializeComponent();
/// </summary>
[SuppressMessage("", "CA1001")]
public class ScopedPage : Page
{
private readonly CancellationTokenSource viewLoadingCancellationTokenSource = new();
@@ -26,16 +25,18 @@ public class ScopedPage : Page
public ScopedPage()
{
serviceScope = Ioc.Default.CreateScope();
serviceScope.Track();
}
/// <summary>
/// 初始化
/// 应当在 InitializeComponent() 前调用
/// </summary>
/// <typeparam name="TViewModel">视图模型类型</typeparam>
public void InitializeWith<TViewModel>()
where TViewModel : class, ISupportCancellation
where TViewModel : class, IViewModel
{
ISupportCancellation viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
IViewModel viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token;
DataContext = viewModel;
}
@@ -59,10 +60,22 @@ public class ScopedPage : Page
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
viewLoadingCancellationTokenSource.Cancel();
using (viewLoadingCancellationTokenSource)
{
// Cancel tasks executed by the view model
viewLoadingCancellationTokenSource.Cancel();
IViewModel viewModel = (IViewModel)DataContext;
// Try dispose scope when page is not presented
serviceScope.Dispose();
using (SemaphoreSlim locker = viewModel.DisposeLock)
{
// Wait to ensure viewmodel operation is completed
locker.Wait();
viewModel.IsViewDisposed = true;
// Dispose the scope
serviceScope.Dispose();
}
}
}
/// <inheritdoc/>

View File

@@ -8,8 +8,8 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Media;
using Snap.Hutao.Core;
using System.Runtime.InteropServices;
using Windows.UI;
namespace Snap.Hutao.Control.Text;
@@ -19,8 +19,7 @@ namespace Snap.Hutao.Control.Text;
/// </summary>
public class DescriptionTextBlock : ContentControl
{
private static readonly DependencyProperty DescriptionProperty =
Property<DescriptionTextBlock>.Depend(nameof(Description), string.Empty, OnDescriptionChanged);
private static readonly DependencyProperty DescriptionProperty = Property<DescriptionTextBlock>.Depend(nameof(Description), string.Empty, OnDescriptionChanged);
private static readonly int ColorTagFullLength = "<color=#FFFFFFFF></color>".Length;
private static readonly int ColorTagLeftLength = "<color=#FFFFFFFF>".Length;
@@ -34,7 +33,10 @@ public class DescriptionTextBlock : ContentControl
public DescriptionTextBlock()
{
IsTabStop = false;
Content = new TextBlock();
Content = new TextBlock()
{
TextWrapping = TextWrapping.Wrap,
};
ActualThemeChanged += OnActualThemeChanged;
}
@@ -76,7 +78,7 @@ public class DescriptionTextBlock : ContentControl
else if (description[i] == '<' && description[i + 1] == 'c')
{
AppendText(text, description[last..i]);
HexColor color = new(description.Slice(i + 8, 8));
Rgba8 color = new(description.Slice(i + 8, 8));
int length = description[(i + ColorTagLeftLength)..].IndexOf('<');
AppendColorText(text, description.Slice(i + ColorTagLeftLength, length), color);
@@ -112,7 +114,7 @@ public class DescriptionTextBlock : ContentControl
text.Inlines.Add(new Run { Text = slice.ToString() });
}
private static void AppendColorText(TextBlock text, ReadOnlySpan<char> slice, HexColor color)
private static void AppendColorText(TextBlock text, ReadOnlySpan<char> slice, Rgba8 color)
{
Color targetColor;
if (ThemeHelper.IsDarkMode(text.ActualTheme))
@@ -123,7 +125,7 @@ public class DescriptionTextBlock : ContentControl
{
HslColor hsl = color.ToHsl();
hsl.L *= 0.3;
targetColor = HexColor.FromHsl(hsl);
targetColor = Rgba8.FromHsl(hsl);
}
text.Inlines.Add(new Run
@@ -151,138 +153,4 @@ public class DescriptionTextBlock : ContentControl
{
ApplyDescription((TextBlock)Content, Description);
}
[StructLayout(LayoutKind.Explicit)]
private struct HexColor
{
[FieldOffset(3)]
public byte R;
[FieldOffset(2)]
public byte G;
[FieldOffset(1)]
public byte B;
[FieldOffset(0)]
public byte A;
[FieldOffset(0)]
private readonly uint data;
public HexColor(ReadOnlySpan<char> hex)
{
Must.Argument(hex.Length == 8, "色值长度不为8");
R = 0;
G = 0;
B = 0;
A = 0;
data = Convert.ToUInt32(hex.ToString(), 16);
}
private HexColor(byte r, byte g, byte b, byte a)
{
data = 0;
R = r;
G = g;
B = b;
A = a;
}
public static implicit operator Color(HexColor hexColor)
{
return Color.FromArgb(hexColor.A, hexColor.R, hexColor.G, hexColor.B);
}
public static HexColor FromHsl(HslColor hsl)
{
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
double h1 = hsl.H / 60;
double x = chroma * (1 - Math.Abs((h1 % 2) - 1));
double m = hsl.L - (0.5 * chroma);
double r1, g1, b1;
if (h1 < 1)
{
r1 = chroma;
g1 = x;
b1 = 0;
}
else if (h1 < 2)
{
r1 = x;
g1 = chroma;
b1 = 0;
}
else if (h1 < 3)
{
r1 = 0;
g1 = chroma;
b1 = x;
}
else if (h1 < 4)
{
r1 = 0;
g1 = x;
b1 = chroma;
}
else if (h1 < 5)
{
r1 = x;
g1 = 0;
b1 = chroma;
}
else
{
r1 = chroma;
g1 = 0;
b1 = x;
}
byte r = (byte)(255 * (r1 + m));
byte g = (byte)(255 * (g1 + m));
byte b = (byte)(255 * (b1 + m));
byte a = (byte)(255 * hsl.A);
return new(r, g, b, a);
}
public HslColor ToHsl()
{
const double toDouble = 1.0 / 255;
double r = toDouble * R;
double g = toDouble * G;
double b = toDouble * B;
double max = Math.Max(Math.Max(r, g), b);
double min = Math.Min(Math.Min(r, g), b);
double chroma = max - min;
double h1;
if (chroma == 0)
{
h1 = 0;
}
else if (max == r)
{
// The % operator doesn't do proper modulo on negative
// numbers, so we'll add 6 before using it
h1 = (((g - b) / chroma) + 6) % 6;
}
else if (max == g)
{
h1 = 2 + ((b - r) / chroma);
}
else
{
h1 = 4 + ((r - g) / chroma);
}
double lightness = 0.5 * (max + min);
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
HslColor ret;
ret.H = 60 * h1;
ret.S = saturation;
ret.L = lightness;
ret.A = toDouble * A;
return ret;
}
}
}
}

View File

@@ -0,0 +1,210 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wsc="using:WinUICommunity.SettingsUI.Controls">
<FontFamily x:Key="MiSans">ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
<FontFamily x:Key="CascadiaMonoAndMiSans">ms-appx:///Resource/Font/CascadiaMono.ttf#Cascadia Mono, ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
<StaticResource x:Key="PivotHeaderItemFontFamily" ResourceKey="MiSans"/>
<StaticResource x:Key="ContentControlThemeFontFamily" ResourceKey="MiSans"/>
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock"/>
<Style x:Key="BaseTextBlockStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
<Setter Property="FontSize" Value="{StaticResource BodyTextBlockFontSize}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="LineStackingStrategy" Value="MaxHeight"/>
<Setter Property="TextLineBounds" Value="Full"/>
</Style>
<Style
x:Key="HeaderTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="46"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="SubheaderTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="34"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="TitleTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource TitleTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="SubtitleTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource SubtitleTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="BodyTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontWeight" Value="Normal"/>
</Style>
<Style
x:Key="CaptionTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource CaptionTextBlockFontSize}"/>
<Setter Property="FontWeight" Value="Normal"/>
</Style>
<Style
x:Key="BodyStrongTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource BodyStrongTextBlockFontSize}"/>
</Style>
<Style
x:Key="TitleLargeTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource TitleLargeTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="DisplayTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource DisplayTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style BasedOn="{StaticResource DefaultMenuFlyoutItemStyle}" TargetType="MenuFlyoutItem">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultMenuFlyoutSubItemStyle}" TargetType="MenuFlyoutSubItem">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style TargetType="InfoBar">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultSettingStyle}" TargetType="wsc:Setting"/>
<Style x:Key="DefaultSettingStyle" TargetType="wsc:Setting">
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="Background" Value="{ThemeResource CardBackgroundBrush}"/>
<Setter Property="BorderThickness" Value="{ThemeResource CardBorderThickness}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Margin" Value="0,0,0,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="wsc:Setting">
<Grid
x:Name="RootGrid"
MinHeight="48"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<!-- Icon -->
<ColumnDefinition Width="*"/>
<!-- Header and subtitle -->
<ColumnDefinition Width="Auto"/>
<!-- Action control -->
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="IconPresenter"
MaxWidth="20"
Margin="2,0,18,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Icon}"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="20"
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
IsTextScaleFactorEnabled="False"/>
<StackPanel
Grid.Column="1"
Margin="0,0,16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<TextBlock
x:Name="HeaderPresenter"
VerticalAlignment="Center"
FontFamily="{StaticResource MiSans}"
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
Text="{TemplateBinding Header}"/>
<ContentPresenter
x:Name="DescriptionPresenter"
Content="{TemplateBinding Description}"
FontFamily="{StaticResource MiSans}"
FontSize="{StaticResource SecondaryTextFontSize}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="WrapWholeWords">
<ContentPresenter.Resources>
<Style BasedOn="{StaticResource CaptionTextBlockStyle}" TargetType="TextBlock">
<Style.Setters>
<Setter Property="TextWrapping" Value="WrapWholeWords"/>
</Style.Setters>
</Style>
<Style BasedOn="{StaticResource TextButtonStyle}" TargetType="HyperlinkButton">
<Style.Setters>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Padding" Value="0,0,0,0"/>
</Style.Setters>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
</StackPanel>
<ContentPresenter
x:Name="ContentPresenter"
Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding ActionContent}"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="HeaderPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="DescriptionPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Control;
/// </summary>
/// <typeparam name="TFrom">源类型</typeparam>
/// <typeparam name="TTo">目标类型</typeparam>
public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
public abstract class ValueConverter<TFrom, TTo> : IValueConverter
{
/// <inheritdoc/>
public object? Convert(object value, Type targetType, object parameter, string language)
@@ -23,7 +23,7 @@ public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
catch (Exception ex)
{
Ioc.Default
.GetRequiredService<ILogger<ValueConverterBase<TFrom, TTo>>>()
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
.LogError(ex, "值转换器异常");
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
[SuppressMessage("", "SA1600")]
public abstract class DisposableObject : IDisposable
{
public bool IsDisposed { get; private set; }
public void Dispose()
{
if (!IsDisposed)
{
GC.SuppressFinalize(this);
Dispose(isDisposing: true);
}
}
protected virtual void Dispose(bool isDisposing)
{
IsDisposed = true;
}
protected void VerifyNotDisposed()
{
if (IsDisposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
}
}

View File

@@ -18,5 +18,5 @@ internal interface ISupportAsyncInitialization
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>初始化任务</returns>
ValueTask<bool> InitializeAsync(CancellationToken token = default);
ValueTask<bool> InitializeAsync();
}

View File

@@ -1,246 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Windows.Storage;
using Windows.Storage.FileProperties;
namespace Snap.Hutao.Core.Caching;
/// <summary>
/// Provides methods and tools to cache files in a folder
/// 经过简化
/// </summary>
/// <typeparam name="T">Generic type as supplied by consumer of the class</typeparam>
public abstract class CacheBase<T>
where T : class
{
private readonly SemaphoreSlim cacheFolderSemaphore = new(1);
private readonly ILogger logger;
// violate di rule
private readonly HttpClient httpClient;
private StorageFolder? baseFolder;
private string? cacheFolderName;
private StorageFolder? cacheFolder;
/// <summary>
/// Initializes a new instance of the <see cref="CacheBase{T}"/> class.
/// </summary>
/// <param name="logger">日志器</param>
/// <param name="httpClient">http客户端</param>
protected CacheBase(ILogger logger, HttpClient httpClient)
{
this.logger = logger;
this.httpClient = httpClient;
CacheDuration = TimeSpan.FromDays(30);
RetryCount = 3;
}
/// <summary>
/// Gets or sets the life duration of every cache entry.
/// </summary>
public TimeSpan CacheDuration { get; }
/// <summary>
/// Gets or sets the number of retries trying to ensure the file is cached.
/// </summary>
public uint RetryCount { get; }
/// <summary>
/// Clears all files in the cache
/// </summary>
/// <returns>awaitable task</returns>
public async Task ClearAsync()
{
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
await RemoveAsync(files).ConfigureAwait(false);
}
/// <summary>
/// Removes cached files that have expired
/// </summary>
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param>
/// <returns>awaitable task</returns>
public async Task RemoveExpiredAsync(TimeSpan? duration = null)
{
TimeSpan expiryDuration = duration ?? CacheDuration;
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
List<StorageFile> filesToDelete = new();
foreach (StorageFile file in files)
{
if (file == null)
{
continue;
}
if (await IsFileOutOfDateAsync(file, expiryDuration, false).ConfigureAwait(false))
{
filesToDelete.Add(file);
}
}
await RemoveAsync(filesToDelete).ConfigureAwait(false);
}
/// <summary>
/// Removed items based on uri list passed
/// </summary>
/// <param name="uriForCachedItems">Enumerable uri list</param>
/// <returns>awaitable Task</returns>
public async Task RemoveAsync(IEnumerable<Uri> uriForCachedItems)
{
if (uriForCachedItems == null || !uriForCachedItems.Any())
{
return;
}
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
IReadOnlyList<StorageFile> files = await folder.GetFilesAsync().AsTask().ConfigureAwait(false);
List<StorageFile> filesToDelete = new();
Dictionary<string, StorageFile> cachedFiles = files.ToDictionary(file => file.Name);
foreach (Uri uri in uriForCachedItems)
{
string fileName = GetCacheFileName(uri);
if (cachedFiles.TryGetValue(fileName, out StorageFile? file))
{
filesToDelete.Add(file);
}
}
await RemoveAsync(filesToDelete).ConfigureAwait(false);
}
/// <summary>
/// Gets the StorageFile containing cached item for given Uri
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>a StorageFile</returns>
public async Task<StorageFile> GetFileFromCacheAsync(Uri uri)
{
StorageFolder folder = await GetCacheFolderAsync().ConfigureAwait(false);
string fileName = GetCacheFileName(uri);
IStorageItem? item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
if (item == null || (await item.GetBasicPropertiesAsync()).Size == 0)
{
StorageFile baseFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false);
await DownloadFileAsync(uri, baseFile).ConfigureAwait(false);
item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
}
return Must.NotNull((item as StorageFile)!);
}
/// <summary>
/// Override-able method that checks whether file is valid or not.
/// </summary>
/// <param name="file">storage file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected virtual async Task<bool> IsFileOutOfDateAsync(StorageFile file, TimeSpan duration, bool treatNullFileAsOutOfDate = true)
{
if (file == null)
{
return treatNullFileAsOutOfDate;
}
BasicProperties? properties = await file.GetBasicPropertiesAsync().AsTask().ConfigureAwait(false);
return properties.Size == 0 || DateTime.Now.Subtract(properties.DateModified.DateTime) > duration;
}
private static string GetCacheFileName(Uri uri)
{
string url = uri.ToString();
byte[] chars = Encoding.UTF8.GetBytes(url);
byte[] hash = SHA1.HashData(chars);
return System.Convert.ToHexString(hash);
}
private async Task DownloadFileAsync(Uri uri, StorageFile baseFile)
{
logger.LogInformation(EventIds.FileCaching, "Begin downloading for {uri}", uri);
using (Stream httpStream = await httpClient.GetStreamAsync(uri).ConfigureAwait(false))
{
using (FileStream fileStream = File.Create(baseFile.Path))
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
}
/// <summary>
/// Initializes with default values if user has not initialized explicitly
/// </summary>
/// <returns>awaitable task</returns>
private async Task InitializeInternalAsync()
{
if (cacheFolder != null)
{
return;
}
using (await cacheFolderSemaphore.EnterAsync().ConfigureAwait(false))
{
baseFolder ??= ApplicationData.Current.TemporaryFolder;
if (string.IsNullOrWhiteSpace(cacheFolderName))
{
cacheFolderName = GetType().Name;
}
cacheFolder = await baseFolder
.CreateFolderAsync(cacheFolderName, CreationCollisionOption.OpenIfExists)
.AsTask()
.ConfigureAwait(false);
}
}
private async Task<StorageFolder> GetCacheFolderAsync()
{
if (cacheFolder == null)
{
await InitializeInternalAsync().ConfigureAwait(false);
}
return Must.NotNull(cacheFolder!);
}
private async Task RemoveAsync(IEnumerable<StorageFile> files)
{
foreach (StorageFile file in files)
{
try
{
logger.LogInformation(EventIds.CacheRemoveFile, "Removing file {file}", file.Path);
await file.DeleteAsync().AsTask().ConfigureAwait(false);
}
catch
{
logger.LogError(EventIds.CacheException, "Failed to delete file: {file}", file.Path);
}
}
}
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Storage;
namespace Snap.Hutao.Core.Caching;
/// <summary>
@@ -12,23 +10,20 @@ namespace Snap.Hutao.Core.Caching;
internal interface IImageCache
{
/// <summary>
/// Gets the StorageFile containing cached item for given Uri
/// Gets the file path containing cached item for given Uri
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>a StorageFile</returns>
Task<StorageFile> GetFileFromCacheAsync(Uri uri);
/// <returns>a string path</returns>
Task<string> GetFileFromCacheAsync(Uri uri);
/// <summary>
/// Removed items based on uri list passed
/// </summary>
/// <param name="uriForCachedItems">Enumerable uri list</param>
/// <returns>awaitable Task</returns>
Task RemoveAsync(IEnumerable<Uri> uriForCachedItems);
void Remove(IEnumerable<Uri> uriForCachedItems);
/// <summary>
/// Removes cached files that have expired
/// Removes invalid cached files
/// </summary>
/// <param name="duration">Optional timespan to compute whether file has expired or not. If no value is supplied, <see cref="CacheDuration"/> is used.</param>
/// <returns>awaitable task</returns>
Task RemoveExpiredAsync(TimeSpan? duration = null);
void RemoveInvalid();
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Caching;
/// <summary>
/// 图像缓存 文件路径操作
/// </summary>
internal interface IImageCacheFilePathOperation
{
/// <summary>
/// 从分类与文件名获取文件路径
/// </summary>
/// <param name="category">分类</param>
/// <param name="fileName">文件名</param>
/// <returns>文件路径</returns>
string GetFilePathFromCategoryAndFileName(string category, string fileName);
}

View File

@@ -1,11 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Logging;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Windows.Storage;
using Windows.Storage.FileProperties;
namespace Snap.Hutao.Core.Caching;
@@ -15,12 +20,28 @@ namespace Snap.Hutao.Core.Caching;
/// </summary>
[Injection(InjectAs.Singleton, typeof(IImageCache))]
[HttpClient(HttpClientConfigration.Default)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 16)]
public class ImageCache : CacheBase<BitmapImage>, IImageCache
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
public class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string DateAccessedProperty = "System.DateAccessed";
private const string CacheFolderName = nameof(ImageCache);
private readonly List<string> extendedPropertyNames = new() { DateAccessedProperty };
private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
{
[0] = TimeSpan.FromSeconds(4),
[1] = TimeSpan.FromSeconds(16),
[2] = TimeSpan.FromSeconds(64),
[3] = TimeSpan.FromSeconds(4),
[4] = TimeSpan.FromSeconds(16),
[5] = TimeSpan.FromSeconds(64),
};
private readonly ILogger logger;
private readonly HttpClient httpClient;
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
private string? baseFolder;
private string? cacheFolder;
/// <summary>
/// Initializes a new instance of the <see cref="ImageCache"/> class.
@@ -28,48 +49,182 @@ public class ImageCache : CacheBase<BitmapImage>, IImageCache
/// <param name="logger">日志器</param>
/// <param name="httpClientFactory">http客户端工厂</param>
public ImageCache(ILogger<ImageCache> logger, IHttpClientFactory httpClientFactory)
: base(logger, httpClientFactory.CreateClient(nameof(ImageCache)))
{
this.logger = logger;
httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
}
/// <summary>
/// Override-able method that checks whether file is valid or not.
/// </summary>
/// <param name="file">storage file</param>
/// <param name="duration">cache duration</param>
/// <param name="treatNullFileAsOutOfDate">option to mark uninitialized file as expired</param>
/// <returns>bool indicate whether file has expired or not</returns>
protected override async Task<bool> IsFileOutOfDateAsync(StorageFile file, TimeSpan duration, bool treatNullFileAsOutOfDate = true)
/// <inheritdoc/>
public void RemoveInvalid()
{
if (file == null)
string folder = GetCacheFolder();
string[] files = Directory.GetFiles(folder);
List<string> filesToDelete = new();
foreach (string file in files)
{
return treatNullFileAsOutOfDate;
}
// Get extended properties.
IDictionary<string, object> extraProperties = await file.Properties
.RetrievePropertiesAsync(extendedPropertyNames)
.AsTask()
.ConfigureAwait(false);
// Get date-accessed property.
object? propValue = extraProperties[DateAccessedProperty];
if (propValue != null)
{
DateTimeOffset? lastAccess = propValue as DateTimeOffset?;
if (lastAccess.HasValue)
if (IsFileInvalid(file, false))
{
return DateTime.Now.Subtract(lastAccess.Value.DateTime) > duration;
filesToDelete.Add(file);
}
}
BasicProperties properties = await file
.GetBasicPropertiesAsync()
.AsTask()
.ConfigureAwait(false);
RemoveInternal(filesToDelete);
}
return properties.Size == 0 || DateTime.Now.Subtract(properties.DateModified.DateTime) > duration;
/// <inheritdoc/>
public void Remove(IEnumerable<Uri> uriForCachedItems)
{
if (uriForCachedItems == null || !uriForCachedItems.Any())
{
return;
}
string folder = GetCacheFolder();
string[] files = Directory.GetFiles(folder);
List<string> filesToDelete = new();
foreach (Uri uri in uriForCachedItems)
{
string filePath = Path.Combine(folder, GetCacheFileName(uri));
if (files.Contains(filePath))
{
filesToDelete.Add(filePath);
}
}
RemoveInternal(filesToDelete);
}
/// <inheritdoc/>
public async Task<string> GetFileFromCacheAsync(Uri uri)
{
string fileName = GetCacheFileName(uri);
string filePath = Path.Combine(GetCacheFolder(), fileName);
if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0)
{
TaskCompletionSource taskCompletionSource = new();
try
{
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
{
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
}
else
{
if (concurrentTasks.TryGetValue(fileName, out Task? task))
{
await task.ConfigureAwait(false);
}
}
}
finally
{
taskCompletionSource.TrySetResult();
}
}
return filePath;
}
/// <inheritdoc/>
public string GetFilePathFromCategoryAndFileName(string category, string fileName)
{
Uri dummyUri = new(Web.HutaoEndpoints.StaticFile(category, fileName));
return Path.Combine(GetCacheFolder(), GetCacheFileName(dummyUri));
}
private static void RemoveInternal(IEnumerable<string> filePaths)
{
foreach (string filePath in filePaths)
{
try
{
File.Delete(filePath);
}
catch
{
}
}
}
private static string GetCacheFileName(Uri uri)
{
string url = uri.ToString();
byte[] chars = Encoding.UTF8.GetBytes(url);
byte[] hash = SHA1.HashData(chars);
return System.Convert.ToHexString(hash);
}
private static bool IsFileInvalid(string file, bool treatNullFileAsInvalid = true)
{
if (!File.Exists(file))
{
return treatNullFileAsInvalid;
}
// Get extended properties.
FileInfo fileInfo = new(file);
return fileInfo.Length == 0;
}
private async Task DownloadFileAsync(Uri uri, string baseFile)
{
logger.LogInformation(EventIds.FileCaching, "Begin downloading for {uri}", uri);
int retryCount = 0;
while (retryCount < 6)
{
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
if (message.IsSuccessStatusCode)
{
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
using (FileStream fileStream = File.Create(baseFile))
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
}
}
else if (message.StatusCode == HttpStatusCode.NotFound)
{
// directly goto https://static.hut.ao
retryCount = 3;
}
else if (message.StatusCode == HttpStatusCode.TooManyRequests)
{
retryCount++;
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount];
logger.LogInformation("Retry {uri} after {delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
}
else
{
return;
}
}
if (retryCount == 3)
{
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao }.Uri;
}
}
}
private string GetCacheFolder()
{
if (cacheFolder == null)
{
baseFolder ??= ApplicationData.Current.LocalCacheFolder.Path;
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
cacheFolder = info.FullName;
}
return cacheFolder!;
}
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text;
namespace Snap.Hutao.Core;
/// <summary>
/// 命令行建造器
/// </summary>
public class CommandLineBuilder
{
private const char WhiteSpace = ' ';
private readonly Dictionary<string, string?> options = new();
/// <summary>
/// 当符合条件时添加参数
/// </summary>
/// <param name="name">参数名称</param>
/// <param name="condition">条件</param>
/// <param name="value">值</param>
/// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIf(string name, bool condition, object? value = null)
{
return condition ? Append(name, value) : this;
}
/// <summary>
/// 当参数不为 null 时添加参数
/// </summary>
/// <param name="name">参数名称</param>
/// <param name="value">值</param>
/// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIfNotNull(string name, object? value = null)
{
return AppendIf(name, value != null, value);
}
/// <summary>
/// 添加参数
/// </summary>
/// <param name="name">参数名称</param>
/// <param name="value">值</param>
/// <returns>命令行建造器</returns>
public CommandLineBuilder Append(string name, object? value = null)
{
options.Add(name, value?.ToString());
return this;
}
/// <inheritdoc/>
public override string ToString()
{
StringBuilder s = new();
foreach ((string key, string? value) in options)
{
s.Append(WhiteSpace);
s.Append(key);
if (!string.IsNullOrEmpty(value))
{
s.Append(WhiteSpace);
s.Append(value);
}
}
return s.ToString();
}
}

View File

@@ -21,4 +21,4 @@ internal abstract class Md5Convert
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
return System.Convert.ToHexString(hash);
}
}
}

View File

@@ -2,10 +2,13 @@
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Core.Convert;
using Snap.Hutao.Core.Json;
using Snap.Hutao.Extension;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Collections.Immutable;
using System.IO;
using System.Text.Json.Serialization.Metadata;
using Windows.ApplicationModel;
namespace Snap.Hutao.Core;
@@ -15,30 +18,33 @@ namespace Snap.Hutao.Core;
/// </summary>
internal static class CoreEnvironment
{
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
private const string MachineGuidValue = "MachineGuid";
// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// <summary>
/// 动态密钥1的盐
/// </summary>
public const string DynamicSecret1Salt = "yUZ3s0Sna1IrSNfk29Vo6vRapdOyqyhB";
/// <summary>
/// 动态密钥2的盐
/// </summary>
public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
/// <summary>
/// 米游社请求UA
/// </summary>
public const string HoyolabUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{HoyolabXrpcVersion}";
/// <summary>
/// 米游社移动端请求UA
/// </summary>
public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/106.0.5249.126 Mobile Safari/537.36 miHoYoBBS/{HoyolabXrpcVersion}";
/// <summary>
/// 米游社 Rpc 版本
/// </summary>
public const string HoyolabXrpcVersion = "2.38.1";
public const string HoyolabXrpcVersion = "2.43.1";
/// <summary>
/// 盐
/// </summary>
// https://github.com/UIGF-org/Hoyolab.Salt
public static readonly ImmutableDictionary<string, string> DynamicSecrets = new Dictionary<string, string>()
{
[nameof(SaltType.K2)] = "ODzG1Jrn6zebX19VRmaJwjFI2CDvBUGq",
[nameof(SaltType.LK2)] = "V1PYbXKQY7ysdx3MNCcNbsE1LtY2QZpW",
[nameof(SaltType.X4)] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
[nameof(SaltType.X6)] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
[nameof(SaltType.PROD)] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
}.ToImmutableDictionary();
/// <summary>
/// 标准UA
@@ -56,9 +62,19 @@ internal static class CoreEnvironment
public static readonly string HoyolabDeviceId;
/// <summary>
/// AppCenter 设备Id
/// 胡桃设备Id
/// </summary>
public static readonly string AppCenterDeviceId;
public static readonly string HutaoDeviceId;
/// <summary>
/// 包家族名称
/// </summary>
public static readonly string FamilyName;
/// <summary>
/// 数据文件夹
/// </summary>
public static readonly string DataFolder;
/// <summary>
/// 默认的Json序列化选项
@@ -66,27 +82,52 @@ internal static class CoreEnvironment
public static readonly JsonSerializerOptions JsonOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Encoder = new JsonTextEncoder(),
PropertyNameCaseInsensitive = true,
WriteIndented = true,
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers =
{
JsonTypeInfoResolvers.ResolveEnumType,
},
},
};
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
private const string MachineGuidValue = "MachineGuid";
static CoreEnvironment()
{
DataFolder = GetDocumentsHutaoPath();
Version = Package.Current.Id.Version.ToVersion();
FamilyName = Package.Current.Id.FamilyName;
CommonUA = $"Snap Hutao/{Version}";
// simply assign a random guid
HoyolabDeviceId = Guid.NewGuid().ToString();
AppCenterDeviceId = GetUniqueUserID();
HutaoDeviceId = GetUniqueUserID();
}
private static string GetUniqueUserID()
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
byte[] bytes = Encoding.UTF8.GetBytes($"{userName}{machineGuid}");
byte[] hash = MD5.Create().ComputeHash(bytes);
return System.Convert.ToHexString(hash);
return Md5Convert.ToHexString($"{userName}{machineGuid}");
}
private static string GetDocumentsHutaoPath()
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
Directory.CreateDirectory(path);
return path;
}
}

View File

@@ -26,7 +26,6 @@ internal class DbCurrent<TEntity, TMessage>
/// </summary>
/// <param name="dbSet">数据集</param>
/// <param name="messenger">消息器</param>
///
public DbCurrent(DbSet<TEntity> dbSet, IMessenger messenger)
{
this.dbSet = dbSet;

View File

@@ -23,33 +23,6 @@ public static class DbSetExtension
return dbSet.GetService<ICurrentDbContext>().Context;
}
/// <summary>
/// 获取或添加一个对应的实体
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="predicate">谓词</param>
/// <param name="entityFactory">实体工厂</param>
/// <param name="added">是否添加</param>
/// <returns>实体</returns>
public static TEntity SingleOrAdd<TEntity>(this DbSet<TEntity> dbSet, Func<TEntity, bool> predicate, Func<TEntity> entityFactory, out bool added)
where TEntity : class
{
added = false;
TEntity? entry = dbSet.SingleOrDefault(predicate);
if (entry == null)
{
entry = entityFactory();
dbSet.Add(entry);
dbSet.Context().SaveChanges();
added = true;
}
return entry;
}
/// <summary>
/// 添加并保存
/// </summary>
@@ -64,6 +37,48 @@ public static class DbSetExtension
return dbSet.Context().SaveChanges();
}
/// <summary>
/// 异步添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static async ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Add(entity);
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
}
/// <summary>
/// 添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
dbSet.AddRange(entities);
return dbSet.Context().SaveChanges();
}
/// <summary>
/// 异步添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static async ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
dbSet.AddRange(entities);
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
}
/// <summary>
/// 移除并保存
/// </summary>
@@ -78,6 +93,20 @@ public static class DbSetExtension
return dbSet.Context().SaveChanges();
}
/// <summary>
/// 异步移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static async ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Remove(entity);
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
}
/// <summary>
/// 更新并保存
/// </summary>
@@ -91,4 +120,18 @@ public static class DbSetExtension
dbSet.Update(entity);
return dbSet.Context().SaveChanges();
}
}
/// <summary>
/// 异步更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static async ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Update(entity);
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 范围化的数据库当前项
/// 简化对数据库中选中项的管理
/// </summary>
/// <typeparam name="TEntity">实体的类型</typeparam>
/// <typeparam name="TMessage">消息的类型</typeparam>
internal class ScopedDbCurrent<TEntity, TMessage>
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntity>, new()
{
private readonly IServiceScopeFactory scopeFactory;
private readonly Func<IServiceProvider, DbSet<TEntity>> dbSetFunc;
private readonly IMessenger messenger;
private TEntity? current;
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="scopeFactory">范围工厂</param>
/// <param name="dbSetFunc">数据集</param>
/// <param name="messenger">消息器</param>
public ScopedDbCurrent(IServiceScopeFactory scopeFactory, Func<IServiceProvider, DbSet<TEntity>> dbSetFunc, IMessenger messenger)
{
this.scopeFactory = scopeFactory;
this.dbSetFunc = dbSetFunc;
this.messenger = messenger;
}
/// <summary>
/// 当前选中的项
/// </summary>
public TEntity? Current
{
get => current;
set
{
// prevent useless sets
if (current == value)
{
return;
}
using (IServiceScope scope = scopeFactory.CreateScope())
{
DbSet<TEntity> dbSet = dbSetFunc(scope.ServiceProvider);
// only update when not processing a deletion
if (value != null)
{
if (current != null)
{
current.IsSelected = false;
dbSet.UpdateAndSave(current);
}
}
TMessage message = new() { OldValue = current, NewValue = value };
current = value;
if (current != null)
{
current.IsSelected = true;
dbSet.UpdateAndSave(current);
}
messenger.Send(message);
}
}
}
}

View File

@@ -0,0 +1,104 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 设置帮助类
/// </summary>
public static class SettingEntryHelper
{
/// <summary>
/// "True"
/// </summary>
public static readonly string TrueString = true.ToString();
/// <summary>
/// "False"
/// </summary>
public static readonly string FalseString = false.ToString();
/// <summary>
/// 获取或添加一个对应的设置
/// </summary>
/// <param name="dbSet">设置集</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <returns>设置</returns>
public static SettingEntry SingleOrAdd(this DbSet<SettingEntry> dbSet, string key, string value)
{
SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key);
if (entry == null)
{
entry = new(key, value);
dbSet.Add(entry);
dbSet.Context().SaveChanges();
}
return entry;
}
/// <summary>
/// 获取或添加一个对应的设置
/// </summary>
/// <param name="dbSet">设置集</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <returns>设置</returns>
public static async Task<SettingEntry> SingleOrAddAsync(this DbSet<SettingEntry> dbSet, string key, string value)
{
SettingEntry? entry = await dbSet.SingleOrDefaultAsync(entry => key == entry.Key).ConfigureAwait(false);
if (entry == null)
{
entry = new(key, value);
await dbSet.AddAndSaveAsync(entry).ConfigureAwait(false);
}
return entry;
}
/// <summary>
/// 获取 Boolean 值
/// </summary>
/// <param name="entry">设置</param>
/// <returns>值</returns>
public static bool GetBoolean(this SettingEntry entry)
{
return bool.Parse(entry.Value!);
}
/// <summary>
/// 设置 Boolean 值
/// </summary>
/// <param name="entry">设置</param>
/// <param name="value">值</param>
public static void SetBoolean(this SettingEntry entry, bool value)
{
entry.Value = value.ToString();
}
/// <summary>
/// 获取 Int32 值
/// </summary>
/// <param name="entry">设置</param>
/// <returns>值</returns>
public static int GetInt32(this SettingEntry entry)
{
return int.Parse(entry.Value!);
}
/// <summary>
/// 设置 Int32 值
/// </summary>
/// <param name="entry">设置</param>
/// <param name="value">值</param>
public static void SetInt32(this SettingEntry entry, int value)
{
entry.Value = value.ToString();
}
}

View File

@@ -17,4 +17,9 @@ public enum HttpClientConfigration
/// 米游社请求配置
/// </summary>
XRpc,
/// <summary>
/// 米游社登录请求配置
/// </summary>
XRpc2,
}

View File

@@ -3,8 +3,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Model.Entity.Database;
using System.Diagnostics;
namespace Snap.Hutao.Core.DependencyInjection;
@@ -31,9 +30,7 @@ internal static class IocConfiguration
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddDatebase(this IServiceCollection services)
{
HutaoContext myDocument = new(new());
string dbFile = myDocument.Locate("Userdata.db");
string dbFile = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
// temporarily create a context
@@ -46,6 +43,13 @@ internal static class IocConfiguration
}
}
return services.AddDbContextPool<AppDbContext>(builder => builder.UseSqlite(sqlConnectionString));
return services.AddDbContext<AppDbContext>(builder =>
{
builder
#if DEBUG
.EnableSensitiveDataLogging()
#endif
.UseSqlite(sqlConnectionString);
});
}
}

View File

@@ -41,4 +41,22 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
}
/// <summary>
/// 对于需要添加动态密钥的客户端使用此配置
/// </summary>
/// <param name="client">配置后的客户端</param>
private static void XRpc2Configuration(HttpClient client)
{
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
client.DefaultRequestHeaders.Add("x-rpc-aigis", string.Empty);
client.DefaultRequestHeaders.Add("x-rpc-app_id", "bll8iq97cem8");
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2");
}
}

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.DependencyInjection;
/// 服务管理器
/// 依赖注入的核心管理类
/// </summary>
internal static partial class ServiceCollectionExtensions
internal static partial class ServiceCollectionExtension
{
/// <summary>
/// 向容器注册服务
@@ -17,4 +17,4 @@ internal static partial class ServiceCollectionExtensions
/// <param name="services">容器</param>
/// <returns>可继续操作的服务集合</returns>
public static partial IServiceCollection AddInjections(this IServiceCollection services);
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 服务范围扩展
/// </summary>
public static class ServiceScopeExtension
{
// Allow GC to Collect the IServiceScope
private static readonly WeakReference<IServiceScope> ScopeReference = new(null!);
/// <summary>
/// 追踪服务范围
/// </summary>
/// <param name="scope">范围</param>
public static void Track(this IServiceScope scope)
{
DisposeLast();
ScopeReference.SetTarget(scope);
}
/// <summary>
/// 释放上个范围
/// </summary>
public static void DisposeLast()
{
if (ScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
}
}

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Diagnostics;
/// <summary>
/// 值类型的<see cref="Stopwatch"/>
/// </summary>
internal struct ValueStopwatch
internal readonly struct ValueStopwatch
{
private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
@@ -40,7 +40,6 @@ internal struct ValueStopwatch
/// 获取经过的时间
/// </summary>
/// <returns>经过的时间</returns>
/// <exception cref="InvalidOperationException">当前的停表未合理的初始化</exception>
public long GetElapsedTimestamp()
{
// Start timestamp can't be zero in an initialized ValueStopwatch.
@@ -59,7 +58,6 @@ internal struct ValueStopwatch
/// 获取经过的时间
/// </summary>
/// <returns>经过的时间</returns>
/// <exception cref="InvalidOperationException">当前的停表未合理的初始化</exception>
public TimeSpan GetElapsedTime()
{
return new TimeSpan(GetElapsedTimestamp());

View File

@@ -1,20 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Exception;
/// <summary>
/// Error codes used by COM-based APIs.
/// </summary>
public enum COMError : uint
{
/// <summary>
/// could not be found.
/// </summary>
STG_E_FILENOTFOUND = 0x80030002,
/// <summary>
/// The component cannot be found.
/// </summary>
WINCODEC_ERR_COMPONENTNOTFOUND = 0x88982F50,
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core.Exception;
/// <summary>
/// COM异常扩展
/// </summary>
internal static class COMExceptionExtensions
{
/// <summary>
/// 比较COM异常是否与某个错误代码等价
/// </summary>
/// <param name="exception">异常</param>
/// <param name="code">错误代码</param>
/// <returns>是否为该错误</returns>
public static bool Is(this COMException exception, COMError code)
{
return exception.HResult == unchecked((int)code);
}
}

View File

@@ -3,9 +3,8 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Service.AppCenter;
namespace Snap.Hutao.Core.Exception;
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 异常记录器
@@ -13,18 +12,15 @@ namespace Snap.Hutao.Core.Exception;
internal class ExceptionRecorder
{
private readonly ILogger logger;
private readonly AppCenter appCenter;
/// <summary>
/// 构造一个新的异常记录器
/// </summary>
/// <param name="application">应用程序</param>
/// <param name="logger">日志器</param>
/// <param name="appCenter">App Center</param>
public ExceptionRecorder(Application application, ILogger logger, AppCenter appCenter)
public ExceptionRecorder(Application application, ILogger logger)
{
this.logger = logger;
this.appCenter = appCenter;
application.UnhandledException += OnAppUnhandledException;
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
@@ -32,7 +28,11 @@ internal class ExceptionRecorder
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
appCenter.TrackCrash(e.Exception);
#if RELEASE
#pragma warning disable VSTHRD002
Ioc.Default.GetRequiredService<Web.Hutao.HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
#pragma warning restore VSTHRD002
#endif
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())
@@ -45,4 +45,4 @@ internal class ExceptionRecorder
{
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);
}
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 用户数据损坏异常
/// </summary>
internal class UserdataCorruptedException : Exception
{
/// <summary>
/// 构造一个新的用户数据损坏异常
/// </summary>
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public UserdataCorruptedException(string message, Exception innerException)
: base($"用户数据已损坏: {message}", innerException)
{
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Linq.Expressions;
namespace Snap.Hutao.Core.ExpressionService;
/// <summary>
/// Class to cast to type <see cref="TTo"/>
/// </summary>
/// <typeparam name="TTo">Target type</typeparam>
public static class CastTo<TTo>
{
/// <summary>
/// Casts <see cref="s"/> to <see cref="TTo"/>.
/// This does not cause boxing for value types.
/// Useful in generic methods.
/// </summary>
/// <typeparam name="TFrom">Source type to cast from. Usually a generic type.</typeparam>
/// <param name="from">from value</param>
/// <returns>target value</returns>
public static TTo From<TFrom>(TFrom from)
{
return Cache<TFrom>.Caster(from);
}
private static class Cache<TCachedFrom>
{
public static readonly Func<TCachedFrom, TTo> Caster = Get();
private static Func<TCachedFrom, TTo> Get()
{
// 参数表达式,表示 传入源类型
ParameterExpression param = Expression.Parameter(typeof(TCachedFrom));
// 一元转换 调用 相关类的显式或隐式转换运算符
UnaryExpression convert = Expression.ConvertChecked(param, typeof(TTo));
// 生成一个源类型入,目标类型出的 lamdba
return Expression.Lambda<Func<TCachedFrom, TTo>>(convert, param).Compile();
}
}
}

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