mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39831b0ae1 | ||
|
|
51c9936018 | ||
|
|
3466a98ffb | ||
|
|
a064cc10ee | ||
|
|
3479a19164 | ||
|
|
d4549581c1 | ||
|
|
f97bc344d0 | ||
|
|
26d23fec7f | ||
|
|
7442f7f1ec | ||
|
|
3eb2556393 | ||
|
|
cfff6f39fc | ||
|
|
3005031b39 | ||
|
|
71363f4d8d | ||
|
|
e833578334 | ||
|
|
d529b3cea6 | ||
|
|
1c0ce62885 | ||
|
|
acdf2baa9a | ||
|
|
ec007d5d81 | ||
|
|
5e734ac689 | ||
|
|
b0ecd048b6 | ||
|
|
91010d0d8b | ||
|
|
fc771eb90a | ||
|
|
80f2fed722 | ||
|
|
bdb406c451 | ||
|
|
5bc957c6a5 | ||
|
|
416c6f15a6 | ||
|
|
9eed633e05 | ||
|
|
7e30173990 | ||
|
|
2200e2e58e | ||
|
|
b8886c5cd3 | ||
|
|
43007d8fb4 | ||
|
|
88684bff00 | ||
|
|
0c7ce7a72f | ||
|
|
075d92f754 | ||
|
|
a0cba171cc | ||
|
|
f41185310b | ||
|
|
2a4c93d241 | ||
|
|
c0980fabe8 | ||
|
|
f2ba316059 | ||
|
|
e4e9dd91f1 | ||
|
|
749ef0e138 | ||
|
|
24086ee4d0 | ||
|
|
aeb6962ae4 | ||
|
|
87e5ede91f | ||
|
|
91de6d170e | ||
|
|
3057673cdb | ||
|
|
c3ace405ac | ||
|
|
0b48581e65 | ||
|
|
4ab129e4a2 | ||
|
|
13ad36f5b4 | ||
|
|
f026321aa8 | ||
|
|
d1dfdf107b | ||
|
|
59f8895675 | ||
|
|
4cb3d5f03f | ||
|
|
067c7d7c4d | ||
|
|
1cc072ba28 | ||
|
|
0e7afa8efb | ||
|
|
b753728b7e | ||
|
|
df019da891 | ||
|
|
c6435f30eb | ||
|
|
3ac0be4220 | ||
|
|
24f6a33256 | ||
|
|
dc9278eb4f | ||
|
|
4b2c82db62 | ||
|
|
70f30edd7c | ||
|
|
c8e8213df6 | ||
|
|
7cad996902 | ||
|
|
eec47b72c7 | ||
|
|
5943b1a1fb | ||
|
|
9f9a5670bc | ||
|
|
10ba927136 | ||
|
|
07d42cedd1 | ||
|
|
07c52019f4 | ||
|
|
77cb2fc603 | ||
|
|
b5c16e2dae | ||
|
|
29c954b032 | ||
|
|
8df5d5d6eb | ||
|
|
827d944987 | ||
|
|
4c47f3c08b | ||
|
|
6540cc4577 | ||
|
|
df22d30a96 | ||
|
|
5ea9dd533f | ||
|
|
744c1079e1 | ||
|
|
1539863415 | ||
|
|
3527e43118 | ||
|
|
8388def548 | ||
|
|
d0bfbfa505 | ||
|
|
fa640d27f0 | ||
|
|
389c1417f7 | ||
|
|
6dcacb1bf4 | ||
|
|
4ffd09cce8 | ||
|
|
0dcbac3ee1 | ||
|
|
b1ea5332fc | ||
|
|
b34dab0f99 | ||
|
|
791e517e39 | ||
|
|
4408e3994e | ||
|
|
32cbbefe1a | ||
|
|
d754c0d117 | ||
|
|
4c75295f2c | ||
|
|
34e5312d75 | ||
|
|
465d6b631e | ||
|
|
0d2d1b8115 | ||
|
|
981949651e | ||
|
|
874dac1119 | ||
|
|
3f0694b28e | ||
|
|
6b166b6aed | ||
|
|
b351231c84 | ||
|
|
0603b24466 | ||
|
|
f97ad4eac0 | ||
|
|
28fc4558be | ||
|
|
ea3391b112 | ||
|
|
ec95e42d7d | ||
|
|
e9f12aeb09 | ||
|
|
04850dd136 | ||
|
|
4782d61ed0 | ||
|
|
28ade90926 | ||
|
|
dde97b6489 | ||
|
|
44fe729e1a | ||
|
|
2a1d814cc5 | ||
|
|
c1ee37bd8f | ||
|
|
65b81f0ad8 | ||
|
|
91fea88623 | ||
|
|
026c68229a | ||
|
|
91b2db886f | ||
|
|
101d316525 | ||
|
|
59d62f931d | ||
|
|
f0bb19bc07 | ||
|
|
c0b05e2c2f | ||
|
|
fc02f833a0 | ||
|
|
7b8ebd86b1 | ||
|
|
47b24286b1 | ||
|
|
0e3e3b9e4a | ||
|
|
85d7b22e11 | ||
|
|
7caeb17788 | ||
|
|
b11b90e9f1 | ||
|
|
58643a60b5 | ||
|
|
a29b487c26 | ||
|
|
1bd6023e0a | ||
|
|
579173d464 | ||
|
|
9ed53e8c34 | ||
|
|
830556a043 | ||
|
|
7ba27e184f | ||
|
|
9aa6a2b57b | ||
|
|
5773902f4a | ||
|
|
06c5bcad3e | ||
|
|
c0165c57fd | ||
|
|
4e57520115 | ||
|
|
17c3480dae |
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,8 +1,8 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: [DGP-Studio]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
open_collective: snaphutao
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 问题反馈
|
||||
description: 告诉我们你的问题
|
||||
description: 通过这个议题向开发团队反馈你发现的程序中的问题
|
||||
title: "[Bug]: 在这里填写一个合适的标题"
|
||||
labels: ["BUG"]
|
||||
labels: ["BUG", "priority:none"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: 检查清单
|
||||
description: |-
|
||||
请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略
|
||||
请确保你已完整执行检查清单,否则你的议题可能会被忽略
|
||||
options:
|
||||
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
|
||||
required: true
|
||||
@@ -51,6 +51,7 @@ body:
|
||||
description: |
|
||||
在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID
|
||||
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
|
||||
如果你的程序已经无法启动,请下载并运行[此PowerShell脚本](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1),它将显示你的设备 ID
|
||||
validations:
|
||||
required: false
|
||||
|
||||
27
.github/ISSUE_TEMPLATE/CHS-feature-request.yml
vendored
Normal file
27
.github/ISSUE_TEMPLATE/CHS-feature-request.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: 功能请求
|
||||
description: 通过这个议题来向开发团队分享你的想法
|
||||
title: "[Feat]: 在这里填写一个合适的标题"
|
||||
labels: ["功能", "needs-triage", "priority:none"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
请按下方的要求填写完整的问题表单。
|
||||
|
||||
- type: textarea
|
||||
id: back
|
||||
attributes:
|
||||
label: 背景与动机
|
||||
description: 添加此功能的理由,如果你想要实现多个功能,请分别发起多个单独的议题
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: req
|
||||
attributes:
|
||||
label: 想要实现或优化的功能
|
||||
description: 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,5 +1,5 @@
|
||||
name: 网络问题
|
||||
description: 当网络问题影响到你的程序使用时
|
||||
description: 通过这个议题来反馈网络问题
|
||||
title: "[Network]: 在这里填写一个合适的标题"
|
||||
labels: ["area-Network"]
|
||||
assignees:
|
||||
@@ -19,10 +19,10 @@ body:
|
||||
description: |
|
||||
停下!
|
||||
**在填写下面的问题之前请先使用我们的网络诊断工具**
|
||||
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
|
||||
**这个工具将会生成一份报告并加密压缩,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
|
||||
- 你可以点击下面的链接以下载网络诊断工具:
|
||||
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-hutao.exe)
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/network-diagnosis-hutao.exe)
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/SH-Network-Diagnosis.exe)
|
||||
- [极狐 GitLab](https://jihulab.com/DGP-Studio/network-diagnosis-tool/-/jobs/11144011/artifacts/raw/SH-Network-Diagnosis.exe?inline=false)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -60,7 +60,6 @@ body:
|
||||
- 完全无法连接服务器
|
||||
- 连接速度慢
|
||||
- 获取到了不正确的页面或数据
|
||||
- 客户端提示 429 Error
|
||||
- 客户端图片下载错误
|
||||
- 客户端图片预下载错误
|
||||
- 其它
|
||||
@@ -74,5 +73,12 @@ body:
|
||||
description: 如果你在上一项中选择了`其它`或者你有更多信息需要提供,请在这里写下来
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: 最后一步
|
||||
description: 检查你提交的议题
|
||||
options:
|
||||
- label: 我已经在该议题中上传了包含网络诊断报告的加密压缩包
|
||||
required: true
|
||||
@@ -1,7 +1,7 @@
|
||||
name: BUG Report [English Form]
|
||||
description: Tell us what issue you get
|
||||
title: "[ENG][Bug]: Place your Issue Title Here"
|
||||
labels: ["BUG"]
|
||||
labels: ["BUG", "priority:none"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -50,7 +50,8 @@ body:
|
||||
label: Device ID
|
||||
description: |
|
||||
In Snap Hutao's settings page, you can find and copy your device ID
|
||||
If your issue is about program crash, please fill this so we can dump the log and locate the source easier
|
||||
If your issue is about program crash, please fill this so we can dump the log and locate the source easier
|
||||
If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
27
.github/ISSUE_TEMPLATE/ENG-feature-request.yml
vendored
Normal file
27
.github/ISSUE_TEMPLATE/ENG-feature-request.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Feature Request [English Form]
|
||||
description: Tell us about your thought
|
||||
title: "[Feat]: Place your title here"
|
||||
labels: ["功能", "needs-triage", "priority:none"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please fill the form below
|
||||
|
||||
- type: textarea
|
||||
id: back
|
||||
attributes:
|
||||
label: Background & Motivation
|
||||
description: Reason why this feature is needed. If multiple features is requested, please open multiple issues for each of them.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: req
|
||||
attributes:
|
||||
label: Detail of the Feature
|
||||
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted.
|
||||
validations:
|
||||
required: true
|
||||
79
.github/ISSUE_TEMPLATE/ENG-network-issue.yml
vendored
Normal file
79
.github/ISSUE_TEMPLATE/ENG-network-issue.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: Network Issue [English Form]
|
||||
description: Submit this issue form when network issue affect your client experience
|
||||
title: "[Network]: Place your title here"
|
||||
labels: ["area-Network"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
- Masterain98
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Please use one sentence to briefly describe your issue as title above**
|
||||
**Please follow the instruction below to fill the form, so we can locate the issue quickly**
|
||||
|
||||
- type: textarea
|
||||
id: network-diagnosis-report
|
||||
attributes:
|
||||
label: Submit Your Network Diagnosis Report
|
||||
description: |
|
||||
STOP HERE!
|
||||
**Please run our network diagnosis tool before filling this form**
|
||||
**The diagnosis tool will generate a report and add it into a password-protected archive. Drag the `.zip` archive to the box below so it can be uploaded.**
|
||||
- Use the following link to download the Network Diagnosis Tool:
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/SH-Network-Diagnosis.exe)
|
||||
- [JIHu GitLab](https://jihulab.com/DGP-Studio/network-diagnosis-tool/-/jobs/11144011/artifacts/raw/SH-Network-Diagnosis.exe?inline=false)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: user-geo-location
|
||||
attributes:
|
||||
label: Your Geographical Location
|
||||
description: |
|
||||
Description accurate to country
|
||||
placeholder: USA
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: user-isp
|
||||
attributes:
|
||||
label: Your ISP Name
|
||||
description: |
|
||||
Name of your Internet service provider
|
||||
placeholder: AT&T
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: user-issue-category
|
||||
attributes:
|
||||
label: Issue Category
|
||||
description: Select an issue category
|
||||
options:
|
||||
- Cannot connect to server completely
|
||||
- Slow spped
|
||||
- Fetched wrong page or data
|
||||
- Image download error in the client
|
||||
- Image set pre-download error (client welcome wizard process)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Your Issue (cont.)
|
||||
description: If you selected `Other` in previous dropdown, please explain your issue in detail here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: One Last Step
|
||||
description: Check your issue form
|
||||
options:
|
||||
- label: I confirm I have attached the network diagnosis report archive in the issue
|
||||
required: true
|
||||
55
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
Normal file
55
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Publish Process
|
||||
description: FOR ADMIN USE ONLY. WILL CAUSE A BAN IF NO PERMISSION.
|
||||
title: "[Publish]: Version 1.9.98"
|
||||
labels: ["Publish"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: textarea
|
||||
id: main-body
|
||||
attributes:
|
||||
label: Publish Process
|
||||
value: |
|
||||
|
||||
## 创建版本
|
||||
|
||||
- [ ] 同步一次 [Crowdin](https://crowdin.com/project/snap-hutao) 翻译
|
||||
- [ ] 发布 RC 版本(Optional)
|
||||
- [ ] 合并入主分支
|
||||
- [ ] 整理更新内容,等待翻译
|
||||
- [ ] 打包
|
||||
- [ ] 提交微软商店
|
||||
- [ ] 包含更新日志
|
||||
- [ ] 在 [Snap.Hutao.Docs@next-patch](https://github.com/DGP-Studio/Snap.Hutao.Docs/tree/next-patch) 分支更新文档并直接开 PR
|
||||
- [ ] 更新日志
|
||||
- [ ] 功能文档更新
|
||||
|
||||
## 发布版本
|
||||
|
||||
- [ ] 在 https://store.rg-adguard.net/ 下载新版本安装包
|
||||
- [ ] Store URL: https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52
|
||||
- [ ] 命名格式为 `Snap.Hutao x.x.x.msix`
|
||||
- [ ] Merge 文档 PR
|
||||
- [ ] 发布 Release
|
||||
- [ ] 更新日志格式(以 1.6.2 版本为例)
|
||||
|
||||
```jsx
|
||||
## Update log
|
||||
https://hut.ao/en/statements/update-log.html#_1-6-2
|
||||
|
||||
## 更新日志
|
||||
[此处从文档复制]
|
||||
|
||||
## What's Changed
|
||||
**Full Changelog**: https://github.com/DGP-Studio/Snap.Hutao/compare/1.6.0...1.6.2
|
||||
```
|
||||
|
||||
- [ ] 通知用户
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: Final Check
|
||||
description: Understand what you are doing
|
||||
options:
|
||||
- label: I understand that I will get banned from repository if I don't have permission to use this template
|
||||
required: true
|
||||
65
.github/ISSUE_TEMPLATE/artifact-rating-rules.yml
vendored
65
.github/ISSUE_TEMPLATE/artifact-rating-rules.yml
vendored
@@ -1,65 +0,0 @@
|
||||
name: 圣遗物评分细则建议
|
||||
description: 为圣遗物评分规则提供你的想法
|
||||
title: "[Artifact Rating] 请在这里填写角色名称"
|
||||
labels: area-AvatarInfo
|
||||
assignees: Lightczx
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
请按下方的要求填写完整的问题表单
|
||||
|
||||
- type: textarea
|
||||
id: your-suggested-rule
|
||||
attributes:
|
||||
label: 评分细则
|
||||
description: |
|
||||
请修改下方表格中的**角色名称**和**各属性权重**,并在表格后添加合适的说明
|
||||
你可以点击预览按钮(preview)来查看表格最终会显示出的内容
|
||||
value: |
|
||||
|项目|评分权重(0-100)|
|
||||
|-----|-----|
|
||||
|角色名称| 旅行者 |
|
||||
|生命值| 10 |
|
||||
|攻击力| 10 |
|
||||
|防御力| 10 |
|
||||
|暴击率| 10 |
|
||||
|暴击伤害| 10 |
|
||||
|元素精通| 10 |
|
||||
|充能效率| 10 |
|
||||
|治疗加成| 10 |
|
||||
|元素伤害| 10 |
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: no-duplicated-dropdown
|
||||
attributes:
|
||||
label: 我确认当前没有其它的该角色的圣遗物评分细则建议
|
||||
description: 如果有,你应该在已有的工单内回复以提出你的建议
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: title-filled-dropdown
|
||||
attributes:
|
||||
label: 我确认已设置合适的标题
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: all-filled-dropdown
|
||||
attributes:
|
||||
label: 我确认已完整填写表格
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
28
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
28
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Feature Request 功能请求
|
||||
description: Tell us about your thought 告诉我们你的想法
|
||||
title: "[Feat]: Place your title here 在这里填写一个合适的标题"
|
||||
labels: ["功能"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please fill the form below
|
||||
请按下方的要求填写完整的问题表单。
|
||||
|
||||
- type: textarea
|
||||
id: back
|
||||
attributes:
|
||||
label: Background & Motivation 背景与动机
|
||||
description: Reason why this feature is needed. If multiple features is requested, please open multiple issues for each of them. 添加此功能的理由,如果你想要实现多个功能,请分别发起多个单独的 Issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: req
|
||||
attributes:
|
||||
label: Detail of the Feature 想要实现或优化的功能
|
||||
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted. 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高
|
||||
validations:
|
||||
required: true
|
||||
26
.github/workflows/qodana_code_quality.yml
vendored
26
.github/workflows/qodana_code_quality.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Qodana
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.yml'
|
||||
- '**.resx'
|
||||
|
||||
jobs:
|
||||
qodana:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Qodana Scan'
|
||||
uses: JetBrains/qodana-action@v2023.2
|
||||
with:
|
||||
pr-mode: false
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ src/Snap.Hutao/_ReSharper.Caches
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao/bin/
|
||||
src/Snap.Hutao/Snap.Hutao/obj/
|
||||
src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs
|
||||
src/Snap.Hutao/Snap.Hutao/Snap.Hutao_TemporaryKey.pfx
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao.Win32/bin/
|
||||
|
||||
33
qodana.yaml
33
qodana.yaml
@@ -1,33 +0,0 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
exclude:
|
||||
- name: Test
|
||||
paths:
|
||||
- Snap.Hutao.Test
|
||||
- Snap.Hutao.SourceGeneration
|
||||
- name: All
|
||||
paths:
|
||||
- Snap.Hutao.SourceGeneration
|
||||
- Snap.Hutao.Test
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-dotnet:2023.2-eap
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
@@ -10,7 +11,18 @@ namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class SaltConstantGenerator : IIncrementalGenerator
|
||||
{
|
||||
private static readonly HttpClient httpClient = new();
|
||||
private static readonly HttpClient httpClient;
|
||||
private static readonly Lazy<Response<SaltLatest>> lazySaltInfo;
|
||||
|
||||
static SaltConstantGenerator()
|
||||
{
|
||||
httpClient = new();
|
||||
lazySaltInfo = new Lazy<Response<SaltLatest>>(() =>
|
||||
{
|
||||
string body = httpClient.GetStringAsync("https://internal.snapgenshin.cn/Archive/Salt/Latest").GetAwaiter().GetResult();
|
||||
return JsonParser.FromJson<Response<SaltLatest>>(body)!;
|
||||
});
|
||||
}
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
@@ -19,8 +31,7 @@ internal sealed class SaltConstantGenerator : IIncrementalGenerator
|
||||
|
||||
private static void GenerateSaltContstants(IncrementalGeneratorPostInitializationContext context)
|
||||
{
|
||||
string body = httpClient.GetStringAsync("https://internal.snapgenshin.cn/Archive/Salt/Latest").GetAwaiter().GetResult();
|
||||
Response<SaltLatest> saltInfo = JsonParser.FromJson<Response<SaltLatest>>(body)!;
|
||||
Response<SaltLatest> saltInfo = lazySaltInfo.Value;
|
||||
string code = $$"""
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
namespace Snap.Hutao.Resource.Localization;
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(LocalizedEnumGenerator)}}", "1.0.0.0")]
|
||||
@@ -79,7 +81,7 @@ internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
}
|
||||
else
|
||||
{
|
||||
return SH.ResourceManager.GetString(key);
|
||||
return SH.ResourceManager.GetString(key, CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +104,7 @@ internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
return SH.ResourceManager.GetString(key);
|
||||
return SH.ResourceManager.GetString(key, CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
607
src/Snap.Hutao/Snap.Hutao.SourceGeneration/Resx/ResxGenerator.cs
Normal file
607
src/Snap.Hutao/Snap.Hutao.SourceGeneration/Resx/ResxGenerator.cs
Normal file
@@ -0,0 +1,607 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.XPath;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Resx;
|
||||
|
||||
[Generator]
|
||||
public sealed class ResxGenerator : IIncrementalGenerator
|
||||
{
|
||||
private static readonly DiagnosticDescriptor InvalidResx = new("SH401", "Couldn't parse Resx file", "Couldn't parse Resx file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
private static readonly DiagnosticDescriptor InvalidPropertiesForNamespace = new("SH402", "Couldn't compute namespace", "Couldn't compute namespace for file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
private static readonly DiagnosticDescriptor InvalidPropertiesForResourceName = new("SH403", "Couldn't compute resource name", "Couldn't compute resource name for file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
private static readonly DiagnosticDescriptor InconsistentProperties = new("SH404", "Inconsistent properties", "Property '{0}' values for '{1}' are inconsistent", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<(string? AssemblyName, bool SupportNullableReferenceTypes)> compilationProvider = context.CompilationProvider
|
||||
.Select(static (compilation, cancellationToken) => (compilation.AssemblyName, SupportNullableReferenceTypes: compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute") is not null));
|
||||
|
||||
IncrementalValueProvider<ImmutableArray<AdditionalText>> resxProvider = context.AdditionalTextsProvider
|
||||
.Where(text => text.Path.EndsWith(".resx", StringComparison.OrdinalIgnoreCase))
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(
|
||||
source: context.AnalyzerConfigOptionsProvider.Combine(compilationProvider.Combine(resxProvider)),
|
||||
action: (ctx, source) => Execute(ctx, source.Left, source.Right.Left.AssemblyName, source.Right.Left.SupportNullableReferenceTypes, source.Right.Right));
|
||||
}
|
||||
|
||||
private static void Execute(SourceProductionContext context, AnalyzerConfigOptionsProvider options, string? assemblyName, bool supportNullableReferenceTypes, ImmutableArray<AdditionalText> files)
|
||||
{
|
||||
// Group additional file by resource kind ((a.resx, a.en.resx, a.en-us.resx), (b.resx, b.en-us.resx))
|
||||
List<IGrouping<string, AdditionalText>> resxGroups = files
|
||||
.GroupBy(file => GetResourceName(file.Path), StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x.Key, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
foreach (IGrouping<string, AdditionalText>? resxGroug in resxGroups)
|
||||
{
|
||||
string? rootNamespaceConfiguration = GetMetadataValue(context, options, "RootNamespace", resxGroug);
|
||||
string? projectDirConfiguration = GetMetadataValue(context, options, "ProjectDir", resxGroug);
|
||||
string? namespaceConfiguration = GetMetadataValue(context, options, "Namespace", "DefaultResourcesNamespace", resxGroug);
|
||||
string? resourceNameConfiguration = GetMetadataValue(context, options, "ResourceName", globalName: null, resxGroug);
|
||||
string? classNameConfiguration = GetMetadataValue(context, options, "ClassName", globalName: null, resxGroug);
|
||||
|
||||
string rootNamespace = rootNamespaceConfiguration ?? assemblyName ?? "";
|
||||
string projectDir = projectDirConfiguration ?? assemblyName ?? "";
|
||||
string? defaultResourceName = ComputeResourceName(rootNamespace, projectDir, resxGroug.Key);
|
||||
string? defaultNamespace = ComputeNamespace(rootNamespace, projectDir, resxGroug.Key);
|
||||
|
||||
string? ns = namespaceConfiguration ?? defaultNamespace;
|
||||
string? resourceName = resourceNameConfiguration ?? defaultResourceName;
|
||||
string className = classNameConfiguration ?? ToCSharpNameIdentifier(Path.GetFileName(resxGroug.Key));
|
||||
|
||||
if (ns == null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForNamespace, location: null, resxGroug.First().Path));
|
||||
}
|
||||
|
||||
if (resourceName == null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForResourceName, location: null, resxGroug.First().Path));
|
||||
}
|
||||
|
||||
List<ResxEntry>? entries = LoadResourceFiles(context, resxGroug);
|
||||
|
||||
string content = $"""
|
||||
// Debug info:
|
||||
// key: {resxGroug.Key}
|
||||
// files: {string.Join(", ", resxGroug.Select(f => f.Path))}
|
||||
// RootNamespace (metadata): {rootNamespaceConfiguration}
|
||||
// ProjectDir (metadata): {projectDirConfiguration}
|
||||
// Namespace / DefaultResourcesNamespace (metadata): {namespaceConfiguration}
|
||||
// ResourceName (metadata): {resourceNameConfiguration}
|
||||
// ClassName (metadata): {classNameConfiguration}
|
||||
// AssemblyName: {assemblyName}
|
||||
// RootNamespace (computed): {rootNamespace}
|
||||
// ProjectDir (computed): {projectDir}
|
||||
// defaultNamespace: {defaultNamespace}
|
||||
// defaultResourceName: {defaultResourceName}
|
||||
// Namespace: {ns}
|
||||
// ResourceName: {resourceName}
|
||||
// ClassName: {className}
|
||||
""";
|
||||
|
||||
if (resourceName != null && entries != null)
|
||||
{
|
||||
content += GenerateCode(ns, className, resourceName, entries, supportNullableReferenceTypes);
|
||||
}
|
||||
|
||||
context.AddSource($"{Path.GetFileName(resxGroug.Key)}.resx.g.cs", SourceText.From(content, Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateCode(string? ns, string className, string resourceName, List<ResxEntry> entries, bool enableNullableAttributes)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#nullable enable");
|
||||
|
||||
if (ns != null)
|
||||
{
|
||||
sb.AppendLine($$"""
|
||||
|
||||
namespace {{ns}};
|
||||
|
||||
""");
|
||||
}
|
||||
|
||||
sb.AppendLine($$"""
|
||||
internal partial class {{className}}
|
||||
{
|
||||
private static global::System.Resources.ResourceManager? resourceMan;
|
||||
|
||||
public {{className}}()
|
||||
{
|
||||
}
|
||||
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (resourceMan is null)
|
||||
{
|
||||
resourceMan = new global::System.Resources.ResourceManager("{{resourceName}}", typeof({{className}}).Assembly);
|
||||
}
|
||||
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo? Culture { get; set; }
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static object? GetObject(global::System.Globalization.CultureInfo? culture, string name, object? defaultValue)
|
||||
{
|
||||
culture ??= Culture;
|
||||
object? obj = ResourceManager.GetObject(name, culture);
|
||||
if (obj == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static object? GetObject(global::System.Globalization.CultureInfo? culture, string name)
|
||||
{
|
||||
return GetObject(culture: culture, name: name, defaultValue: null);
|
||||
}
|
||||
|
||||
public static object? GetObject(string name)
|
||||
{
|
||||
return GetObject(culture: null, name: name, defaultValue: null);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static object? GetObject(string name, object? defaultValue)
|
||||
{
|
||||
return GetObject(culture: null, name: name, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
public static global::System.IO.Stream? GetStream(string name)
|
||||
{
|
||||
return GetStream(culture: null, name: name);
|
||||
}
|
||||
|
||||
public static global::System.IO.Stream? GetStream(global::System.Globalization.CultureInfo? culture, string name)
|
||||
{
|
||||
culture ??= Culture;
|
||||
return ResourceManager.GetStream(name, culture);
|
||||
}
|
||||
|
||||
public static string? GetString(global::System.Globalization.CultureInfo? culture, string name)
|
||||
{
|
||||
return GetString(culture: culture, name: name, args: null);
|
||||
}
|
||||
|
||||
public static string? GetString(global::System.Globalization.CultureInfo? culture, string name, params object?[]? args)
|
||||
{
|
||||
culture ??= Culture;
|
||||
string? str = ResourceManager.GetString(name, culture);
|
||||
if (str == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args != null)
|
||||
{
|
||||
return string.Format(culture, str, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
public static string? GetString(string name, params object?[]? args)
|
||||
{
|
||||
return GetString(culture: null, name: name, args: args);
|
||||
}
|
||||
|
||||
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetString(string name, string? defaultValue)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: null);
|
||||
}
|
||||
|
||||
public static string? GetString(string name)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: null, args: null);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(global::System.Globalization.CultureInfo? culture, string name, string? defaultValue)
|
||||
{
|
||||
return GetStringOrDefault(culture: culture, name: name, defaultValue: defaultValue, args: null);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(global::System.Globalization.CultureInfo? culture, string name, string? defaultValue, params object?[]? args)
|
||||
{
|
||||
culture ??= Culture;
|
||||
string? str = ResourceManager.GetString(name, culture);
|
||||
if (str == null)
|
||||
{
|
||||
if (defaultValue == null || args == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Format(culture, defaultValue, args);
|
||||
}
|
||||
}
|
||||
|
||||
if (args != null)
|
||||
{
|
||||
return string.Format(culture, str, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(string name, string? defaultValue, params object?[]? args)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: args);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(string name, string? defaultValue)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: null);
|
||||
}
|
||||
""");
|
||||
|
||||
foreach (ResxEntry? entry in entries.OrderBy(e => e.Name, StringComparer.Ordinal))
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.IsText)
|
||||
{
|
||||
XElement summary = new("summary", new XElement("para", $"Looks up a localized string for \"{entry.Name}\"."));
|
||||
if (!string.IsNullOrWhiteSpace(entry.Comment))
|
||||
{
|
||||
summary.Add(new XElement("para", entry.Comment));
|
||||
}
|
||||
|
||||
if (!entry.IsFileRef)
|
||||
{
|
||||
summary.Add(new XElement("para", $"Value: \"{entry.Value}\"."));
|
||||
}
|
||||
|
||||
string comment = summary.ToString().Replace("\r\n", "\r\n /// ", StringComparison.Ordinal);
|
||||
|
||||
sb.AppendLine($$"""
|
||||
/// {{comment}}
|
||||
public static string {{ToCSharpNameIdentifier(entry.Name!)}}
|
||||
{
|
||||
get => GetString("{{entry.Name}}")!;
|
||||
}
|
||||
|
||||
""");
|
||||
|
||||
if (entry.Value != null)
|
||||
{
|
||||
int args = Regex.Matches(entry.Value, "\\{(?<num>[0-9]+)(\\:[^}]*)?\\}", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant)
|
||||
.Cast<Match>()
|
||||
.Select(m => int.Parse(m.Groups["num"].Value, CultureInfo.InvariantCulture))
|
||||
.Distinct()
|
||||
.DefaultIfEmpty(-1)
|
||||
.Max();
|
||||
|
||||
if (args >= 0)
|
||||
{
|
||||
string inParams = string.Join(", ", Enumerable.Range(0, args + 1).Select(arg => "object? arg" + arg.ToString(CultureInfo.InvariantCulture)));
|
||||
string callParams = string.Join(", ", Enumerable.Range(0, args + 1).Select(arg => "arg" + arg.ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
sb.AppendLine($$"""
|
||||
/// {{comment}}
|
||||
public static string Format{{ToCSharpNameIdentifier(entry.Name!)}}(global::System.Globalization.CultureInfo? provider, {{inParams}})
|
||||
{
|
||||
return GetString(provider, "{{entry.Name}}", {{callParams}})!;
|
||||
}
|
||||
|
||||
/// {{comment}}
|
||||
public static string Format{{ToCSharpNameIdentifier(entry.Name!)}}({{inParams}})
|
||||
{
|
||||
return GetString("{{entry.Name}}", {{callParams}})!;
|
||||
}
|
||||
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($$"""
|
||||
public static global::{{entry.FullTypeName}}? {{ToCSharpNameIdentifier(entry.Name!)}}
|
||||
{
|
||||
get => (global::{{entry.FullTypeName}}?)GetObject("{{entry.Name}}");
|
||||
}
|
||||
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine($$"""
|
||||
}
|
||||
|
||||
internal partial class {{className}}Names
|
||||
{
|
||||
""");
|
||||
|
||||
foreach (ResxEntry entry in entries)
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.AppendLine($$"""
|
||||
public const string {{ToCSharpNameIdentifier(entry.Name!)}} = "entry.Name";
|
||||
""");
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string? ComputeResourceName(string rootNamespace, string projectDir, string resourcePath)
|
||||
{
|
||||
string fullProjectDir = EnsureEndSeparator(Path.GetFullPath(projectDir));
|
||||
string fullResourcePath = Path.GetFullPath(resourcePath);
|
||||
|
||||
if (fullProjectDir == fullResourcePath)
|
||||
{
|
||||
return rootNamespace;
|
||||
}
|
||||
|
||||
if (fullResourcePath.StartsWith(fullProjectDir, StringComparison.Ordinal))
|
||||
{
|
||||
string relativePath = fullResourcePath.Substring(fullProjectDir.Length);
|
||||
return rootNamespace + '.' + relativePath.Replace('/', '.').Replace('\\', '.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ComputeNamespace(string rootNamespace, string projectDir, string resourcePath)
|
||||
{
|
||||
string fullProjectDir = EnsureEndSeparator(Path.GetFullPath(projectDir));
|
||||
string fullResourcePath = EnsureEndSeparator(Path.GetDirectoryName(Path.GetFullPath(resourcePath))!);
|
||||
|
||||
if (fullProjectDir == fullResourcePath)
|
||||
{
|
||||
return rootNamespace;
|
||||
}
|
||||
|
||||
if (fullResourcePath.StartsWith(fullProjectDir, StringComparison.Ordinal))
|
||||
{
|
||||
string relativePath = fullResourcePath.Substring(fullProjectDir.Length);
|
||||
return rootNamespace + '.' + relativePath.Replace('/', '.').Replace('\\', '.').TrimEnd('.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<ResxEntry>? LoadResourceFiles(SourceProductionContext context, IGrouping<string, AdditionalText> resxGroug)
|
||||
{
|
||||
List<ResxEntry> entries = new();
|
||||
foreach (AdditionalText? entry in resxGroug.OrderBy(file => file.Path, StringComparer.Ordinal))
|
||||
{
|
||||
SourceText? content = entry.GetText(context.CancellationToken);
|
||||
if (content == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
XDocument document = XDocument.Parse(content.ToString());
|
||||
foreach (XElement? element in document.XPathSelectElements("/root/data"))
|
||||
{
|
||||
string? name = element.Attribute("name")?.Value;
|
||||
string? type = element.Attribute("type")?.Value;
|
||||
string? comment = element.Attribute("comment")?.Value;
|
||||
string? value = element.Element("value")?.Value;
|
||||
|
||||
ResxEntry existingEntry = entries.Find(e => e.Name == name);
|
||||
if (existingEntry != null)
|
||||
{
|
||||
existingEntry.Comment ??= comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
entries.Add(new ResxEntry { Name = name, Value = value, Comment = comment, Type = type });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InvalidResx, location: null, entry.Path));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static string? GetMetadataValue(SourceProductionContext context, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, string name, IEnumerable<AdditionalText> additionalFiles)
|
||||
{
|
||||
return GetMetadataValue(context, analyzerConfigOptionsProvider, name, name, additionalFiles);
|
||||
}
|
||||
|
||||
private static string? GetMetadataValue(SourceProductionContext context, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, string name, string? globalName, IEnumerable<AdditionalText> additionalFiles)
|
||||
{
|
||||
string? result = null;
|
||||
foreach (AdditionalText file in additionalFiles)
|
||||
{
|
||||
if (analyzerConfigOptionsProvider.GetOptions(file).TryGetValue("build_metadata.AdditionalFiles." + name, out string? value))
|
||||
{
|
||||
if (result != null && value != result)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InconsistentProperties, location: null, name, file.Path));
|
||||
return null;
|
||||
}
|
||||
|
||||
result = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (globalName != null && analyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property." + globalName, out string? globalValue) && !string.IsNullOrEmpty(globalValue))
|
||||
{
|
||||
return globalValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ToCSharpNameIdentifier(string name)
|
||||
{
|
||||
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#identifiers
|
||||
// https://docs.microsoft.com/en-us/dotnet/api/system.globalization.unicodecategory?view=net-5.0
|
||||
StringBuilder sb = new();
|
||||
foreach (char c in name)
|
||||
{
|
||||
UnicodeCategory category = char.GetUnicodeCategory(c);
|
||||
switch (category)
|
||||
{
|
||||
case UnicodeCategory.UppercaseLetter:
|
||||
case UnicodeCategory.LowercaseLetter:
|
||||
case UnicodeCategory.TitlecaseLetter:
|
||||
case UnicodeCategory.ModifierLetter:
|
||||
case UnicodeCategory.OtherLetter:
|
||||
case UnicodeCategory.LetterNumber:
|
||||
sb.Append(c);
|
||||
break;
|
||||
|
||||
case UnicodeCategory.DecimalDigitNumber:
|
||||
case UnicodeCategory.ConnectorPunctuation:
|
||||
case UnicodeCategory.Format:
|
||||
if (sb.Length == 0)
|
||||
{
|
||||
sb.Append('_');
|
||||
}
|
||||
sb.Append(c);
|
||||
break;
|
||||
|
||||
default:
|
||||
sb.Append('_');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string EnsureEndSeparator(string path)
|
||||
{
|
||||
if (path[path.Length - 1] == Path.DirectorySeparatorChar)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return path + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
private static string GetResourceName(string path)
|
||||
{
|
||||
string pathWithoutExtension = Path.Combine(Path.GetDirectoryName(path)!, Path.GetFileNameWithoutExtension(path));
|
||||
int indexOf = pathWithoutExtension.LastIndexOf('.');
|
||||
if (indexOf < 0)
|
||||
{
|
||||
return pathWithoutExtension;
|
||||
}
|
||||
|
||||
return Regex.IsMatch(pathWithoutExtension.Substring(indexOf + 1), "^[a-zA-Z]{2}(-[a-zA-Z]{2})?$", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1))
|
||||
? pathWithoutExtension.Substring(0, indexOf)
|
||||
: pathWithoutExtension;
|
||||
}
|
||||
|
||||
private sealed class ResxEntry
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Value { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
public string? Type { get; set; }
|
||||
|
||||
public bool IsText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Type == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Value != null)
|
||||
{
|
||||
string[] parts = Value.Split(';');
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
string type = parts[1];
|
||||
if (type.StartsWith("System.String,", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string? FullTypeName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsText)
|
||||
{
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (Value != null)
|
||||
{
|
||||
string[] parts = Value.Split(';');
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
string type = parts[1];
|
||||
return type.Split(',')[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsFileRef
|
||||
{
|
||||
get => Type != null && Type.StartsWith("System.Resources.ResXFileRef,", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Resx;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
|
||||
int previousIndex = 0;
|
||||
int index = str.IndexOf(oldValue, comparison);
|
||||
while (index is not -1)
|
||||
{
|
||||
sb.Append(str, previousIndex, index - previousIndex);
|
||||
sb.Append(newValue);
|
||||
index += oldValue.Length;
|
||||
|
||||
previousIndex = index;
|
||||
index = str.IndexOf(oldValue, index, comparison);
|
||||
}
|
||||
|
||||
sb.Append(str, previousIndex, str.Length - previousIndex);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
|
||||
"allowMarshaling": true,
|
||||
"useSafeHandles": false,
|
||||
"emitSingleFile": true
|
||||
"useSafeHandles": false
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Color.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Converter.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/CornerRadius.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/FlyoutStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/FontStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Glyph.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/InfoBarOverride.xaml"/>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
/// <summary>
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
public AutoHeightBehavior()
|
||||
{
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Initialize()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateElement();
|
||||
}
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Height = AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
/// <summary>
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
public AutoWidthBehavior()
|
||||
{
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Initialize()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateElement();
|
||||
}
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Width = AssociatedObject.Height * (TargetWidth / TargetHeight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Labs.WinUI.MarqueeTextRns;
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
internal sealed class MarqueeTextBehavior : BehaviorBase<MarqueeText>
|
||||
{
|
||||
private readonly PointerEventHandler pointerEnteredEventHandler;
|
||||
private readonly PointerEventHandler pointerExitedEventHandler;
|
||||
|
||||
public MarqueeTextBehavior()
|
||||
{
|
||||
pointerEnteredEventHandler = OnPointerEntered;
|
||||
pointerExitedEventHandler = OnPointerExited;
|
||||
}
|
||||
|
||||
protected override bool Initialize()
|
||||
{
|
||||
AssociatedObject.PointerEntered += pointerEnteredEventHandler;
|
||||
AssociatedObject.PointerExited += pointerExitedEventHandler;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.PointerEntered -= pointerEnteredEventHandler;
|
||||
AssociatedObject.PointerExited -= pointerExitedEventHandler;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnPointerEntered(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
AssociatedObject.StartMarquee();
|
||||
}
|
||||
|
||||
private void OnPointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
AssociatedObject.StopMarquee();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
@@ -12,7 +13,7 @@ internal sealed class SelectedItemInViewBehavior : BehaviorBase<ListViewBase>
|
||||
{
|
||||
if (AssociatedObject.SelectedItem is { } item)
|
||||
{
|
||||
AssociatedObject.ScrollIntoView(item);
|
||||
AssociatedObject.SmoothScrollIntoViewWithItemAsync(item, ScrollItemPlacement.Center).SafeForget();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Xaml.Interactivity;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
[DependencyProperty("Animation", typeof(AnimationSet))]
|
||||
[DependencyProperty("TargetObject", typeof(UIElement))]
|
||||
internal sealed partial class StartAnimationActionNoThrow : DependencyObject, IAction
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object Execute(object sender, object parameter)
|
||||
{
|
||||
if (Animation is not null)
|
||||
{
|
||||
if (TargetObject is not null)
|
||||
{
|
||||
Animation.Start(TargetObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Animation.Start(sender as UIElement);
|
||||
}
|
||||
}
|
||||
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ internal struct ContentDialogHideToken : IDisposable, IAsyncDisposable
|
||||
private readonly ContentDialog contentDialog;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
private bool disposed = false;
|
||||
private bool disposing = false;
|
||||
private bool disposed = false;
|
||||
|
||||
public ContentDialogHideToken(ContentDialog contentDialog, ITaskContext taskContext)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Helper;
|
||||
|
||||
[SuppressMessage("", "SH001")]
|
||||
[DependencyProperty("IsItemsEnabled", typeof(bool), true, nameof(OnIsItemsEnabledChanged), IsAttached = true, AttachedType = typeof(SettingsExpander))]
|
||||
public sealed partial class SettingsExpanderHelper
|
||||
{
|
||||
private static void OnIsItemsEnabledChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
foreach (object item in ((SettingsExpander)dp).Items)
|
||||
{
|
||||
if (item is Microsoft.UI.Xaml.Controls.Control control)
|
||||
{
|
||||
control.IsEnabled = (bool)e.NewValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using System.Numerics;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class DefaultItemCollectionTransitionProvider : ItemCollectionTransitionProvider
|
||||
{
|
||||
private const double DefaultAnimationDurationInMs = 300.0;
|
||||
|
||||
static DefaultItemCollectionTransitionProvider()
|
||||
{
|
||||
AnimationSlowdownFactor = 1.0;
|
||||
}
|
||||
|
||||
public static double AnimationSlowdownFactor { get; set; }
|
||||
|
||||
protected override bool ShouldAnimateCore(ItemCollectionTransition transition)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void StartTransitions(IList<ItemCollectionTransition> transitions)
|
||||
{
|
||||
List<ItemCollectionTransition> addTransitions = new();
|
||||
List<ItemCollectionTransition> removeTransitions = new();
|
||||
List<ItemCollectionTransition> moveTransitions = new();
|
||||
|
||||
foreach (ItemCollectionTransition transition in addTransitions)
|
||||
{
|
||||
switch (transition.Operation)
|
||||
{
|
||||
case ItemCollectionTransitionOperation.Add:
|
||||
addTransitions.Add(transition);
|
||||
break;
|
||||
case ItemCollectionTransitionOperation.Remove:
|
||||
removeTransitions.Add(transition);
|
||||
break;
|
||||
case ItemCollectionTransitionOperation.Move:
|
||||
moveTransitions.Add(transition);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
StartAddTransitions(addTransitions, removeTransitions.Count > 0, moveTransitions.Count > 0);
|
||||
StartRemoveTransitions(removeTransitions);
|
||||
StartMoveTransitions(moveTransitions, removeTransitions.Count > 0);
|
||||
}
|
||||
|
||||
private static void StartAddTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveTransitions, bool hasMoveTransitions)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
|
||||
ScalarKeyFrameAnimation fadeInAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
fadeInAnimation.InsertKeyFrame(0.0f, 0.0f);
|
||||
|
||||
if (hasMoveTransitions && hasRemoveTransitions)
|
||||
{
|
||||
fadeInAnimation.InsertKeyFrame(0.66f, 0.0f);
|
||||
}
|
||||
else if (hasMoveTransitions || hasRemoveTransitions)
|
||||
{
|
||||
fadeInAnimation.InsertKeyFrame(0.5f, 0.0f);
|
||||
}
|
||||
|
||||
fadeInAnimation.InsertKeyFrame(1.0f, 1.0f);
|
||||
fadeInAnimation.Duration = TimeSpan.FromMilliseconds(
|
||||
DefaultAnimationDurationInMs * ((hasRemoveTransitions ? 1 : 0) + (hasMoveTransitions ? 1 : 0) + 1) * AnimationSlowdownFactor);
|
||||
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
visual.StartAnimation("Opacity", fadeInAnimation);
|
||||
batch.End();
|
||||
batch.Completed += (_, _) => progress.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private static void StartRemoveTransitions(IList<ItemCollectionTransition> transitions)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
|
||||
ScalarKeyFrameAnimation fadeOutAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
fadeOutAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue");
|
||||
fadeOutAnimation.InsertKeyFrame(1.0f, 0.0f);
|
||||
fadeOutAnimation.Duration = TimeSpan.FromMilliseconds(DefaultAnimationDurationInMs * AnimationSlowdownFactor);
|
||||
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
visual.StartAnimation(nameof(Visual.Opacity), fadeOutAnimation);
|
||||
batch.End();
|
||||
batch.Completed += (_, _) =>
|
||||
{
|
||||
visual.Opacity = 1.0f;
|
||||
progress.Complete();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void StartMoveTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveAnimations)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
|
||||
// Animate offset.
|
||||
if (transition.OldBounds.X != transition.NewBounds.X ||
|
||||
transition.OldBounds.Y != transition.NewBounds.Y)
|
||||
{
|
||||
AnimateOffset(visual, compositor, transition.OldBounds, transition.NewBounds, hasRemoveAnimations);
|
||||
}
|
||||
|
||||
batch.End();
|
||||
batch.Completed += (_, _) => progress.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AnimateOffset(Visual visual, Compositor compositor, Rect oldBounds, Rect newBounds, bool hasRemoveAnimations)
|
||||
{
|
||||
Vector2KeyFrameAnimation offsetAnimation = compositor.CreateVector2KeyFrameAnimation();
|
||||
|
||||
offsetAnimation.SetVector2Parameter("delta", new Vector2(
|
||||
(float)(oldBounds.X - newBounds.X),
|
||||
(float)(oldBounds.Y - newBounds.Y)));
|
||||
offsetAnimation.SetVector2Parameter("final", default);
|
||||
offsetAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue + delta");
|
||||
if (hasRemoveAnimations)
|
||||
{
|
||||
offsetAnimation.InsertExpressionKeyFrame(0.5f, "delta");
|
||||
}
|
||||
|
||||
offsetAnimation.InsertExpressionKeyFrame(1.0f, "final");
|
||||
offsetAnimation.Duration = TimeSpan.FromMilliseconds(
|
||||
DefaultAnimationDurationInMs * ((hasRemoveAnimations ? 1 : 0) + 1) * AnimationSlowdownFactor);
|
||||
|
||||
visual.StartAnimation("TransformMatrix._41_42", offsetAnimation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
[DebuggerDisplay("Count = {Count}, Height = {Height}")]
|
||||
internal class UniformStaggeredColumnLayout : List<UniformStaggeredItem>
|
||||
{
|
||||
public double Height { get; private set; }
|
||||
|
||||
public new void Add(UniformStaggeredItem item)
|
||||
{
|
||||
Height = item.Top + item.Height;
|
||||
base.Add(item);
|
||||
}
|
||||
|
||||
public new void Clear()
|
||||
{
|
||||
Height = 0;
|
||||
base.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class UniformStaggeredItem
|
||||
{
|
||||
public UniformStaggeredItem(int index)
|
||||
{
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public double Top { get; internal set; }
|
||||
|
||||
public double Height { get; internal set; }
|
||||
|
||||
public int Index { get; }
|
||||
|
||||
public UIElement? Element { get; internal set; }
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Collections.Specialized;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
[DependencyProperty("MinItemWidth", typeof(double), 0D, nameof(OnMinItemWidthChanged))]
|
||||
[DependencyProperty("MinColumnSpacing", typeof(double), 0D, nameof(OnSpacingChanged))]
|
||||
[DependencyProperty("MinRowSpacing", typeof(double), 0D, nameof(OnSpacingChanged))]
|
||||
internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
|
||||
{
|
||||
context.LayoutState = new UniformStaggeredLayoutState(context);
|
||||
base.InitializeForContextCore(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
|
||||
{
|
||||
context.LayoutState = null;
|
||||
base.UninitializeForContextCore(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
state.RemoveFromIndex(args.NewStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
state.RemoveFromIndex(args.NewStartingIndex);
|
||||
state.RecycleElementAt(args.NewStartingIndex); // We must recycle the element to ensure that it gets the correct context
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
|
||||
int maxIndex = Math.Max(args.NewStartingIndex, args.OldStartingIndex);
|
||||
state.RemoveRange(minIndex, maxIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
state.RemoveFromIndex(args.OldStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
state.Clear();
|
||||
break;
|
||||
}
|
||||
|
||||
base.OnItemsChangedCore(context, source, args);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
|
||||
{
|
||||
if (context.ItemCount == 0)
|
||||
{
|
||||
return new Size(availableSize.Width, 0);
|
||||
}
|
||||
|
||||
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
|
||||
{
|
||||
return new Size(availableSize.Width, 0.0f);
|
||||
}
|
||||
|
||||
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
|
||||
|
||||
double availableWidth = availableSize.Width;
|
||||
double availableHeight = availableSize.Height;
|
||||
|
||||
(int numberOfColumns, double columnWidth) = GetNumberOfColumnsAndWidth(availableWidth, MinItemWidth, MinColumnSpacing);
|
||||
|
||||
if (columnWidth != state.ColumnWidth)
|
||||
{
|
||||
// The items will need to be remeasured
|
||||
state.Clear();
|
||||
}
|
||||
|
||||
state.ColumnWidth = columnWidth;
|
||||
|
||||
// adjust for column spacing on all columns expect the first
|
||||
double totalWidth = state.ColumnWidth + ((numberOfColumns - 1) * (state.ColumnWidth + MinColumnSpacing));
|
||||
if (totalWidth > availableWidth)
|
||||
{
|
||||
numberOfColumns--;
|
||||
}
|
||||
else if (double.IsInfinity(availableWidth))
|
||||
{
|
||||
availableWidth = totalWidth;
|
||||
}
|
||||
|
||||
if (numberOfColumns != state.NumberOfColumns)
|
||||
{
|
||||
// The items will not need to be remeasured, but they will need to go into new columns
|
||||
state.ClearColumns();
|
||||
}
|
||||
|
||||
if (MinRowSpacing != state.RowSpacing)
|
||||
{
|
||||
// If the RowSpacing changes the height of the rows will be different.
|
||||
// The columns stores the height so we'll want to clear them out to
|
||||
// get the proper height
|
||||
state.ClearColumns();
|
||||
state.RowSpacing = MinRowSpacing;
|
||||
}
|
||||
|
||||
Span<double> columnHeights = new double[numberOfColumns];
|
||||
Span<int> itemsPerColumn = new int[numberOfColumns];
|
||||
HashSet<int> deadColumns = new();
|
||||
|
||||
for (int i = 0; i < context.ItemCount; i++)
|
||||
{
|
||||
int columnIndex = GetLowestColumnIndex(columnHeights);
|
||||
|
||||
bool measured = false;
|
||||
UniformStaggeredItem item = state.GetItemAt(i);
|
||||
if (item.Height == 0)
|
||||
{
|
||||
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
|
||||
// The first element must be force refreshed otherwise
|
||||
// it will use the old one realized
|
||||
ElementRealizationOptions options = i == 0 ? ElementRealizationOptions.ForceCreate : ElementRealizationOptions.None;
|
||||
|
||||
// Item has not been measured yet. Get the element and store the values
|
||||
UIElement element = context.GetOrCreateElementAt(i, options);
|
||||
element.Measure(new Size(state.ColumnWidth, availableHeight));
|
||||
item.Height = element.DesiredSize.Height;
|
||||
item.Element = element;
|
||||
measured = true;
|
||||
}
|
||||
|
||||
double spacing = itemsPerColumn[columnIndex] > 0 ? MinRowSpacing : 0;
|
||||
item.Top = columnHeights[columnIndex] + spacing;
|
||||
double bottom = item.Top + item.Height;
|
||||
columnHeights[columnIndex] = bottom;
|
||||
itemsPerColumn[columnIndex]++;
|
||||
state.AddItemToColumn(item, columnIndex);
|
||||
|
||||
if (bottom < context.RealizationRect.Top)
|
||||
{
|
||||
// The bottom of the element is above the realization area
|
||||
if (item.Element is not null)
|
||||
{
|
||||
context.RecycleElement(item.Element);
|
||||
item.Element = null;
|
||||
}
|
||||
}
|
||||
else if (item.Top > context.RealizationRect.Bottom)
|
||||
{
|
||||
// The top of the element is below the realization area
|
||||
if (item.Element is not null)
|
||||
{
|
||||
context.RecycleElement(item.Element);
|
||||
item.Element = null;
|
||||
}
|
||||
|
||||
deadColumns.Add(columnIndex);
|
||||
}
|
||||
else if (measured == false)
|
||||
{
|
||||
// We ALWAYS want to measure an item that will be in the bounds
|
||||
item.Element = context.GetOrCreateElementAt(i);
|
||||
item.Element.Measure(new Size(state.ColumnWidth, availableHeight));
|
||||
if (item.Height != item.Element.DesiredSize.Height)
|
||||
{
|
||||
// this item changed size; we need to recalculate layout for everything after this
|
||||
state.RemoveFromIndex(i + 1);
|
||||
item.Height = item.Element.DesiredSize.Height;
|
||||
columnHeights[columnIndex] = item.Top + item.Height;
|
||||
}
|
||||
}
|
||||
|
||||
if (deadColumns.Count == numberOfColumns)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
double desiredHeight = state.GetHeight();
|
||||
|
||||
return new Size(availableWidth, desiredHeight);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
|
||||
{
|
||||
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
|
||||
{
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
|
||||
|
||||
// Cycle through each column and arrange the items that are within the realization bounds
|
||||
for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
|
||||
{
|
||||
UniformStaggeredColumnLayout layout = state.GetColumnLayout(columnIndex);
|
||||
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
|
||||
for (int i = 0; i < layoutSpan.Length; i++)
|
||||
{
|
||||
ref readonly UniformStaggeredItem item = ref layoutSpan[i];
|
||||
|
||||
double bottom = item.Top + item.Height;
|
||||
if (bottom < context.RealizationRect.Top)
|
||||
{
|
||||
// element is above the realization bounds
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.Top <= context.RealizationRect.Bottom)
|
||||
{
|
||||
double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex);
|
||||
|
||||
Rect bounds = new(itemHorizontalOffset, item.Top, state.ColumnWidth, item.Height);
|
||||
UIElement element = context.GetOrCreateElementAt(item.Index);
|
||||
element.Arrange(bounds);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double minColumnSpacing)
|
||||
{
|
||||
// test if the width can fit in 2 items
|
||||
if ((2 * minItemWidth) + minColumnSpacing > availableWidth)
|
||||
{
|
||||
return (1, availableWidth);
|
||||
}
|
||||
|
||||
int columnCount = Math.Max(1, (int)((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing)));
|
||||
double columnWidthAddSpacing = (availableWidth + minColumnSpacing) / columnCount;
|
||||
return (columnCount, columnWidthAddSpacing - minColumnSpacing);
|
||||
}
|
||||
|
||||
private static int GetLowestColumnIndex(in ReadOnlySpan<double> columnHeights)
|
||||
{
|
||||
int columnIndex = 0;
|
||||
double height = columnHeights[0];
|
||||
for (int j = 1; j < columnHeights.Length; j++)
|
||||
{
|
||||
if (columnHeights[j] < height)
|
||||
{
|
||||
columnIndex = j;
|
||||
height = columnHeights[j];
|
||||
}
|
||||
}
|
||||
|
||||
return columnIndex;
|
||||
}
|
||||
|
||||
private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
|
||||
panel.InvalidateMeasure();
|
||||
}
|
||||
|
||||
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
|
||||
panel.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class UniformStaggeredLayoutState
|
||||
{
|
||||
private readonly List<UniformStaggeredItem> items = new();
|
||||
private readonly VirtualizingLayoutContext context;
|
||||
private readonly Dictionary<int, UniformStaggeredColumnLayout> columnLayout = new();
|
||||
private double lastAverageHeight;
|
||||
|
||||
public UniformStaggeredLayoutState(VirtualizingLayoutContext context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public double ColumnWidth { get; internal set; }
|
||||
|
||||
public int NumberOfColumns
|
||||
{
|
||||
get => columnLayout.Count;
|
||||
}
|
||||
|
||||
public double RowSpacing { get; internal set; }
|
||||
|
||||
internal void AddItemToColumn(UniformStaggeredItem item, int columnIndex)
|
||||
{
|
||||
if (!this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout))
|
||||
{
|
||||
columnLayout = new();
|
||||
this.columnLayout[columnIndex] = columnLayout;
|
||||
}
|
||||
|
||||
if (!columnLayout.Contains(item))
|
||||
{
|
||||
columnLayout.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "CA2201")]
|
||||
internal UniformStaggeredItem GetItemAt(int index)
|
||||
{
|
||||
if (index < 0)
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
if (index <= (items.Count - 1))
|
||||
{
|
||||
return items[index];
|
||||
}
|
||||
else
|
||||
{
|
||||
UniformStaggeredItem item = new(index);
|
||||
items.Add(item);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH007")]
|
||||
internal UniformStaggeredColumnLayout GetColumnLayout(int columnIndex)
|
||||
{
|
||||
this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout);
|
||||
return columnLayout!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear everything that has been calculated.
|
||||
/// </summary>
|
||||
internal void Clear()
|
||||
{
|
||||
columnLayout.Clear();
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the layout columns so they will be recalculated.
|
||||
/// </summary>
|
||||
internal void ClearColumns()
|
||||
{
|
||||
columnLayout.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the estimated height of the layout.
|
||||
/// </summary>
|
||||
/// <returns>The estimated height of the layout.</returns>
|
||||
/// <remarks>
|
||||
/// If all of the items have been calculated then the actual height will be returned.
|
||||
/// If all of the items have not been calculated then an estimated height will be calculated based on the average height of the items.
|
||||
/// </remarks>
|
||||
internal double GetHeight()
|
||||
{
|
||||
double desiredHeight = columnLayout.Values.Max(c => c.Height);
|
||||
int itemCount = columnLayout.Values.Sum(c => c.Count);
|
||||
|
||||
if (itemCount == context.ItemCount)
|
||||
{
|
||||
return desiredHeight;
|
||||
}
|
||||
|
||||
double averageHeight = 0;
|
||||
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
|
||||
{
|
||||
averageHeight += layout.Height / layout.Count;
|
||||
}
|
||||
|
||||
averageHeight /= columnLayout.Count;
|
||||
double estimatedHeight = (averageHeight * context.ItemCount) / columnLayout.Count;
|
||||
if (estimatedHeight > desiredHeight)
|
||||
{
|
||||
desiredHeight = estimatedHeight;
|
||||
}
|
||||
|
||||
if (Math.Abs(desiredHeight - lastAverageHeight) < 5)
|
||||
{
|
||||
return lastAverageHeight;
|
||||
}
|
||||
|
||||
lastAverageHeight = desiredHeight;
|
||||
return desiredHeight;
|
||||
}
|
||||
|
||||
internal void RecycleElementAt(int index)
|
||||
{
|
||||
UIElement element = context.GetOrCreateElementAt(index);
|
||||
context.RecycleElement(element);
|
||||
}
|
||||
|
||||
internal void RemoveFromIndex(int index)
|
||||
{
|
||||
if (index >= items.Count)
|
||||
{
|
||||
// Item was added/removed but we haven't realized that far yet
|
||||
return;
|
||||
}
|
||||
|
||||
int numToRemove = items.Count - index;
|
||||
items.RemoveRange(index, numToRemove);
|
||||
|
||||
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
|
||||
{
|
||||
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
|
||||
for (int i = 0; i < layoutSpan.Length; i++)
|
||||
{
|
||||
if (layoutSpan[i].Index >= index)
|
||||
{
|
||||
numToRemove = layoutSpan.Length - i;
|
||||
layout.RemoveRange(i, numToRemove);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveRange(int startIndex, int endIndex)
|
||||
{
|
||||
for (int i = startIndex; i <= endIndex; i++)
|
||||
{
|
||||
if (i > items.Count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ref readonly UniformStaggeredItem item = ref CollectionsMarshal.AsSpan(items)[i];
|
||||
item.Height = 0;
|
||||
item.Top = 0;
|
||||
|
||||
// We must recycle all elements to ensure that it gets the correct context
|
||||
RecycleElementAt(i);
|
||||
}
|
||||
|
||||
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
|
||||
{
|
||||
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
|
||||
for (int i = 0; i < layoutSpan.Length; i++)
|
||||
{
|
||||
if ((startIndex <= layoutSpan[i].Index) && (layoutSpan[i].Index <= endIndex))
|
||||
{
|
||||
int numToRemove = layoutSpan.Length - i;
|
||||
layout.RemoveRange(i, numToRemove);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,11 @@
|
||||
mc:Ignorable="d">
|
||||
|
||||
<cwc:SegmentedItem
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentBulletedList}}"
|
||||
Tag="List"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
|
||||
<cwc:SegmentedItem
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentGridView}}"
|
||||
Tag="Grid"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
@@ -11,6 +12,8 @@ namespace Snap.Hutao.Control.Panel;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("Current", typeof(string), List)]
|
||||
[DependencyProperty("LocalSettingKeySuffixForCurrent", typeof(string))]
|
||||
[DependencyProperty("LocalSettingKeyExtraForCurrent", typeof(string), "")]
|
||||
internal sealed partial class PanelSelector : Segmented
|
||||
{
|
||||
public const string List = nameof(List);
|
||||
@@ -42,21 +45,41 @@ internal sealed partial class PanelSelector : Segmented
|
||||
selectedIndexChangedCallbackToken = RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged);
|
||||
}
|
||||
|
||||
private void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp)
|
||||
{
|
||||
Current = IndexTypeMap[(int)GetValue(dp)];
|
||||
}
|
||||
|
||||
private void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
private static void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp)
|
||||
{
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
selector.SelectedItem = selector.Items.Cast<SegmentedItem>().Single(item => (string)item.Tag == Current);
|
||||
selector.Current = IndexTypeMap[(int)selector.GetValue(dp)];
|
||||
|
||||
if (!string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent))
|
||||
{
|
||||
LocalSetting.Set(GetSettingKey(selector), selector.Current);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRootUnload(object sender, RoutedEventArgs e)
|
||||
private static void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UnregisterPropertyChangedCallback(SelectedIndexProperty, selectedIndexChangedCallbackToken);
|
||||
Loaded -= loadedEventHandler;
|
||||
Unloaded -= unloadedEventHandler;
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
|
||||
if (string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string value = LocalSetting.Get(GetSettingKey(selector), selector.Current);
|
||||
selector.Current = value;
|
||||
|
||||
selector.SelectedItem = selector.Items.Cast<SegmentedItem>().Single(item => (string)item.Tag == selector.Current);
|
||||
}
|
||||
|
||||
private static void OnRootUnload(object sender, RoutedEventArgs e)
|
||||
{
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
selector.UnregisterPropertyChangedCallback(SelectedIndexProperty, selector.selectedIndexChangedCallbackToken);
|
||||
selector.Unloaded -= selector.unloadedEventHandler;
|
||||
}
|
||||
|
||||
private static string GetSettingKey(PanelSelector selector)
|
||||
{
|
||||
return $"Control.PanelSelector.{selector.LocalSettingKeySuffixForCurrent}{selector.LocalSettingKeyExtraForCurrent}";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<CornerRadius x:Key="ControlCornerRadiusTop">4,4,0,0</CornerRadius>
|
||||
<CornerRadius x:Key="ControlCornerRadiusBottom">0,0,4,4</CornerRadius>
|
||||
<CornerRadius x:Key="ControlCornerRadiusTopRightAndBottomLeft">0,4,0,4</CornerRadius>
|
||||
</ResourceDictionary>
|
||||
|
||||
21
src/Snap.Hutao/Snap.Hutao/Control/Theme/FlyoutStyle.xaml
Normal file
21
src/Snap.Hutao/Snap.Hutao/Control/Theme/FlyoutStyle.xaml
Normal file
@@ -0,0 +1,21 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Style
|
||||
x:Key="WebViewerFlyoutPresenterStyle"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="FlyoutPresenterPadding0And2Style"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="0,2"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="FlyoutPresenterPadding6Style"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="6"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -3,8 +3,18 @@
|
||||
<x:String x:Key="FontIconContentSetting"></x:String>
|
||||
<x:String x:Key="FontIconContentRefresh"></x:String>
|
||||
<x:String x:Key="FontIconContentDelete"></x:String>
|
||||
<x:String x:Key="FontIconContentChevronRight"></x:String>
|
||||
<x:String x:Key="FontIconContentWarning"></x:String>
|
||||
<x:String x:Key="FontIconContentGame"></x:String>
|
||||
<x:String x:Key="FontIconContentOpenInNewWindow"></x:String>
|
||||
<x:String x:Key="FontIconContentFolder"></x:String>
|
||||
<x:String x:Key="FontIconContentCopy"></x:String>
|
||||
<x:String x:Key="FontIconContentBulletedList"></x:String>
|
||||
<x:String x:Key="FontIconContentCheckList"></x:String>
|
||||
<x:String x:Key="FontIconContentWebsite"></x:String>
|
||||
<x:String x:Key="FontIconContentHomeGroup"></x:String>
|
||||
<x:String x:Key="FontIconContentAsteriskBadge12"></x:String>
|
||||
<x:String x:Key="FontIconContentZipFolder"></x:String>
|
||||
</ResourceDictionary>
|
||||
<x:String x:Key="FontIconContentGridView"></x:String>
|
||||
<x:String x:Key="FontIconContentGiftboxOpen"></x:String>
|
||||
</ResourceDictionary>
|
||||
@@ -11,13 +11,28 @@
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing4Template">
|
||||
<cwcont:WrapPanel HorizontalSpacing="4" VerticalSpacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelTemplate">
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing0Template">
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing2Template">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
|
||||
<cwcont:UniformGrid
|
||||
ColumnSpacing="2"
|
||||
Columns="2"
|
||||
RowSpacing="2"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="UniformGridColumns5Spacing4Template">
|
||||
<cwcont:UniformGrid
|
||||
ColumnSpacing="4"
|
||||
Columns="5"
|
||||
RowSpacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="UniformGridColumns5Spacing8Template">
|
||||
<cwcont:UniformGrid
|
||||
ColumnSpacing="8"
|
||||
Columns="5"
|
||||
RowSpacing="8"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
<x:Double x:Key="SettingsCardMinHeight">0</x:Double>
|
||||
<x:Double x:Key="SettingsCardWrapThreshold">0</x:Double>
|
||||
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
|
||||
|
||||
<x:Double x:Key="SettingsCardContentControlMinWidth">120</x:Double>
|
||||
|
||||
<Style
|
||||
x:Key="SettingsSectionHeaderTextBlockStyle"
|
||||
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
|
||||
@@ -16,16 +19,17 @@
|
||||
x:Key="SettingsContentComboBoxStyle"
|
||||
BasedOn="{StaticResource DefaultComboBoxStyle}"
|
||||
TargetType="ComboBox">
|
||||
<Setter Property="MinWidth" Value="120"/>
|
||||
<Setter Property="MinWidth" Value="{ThemeResource SettingsCardContentControlMinWidth}"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="SettingButtonStyle"
|
||||
BasedOn="{StaticResource DefaultButtonStyle}"
|
||||
TargetType="Button">
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
<Setter Property="Padding" Value="16,6,16,6"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="MinWidth" Value="{ThemeResource SettingsCardContentControlMinWidth}"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -14,4 +14,10 @@
|
||||
<TransitionCollection x:Key="ReorderThemeTransitions">
|
||||
<ReorderThemeTransition/>
|
||||
</TransitionCollection>
|
||||
<TransitionCollection x:Key="RepositionThemeTransitions">
|
||||
<RepositionThemeTransition/>
|
||||
</TransitionCollection>
|
||||
<TransitionCollection x:Key="NavigationThemeTransitions">
|
||||
<NavigationThemeTransition/>
|
||||
</TransitionCollection>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -15,22 +15,7 @@ internal abstract class ValueConverter<TFrom, TTo> : IValueConverter
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
#if DEBUG
|
||||
try
|
||||
{
|
||||
return Convert((TFrom)value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
|
||||
.LogError(ex, "值转换器异常");
|
||||
|
||||
throw;
|
||||
}
|
||||
#else
|
||||
return Convert((TFrom)value);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Service;
|
||||
using System.Globalization;
|
||||
|
||||
@@ -36,7 +36,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// 对于需要添加动态密钥1的客户端使用此配置
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void XRpcConfiguration(HttpClient client)
|
||||
@@ -50,7 +50,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// 对于需要添加动态密钥2的客户端使用此配置
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void XRpc2Configuration(HttpClient client)
|
||||
@@ -64,11 +64,11 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// 对于需要添加动态密钥1的客户端使用此配置
|
||||
/// HoYoLAB app
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
@@ -84,7 +84,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// 对于需要添加动态密钥2的客户端使用此配置
|
||||
/// HoYoLAB web
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
|
||||
@@ -27,13 +27,17 @@ internal sealed partial class ExceptionRecorder
|
||||
app.DebugSettings.XamlResourceReferenceFailed += OnXamlResourceReferenceFailed;
|
||||
}
|
||||
|
||||
[SuppressMessage("", "CA2012")]
|
||||
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
serviceProvider
|
||||
ValueTask<string?> task = serviceProvider
|
||||
.GetRequiredService<Web.Hutao.Log.HomaLogUploadClient>()
|
||||
.UploadLogAsync(e.Exception)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
.UploadLogAsync(e.Exception);
|
||||
|
||||
if (!task.IsCompleted)
|
||||
{
|
||||
task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
logger.LogError("未经处理的全局异常:\r\n{Detail}", ExceptionFormat.Format(e.Exception));
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ internal sealed class RuntimeEnvironmentException : Exception
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="innerException">内部错误</param>
|
||||
public RuntimeEnvironmentException(string message, Exception innerException)
|
||||
: base($"{message}\n{innerException.Message}", innerException)
|
||||
public RuntimeEnvironmentException(string message, Exception? innerException)
|
||||
: base($"{message}\n{innerException?.Message}", innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -14,27 +14,27 @@ namespace Snap.Hutao.Core.ExceptionService;
|
||||
[System.Diagnostics.StackTraceHidden]
|
||||
internal static class ThrowHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 操作取消
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="inner">内部错误</param>
|
||||
/// <returns>nothing</returns>
|
||||
/// <exception cref="OperationCanceledException">操作取消异常</exception>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static OperationCanceledException OperationCanceled(string message, Exception? inner = default)
|
||||
public static ArgumentException Argument(string message, string? paramName)
|
||||
{
|
||||
throw new OperationCanceledException(message, inner);
|
||||
throw new ArgumentException(message, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static DatabaseCorruptedException DatabaseCorrupted(string message, Exception? inner)
|
||||
{
|
||||
throw new DatabaseCorruptedException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static GameFileOperationException GameFileOperation(string message, Exception? inner)
|
||||
{
|
||||
throw new GameFileOperationException(message, inner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 无效操作
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="inner">内部错误</param>
|
||||
/// <returns>nothing</returns>
|
||||
/// <exception cref="InvalidOperationException">无效操作异常</exception>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static InvalidOperationException InvalidOperation(string message, Exception? inner = default)
|
||||
@@ -42,71 +42,38 @@ internal static class ThrowHelper
|
||||
throw new InvalidOperationException(message, inner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏文件操作失败
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="inner">内部错误</param>
|
||||
/// <returns>nothing</returns>
|
||||
/// <exception cref="GameFileOperationException">文件操作失败</exception>
|
||||
public static GameFileOperationException GameFileOperation(string message, Exception inner)
|
||||
{
|
||||
throw new GameFileOperationException(message, inner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 包转换错误
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="inner">内部错误</param>
|
||||
/// <returns>nothing</returns>
|
||||
/// <exception cref="PackageConvertException">包转换错误异常</exception>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static PackageConvertException PackageConvert(string message, Exception inner)
|
||||
{
|
||||
throw new PackageConvertException(message, inner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户数据损坏
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="inner">内部错误</param>
|
||||
/// <returns>nothing</returns>
|
||||
/// <exception cref="UserdataCorruptedException">数据损坏</exception>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static UserdataCorruptedException UserdataCorrupted(string message, Exception inner)
|
||||
{
|
||||
throw new UserdataCorruptedException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static DatabaseCorruptedException DatabaseCorrupted(string message, Exception inner)
|
||||
{
|
||||
throw new DatabaseCorruptedException(message, inner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行环境异常
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="inner">内部错误</param>
|
||||
/// <returns>nothing</returns>
|
||||
/// <exception cref="RuntimeEnvironmentException">环境异常</exception>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static RuntimeEnvironmentException RuntimeEnvironment(string message, Exception inner)
|
||||
{
|
||||
throw new RuntimeEnvironmentException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static NotSupportedException NotSupported()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static OperationCanceledException OperationCanceled(string message, Exception? inner = default)
|
||||
{
|
||||
throw new OperationCanceledException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static PackageConvertException PackageConvert(string message, Exception? inner)
|
||||
{
|
||||
throw new PackageConvertException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static RuntimeEnvironmentException RuntimeEnvironment(string message, Exception? inner)
|
||||
{
|
||||
throw new RuntimeEnvironmentException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static UserdataCorruptedException UserdataCorrupted(string message, Exception? inner)
|
||||
{
|
||||
throw new UserdataCorruptedException(message, inner);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ namespace Snap.Hutao.Core.Json.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// 实现日期的转换
|
||||
/// 此转换器无法实现无损往返
|
||||
/// 必须在反序列化后调整 Offset
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
|
||||
@@ -18,7 +20,10 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
if (reader.GetString() is { } dataTimeString)
|
||||
{
|
||||
return DateTimeOffset.ParseExact(dataTimeString, Format, CultureInfo.CurrentCulture);
|
||||
// By doing so, the DateTimeOffset parsed out will be a
|
||||
// no offset datetime, and need to be adjusted later
|
||||
DateTime dateTime = DateTime.ParseExact(dataTimeString, Format, CultureInfo.InvariantCulture);
|
||||
return new DateTimeOffset(dateTime, default);
|
||||
}
|
||||
|
||||
return default;
|
||||
@@ -27,6 +32,6 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString(Format, CultureInfo.CurrentCulture));
|
||||
writer.WriteStringValue(value.DateTime.ToString(Format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@ internal sealed partial class Activation : IActivation
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
currentWindowReference.Window = serviceProvider.GetRequiredService<MainWindow>();
|
||||
serviceProvider.GetRequiredService<MainWindow>();
|
||||
|
||||
serviceProvider
|
||||
.GetRequiredService<IMetadataService>()
|
||||
@@ -270,7 +270,7 @@ internal sealed partial class Activation : IActivation
|
||||
|
||||
if (currentWindowReference.Window is null)
|
||||
{
|
||||
currentWindowReference.Window = serviceProvider.GetRequiredService<LaunchGameWindow>();
|
||||
serviceProvider.GetRequiredService<LaunchGameWindow>();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Windows.Win32.Foundation;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
internal static class CurrentWindowReferenceExtension
|
||||
{
|
||||
public static XamlRoot GetXamlRoot(this ICurrentWindowReference reference)
|
||||
{
|
||||
return reference.Window.Content.XamlRoot;
|
||||
}
|
||||
|
||||
public static HWND GetWindowHandle(this ICurrentWindowReference reference)
|
||||
{
|
||||
return reference.Window is IWindowOptionsSource optionsSource
|
||||
? optionsSource.WindowOptions.Hwnd
|
||||
: (HWND)WindowNative.GetWindowHandle(reference.Window);
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,8 @@ namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
internal interface ICurrentWindowReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Only set in WindowController
|
||||
/// </summary>
|
||||
public Window Window { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -81,4 +81,6 @@ internal static class SettingKeys
|
||||
public const string IsHomeCardGachaStatisticsPresented = "IsHomeCardGachaStatisticsPresented";
|
||||
public const string IsHomeCardAchievementPresented = "IsHomeCardAchievementPresented";
|
||||
public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented";
|
||||
|
||||
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
|
||||
}
|
||||
@@ -49,7 +49,14 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
|
||||
string target = Path.Combine(desktop, $"{SH.AppNameAndVersion.Format(runtimeOptions.Version)}.lnk");
|
||||
|
||||
IPersistFile persistFile = (IPersistFile)shellLink;
|
||||
persistFile.Save(target, false);
|
||||
try
|
||||
{
|
||||
persistFile.Save(target, false);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Dispatching;
|
||||
using System.Runtime.ExceptionServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
@@ -18,15 +19,35 @@ internal static class DispatcherQueueExtension
|
||||
/// <param name="action">执行的回调</param>
|
||||
public static void Invoke(this DispatcherQueue dispatcherQueue, Action action)
|
||||
{
|
||||
using (ManualResetEventSlim blockEvent = new())
|
||||
if (dispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
ExceptionDispatchInfo? exceptionDispatchInfo = null;
|
||||
using (ManualResetEventSlim blockEvent = new(false))
|
||||
{
|
||||
dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
action();
|
||||
blockEvent.Set();
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
blockEvent.Set();
|
||||
}
|
||||
});
|
||||
|
||||
blockEvent.Wait();
|
||||
#pragma warning disable CA1508
|
||||
exceptionDispatchInfo?.Throw();
|
||||
#pragma warning restore CA1508
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Threading;
|
||||
/// </summary>
|
||||
internal interface ITaskContext
|
||||
{
|
||||
IProgress<T> CreateProgressForMainThread<T>(Action<T> handler);
|
||||
SynchronizationContext GetSynchronizationContext();
|
||||
|
||||
/// <summary>
|
||||
/// 在主线程上同步等待执行操作
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core.Threading;
|
||||
[Injection(InjectAs.Singleton, typeof(ITaskContext))]
|
||||
internal sealed class TaskContext : ITaskContext
|
||||
{
|
||||
private readonly DispatcherQueueSynchronizationContext dispatcherQueueSynchronizationContext;
|
||||
private readonly DispatcherQueueSynchronizationContext synchronizationContext;
|
||||
private readonly DispatcherQueue dispatcherQueue;
|
||||
|
||||
/// <summary>
|
||||
@@ -20,8 +20,8 @@ internal sealed class TaskContext : ITaskContext
|
||||
public TaskContext()
|
||||
{
|
||||
dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
dispatcherQueueSynchronizationContext = new(dispatcherQueue);
|
||||
SynchronizationContext.SetSynchronizationContext(dispatcherQueueSynchronizationContext);
|
||||
synchronizationContext = new(dispatcherQueue);
|
||||
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -39,18 +39,11 @@ internal sealed class TaskContext : ITaskContext
|
||||
/// <inheritdoc/>
|
||||
public void InvokeOnMainThread(Action action)
|
||||
{
|
||||
if (dispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
action();
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatcherQueue.Invoke(action);
|
||||
}
|
||||
dispatcherQueue.Invoke(action);
|
||||
}
|
||||
|
||||
public IProgress<T> CreateProgressForMainThread<T>(Action<T> handler)
|
||||
public SynchronizationContext GetSynchronizationContext()
|
||||
{
|
||||
return new DispatcherQueueProgress<T>(handler, dispatcherQueueSynchronizationContext);
|
||||
return synchronizationContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Model;
|
||||
using System.Text;
|
||||
using Windows.System;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using static Windows.Win32.PInvoke;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing.HotKey;
|
||||
|
||||
[SuppressMessage("", "SA1124")]
|
||||
internal sealed class HotKeyCombination : ObservableObject
|
||||
{
|
||||
private readonly ICurrentWindowReference currentWindowReference;
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
|
||||
private readonly string settingKey;
|
||||
private readonly int hotKeyId;
|
||||
private readonly HotKeyParameter defaultHotKeyParameter;
|
||||
|
||||
private bool registered;
|
||||
|
||||
private bool modifierHasWindows;
|
||||
private bool modifierHasControl;
|
||||
private bool modifierHasShift;
|
||||
private bool modifierHasAlt;
|
||||
private NameValue<VirtualKey> keyNameValue;
|
||||
private HOT_KEY_MODIFIERS modifiers;
|
||||
private VirtualKey key;
|
||||
private bool isEnabled;
|
||||
|
||||
public HotKeyCombination(IServiceProvider serviceProvider, string settingKey, int hotKeyId, HOT_KEY_MODIFIERS defaultModifiers, VirtualKey defaultKey)
|
||||
{
|
||||
currentWindowReference = serviceProvider.GetRequiredService<ICurrentWindowReference>();
|
||||
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
|
||||
this.settingKey = settingKey;
|
||||
this.hotKeyId = hotKeyId;
|
||||
defaultHotKeyParameter = new(defaultModifiers, defaultKey);
|
||||
|
||||
// Initialize Property backing fields
|
||||
{
|
||||
// Retrieve from LocalSetting
|
||||
isEnabled = LocalSetting.Get($"{settingKey}.IsEnabled", true);
|
||||
|
||||
HotKeyParameter actual = LocalSettingGetHotKeyParameter();
|
||||
modifiers = actual.Modifiers;
|
||||
InitializeModifiersComposeFields();
|
||||
key = actual.Key;
|
||||
|
||||
keyNameValue = VirtualKeys.GetList().Single(v => v.Value == key);
|
||||
}
|
||||
}
|
||||
|
||||
#region Binding Property
|
||||
public bool ModifierHasWindows
|
||||
{
|
||||
get => modifierHasWindows;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref modifierHasWindows, value))
|
||||
{
|
||||
UpdateModifiers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ModifierHasControl
|
||||
{
|
||||
get => modifierHasControl;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref modifierHasControl, value))
|
||||
{
|
||||
UpdateModifiers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ModifierHasShift
|
||||
{
|
||||
get => modifierHasShift;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref modifierHasShift, value))
|
||||
{
|
||||
UpdateModifiers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ModifierHasAlt
|
||||
{
|
||||
get => modifierHasAlt;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref modifierHasAlt, value))
|
||||
{
|
||||
UpdateModifiers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public NameValue<VirtualKey> KeyNameValue
|
||||
{
|
||||
get => keyNameValue;
|
||||
set
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (SetProperty(ref keyNameValue, value))
|
||||
{
|
||||
Key = value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public HOT_KEY_MODIFIERS Modifiers
|
||||
{
|
||||
get => modifiers;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref modifiers, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(DisplayName));
|
||||
LocalSettingSetHotKeyParameterAndRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public VirtualKey Key
|
||||
{
|
||||
get => key;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref key, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(DisplayName));
|
||||
LocalSettingSetHotKeyParameterAndRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => isEnabled;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref isEnabled, value))
|
||||
{
|
||||
LocalSetting.Set($"{settingKey}.IsEnabled", value);
|
||||
|
||||
_ = (value, registered) switch
|
||||
{
|
||||
(true, false) => RegisterForCurrentWindow(),
|
||||
(false, true) => UnregisterForCurrentWindow(),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string DisplayName { get => ToString(); }
|
||||
|
||||
public bool RegisterForCurrentWindow()
|
||||
{
|
||||
if (!runtimeOptions.IsElevated || !IsEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (registered)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
HWND hwnd = currentWindowReference.GetWindowHandle();
|
||||
BOOL result = RegisterHotKey(hwnd, hotKeyId, Modifiers, (uint)Key);
|
||||
registered = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool UnregisterForCurrentWindow()
|
||||
{
|
||||
if (!runtimeOptions.IsElevated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!registered)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
HWND hwnd = currentWindowReference.GetWindowHandle();
|
||||
BOOL result = UnregisterHotKey(hwnd, hotKeyId);
|
||||
registered = !result;
|
||||
return result;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder stringBuilder = new();
|
||||
|
||||
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_WIN))
|
||||
{
|
||||
stringBuilder.Append("Win").Append(" + ");
|
||||
}
|
||||
|
||||
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_CONTROL))
|
||||
{
|
||||
stringBuilder.Append("Ctrl").Append(" + ");
|
||||
}
|
||||
|
||||
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_SHIFT))
|
||||
{
|
||||
stringBuilder.Append("Shift").Append(" + ");
|
||||
}
|
||||
|
||||
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_ALT))
|
||||
{
|
||||
stringBuilder.Append("Alt").Append(" + ");
|
||||
}
|
||||
|
||||
stringBuilder.Append(Key);
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private void UpdateModifiers()
|
||||
{
|
||||
HOT_KEY_MODIFIERS modifiers = default;
|
||||
|
||||
if (ModifierHasWindows)
|
||||
{
|
||||
modifiers |= HOT_KEY_MODIFIERS.MOD_WIN;
|
||||
}
|
||||
|
||||
if (ModifierHasControl)
|
||||
{
|
||||
modifiers |= HOT_KEY_MODIFIERS.MOD_CONTROL;
|
||||
}
|
||||
|
||||
if (ModifierHasShift)
|
||||
{
|
||||
modifiers |= HOT_KEY_MODIFIERS.MOD_SHIFT;
|
||||
}
|
||||
|
||||
if (ModifierHasAlt)
|
||||
{
|
||||
modifiers |= HOT_KEY_MODIFIERS.MOD_ALT;
|
||||
}
|
||||
|
||||
Modifiers = modifiers;
|
||||
}
|
||||
|
||||
private void InitializeModifiersComposeFields()
|
||||
{
|
||||
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_WIN))
|
||||
{
|
||||
modifierHasWindows = true;
|
||||
}
|
||||
|
||||
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_CONTROL))
|
||||
{
|
||||
modifierHasControl = true;
|
||||
}
|
||||
|
||||
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_SHIFT))
|
||||
{
|
||||
modifierHasShift = true;
|
||||
}
|
||||
|
||||
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_ALT))
|
||||
{
|
||||
modifierHasAlt = true;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe HotKeyParameter LocalSettingGetHotKeyParameter()
|
||||
{
|
||||
fixed (HotKeyParameter* pDefaultHotKey = &defaultHotKeyParameter)
|
||||
{
|
||||
int value = LocalSetting.Get(settingKey, *(int*)pDefaultHotKey);
|
||||
return *(HotKeyParameter*)&value;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void LocalSettingSetHotKeyParameterAndRefresh()
|
||||
{
|
||||
HotKeyParameter current = new(Modifiers, Key);
|
||||
LocalSetting.Set(settingKey, *(int*)¤t);
|
||||
|
||||
UnregisterForCurrentWindow();
|
||||
RegisterForCurrentWindow();
|
||||
}
|
||||
}
|
||||
@@ -2,69 +2,38 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using static Windows.Win32.PInvoke;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing.HotKey;
|
||||
|
||||
[SuppressMessage("", "CA1001")]
|
||||
internal sealed class HotKeyController : IHotKeyController
|
||||
[ConstructorGenerated]
|
||||
internal sealed partial class HotKeyController : IHotKeyController
|
||||
{
|
||||
private const int DefaultId = 100000;
|
||||
private static readonly WaitCallback RunMouseClickRepeatForever = MouseClickRepeatForever;
|
||||
|
||||
private readonly object locker = new();
|
||||
private readonly WaitCallback runMouseClickRepeatForever;
|
||||
|
||||
private readonly HotKeyOptions hotKeyOptions;
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
|
||||
private volatile CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
public HotKeyController(IServiceProvider serviceProvider)
|
||||
public void RegisterAll()
|
||||
{
|
||||
hotKeyOptions = serviceProvider.GetRequiredService<HotKeyOptions>();
|
||||
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
runMouseClickRepeatForever = MouseClickRepeatForever;
|
||||
hotKeyOptions.MouseClickRepeatForeverKeyCombination.RegisterForCurrentWindow();
|
||||
}
|
||||
|
||||
public bool Register(in HWND hwnd)
|
||||
public void UnregisterAll()
|
||||
{
|
||||
if (runtimeOptions.IsElevated)
|
||||
{
|
||||
return RegisterHotKey(hwnd, DefaultId, default, (uint)VIRTUAL_KEY.VK_F8);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Unregister(in HWND hwnd)
|
||||
{
|
||||
if (runtimeOptions.IsElevated)
|
||||
{
|
||||
return UnregisterHotKey(hwnd, DefaultId);
|
||||
}
|
||||
|
||||
return false;
|
||||
hotKeyOptions.MouseClickRepeatForeverKeyCombination.UnregisterForCurrentWindow();
|
||||
}
|
||||
|
||||
public void OnHotKeyPressed(in HotKeyParameter parameter)
|
||||
{
|
||||
if (parameter is { Key: VIRTUAL_KEY.VK_F8, NativeModifier: 0 })
|
||||
if (parameter.Equals(hotKeyOptions.MouseClickRepeatForeverKeyCombination))
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (hotKeyOptions.IsMouseClickRepeatForeverOn)
|
||||
{
|
||||
cancellationTokenSource?.Cancel();
|
||||
cancellationTokenSource = default;
|
||||
hotKeyOptions.IsMouseClickRepeatForeverOn = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
cancellationTokenSource = new();
|
||||
ThreadPool.QueueUserWorkItem(runMouseClickRepeatForever, cancellationTokenSource.Token);
|
||||
hotKeyOptions.IsMouseClickRepeatForeverOn = true;
|
||||
}
|
||||
}
|
||||
ToggleMouseClickRepeatForever();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +45,7 @@ internal sealed class HotKeyController : IHotKeyController
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH007")]
|
||||
private unsafe void MouseClickRepeatForever(object? state)
|
||||
private static unsafe void MouseClickRepeatForever(object? state)
|
||||
{
|
||||
CancellationToken token = (CancellationToken)state!;
|
||||
|
||||
@@ -102,4 +71,25 @@ internal sealed class HotKeyController : IHotKeyController
|
||||
Thread.Sleep(Random.Shared.Next(100, 150));
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleMouseClickRepeatForever()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (hotKeyOptions.IsMouseClickRepeatForeverOn)
|
||||
{
|
||||
// Turn off
|
||||
cancellationTokenSource?.Cancel();
|
||||
cancellationTokenSource = default;
|
||||
hotKeyOptions.IsMouseClickRepeatForeverOn = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Turn on
|
||||
cancellationTokenSource = new();
|
||||
ThreadPool.QueueUserWorkItem(RunMouseClickRepeatForever, cancellationTokenSource.Token);
|
||||
hotKeyOptions.IsMouseClickRepeatForeverOn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,34 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Model;
|
||||
using Windows.System;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing.HotKey;
|
||||
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed class HotKeyOptions : ObservableObject
|
||||
internal sealed partial class HotKeyOptions : ObservableObject
|
||||
{
|
||||
private bool isVirtualKeyF8Pressed;
|
||||
private bool isMouseClickRepeatForeverOn;
|
||||
private HotKeyCombination mouseClickRepeatForeverKeyCombination;
|
||||
|
||||
public bool IsMouseClickRepeatForeverOn { get => isVirtualKeyF8Pressed; set => SetProperty(ref isVirtualKeyF8Pressed, value); }
|
||||
public HotKeyOptions(IServiceProvider serviceProvider)
|
||||
{
|
||||
mouseClickRepeatForeverKeyCombination = new(serviceProvider, SettingKeys.HotKeyMouseClickRepeatForever, 100000, default, VirtualKey.F8);
|
||||
}
|
||||
|
||||
public List<NameValue<VirtualKey>> VirtualKeys { get; } = HotKey.VirtualKeys.GetList();
|
||||
|
||||
public bool IsMouseClickRepeatForeverOn
|
||||
{
|
||||
get => isMouseClickRepeatForeverOn;
|
||||
set => SetProperty(ref isMouseClickRepeatForeverOn, value);
|
||||
}
|
||||
|
||||
public HotKeyCombination MouseClickRepeatForeverKeyCombination
|
||||
{
|
||||
get => mouseClickRepeatForeverKeyCombination;
|
||||
set => SetProperty(ref mouseClickRepeatForeverKeyCombination, value);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,43 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.System;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing.HotKey;
|
||||
|
||||
internal readonly struct HotKeyParameter
|
||||
/// <summary>
|
||||
/// HotKeyParameter
|
||||
/// The size of this struct must be sizeof(LPARAM) or 4
|
||||
/// </summary>
|
||||
internal readonly struct HotKeyParameter : IEquatable<HotKeyCombination>
|
||||
{
|
||||
public readonly ushort NativeModifier;
|
||||
public readonly VIRTUAL_KEY Key;
|
||||
public readonly ushort NativeModifiers;
|
||||
public readonly VIRTUAL_KEY NativeKey;
|
||||
|
||||
public readonly HOT_KEY_MODIFIERS Modifier
|
||||
public HotKeyParameter(HOT_KEY_MODIFIERS modifiers, VirtualKey key)
|
||||
{
|
||||
get => (HOT_KEY_MODIFIERS)NativeModifier;
|
||||
NativeModifiers = (ushort)modifiers;
|
||||
NativeKey = (VIRTUAL_KEY)key;
|
||||
}
|
||||
|
||||
public readonly HOT_KEY_MODIFIERS Modifiers
|
||||
{
|
||||
get => (HOT_KEY_MODIFIERS)NativeModifiers;
|
||||
}
|
||||
|
||||
public readonly VirtualKey Key
|
||||
{
|
||||
get => (VirtualKey)NativeKey;
|
||||
}
|
||||
|
||||
public bool Equals(HotKeyCombination? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Modifiers == other.Modifiers && Key == other.Key;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Win32.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing.HotKey;
|
||||
|
||||
internal interface IHotKeyController
|
||||
{
|
||||
void OnHotKeyPressed(in HotKeyParameter parameter);
|
||||
|
||||
bool Register(in HWND hwnd);
|
||||
void RegisterAll();
|
||||
|
||||
bool Unregister(in HWND hwnd);
|
||||
void UnregisterAll();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model;
|
||||
using Windows.System;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing.HotKey;
|
||||
|
||||
internal static class VirtualKeys
|
||||
{
|
||||
private static readonly List<NameValue<VirtualKey>> Values = CollectionsNameValue.ListFromEnum<VirtualKey>();
|
||||
|
||||
public static List<NameValue<VirtualKey>> GetList()
|
||||
{
|
||||
return Values;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Service;
|
||||
using System.IO;
|
||||
@@ -32,16 +33,20 @@ internal sealed class WindowController
|
||||
this.options = options;
|
||||
this.serviceProvider = serviceProvider;
|
||||
|
||||
// Window reference must be set before Window Subclass created
|
||||
serviceProvider.GetRequiredService<ICurrentWindowReference>().Window = window;
|
||||
subclass = new(window, options, serviceProvider);
|
||||
|
||||
InitializeCore();
|
||||
}
|
||||
|
||||
private static void TransformToCenterScreen(ref RectInt32 rect)
|
||||
{
|
||||
DisplayArea displayArea = DisplayArea.GetFromRect(rect, DisplayAreaFallback.Primary);
|
||||
DisplayArea displayArea = DisplayArea.GetFromRect(rect, DisplayAreaFallback.Nearest);
|
||||
RectInt32 workAreaRect = displayArea.WorkArea;
|
||||
|
||||
rect.Width = Math.Min(workAreaRect.Width, rect.Width);
|
||||
rect.Height = Math.Min(workAreaRect.Height, rect.Height);
|
||||
|
||||
rect.X = workAreaRect.X + ((workAreaRect.Width - rect.Width) / 2);
|
||||
rect.Y = workAreaRect.Y + ((workAreaRect.Height - rect.Height) / 2);
|
||||
}
|
||||
@@ -75,7 +80,7 @@ internal sealed class WindowController
|
||||
private void RecoverOrInitWindowSize()
|
||||
{
|
||||
// Set first launch size
|
||||
double scale = options.GetWindowScale();
|
||||
double scale = options.GetRasterizationScale();
|
||||
SizeInt32 scaledSize = options.InitSize.Scale(scale);
|
||||
RectInt32 rect = StructMarshal.RectInt32(scaledSize);
|
||||
|
||||
@@ -105,14 +110,14 @@ internal sealed class WindowController
|
||||
// prevent save value when we are maximized.
|
||||
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
|
||||
{
|
||||
double scale = 1 / options.GetWindowScale();
|
||||
double scale = 1.0 / options.GetRasterizationScale();
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)window.AppWindow.GetRect().Scale(scale));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOptionsPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(AppOptions.BackdropType))
|
||||
if (e.PropertyName is nameof(AppOptions.BackdropType))
|
||||
{
|
||||
if (sender is AppOptions options)
|
||||
{
|
||||
@@ -195,7 +200,7 @@ internal sealed class WindowController
|
||||
{
|
||||
AppWindowTitleBar appTitleBar = window.AppWindow.TitleBar;
|
||||
|
||||
double scale = options.GetWindowScale();
|
||||
double scale = options.GetRasterizationScale();
|
||||
|
||||
// 48 is the navigation button leftInset
|
||||
RectInt32 dragRect = StructMarshal.RectInt32(48, 0, options.TitleBar.ActualSize).Scale(scale);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Graphics;
|
||||
@@ -20,6 +21,11 @@ internal readonly struct WindowOptions
|
||||
/// </summary>
|
||||
public readonly HWND Hwnd;
|
||||
|
||||
/// <summary>
|
||||
/// 非客户端区域指针源
|
||||
/// </summary>
|
||||
public readonly InputNonClientPointerSource InputNonClientPointerSource;
|
||||
|
||||
/// <summary>
|
||||
/// 标题栏元素
|
||||
/// </summary>
|
||||
@@ -50,6 +56,7 @@ internal readonly struct WindowOptions
|
||||
public WindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, bool persistSize = false)
|
||||
{
|
||||
Hwnd = (HWND)WindowNative.GetWindowHandle(window);
|
||||
InputNonClientPointerSource = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);
|
||||
TitleBar = titleBar;
|
||||
InitSize = initSize;
|
||||
PersistSize = persistSize;
|
||||
@@ -59,7 +66,7 @@ internal readonly struct WindowOptions
|
||||
/// 获取窗体当前的DPI缩放比
|
||||
/// </summary>
|
||||
/// <returns>缩放比</returns>
|
||||
public double GetWindowScale()
|
||||
public double GetRasterizationScale()
|
||||
{
|
||||
uint dpi = GetDpiForWindow(Hwnd);
|
||||
return Math.Round(dpi / 96D, 2, MidpointRounding.AwayFromZero);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Core.Windowing.HotKey;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
@@ -44,7 +45,7 @@ internal sealed class WindowSubclass : IDisposable
|
||||
{
|
||||
windowProc = OnSubclassProcedure;
|
||||
bool windowHooked = SetWindowSubclass(options.Hwnd, windowProc, WindowSubclassId, 0);
|
||||
hotKeyController.Register(options.Hwnd);
|
||||
hotKeyController.RegisterAll();
|
||||
|
||||
bool titleBarHooked = true;
|
||||
|
||||
@@ -71,7 +72,7 @@ internal sealed class WindowSubclass : IDisposable
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
hotKeyController.Unregister(options.Hwnd);
|
||||
hotKeyController.UnregisterAll();
|
||||
|
||||
RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId);
|
||||
windowProc = null;
|
||||
@@ -92,7 +93,7 @@ internal sealed class WindowSubclass : IDisposable
|
||||
{
|
||||
if (window is IMinMaxInfoHandler handler)
|
||||
{
|
||||
handler.HandleMinMaxInfo(ref *(MINMAXINFO*)lParam.Value, options.GetWindowScale());
|
||||
handler.HandleMinMaxInfo(ref *(MINMAXINFO*)lParam.Value, options.GetRasterizationScale());
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
20
src/Snap.Hutao/Snap.Hutao/Extension/UnsafeDateTimeOffset.cs
Normal file
20
src/Snap.Hutao/Snap.Hutao/Extension/UnsafeDateTimeOffset.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
internal struct UnsafeDateTimeOffset
|
||||
{
|
||||
private DateTime dateTime;
|
||||
private short offsetMinutes;
|
||||
|
||||
public DateTime DateTime { readonly get => dateTime; set => dateTime = value; }
|
||||
|
||||
[SuppressMessage("", "SH002")]
|
||||
public static unsafe DateTimeOffset AdjustOffsetOnly(DateTimeOffset dateTimeOffset, in TimeSpan offset)
|
||||
{
|
||||
UnsafeDateTimeOffset* pUnsafe = (UnsafeDateTimeOffset*)&dateTimeOffset;
|
||||
pUnsafe->offsetMinutes = (short)(offset.Ticks / TimeSpan.TicksPerMinute);
|
||||
return dateTimeOffset;
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,8 @@
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Factory;
|
||||
namespace Snap.Hutao.Factory.ContentDialog;
|
||||
|
||||
/// <inheritdoc cref="IContentDialogFactory"/>
|
||||
[HighQuality]
|
||||
@@ -13,17 +12,17 @@ namespace Snap.Hutao.Factory;
|
||||
[Injection(InjectAs.Singleton, typeof(IContentDialogFactory))]
|
||||
internal sealed partial class ContentDialogFactory : IContentDialogFactory
|
||||
{
|
||||
private readonly ICurrentWindowReference currentWindowReference;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly ICurrentWindowReference currentWindowReference;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ContentDialogResult> CreateForConfirmAsync(string title, string content)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
Microsoft.UI.Xaml.Controls.ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = currentWindowReference.Window.Content.XamlRoot,
|
||||
XamlRoot = currentWindowReference.GetXamlRoot(),
|
||||
Title = title,
|
||||
Content = content,
|
||||
DefaultButton = ContentDialogButton.Primary,
|
||||
@@ -37,9 +36,9 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
|
||||
public async ValueTask<ContentDialogResult> CreateForConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
Microsoft.UI.Xaml.Controls.ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = currentWindowReference.Window.Content.XamlRoot,
|
||||
XamlRoot = currentWindowReference.GetXamlRoot(),
|
||||
Title = title,
|
||||
Content = content,
|
||||
DefaultButton = defaultButton,
|
||||
@@ -51,12 +50,12 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title)
|
||||
public async ValueTask<Microsoft.UI.Xaml.Controls.ContentDialog> CreateForIndeterminateProgressAsync(string title)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
Microsoft.UI.Xaml.Controls.ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = currentWindowReference.Window.Content.XamlRoot,
|
||||
XamlRoot = currentWindowReference.GetXamlRoot(),
|
||||
Title = title,
|
||||
Content = new ProgressBar() { IsIndeterminate = true },
|
||||
};
|
||||
@@ -65,15 +64,19 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
|
||||
}
|
||||
|
||||
public async ValueTask<TContentDialog> CreateInstanceAsync<TContentDialog>(params object[] parameters)
|
||||
where TContentDialog : ContentDialog
|
||||
where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
return serviceProvider.CreateInstance<TContentDialog>(parameters);
|
||||
TContentDialog contentDialog = serviceProvider.CreateInstance<TContentDialog>(parameters);
|
||||
contentDialog.XamlRoot = currentWindowReference.GetXamlRoot();
|
||||
return contentDialog;
|
||||
}
|
||||
|
||||
public TContentDialog CreateInstance<TContentDialog>(params object[] parameters)
|
||||
where TContentDialog : ContentDialog
|
||||
where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog
|
||||
{
|
||||
return serviceProvider.CreateInstance<TContentDialog>(parameters);
|
||||
TContentDialog contentDialog = serviceProvider.CreateInstance<TContentDialog>(parameters);
|
||||
contentDialog.XamlRoot = currentWindowReference.GetXamlRoot();
|
||||
return contentDialog;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Factory.Abstraction;
|
||||
namespace Snap.Hutao.Factory.ContentDialog;
|
||||
|
||||
/// <summary>
|
||||
/// 内容对话框工厂
|
||||
@@ -33,11 +33,11 @@ internal interface IContentDialogFactory
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <returns>内容对话框</returns>
|
||||
ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title);
|
||||
ValueTask<Microsoft.UI.Xaml.Controls.ContentDialog> CreateForIndeterminateProgressAsync(string title);
|
||||
|
||||
TContentDialog CreateInstance<TContentDialog>(params object[] parameters)
|
||||
where TContentDialog : ContentDialog;
|
||||
where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog;
|
||||
|
||||
ValueTask<TContentDialog> CreateInstanceAsync<TContentDialog>(params object[] parameters)
|
||||
where TContentDialog : ContentDialog;
|
||||
where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Windows.Storage.Pickers;
|
||||
|
||||
namespace Snap.Hutao.Factory.Abstraction;
|
||||
namespace Snap.Hutao.Factory.Picker;
|
||||
|
||||
/// <summary>
|
||||
/// 文件选择器工厂
|
||||
@@ -2,11 +2,13 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Windows.Storage.Pickers;
|
||||
using Windows.Win32.Foundation;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Snap.Hutao.Factory;
|
||||
namespace Snap.Hutao.Factory.Picker;
|
||||
|
||||
/// <inheritdoc cref="IPickerFactory"/>
|
||||
[HighQuality]
|
||||
@@ -16,7 +18,7 @@ internal sealed partial class PickerFactory : IPickerFactory
|
||||
{
|
||||
private const string AnyType = "*";
|
||||
|
||||
private readonly MainWindow mainWindow;
|
||||
private readonly ICurrentWindowReference currentWindowReference;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FileOpenPicker GetFileOpenPicker(PickerLocationId location, string commitButton, params string[] fileTypes)
|
||||
@@ -78,7 +80,9 @@ internal sealed partial class PickerFactory : IPickerFactory
|
||||
{
|
||||
// Create a folder picker.
|
||||
T picker = new();
|
||||
InitializeWithWindow.Initialize(picker, mainWindow.WindowOptions.Hwnd);
|
||||
|
||||
HWND hwnd = currentWindowReference.GetWindowHandle();
|
||||
InitializeWithWindow.Initialize(picker, hwnd);
|
||||
|
||||
return picker;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Factory.Progress;
|
||||
|
||||
internal interface IProgressFactory
|
||||
{
|
||||
IProgress<T> CreateForMainThread<T>(Action<T> handler);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Snap.Hutao.Factory.Progress;
|
||||
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Transient, typeof(IProgressFactory))]
|
||||
internal sealed partial class ProgressFactory : IProgressFactory
|
||||
{
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
public IProgress<T> CreateForMainThread<T>(Action<T> handler)
|
||||
{
|
||||
return new DispatcherQueueProgress<T>(handler, taskContext.GetSynchronizationContext());
|
||||
}
|
||||
}
|
||||
549
src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.Designer.cs
generated
Normal file
549
src/Snap.Hutao/Snap.Hutao/Migrations/20231103032056_AddUserFingerprint.Designer.cs
generated
Normal file
@@ -0,0 +1,549 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Snap.Hutao.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20231103032056_AddUserFingerprint")]
|
||||
partial class AddUserFingerprint
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.13");
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ArchiveId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("Current")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Time")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ArchiveId");
|
||||
|
||||
b.ToTable("achievements");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("achievement_archives");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CalculatorRefreshTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("GameRecordRefreshTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Info")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ShowcaseRefreshTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("avatar_infos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("cultivate_entries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("EntryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsFinished")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("EntryId");
|
||||
|
||||
b.ToTable("cultivate_items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachedUid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("cultivate_projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DailyNote")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DailyTaskNotify")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("DailyTaskNotifySuppressed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ExpeditionNotify")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ExpeditionNotifySuppressed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("HomeCoinNotifySuppressed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("HomeCoinNotifyThreshold")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("RefreshTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("ResinNotifySuppressed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ResinNotifyThreshold")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("TransformerNotify")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("TransformerNotifySuppressed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("daily_notes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("gacha_archives");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ArchiveId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("GachaType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("QueryType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Time")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ArchiveId");
|
||||
|
||||
b.ToTable("gacha_items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachUid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MihoyoSDK")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("game_accounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("inventory_items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AppendPropIdList")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MainPropId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("inventory_reliquaries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PromoteLevel")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("inventory_weapons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpireTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("object_cache");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("ScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SpiralAbyss")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("spiral_abysses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Aid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CookieToken")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Fingerprint")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsOversea")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("Ltoken");
|
||||
|
||||
b.Property<string>("Mid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SToken")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("Stoken");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
|
||||
.WithMany()
|
||||
.HasForeignKey("ArchiveId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Archive");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
|
||||
.WithMany()
|
||||
.HasForeignKey("EntryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Entry");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
|
||||
.WithMany()
|
||||
.HasForeignKey("ArchiveId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Archive");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Snap.Hutao.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserFingerprint : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Fingerprint",
|
||||
table: "users",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Fingerprint",
|
||||
table: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.10");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.13");
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
@@ -400,7 +400,7 @@ namespace Snap.Hutao.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ScheduleId")
|
||||
b.Property<uint>("ScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SpiralAbyss")
|
||||
@@ -428,6 +428,9 @@ namespace Snap.Hutao.Migrations
|
||||
b.Property<string>("CookieToken")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Fingerprint")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsOversea")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
13
src/Snap.Hutao/Snap.Hutao/Model/CollectionsNameValue.cs
Normal file
13
src/Snap.Hutao/Snap.Hutao/Model/CollectionsNameValue.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model;
|
||||
|
||||
internal static class CollectionsNameValue
|
||||
{
|
||||
public static List<NameValue<T>> ListFromEnum<T>()
|
||||
where T : struct, Enum
|
||||
{
|
||||
return Enum.GetValues<T>().Select(x => new NameValue<T>(x.ToString(), x)).ToList();
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,16 @@ internal sealed partial class SettingEntry
|
||||
/// </summary>
|
||||
public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame";
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺 WebhookUrl
|
||||
/// </summary>
|
||||
public const string DailyNoteWebhookUrl = "DailyNote.WebhookUrl";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 总开关
|
||||
/// </summary>
|
||||
public const string LaunchIsLaunchOptionsEnabled = "Launch.IsLaunchOptionsEnabled";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 独占全屏
|
||||
/// </summary>
|
||||
@@ -68,11 +78,15 @@ internal sealed partial class SettingEntry
|
||||
/// </summary>
|
||||
public const string LaunchScreenWidth = "Launch.ScreenWidth";
|
||||
|
||||
public const string LaunchIsScreenWidthEnabled = "Launch.IsScreenWidthEnabled";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 高度
|
||||
/// </summary>
|
||||
public const string LaunchScreenHeight = "Launch.ScreenHeight";
|
||||
|
||||
public const string LaunchIsScreenHeightEnabled = "Launch.IsScreenHeightEnabled";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 解锁帧率
|
||||
/// </summary>
|
||||
@@ -88,6 +102,10 @@ internal sealed partial class SettingEntry
|
||||
/// </summary>
|
||||
public const string LaunchMonitor = "Launch.Monitor";
|
||||
|
||||
public const string LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled";
|
||||
|
||||
public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 多倍启动
|
||||
/// </summary>
|
||||
|
||||
@@ -60,6 +60,11 @@ internal sealed class User : ISelectable, IMappingFrom<User, Cookie, bool>
|
||||
/// </summary>
|
||||
public bool IsOversea { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户指纹 Id
|
||||
/// </summary>
|
||||
public string? Fingerprint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的用户
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Model.InterChange.GachaLog;
|
||||
|
||||
/// <summary>
|
||||
@@ -8,12 +11,12 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
|
||||
/// https://uigf.org/standards/UIGF.html
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class UIGF
|
||||
internal sealed class UIGF : IJsonOnSerializing, IJsonOnDeserialized
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前版本
|
||||
/// </summary>
|
||||
public const string CurrentVersion = "v2.3";
|
||||
public const string CurrentVersion = "v2.4";
|
||||
|
||||
/// <summary>
|
||||
/// 信息
|
||||
@@ -28,11 +31,27 @@ internal sealed class UIGF
|
||||
[JsonPropertyName("list")]
|
||||
public List<UIGFItem> List { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 确认当前UIGF对象的版本是否受支持
|
||||
/// </summary>
|
||||
/// <param name="version">版本</param>
|
||||
/// <returns>当前UIAF对象是否受支持</returns>
|
||||
public void OnSerializing()
|
||||
{
|
||||
TimeSpan offset = GetRegionTimeZoneUtcOffset();
|
||||
|
||||
foreach (UIGFItem item in List)
|
||||
{
|
||||
item.Time = item.Time.ToOffset(offset);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDeserialized()
|
||||
{
|
||||
// Adjust items timezone
|
||||
TimeSpan offset = GetRegionTimeZoneUtcOffset();
|
||||
|
||||
foreach (UIGFItem item in List)
|
||||
{
|
||||
item.Time = UnsafeDateTimeOffset.AdjustOffsetOnly(item.Time, offset);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCurrentVersionSupported(out UIGFVersion version)
|
||||
{
|
||||
version = Info.UIGFVersion switch
|
||||
@@ -40,29 +59,50 @@ internal sealed class UIGF
|
||||
"v2.1" => UIGFVersion.Major2Minor2OrLower,
|
||||
"v2.2" => UIGFVersion.Major2Minor2OrLower,
|
||||
"v2.3" => UIGFVersion.Major2Minor3OrHigher,
|
||||
"v2.4" => UIGFVersion.Major2Minor3OrHigher,
|
||||
_ => UIGFVersion.NotSupported,
|
||||
};
|
||||
|
||||
return version != UIGFVersion.NotSupported;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 列表物品是否正常
|
||||
/// </summary>
|
||||
/// <param name="itemId">首个出错的Id</param>
|
||||
/// <returns>是否正常</returns>
|
||||
public bool IsMajor2Minor2OrLowerListValid([NotNullWhen(false)] out long itemId)
|
||||
public bool IsMajor2Minor2OrLowerListValid([NotNullWhen(false)] out long id)
|
||||
{
|
||||
foreach (UIGFItem item in List)
|
||||
foreach (ref readonly UIGFItem item in CollectionsMarshal.AsSpan(List))
|
||||
{
|
||||
if (item.ItemType != SH.ModelInterchangeUIGFItemTypeAvatar && item.ItemType != SH.ModelInterchangeUIGFItemTypeWeapon)
|
||||
{
|
||||
itemId = item.Id;
|
||||
id = item.Id;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
itemId = 0;
|
||||
id = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsMajor2Minor3OrHigherListValid([NotNullWhen(false)] out long id)
|
||||
{
|
||||
foreach (ref readonly UIGFItem item in CollectionsMarshal.AsSpan(List))
|
||||
{
|
||||
if (string.IsNullOrEmpty(item.ItemId))
|
||||
{
|
||||
id = item.Id;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
id = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
private TimeSpan GetRegionTimeZoneUtcOffset()
|
||||
{
|
||||
if (Info.RegionTimeZone is int offsetHours)
|
||||
{
|
||||
return new TimeSpan(offsetHours, 0, 0);
|
||||
}
|
||||
|
||||
return PlayerUid.GetRegionTimeZoneUtcOffset(Info.Uid);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
namespace Snap.Hutao.Model.InterChange.GachaLog;
|
||||
|
||||
@@ -58,6 +59,12 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, Metadata
|
||||
[JsonPropertyName("uigf_version")]
|
||||
public string UIGFVersion { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 时区偏移
|
||||
/// </summary>
|
||||
[JsonPropertyName("region_time_zone")]
|
||||
public int? RegionTimeZone { get; set; } = default!;
|
||||
|
||||
public static UIGFInfo From(RuntimeOptions runtimeOptions, MetadataOptions metadataOptions, string uid)
|
||||
{
|
||||
return new()
|
||||
@@ -68,6 +75,7 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, Metadata
|
||||
ExportApp = SH.AppName,
|
||||
ExportAppVersion = runtimeOptions.Version.ToString(),
|
||||
UIGFVersion = UIGF.CurrentVersion,
|
||||
RegionTimeZone = PlayerUid.GetRegionTimeZoneUtcOffset(uid).Hours,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,17 @@ internal enum Arkhe
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// 芒性
|
||||
/// 荒性
|
||||
/// </summary>
|
||||
Ousia,
|
||||
|
||||
/// <summary>
|
||||
/// 荒性
|
||||
/// 芒性
|
||||
/// </summary>
|
||||
Pneuma,
|
||||
|
||||
/// <summary>
|
||||
/// 圣俗杂座
|
||||
/// </summary>
|
||||
Furina,
|
||||
}
|
||||
@@ -59,4 +59,5 @@ internal enum MaterialType
|
||||
MATERIAL_GCG_EXCHANGE_ITEM = 48,
|
||||
MATERIAL_QUEST_EVENT_BOOK = 49,
|
||||
MATERIAL_PROFILE_PICTURE = 50,
|
||||
MATERIAL_RAINBOW_PRINCE_HAND_BOOK = 51,
|
||||
}
|
||||
@@ -91,6 +91,8 @@ internal static class AvatarIds
|
||||
public static readonly AvatarId Freminet = 10000085;
|
||||
public static readonly AvatarId Wriothesley = 10000086;
|
||||
public static readonly AvatarId Neuvillette = 10000087;
|
||||
public static readonly AvatarId Charlotte = 10000088;
|
||||
public static readonly AvatarId Furina = 10000089;
|
||||
|
||||
/// <summary>
|
||||
/// 检查该角色是否为主角
|
||||
|
||||
@@ -19,6 +19,8 @@ internal static class MonsterRelationship
|
||||
5071U => 507U, // 幻形花鼠 · 水 (强化)
|
||||
5102U => 510U, // 历经百战的浊水粉碎幻灵
|
||||
5112U => 511U, // 历经百战的浊水喷吐幻灵
|
||||
30605U => 30603U, // 历经百战的霜剑律从
|
||||
30606U => 30604U, // 历经百战的幽风铃兰
|
||||
60402U => 60401U, // (火)岩龙蜥
|
||||
60403U => 60401U, // (冰)岩龙蜥
|
||||
60404U => 60401U, // (雷)岩龙蜥
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace Snap.Hutao.Model;
|
||||
/// <summary>
|
||||
/// 封装带有名称描述的值
|
||||
/// 在绑定枚举变量时非常有用
|
||||
/// https://github.com/microsoft/microsoft-ui-xaml/issues/4266
|
||||
/// 直接绑定枚举变量会显示 Windows.Foundation.IReference{T}
|
||||
/// </summary>
|
||||
/// <typeparam name="T">包含值的类型</typeparam>
|
||||
[HighQuality]
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutao"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.7.9.0" />
|
||||
Version="1.7.17.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao</DisplayName>
|
||||
|
||||
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_Gift.png
Normal file
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Icon/UI_Icon_Gift.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 913 B |
File diff suppressed because it is too large
Load Diff
@@ -506,6 +506,84 @@
|
||||
<data name="MustSelectUserAndUid" xml:space="preserve">
|
||||
<value>You must select a user and a role first</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientRecordSlot" xml:space="preserve">
|
||||
<value>Reached max allowed number of wish history archives on Snap Hutao Cloud</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientTime" xml:space="preserve">
|
||||
<value>No valid wish history backup service privilege</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInvalidGachaLogData" xml:space="preserve">
|
||||
<value>Wish history data contains invalid item, unable to upload to Snap Hutao Cloud</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceServerDatabaseError" xml:space="preserve">
|
||||
<value>Found abnormal data, unable to upload to Snap Hutao Cloud. Please do not upload across accounts or you can attempt to delete cloud data and try again.</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceEmailHasNotRegistered" xml:space="preserve">
|
||||
<value>Current email adress is not registered</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceEmailHasRegistered" xml:space="preserve">
|
||||
<value>Current emaill address is registered</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceInternalException" xml:space="preserve">
|
||||
<value>Register failed, server error, please contact developer to fix it</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceUnregisterFailed" xml:space="preserve">
|
||||
<value>User does not exist, failed to delete account</value>
|
||||
</data>
|
||||
<data name="ServerPassportUserInfoNotExist" xml:space="preserve">
|
||||
<value>User does not exist, failed to fetch user's data</value>
|
||||
</data>
|
||||
<data name="ServerPassportUsernameOrPassportIncorrect" xml:space="preserve">
|
||||
<value>Wrong username or password</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyFailed" xml:space="preserve">
|
||||
<value>Verification failed</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
|
||||
<value>The verification request failed, it is not the currently logged in account</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
|
||||
<value>The verification code has been sent to your e-mail.</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
|
||||
<value>The verification request failed, the current email address has been registered</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
|
||||
<value>Validation request is too frequent. Please try again in 1 minute.</value>
|
||||
</data>
|
||||
<data name="ServerRecordBannedUid" xml:space="preserve">
|
||||
<value>Failed to upload Sprial Abyss record, current UID is banned by Hutao Database</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics" xml:space="preserve">
|
||||
<value>Failed to upload Sprial Abyss record, server is calculating statistical data</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics2" xml:space="preserve">
|
||||
<value>Failed to fetch data, server is calculating statistical data</value>
|
||||
</data>
|
||||
<data name="ServerRecordInternalException" xml:space="preserve">
|
||||
<value>Failed to upload Sprial Abyss record, server error, please contact developer to fix it</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidData" xml:space="preserve">
|
||||
<value>Failed to upload Sprial Abyss record, invalid data detected</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidUid" xml:space="preserve">
|
||||
<value>Invalid UID</value>
|
||||
</data>
|
||||
<data name="ServerRecordNotCurrentSchedule" xml:space="preserve">
|
||||
<value>Failed to upload Spiral Abyss record. It is not data for the current schedule.</value>
|
||||
</data>
|
||||
<data name="ServerRecordPreviousRequestNotCompleted" xml:space="preserve">
|
||||
<value>Failed to upload Sprial Abyss record. The record for the current Uid is still being processed. Please do not repeat the operation.</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessAndGachaLogServiceTimeExtended" xml:space="preserve">
|
||||
<value>Uploaded Spiral Abyss record successfully. Received a privilege extension for Snap Hutao Cloud service.</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNoPassport" xml:space="preserve">
|
||||
<value>Uploaded abyss record successfully. No Snap Hutao Cloud privilege received as no Snap Hutao account logged in.</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNotFirstTimeAtCurrentSchedule" xml:space="preserve">
|
||||
<value>Uploaded abyss record successfully. No Snap Hutao Cloud privilege received as there is not first upload of current schedule.</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementImportResultFormat" xml:space="preserve">
|
||||
<value>New: {0} Achievements | Updated: {1} Achievements | Delete: {2} Achievements</value>
|
||||
</data>
|
||||
@@ -959,6 +1037,9 @@
|
||||
<data name="ViewControlStatisticsSegmentedItemContentStatistics" xml:space="preserve">
|
||||
<value>Statistics</value>
|
||||
</data>
|
||||
<data name="ViewControlWebViewerCoreWebView2ProfileQueryInterfaceFailed" xml:space="preserve">
|
||||
<value>The current version of WebView2 does not support management configuration, continue to use may cause abnormalities, please upgrade as soon as possible</value>
|
||||
</data>
|
||||
<data name="ViewCultivationHeader" xml:space="preserve">
|
||||
<value>Dev Plan</value>
|
||||
</data>
|
||||
@@ -1040,6 +1121,12 @@
|
||||
<data name="ViewDialogDailyNoteNotificationTransformerNotify" xml:space="preserve">
|
||||
<value>Parametric Transformer Notification</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteWebhookUrlInputPlaceholder" xml:space="preserve">
|
||||
<value>Input URL</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteWebhookUrlTitle" xml:space="preserve">
|
||||
<value>Realtime Note Webhook URL</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogImportTitle" xml:space="preserve">
|
||||
<value>Import wish history</value>
|
||||
</data>
|
||||
@@ -1085,6 +1172,18 @@
|
||||
<data name="ViewDialogGeetestCustomUrlTitle" xml:space="preserve">
|
||||
<value>Configure Geetest CAPTCHA Verficaition API</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportLoginTitle" xml:space="preserve">
|
||||
<value>Login to Snap Hutao Passport</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportRegisterTitle" xml:space="preserve">
|
||||
<value>Signup Snap Hutao Passport</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportResetPasswordTitle" xml:space="preserve">
|
||||
<value>Reset Password of Snap Hutao Passport</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportUnregisterTitle" xml:space="preserve">
|
||||
<value>Delete Snap Hutao Passport</value>
|
||||
</data>
|
||||
<data name="ViewDialogImportExportApp" xml:space="preserve">
|
||||
<value>Export App</value>
|
||||
</data>
|
||||
@@ -1268,6 +1367,9 @@
|
||||
<data name="ViewModelCultivationProjectInvalidName" xml:space="preserve">
|
||||
<value>Can't add plan with invalid name</value>
|
||||
</data>
|
||||
<data name="ViewModelDailyNoteConfigWebhookUrlComplete" xml:space="preserve">
|
||||
<value>Realtime Note Webhook URL successfully configured</value>
|
||||
</data>
|
||||
<data name="ViewModelDailyNoteHoyolabVerificationUnsupported" xml:space="preserve">
|
||||
<value>HoYoLab account does not support Realtime Notes verification</value>
|
||||
</data>
|
||||
@@ -1407,11 +1509,14 @@
|
||||
<value>Failed to create desktop shortcut</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingGeetestCustomUrlSucceed" xml:space="preserve">
|
||||
<value>无感验证复合 Url 配置成功</value>
|
||||
<value>CAPTCHA Verification composite URL successfully configured</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingSetDataFolderSuccess" xml:space="preserve">
|
||||
<value>Set data directory successfully. Restart to apply changes.</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingSetGamePathDatabaseFailedTitle" xml:space="preserve">
|
||||
<value>Failed to save game path</value>
|
||||
</data>
|
||||
<data name="ViewModelUserAdded" xml:space="preserve">
|
||||
<value>User [{0}] added successfully</value>
|
||||
</data>
|
||||
@@ -1589,6 +1694,18 @@
|
||||
<data name="ViewPageDailyNoteAddEntryToolTip" xml:space="preserve">
|
||||
<value>Add</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteAttendanceStatusInfo" xml:space="preserve">
|
||||
<value>Encounter Points Status</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteConfigWebhookDescription" xml:space="preserve">
|
||||
<value>Push data to specific webhook after refreshing Realtime Note</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteConfigWebhookHeader" xml:space="preserve">
|
||||
<value>Config Webhook</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteDataInteropHeader" xml:space="preserve">
|
||||
<value>Data Interoperability</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteNotificationHeader" xml:space="preserve">
|
||||
<value>Notification</value>
|
||||
</data>
|
||||
@@ -1685,6 +1802,9 @@
|
||||
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
|
||||
<value>Input</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
|
||||
<value>Recover Wish Record from Snap Hutao Cloud</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
|
||||
<value>Refresh</value>
|
||||
</data>
|
||||
@@ -1829,6 +1949,9 @@
|
||||
<data name="ViewPageHutaoPassportResetPasswordHeader" xml:space="preserve">
|
||||
<value>Reset Password</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoPassportResetPasswordHint" xml:space="preserve">
|
||||
<value>Delete Snap Hutao Passport will cause your data to lose without any recovery option</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoPassportUserNameHint" xml:space="preserve">
|
||||
<value>Enter your email</value>
|
||||
</data>
|
||||
@@ -1844,6 +1967,12 @@
|
||||
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
|
||||
<value>Advanced Features</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
|
||||
<value>Resolution Ratio Shortcut</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
|
||||
<value>Screen Resolution</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
|
||||
<value>Create window as popup, without frame</value>
|
||||
</data>
|
||||
@@ -1877,12 +2006,24 @@
|
||||
<data name="ViewPageLaunchGameAppearanceScreenWidthHeader" xml:space="preserve">
|
||||
<value>Width</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameArgumentsDescription" xml:space="preserve">
|
||||
<value>Modify its default behavior at game startup</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameArgumentsHeader" xml:space="preserve">
|
||||
<value>Start-up Arguments</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameCommonHeader" xml:space="preserve">
|
||||
<value>General</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
|
||||
<value>All options will be saved only after the game is launched successfully.</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
|
||||
<value>File</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
|
||||
<value>InterProcess</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
|
||||
<value>Run the software on the selected display</value>
|
||||
</data>
|
||||
@@ -1898,6 +2039,18 @@
|
||||
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
|
||||
<value>Game Options</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
|
||||
<value>Try to start the game after the game is started and use Starward for game duration statistics</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
|
||||
<value>Hours Played</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
|
||||
<value>Progress</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameRegistryHeader" xml:space="preserve">
|
||||
<value>Registry</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameResourceDiffHeader" xml:space="preserve">
|
||||
<value>OTA Package</value>
|
||||
</data>
|
||||
@@ -1938,7 +2091,7 @@
|
||||
<value>Server</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSwitchSchemeWarning" xml:space="preserve">
|
||||
<value>版本更新前需要提前转换至与启动器匹配的服务器</value>
|
||||
<value>You need to convert to a server that matches the launcher before updating the version</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameUnlockFpsDescription" xml:space="preserve">
|
||||
<value>Please turn off V-Sync in the game settings. You may need a high-performance graphic card to support a high frame rate limit.</value>
|
||||
@@ -2096,8 +2249,50 @@
|
||||
<data name="ViewpageSettingHomeHeader" xml:space="preserve">
|
||||
<value>Home</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportDangerZoneDescription" xml:space="preserve">
|
||||
<value>Proceed with caution</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportDangerZoneHeader" xml:space="preserve">
|
||||
<value>Danger Zone</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportGachaLogExpiredAtHeader" xml:space="preserve">
|
||||
<value>Snap Hutao Cloud Expiring in</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportHeader" xml:space="preserve">
|
||||
<value>Snap Hutao Account</value>
|
||||
<value>Snap Hutao Passport</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLicensedDeveloperDescription" xml:space="preserve">
|
||||
<value>You are unlimited in any Snap Hutao Cloud features</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLicensedDeveloperHeader" xml:space="preserve">
|
||||
<value>Certificated Developer</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLoginAction" xml:space="preserve">
|
||||
<value>Sign in</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLogoutAction" xml:space="preserve">
|
||||
<value>Sign out</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportMaintainerDescription" xml:space="preserve">
|
||||
<value>You are unlimited in any testing feature</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportMaintainerHeader" xml:space="preserve">
|
||||
<value>Snap Hutao developer and maintainer</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRedeemCodeDescription" xml:space="preserve">
|
||||
<value>We sometimes give away Snap Hutao Cloud redemption codes to some users</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRedeemCodeHeader" xml:space="preserve">
|
||||
<value>Use Redemption Code</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRegisterAction" xml:space="preserve">
|
||||
<value>Register</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportResetPasswordAction" xml:space="preserve">
|
||||
<value>Change Password</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportUnregisterAction" xml:space="preserve">
|
||||
<value>Delete Account</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledDescription" xml:space="preserve">
|
||||
<value>After a full reading of the Genshin Impact and Snap Hutao user agreements, I choose to enable「Game Launcher - Advanced Features」.</value>
|
||||
@@ -2105,6 +2300,15 @@
|
||||
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
|
||||
<value>Enable Advanced Features</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
|
||||
<value>Change Auto Click Shortcut</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
|
||||
<value>Auto Click</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
|
||||
<value>Shortcut Keys</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
|
||||
<value>Official Website</value>
|
||||
</data>
|
||||
@@ -2315,6 +2519,9 @@
|
||||
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
|
||||
<value>Upload Data</value>
|
||||
</data>
|
||||
<data name="ViewTitleAutoClicking" xml:space="preserve">
|
||||
<value>Auto Click</value>
|
||||
</data>
|
||||
<data name="ViewToolHeader" xml:space="preserve">
|
||||
<value>Tools</value>
|
||||
</data>
|
||||
@@ -2328,7 +2535,7 @@
|
||||
<value>Current user</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
|
||||
<value>My Characters</value>
|
||||
<value>Official Tools</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
|
||||
<value>Web Login</value>
|
||||
@@ -2414,6 +2621,27 @@
|
||||
<data name="WebAnnouncementTimeHoursEndFormat" xml:space="preserve">
|
||||
<value>End in {0} hours</value>
|
||||
</data>
|
||||
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
|
||||
<value>Copied to clipboard</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
|
||||
<value>Finished</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusForbid" xml:space="preserve">
|
||||
<value>Forbid to Claim</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusInvalid" xml:space="preserve">
|
||||
<value>Invalid</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusTakenAward" xml:space="preserve">
|
||||
<value>Claimed</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusUnfinished" xml:space="preserve">
|
||||
<value>Unfinished</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusWaitTaken" xml:space="preserve">
|
||||
<value>Ready to claim</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteExpeditionRemainHoursFormat" xml:space="preserve">
|
||||
<value>{0} hrs</value>
|
||||
</data>
|
||||
@@ -2522,6 +2750,12 @@
|
||||
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
|
||||
<value>Weapon Event Wish</value>
|
||||
</data>
|
||||
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
|
||||
<value>Copy Link Successful</value>
|
||||
</data>
|
||||
<data name="WebHoyolabInvalidUid" xml:space="preserve">
|
||||
<value>Invalid UID</value>
|
||||
</data>
|
||||
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
|
||||
<value>Verification failed. Please verify manually or check MiHoYo BBS - My Characters page</value>
|
||||
</data>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -506,6 +506,84 @@
|
||||
<data name="MustSelectUserAndUid" xml:space="preserve">
|
||||
<value>유저와 UID를 선택하세요</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientRecordSlot" xml:space="preserve">
|
||||
<value>胡桃云保存的祈愿记录存档数已达当前账号上限</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientTime" xml:space="preserve">
|
||||
<value>未开通祈愿记录上传服务或已到期</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInvalidGachaLogData" xml:space="preserve">
|
||||
<value>祈愿数据存在无效的物品,无法保存至胡桃云</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceServerDatabaseError" xml:space="preserve">
|
||||
<value>数据异常,无法保存至云端,请勿跨账号上传或尝试删除云端数据后重试</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceEmailHasNotRegistered" xml:space="preserve">
|
||||
<value>当前邮箱尚未注册</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceEmailHasRegistered" xml:space="preserve">
|
||||
<value>当前邮箱已被注册</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceInternalException" xml:space="preserve">
|
||||
<value>注册失败,服务器异常,请尽快联系开发者解决</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceUnregisterFailed" xml:space="preserve">
|
||||
<value>用户不存在,注销失败</value>
|
||||
</data>
|
||||
<data name="ServerPassportUserInfoNotExist" xml:space="preserve">
|
||||
<value>用户不存在,获取用户信息失败</value>
|
||||
</data>
|
||||
<data name="ServerPassportUsernameOrPassportIncorrect" xml:space="preserve">
|
||||
<value>用户名或密码错误</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyFailed" xml:space="preserve">
|
||||
<value>验证失败</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
|
||||
<value>验证请求失败,不是当前登录的账号</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
|
||||
<value>验证码已发送至邮箱</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
|
||||
<value>验证请求失败,当前邮箱已被注册</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
|
||||
<value>验证请求过快,请 1 分钟后再试</value>
|
||||
</data>
|
||||
<data name="ServerRecordBannedUid" xml:space="preserve">
|
||||
<value>上传深渊记录失败,当前 Uid 已被胡桃数据库封禁</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics" xml:space="preserve">
|
||||
<value>上传深渊记录失败,正在计算统计数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics2" xml:space="preserve">
|
||||
<value>获取数据失败,正在计算统计数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordInternalException" xml:space="preserve">
|
||||
<value>上传深渊记录失败,服务器异常,请尽快联系开发者解决</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidData" xml:space="preserve">
|
||||
<value>上传深渊记录失败,存在无效的数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidUid" xml:space="preserve">
|
||||
<value>无效的 Uid</value>
|
||||
</data>
|
||||
<data name="ServerRecordNotCurrentSchedule" xml:space="preserve">
|
||||
<value>上传深渊记录失败,不是本期数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordPreviousRequestNotCompleted" xml:space="preserve">
|
||||
<value>上传深渊记录失败,当前 Uid 的记录仍在处理中,请勿重复操作</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessAndGachaLogServiceTimeExtended" xml:space="preserve">
|
||||
<value>上传深渊记录成功,获赠祈愿记录上传服务时长</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNoPassport" xml:space="preserve">
|
||||
<value>上传深渊记录成功,但未登录胡桃账号,无法获赠祈愿记录上传服务时长</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNotFirstTimeAtCurrentSchedule" xml:space="preserve">
|
||||
<value>上传深渊记录成功,但不是本期首次提交,无法获赠祈愿记录上传服务时长</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementImportResultFormat" xml:space="preserve">
|
||||
<value>新增:{0} 个成就 | 更新:{1} 个成就 | 删除:{2} 个成就</value>
|
||||
</data>
|
||||
@@ -959,6 +1037,9 @@
|
||||
<data name="ViewControlStatisticsSegmentedItemContentStatistics" xml:space="preserve">
|
||||
<value>统计</value>
|
||||
</data>
|
||||
<data name="ViewControlWebViewerCoreWebView2ProfileQueryInterfaceFailed" xml:space="preserve">
|
||||
<value>当前 WebView2 版本不支持管理配置,继续使用可能会导致异常,请尽快升级</value>
|
||||
</data>
|
||||
<data name="ViewCultivationHeader" xml:space="preserve">
|
||||
<value>육성 계획</value>
|
||||
</data>
|
||||
@@ -1040,6 +1121,12 @@
|
||||
<data name="ViewDialogDailyNoteNotificationTransformerNotify" xml:space="preserve">
|
||||
<value>매개 변수 변환기 알림</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteWebhookUrlInputPlaceholder" xml:space="preserve">
|
||||
<value>请输入 Url</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteWebhookUrlTitle" xml:space="preserve">
|
||||
<value>实时便笺 Webhook Url</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogImportTitle" xml:space="preserve">
|
||||
<value>기원 기록 가져오기</value>
|
||||
</data>
|
||||
@@ -1085,6 +1172,18 @@
|
||||
<data name="ViewDialogGeetestCustomUrlTitle" xml:space="preserve">
|
||||
<value>配置无感验证接口</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportLoginTitle" xml:space="preserve">
|
||||
<value>登录胡桃通行证</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportRegisterTitle" xml:space="preserve">
|
||||
<value>注册胡桃通行证</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportResetPasswordTitle" xml:space="preserve">
|
||||
<value>重置胡桃通行证密码</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportUnregisterTitle" xml:space="preserve">
|
||||
<value>注销胡桃通行证账号</value>
|
||||
</data>
|
||||
<data name="ViewDialogImportExportApp" xml:space="preserve">
|
||||
<value>앱 내보내기</value>
|
||||
</data>
|
||||
@@ -1268,6 +1367,9 @@
|
||||
<data name="ViewModelCultivationProjectInvalidName" xml:space="preserve">
|
||||
<value>잘못된 이름을 가진 일정은 추가할 수 없습니다</value>
|
||||
</data>
|
||||
<data name="ViewModelDailyNoteConfigWebhookUrlComplete" xml:space="preserve">
|
||||
<value>实时便笺 Webhook Url 配置成功</value>
|
||||
</data>
|
||||
<data name="ViewModelDailyNoteHoyolabVerificationUnsupported" xml:space="preserve">
|
||||
<value>HoYoLab 계정은 실시간 메모 확인 기능을 지원하지 않습니다</value>
|
||||
</data>
|
||||
@@ -1412,6 +1514,9 @@
|
||||
<data name="ViewModelSettingSetDataFolderSuccess" xml:space="preserve">
|
||||
<value>데이터 경로를 설정했습니다. 변경 사항을 적용하기 위해 재시작합니다</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingSetGamePathDatabaseFailedTitle" xml:space="preserve">
|
||||
<value>保存游戏路径失败</value>
|
||||
</data>
|
||||
<data name="ViewModelUserAdded" xml:space="preserve">
|
||||
<value>사용자 [{0}]가 정상적으로 추가되었습니다</value>
|
||||
</data>
|
||||
@@ -1589,6 +1694,18 @@
|
||||
<data name="ViewPageDailyNoteAddEntryToolTip" xml:space="preserve">
|
||||
<value>추가</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteAttendanceStatusInfo" xml:space="preserve">
|
||||
<value>历练点获取详情</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteConfigWebhookDescription" xml:space="preserve">
|
||||
<value>在实时便笺刷新后推送到指定的 Webhook</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteConfigWebhookHeader" xml:space="preserve">
|
||||
<value>配置 Webhook</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteDataInteropHeader" xml:space="preserve">
|
||||
<value>数据互操作</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteNotificationHeader" xml:space="preserve">
|
||||
<value>알림</value>
|
||||
</data>
|
||||
@@ -1685,6 +1802,9 @@
|
||||
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
|
||||
<value>입력</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
|
||||
<value>从胡桃云恢复祈愿记录</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
|
||||
<value>동기화</value>
|
||||
</data>
|
||||
@@ -1829,6 +1949,9 @@
|
||||
<data name="ViewPageHutaoPassportResetPasswordHeader" xml:space="preserve">
|
||||
<value>비밀번호 재설정</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoPassportResetPasswordHint" xml:space="preserve">
|
||||
<value>注销账号的数据将永远丢失,无法恢复</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoPassportUserNameHint" xml:space="preserve">
|
||||
<value>이메일을 입력하세요</value>
|
||||
</data>
|
||||
@@ -1844,6 +1967,12 @@
|
||||
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
|
||||
<value>고급</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
|
||||
<value>快速切换到指定的分辨率</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
|
||||
<value>分辨率</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
|
||||
<value>테두리 없는 창모드</value>
|
||||
</data>
|
||||
@@ -1877,12 +2006,24 @@
|
||||
<data name="ViewPageLaunchGameAppearanceScreenWidthHeader" xml:space="preserve">
|
||||
<value>너비</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameArgumentsDescription" xml:space="preserve">
|
||||
<value>在游戏启动时修改其默认行为</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameArgumentsHeader" xml:space="preserve">
|
||||
<value>启动参数</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameCommonHeader" xml:space="preserve">
|
||||
<value>보통</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
|
||||
<value>모든 설정은 게임을 성공적으로 실행한 후에 저장됩니다</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
|
||||
<value>文件</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
|
||||
<value>进程间</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
|
||||
<value>지정한 모니터에서 실행</value>
|
||||
</data>
|
||||
@@ -1898,6 +2039,18 @@
|
||||
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
|
||||
<value>게임 설정</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
|
||||
<value>在游戏启动后尝试启动并使用 Starward 进行游戏时长统计</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
|
||||
<value>时长统计</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
|
||||
<value>进程</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameRegistryHeader" xml:space="preserve">
|
||||
<value>注册表</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameResourceDiffHeader" xml:space="preserve">
|
||||
<value>증분 패키지</value>
|
||||
</data>
|
||||
@@ -2096,8 +2249,50 @@
|
||||
<data name="ViewpageSettingHomeHeader" xml:space="preserve">
|
||||
<value>主页</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportDangerZoneDescription" xml:space="preserve">
|
||||
<value>三思而后行</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportDangerZoneHeader" xml:space="preserve">
|
||||
<value>危险操作</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportGachaLogExpiredAtHeader" xml:space="preserve">
|
||||
<value>胡桃云服务到期时间</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportHeader" xml:space="preserve">
|
||||
<value>호두 계정</value>
|
||||
<value>胡桃通行证账号</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLicensedDeveloperDescription" xml:space="preserve">
|
||||
<value>您可以无限制使用任何基于胡桃云服务的功能</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLicensedDeveloperHeader" xml:space="preserve">
|
||||
<value>已认证的合作开发者</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLoginAction" xml:space="preserve">
|
||||
<value>登录</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLogoutAction" xml:space="preserve">
|
||||
<value>退出登录</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportMaintainerDescription" xml:space="preserve">
|
||||
<value>您可以无限制的使用任何测试功能</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportMaintainerHeader" xml:space="preserve">
|
||||
<value>胡桃开发/运维</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRedeemCodeDescription" xml:space="preserve">
|
||||
<value>我们有时会向某些用户赠送胡桃云兑换码</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRedeemCodeHeader" xml:space="preserve">
|
||||
<value>使用兑换码</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRegisterAction" xml:space="preserve">
|
||||
<value>注册</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportResetPasswordAction" xml:space="preserve">
|
||||
<value>修改密码</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportUnregisterAction" xml:space="preserve">
|
||||
<value>注销账号</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledDescription" xml:space="preserve">
|
||||
<value>원신과 호두의 사용자 계약을 완전히 읽은 후, 「게임 고급 기능」을 사용</value>
|
||||
@@ -2105,6 +2300,15 @@
|
||||
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
|
||||
<value>고급 기능 활성화</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
|
||||
<value>更改自动连点功能的快捷键</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
|
||||
<value>自动连点</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
|
||||
<value>快捷键</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
|
||||
<value>공식 홈페이지로 이동</value>
|
||||
</data>
|
||||
@@ -2315,6 +2519,9 @@
|
||||
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
|
||||
<value>데이터 업로드</value>
|
||||
</data>
|
||||
<data name="ViewTitleAutoClicking" xml:space="preserve">
|
||||
<value>自动连点</value>
|
||||
</data>
|
||||
<data name="ViewToolHeader" xml:space="preserve">
|
||||
<value>도구</value>
|
||||
</data>
|
||||
@@ -2328,7 +2535,7 @@
|
||||
<value>当前用户</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
|
||||
<value>我的角色</value>
|
||||
<value>旅行工具</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
|
||||
<value>웹 로그인</value>
|
||||
@@ -2414,6 +2621,27 @@
|
||||
<data name="WebAnnouncementTimeHoursEndFormat" xml:space="preserve">
|
||||
<value>{0}시간 후 종료</value>
|
||||
</data>
|
||||
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
|
||||
<value>已复制到剪贴板</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
|
||||
<value>已完成</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusForbid" xml:space="preserve">
|
||||
<value>禁止领取</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusInvalid" xml:space="preserve">
|
||||
<value>无效</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusTakenAward" xml:space="preserve">
|
||||
<value>已领取</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusUnfinished" xml:space="preserve">
|
||||
<value>尚未完成</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusWaitTaken" xml:space="preserve">
|
||||
<value>等待领取</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteExpeditionRemainHoursFormat" xml:space="preserve">
|
||||
<value>{0}시간</value>
|
||||
</data>
|
||||
@@ -2522,6 +2750,12 @@
|
||||
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
|
||||
<value>무기 이벤트 기원</value>
|
||||
</data>
|
||||
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
|
||||
<value>下载链接复制成功</value>
|
||||
</data>
|
||||
<data name="WebHoyolabInvalidUid" xml:space="preserve">
|
||||
<value>无效的 UID</value>
|
||||
</data>
|
||||
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
|
||||
<value>验证失败,请手动验证或前往「米游社-我的角色」页面查看</value>
|
||||
</data>
|
||||
|
||||
@@ -506,6 +506,84 @@
|
||||
<data name="MustSelectUserAndUid" xml:space="preserve">
|
||||
<value>必须先选择一个用户与角色</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientRecordSlot" xml:space="preserve">
|
||||
<value>胡桃云保存的祈愿记录存档数已达当前账号上限</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientTime" xml:space="preserve">
|
||||
<value>未开通祈愿记录上传服务或已到期</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInvalidGachaLogData" xml:space="preserve">
|
||||
<value>祈愿数据存在无效的物品,无法保存至胡桃云</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceServerDatabaseError" xml:space="preserve">
|
||||
<value>数据异常,无法保存至云端,请勿跨账号上传或尝试删除云端数据后重试</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceEmailHasNotRegistered" xml:space="preserve">
|
||||
<value>当前邮箱尚未注册</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceEmailHasRegistered" xml:space="preserve">
|
||||
<value>当前邮箱已被注册</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceInternalException" xml:space="preserve">
|
||||
<value>注册失败,服务器异常,请尽快联系开发者解决</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceUnregisterFailed" xml:space="preserve">
|
||||
<value>用户不存在,注销失败</value>
|
||||
</data>
|
||||
<data name="ServerPassportUserInfoNotExist" xml:space="preserve">
|
||||
<value>用户不存在,获取用户信息失败</value>
|
||||
</data>
|
||||
<data name="ServerPassportUsernameOrPassportIncorrect" xml:space="preserve">
|
||||
<value>用户名或密码错误</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyFailed" xml:space="preserve">
|
||||
<value>验证失败</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
|
||||
<value>验证请求失败,不是当前登录的账号</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
|
||||
<value>验证码已发送至邮箱</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
|
||||
<value>验证请求失败,当前邮箱已被注册</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
|
||||
<value>验证请求过快,请 1 分钟后再试</value>
|
||||
</data>
|
||||
<data name="ServerRecordBannedUid" xml:space="preserve">
|
||||
<value>上传深渊记录失败,当前 Uid 已被胡桃数据库封禁</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics" xml:space="preserve">
|
||||
<value>上传深渊记录失败,正在计算统计数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics2" xml:space="preserve">
|
||||
<value>获取数据失败,正在计算统计数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordInternalException" xml:space="preserve">
|
||||
<value>上传深渊记录失败,服务器异常,请尽快联系开发者解决</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidData" xml:space="preserve">
|
||||
<value>上传深渊记录失败,存在无效的数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidUid" xml:space="preserve">
|
||||
<value>无效的 Uid</value>
|
||||
</data>
|
||||
<data name="ServerRecordNotCurrentSchedule" xml:space="preserve">
|
||||
<value>上传深渊记录失败,不是本期数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordPreviousRequestNotCompleted" xml:space="preserve">
|
||||
<value>上传深渊记录失败,当前 Uid 的记录仍在处理中,请勿重复操作</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessAndGachaLogServiceTimeExtended" xml:space="preserve">
|
||||
<value>上传深渊记录成功,获赠祈愿记录上传服务时长</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNoPassport" xml:space="preserve">
|
||||
<value>上传深渊记录成功,但未登录胡桃账号,无法获赠祈愿记录上传服务时长</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNotFirstTimeAtCurrentSchedule" xml:space="preserve">
|
||||
<value>上传深渊记录成功,但不是本期首次提交,无法获赠祈愿记录上传服务时长</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementImportResultFormat" xml:space="preserve">
|
||||
<value>新增:{0} 个成就 | 更新:{1} 个成就 | 删除:{2} 个成就</value>
|
||||
</data>
|
||||
@@ -959,6 +1037,9 @@
|
||||
<data name="ViewControlStatisticsSegmentedItemContentStatistics" xml:space="preserve">
|
||||
<value>统计</value>
|
||||
</data>
|
||||
<data name="ViewControlWebViewerCoreWebView2ProfileQueryInterfaceFailed" xml:space="preserve">
|
||||
<value>当前 WebView2 版本不支持管理配置,继续使用可能会导致异常,请尽快升级</value>
|
||||
</data>
|
||||
<data name="ViewCultivationHeader" xml:space="preserve">
|
||||
<value>养成计划</value>
|
||||
</data>
|
||||
@@ -1040,6 +1121,12 @@
|
||||
<data name="ViewDialogDailyNoteNotificationTransformerNotify" xml:space="preserve">
|
||||
<value>参量质变仪提醒</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteWebhookUrlInputPlaceholder" xml:space="preserve">
|
||||
<value>请输入 Url</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteWebhookUrlTitle" xml:space="preserve">
|
||||
<value>实时便笺 Webhook Url</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogImportTitle" xml:space="preserve">
|
||||
<value>导入祈愿记录</value>
|
||||
</data>
|
||||
@@ -1085,6 +1172,18 @@
|
||||
<data name="ViewDialogGeetestCustomUrlTitle" xml:space="preserve">
|
||||
<value>配置无感验证接口</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportLoginTitle" xml:space="preserve">
|
||||
<value>登录胡桃通行证</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportRegisterTitle" xml:space="preserve">
|
||||
<value>注册胡桃通行证</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportResetPasswordTitle" xml:space="preserve">
|
||||
<value>重置胡桃通行证密码</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportUnregisterTitle" xml:space="preserve">
|
||||
<value>注销胡桃通行证账号</value>
|
||||
</data>
|
||||
<data name="ViewDialogImportExportApp" xml:space="preserve">
|
||||
<value>导出 App</value>
|
||||
</data>
|
||||
@@ -1268,6 +1367,9 @@
|
||||
<data name="ViewModelCultivationProjectInvalidName" xml:space="preserve">
|
||||
<value>不能添加名称无效的计划</value>
|
||||
</data>
|
||||
<data name="ViewModelDailyNoteConfigWebhookUrlComplete" xml:space="preserve">
|
||||
<value>实时便笺 Webhook Url 配置成功</value>
|
||||
</data>
|
||||
<data name="ViewModelDailyNoteHoyolabVerificationUnsupported" xml:space="preserve">
|
||||
<value>HoYoLab 账号不支持验证实时便笺</value>
|
||||
</data>
|
||||
@@ -1412,6 +1514,9 @@
|
||||
<data name="ViewModelSettingSetDataFolderSuccess" xml:space="preserve">
|
||||
<value>设置数据目录成功,重启以应用更改</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingSetGamePathDatabaseFailedTitle" xml:space="preserve">
|
||||
<value>保存游戏路径失败</value>
|
||||
</data>
|
||||
<data name="ViewModelUserAdded" xml:space="preserve">
|
||||
<value>用户 [{0}] 添加成功</value>
|
||||
</data>
|
||||
@@ -1589,6 +1694,18 @@
|
||||
<data name="ViewPageDailyNoteAddEntryToolTip" xml:space="preserve">
|
||||
<value>添加</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteAttendanceStatusInfo" xml:space="preserve">
|
||||
<value>历练点获取详情</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteConfigWebhookDescription" xml:space="preserve">
|
||||
<value>在实时便笺刷新后推送到指定的 Webhook</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteConfigWebhookHeader" xml:space="preserve">
|
||||
<value>配置 Webhook</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteDataInteropHeader" xml:space="preserve">
|
||||
<value>数据互操作</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteNotificationHeader" xml:space="preserve">
|
||||
<value>通知</value>
|
||||
</data>
|
||||
@@ -1685,6 +1802,9 @@
|
||||
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
|
||||
<value>输入</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
|
||||
<value>从胡桃云恢复祈愿记录</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
|
||||
<value>刷新</value>
|
||||
</data>
|
||||
@@ -1829,6 +1949,9 @@
|
||||
<data name="ViewPageHutaoPassportResetPasswordHeader" xml:space="preserve">
|
||||
<value>重置密码</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoPassportResetPasswordHint" xml:space="preserve">
|
||||
<value>注销账号的数据将永远丢失,无法恢复</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoPassportUserNameHint" xml:space="preserve">
|
||||
<value>请输入邮箱</value>
|
||||
</data>
|
||||
@@ -1844,6 +1967,12 @@
|
||||
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
|
||||
<value>高级功能</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
|
||||
<value>快速切换到指定的分辨率</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
|
||||
<value>分辨率</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
|
||||
<value>将窗口创建为弹出窗口,不带框架</value>
|
||||
</data>
|
||||
@@ -1877,12 +2006,24 @@
|
||||
<data name="ViewPageLaunchGameAppearanceScreenWidthHeader" xml:space="preserve">
|
||||
<value>宽度</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameArgumentsDescription" xml:space="preserve">
|
||||
<value>在游戏启动时修改其默认行为</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameArgumentsHeader" xml:space="preserve">
|
||||
<value>启动参数</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameCommonHeader" xml:space="preserve">
|
||||
<value>常规</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
|
||||
<value>所有选项仅会在启动游戏成功后保存</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
|
||||
<value>文件</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
|
||||
<value>进程间</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
|
||||
<value>在指定的显示器上运行</value>
|
||||
</data>
|
||||
@@ -1898,6 +2039,18 @@
|
||||
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
|
||||
<value>游戏选项</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
|
||||
<value>在游戏启动后尝试启动并使用 Starward 进行游戏时长统计</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
|
||||
<value>时长统计</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
|
||||
<value>进程</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameRegistryHeader" xml:space="preserve">
|
||||
<value>注册表</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameResourceDiffHeader" xml:space="preserve">
|
||||
<value>增量包</value>
|
||||
</data>
|
||||
@@ -2096,8 +2249,50 @@
|
||||
<data name="ViewpageSettingHomeHeader" xml:space="preserve">
|
||||
<value>主页</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportDangerZoneDescription" xml:space="preserve">
|
||||
<value>三思而后行</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportDangerZoneHeader" xml:space="preserve">
|
||||
<value>危险操作</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportGachaLogExpiredAtHeader" xml:space="preserve">
|
||||
<value>胡桃云服务到期时间</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportHeader" xml:space="preserve">
|
||||
<value>胡桃账号</value>
|
||||
<value>胡桃通行证账号</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLicensedDeveloperDescription" xml:space="preserve">
|
||||
<value>您可以无限制使用任何基于胡桃云服务的功能</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLicensedDeveloperHeader" xml:space="preserve">
|
||||
<value>已认证的合作开发者</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLoginAction" xml:space="preserve">
|
||||
<value>登录</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLogoutAction" xml:space="preserve">
|
||||
<value>退出登录</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportMaintainerDescription" xml:space="preserve">
|
||||
<value>您可以无限制的使用任何测试功能</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportMaintainerHeader" xml:space="preserve">
|
||||
<value>胡桃开发/运维</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRedeemCodeDescription" xml:space="preserve">
|
||||
<value>我们有时会向某些用户赠送胡桃云兑换码</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRedeemCodeHeader" xml:space="preserve">
|
||||
<value>使用兑换码</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRegisterAction" xml:space="preserve">
|
||||
<value>注册</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportResetPasswordAction" xml:space="preserve">
|
||||
<value>修改密码</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportUnregisterAction" xml:space="preserve">
|
||||
<value>注销账号</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledDescription" xml:space="preserve">
|
||||
<value>在完整阅读原神和胡桃工具箱用户协议后,我选择启用「启动游戏-高级功能」</value>
|
||||
@@ -2105,6 +2300,15 @@
|
||||
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
|
||||
<value>启动高级功能</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
|
||||
<value>更改自动连点功能的快捷键</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
|
||||
<value>自动连点</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
|
||||
<value>快捷键</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
|
||||
<value>前往官网</value>
|
||||
</data>
|
||||
@@ -2315,6 +2519,9 @@
|
||||
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
|
||||
<value>上传数据</value>
|
||||
</data>
|
||||
<data name="ViewTitleAutoClicking" xml:space="preserve">
|
||||
<value>自动连点</value>
|
||||
</data>
|
||||
<data name="ViewToolHeader" xml:space="preserve">
|
||||
<value>工具</value>
|
||||
</data>
|
||||
@@ -2328,7 +2535,7 @@
|
||||
<value>当前用户</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
|
||||
<value>我的角色</value>
|
||||
<value>旅行工具</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
|
||||
<value>网页登录</value>
|
||||
@@ -2414,6 +2621,27 @@
|
||||
<data name="WebAnnouncementTimeHoursEndFormat" xml:space="preserve">
|
||||
<value>{0} 小时后结束</value>
|
||||
</data>
|
||||
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
|
||||
<value>已复制到剪贴板</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
|
||||
<value>已完成</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusForbid" xml:space="preserve">
|
||||
<value>禁止领取</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusInvalid" xml:space="preserve">
|
||||
<value>无效</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusTakenAward" xml:space="preserve">
|
||||
<value>已领取</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusUnfinished" xml:space="preserve">
|
||||
<value>尚未完成</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusWaitTaken" xml:space="preserve">
|
||||
<value>等待领取</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteExpeditionRemainHoursFormat" xml:space="preserve">
|
||||
<value>{0} 时</value>
|
||||
</data>
|
||||
@@ -2522,6 +2750,12 @@
|
||||
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
|
||||
<value>武器活动祈愿</value>
|
||||
</data>
|
||||
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
|
||||
<value>下载链接复制成功</value>
|
||||
</data>
|
||||
<data name="WebHoyolabInvalidUid" xml:space="preserve">
|
||||
<value>无效的 UID</value>
|
||||
</data>
|
||||
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
|
||||
<value>验证失败,请手动验证或前往「米游社-我的角色」页面查看</value>
|
||||
</data>
|
||||
|
||||
@@ -506,6 +506,84 @@
|
||||
<data name="MustSelectUserAndUid" xml:space="preserve">
|
||||
<value>必須先選擇一個用戶與角色</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientRecordSlot" xml:space="preserve">
|
||||
<value>胡桃云保存的祈愿记录存档数已达当前账号上限</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientTime" xml:space="preserve">
|
||||
<value>未开通祈愿记录上传服务或已到期</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInvalidGachaLogData" xml:space="preserve">
|
||||
<value>祈愿数据存在无效的物品,无法保存至胡桃云</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceServerDatabaseError" xml:space="preserve">
|
||||
<value>数据异常,无法保存至云端,请勿跨账号上传或尝试删除云端数据后重试</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceEmailHasNotRegistered" xml:space="preserve">
|
||||
<value>当前邮箱尚未注册</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceEmailHasRegistered" xml:space="preserve">
|
||||
<value>当前邮箱已被注册</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceInternalException" xml:space="preserve">
|
||||
<value>注册失败,服务器异常,请尽快联系开发者解决</value>
|
||||
</data>
|
||||
<data name="ServerPassportServiceUnregisterFailed" xml:space="preserve">
|
||||
<value>用户不存在,注销失败</value>
|
||||
</data>
|
||||
<data name="ServerPassportUserInfoNotExist" xml:space="preserve">
|
||||
<value>用户不存在,获取用户信息失败</value>
|
||||
</data>
|
||||
<data name="ServerPassportUsernameOrPassportIncorrect" xml:space="preserve">
|
||||
<value>用户名或密码错误</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyFailed" xml:space="preserve">
|
||||
<value>验证失败</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestNotCurrentUser" xml:space="preserve">
|
||||
<value>验证请求失败,不是当前登录的账号</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestSuccess" xml:space="preserve">
|
||||
<value>验证码已发送至邮箱</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyRequestUserAlreadyExisted" xml:space="preserve">
|
||||
<value>验证请求失败,当前邮箱已被注册</value>
|
||||
</data>
|
||||
<data name="ServerPassportVerifyTooFrequent" xml:space="preserve">
|
||||
<value>验证请求过快,请 1 分钟后再试</value>
|
||||
</data>
|
||||
<data name="ServerRecordBannedUid" xml:space="preserve">
|
||||
<value>上传深渊记录失败,当前 Uid 已被胡桃数据库封禁</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics" xml:space="preserve">
|
||||
<value>上传深渊记录失败,正在计算统计数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics2" xml:space="preserve">
|
||||
<value>获取数据失败,正在计算统计数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordInternalException" xml:space="preserve">
|
||||
<value>上传深渊记录失败,服务器异常,请尽快联系开发者解决</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidData" xml:space="preserve">
|
||||
<value>上传深渊记录失败,存在无效的数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidUid" xml:space="preserve">
|
||||
<value>无效的 Uid</value>
|
||||
</data>
|
||||
<data name="ServerRecordNotCurrentSchedule" xml:space="preserve">
|
||||
<value>上传深渊记录失败,不是本期数据</value>
|
||||
</data>
|
||||
<data name="ServerRecordPreviousRequestNotCompleted" xml:space="preserve">
|
||||
<value>上传深渊记录失败,当前 Uid 的记录仍在处理中,请勿重复操作</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessAndGachaLogServiceTimeExtended" xml:space="preserve">
|
||||
<value>上传深渊记录成功,获赠祈愿记录上传服务时长</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNoPassport" xml:space="preserve">
|
||||
<value>上传深渊记录成功,但未登录胡桃账号,无法获赠祈愿记录上传服务时长</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNotFirstTimeAtCurrentSchedule" xml:space="preserve">
|
||||
<value>上传深渊记录成功,但不是本期首次提交,无法获赠祈愿记录上传服务时长</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementImportResultFormat" xml:space="preserve">
|
||||
<value>新增:{0} 個成就 | 更新:{1} 個成就 | 删除:{2} 個成就</value>
|
||||
</data>
|
||||
@@ -959,6 +1037,9 @@
|
||||
<data name="ViewControlStatisticsSegmentedItemContentStatistics" xml:space="preserve">
|
||||
<value>統計</value>
|
||||
</data>
|
||||
<data name="ViewControlWebViewerCoreWebView2ProfileQueryInterfaceFailed" xml:space="preserve">
|
||||
<value>当前 WebView2 版本不支持管理配置,继续使用可能会导致异常,请尽快升级</value>
|
||||
</data>
|
||||
<data name="ViewCultivationHeader" xml:space="preserve">
|
||||
<value>養成計劃</value>
|
||||
</data>
|
||||
@@ -1040,6 +1121,12 @@
|
||||
<data name="ViewDialogDailyNoteNotificationTransformerNotify" xml:space="preserve">
|
||||
<value>參數質變儀提醒</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteWebhookUrlInputPlaceholder" xml:space="preserve">
|
||||
<value>请输入 Url</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteWebhookUrlTitle" xml:space="preserve">
|
||||
<value>实时便笺 Webhook Url</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogImportTitle" xml:space="preserve">
|
||||
<value>匯入祈願記錄</value>
|
||||
</data>
|
||||
@@ -1085,6 +1172,18 @@
|
||||
<data name="ViewDialogGeetestCustomUrlTitle" xml:space="preserve">
|
||||
<value>配置無感驗證接口</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportLoginTitle" xml:space="preserve">
|
||||
<value>登录胡桃通行证</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportRegisterTitle" xml:space="preserve">
|
||||
<value>注册胡桃通行证</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportResetPasswordTitle" xml:space="preserve">
|
||||
<value>重置胡桃通行证密码</value>
|
||||
</data>
|
||||
<data name="ViewDialogHutaoPassportUnregisterTitle" xml:space="preserve">
|
||||
<value>注销胡桃通行证账号</value>
|
||||
</data>
|
||||
<data name="ViewDialogImportExportApp" xml:space="preserve">
|
||||
<value>匯出 App</value>
|
||||
</data>
|
||||
@@ -1268,6 +1367,9 @@
|
||||
<data name="ViewModelCultivationProjectInvalidName" xml:space="preserve">
|
||||
<value>不能新增名稱無效的計劃</value>
|
||||
</data>
|
||||
<data name="ViewModelDailyNoteConfigWebhookUrlComplete" xml:space="preserve">
|
||||
<value>实时便笺 Webhook Url 配置成功</value>
|
||||
</data>
|
||||
<data name="ViewModelDailyNoteHoyolabVerificationUnsupported" xml:space="preserve">
|
||||
<value>HoYoLAB 賬號不支持驗證實时便箋</value>
|
||||
</data>
|
||||
@@ -1412,6 +1514,9 @@
|
||||
<data name="ViewModelSettingSetDataFolderSuccess" xml:space="preserve">
|
||||
<value>設置數據目錄成功,重啓以應用更改</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingSetGamePathDatabaseFailedTitle" xml:space="preserve">
|
||||
<value>保存游戏路径失败</value>
|
||||
</data>
|
||||
<data name="ViewModelUserAdded" xml:space="preserve">
|
||||
<value>用戶 [{0}] 新增成功</value>
|
||||
</data>
|
||||
@@ -1589,6 +1694,18 @@
|
||||
<data name="ViewPageDailyNoteAddEntryToolTip" xml:space="preserve">
|
||||
<value>新增</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteAttendanceStatusInfo" xml:space="preserve">
|
||||
<value>歷練點獲取詳情</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteConfigWebhookDescription" xml:space="preserve">
|
||||
<value>在实时便笺刷新后推送到指定的 Webhook</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteConfigWebhookHeader" xml:space="preserve">
|
||||
<value>配置 Webhook</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteDataInteropHeader" xml:space="preserve">
|
||||
<value>数据互操作</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteNotificationHeader" xml:space="preserve">
|
||||
<value>通知</value>
|
||||
</data>
|
||||
@@ -1685,6 +1802,9 @@
|
||||
<data name="ViewPageGachaLogInputAction" xml:space="preserve">
|
||||
<value>輸入</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRecoverFromHutaoCloudDescription" xml:space="preserve">
|
||||
<value>從胡桃云恢復祈願紀錄</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRefresh" xml:space="preserve">
|
||||
<value>重新整理</value>
|
||||
</data>
|
||||
@@ -1829,6 +1949,9 @@
|
||||
<data name="ViewPageHutaoPassportResetPasswordHeader" xml:space="preserve">
|
||||
<value>重設密碼</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoPassportResetPasswordHint" xml:space="preserve">
|
||||
<value>注销账号的数据将永远丢失,无法恢复</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoPassportUserNameHint" xml:space="preserve">
|
||||
<value>請輸入電郵地址</value>
|
||||
</data>
|
||||
@@ -1844,6 +1967,12 @@
|
||||
<data name="ViewPageLaunchGameAdvanceHeader" xml:space="preserve">
|
||||
<value>進階功能</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceAspectRatioDescription" xml:space="preserve">
|
||||
<value>快速切换到指定的分辨率</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceAspectRatioHeader" xml:space="preserve">
|
||||
<value>分辨率</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceBorderlessDescription" xml:space="preserve">
|
||||
<value>將窗口創建為彈出窗口,不帶邊框</value>
|
||||
</data>
|
||||
@@ -1877,12 +2006,24 @@
|
||||
<data name="ViewPageLaunchGameAppearanceScreenWidthHeader" xml:space="preserve">
|
||||
<value>寬度</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameArgumentsDescription" xml:space="preserve">
|
||||
<value>在游戏启动时修改其默认行为</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameArgumentsHeader" xml:space="preserve">
|
||||
<value>启动参数</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameCommonHeader" xml:space="preserve">
|
||||
<value>一般</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
|
||||
<value>所有選項盡會在啓動游戲成功後保存</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
|
||||
<value>文件</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
|
||||
<value>进程间</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
|
||||
<value>在指定的屏幕上運行</value>
|
||||
</data>
|
||||
@@ -1898,6 +2039,18 @@
|
||||
<data name="ViewPageLaunchGameOptionsHeader" xml:space="preserve">
|
||||
<value>遊戲選項</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
|
||||
<value>在游戏启动后尝试启动并使用 Starward 进行游戏时长统计</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
|
||||
<value>时长统计</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
|
||||
<value>进程</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameRegistryHeader" xml:space="preserve">
|
||||
<value>注册表</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameResourceDiffHeader" xml:space="preserve">
|
||||
<value>增量包</value>
|
||||
</data>
|
||||
@@ -2096,8 +2249,50 @@
|
||||
<data name="ViewpageSettingHomeHeader" xml:space="preserve">
|
||||
<value>主頁</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportDangerZoneDescription" xml:space="preserve">
|
||||
<value>三思而后行</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportDangerZoneHeader" xml:space="preserve">
|
||||
<value>危险操作</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportGachaLogExpiredAtHeader" xml:space="preserve">
|
||||
<value>胡桃云服务到期时间</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportHeader" xml:space="preserve">
|
||||
<value>Snap Hutao 賬號</value>
|
||||
<value>胡桃通行证账号</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLicensedDeveloperDescription" xml:space="preserve">
|
||||
<value>您可以无限制使用任何基于胡桃云服务的功能</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLicensedDeveloperHeader" xml:space="preserve">
|
||||
<value>已认证的合作开发者</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLoginAction" xml:space="preserve">
|
||||
<value>登录</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportLogoutAction" xml:space="preserve">
|
||||
<value>退出登录</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportMaintainerDescription" xml:space="preserve">
|
||||
<value>您可以无限制的使用任何测试功能</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportMaintainerHeader" xml:space="preserve">
|
||||
<value>胡桃开发/运维</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRedeemCodeDescription" xml:space="preserve">
|
||||
<value>我们有时会向某些用户赠送胡桃云兑换码</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRedeemCodeHeader" xml:space="preserve">
|
||||
<value>使用兑换码</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportRegisterAction" xml:space="preserve">
|
||||
<value>注册</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportResetPasswordAction" xml:space="preserve">
|
||||
<value>修改密码</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHutaoPassportUnregisterAction" xml:space="preserve">
|
||||
<value>注销账号</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledDescription" xml:space="preserve">
|
||||
<value>在完整閱讀原神和胡桃工具箱使用者協定後,我選擇啟用「啟動遊戲 - 高級功能」</value>
|
||||
@@ -2105,6 +2300,15 @@
|
||||
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
|
||||
<value>啟動高級功能</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
|
||||
<value>更改自动连点功能的快捷键</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
|
||||
<value>自动连点</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
|
||||
<value>快捷键</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
|
||||
<value>前往官網</value>
|
||||
</data>
|
||||
@@ -2315,6 +2519,9 @@
|
||||
<data name="ViewSpiralAbyssUploadRecord" xml:space="preserve">
|
||||
<value>上傳資料</value>
|
||||
</data>
|
||||
<data name="ViewTitleAutoClicking" xml:space="preserve">
|
||||
<value>自动连点</value>
|
||||
</data>
|
||||
<data name="ViewToolHeader" xml:space="preserve">
|
||||
<value>工具</value>
|
||||
</data>
|
||||
@@ -2328,7 +2535,7 @@
|
||||
<value>當前用戶</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
|
||||
<value>我的角色</value>
|
||||
<value>旅行工具</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
|
||||
<value>網頁登陸</value>
|
||||
@@ -2414,6 +2621,27 @@
|
||||
<data name="WebAnnouncementTimeHoursEndFormat" xml:space="preserve">
|
||||
<value>{0} 小時後結束</value>
|
||||
</data>
|
||||
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
|
||||
<value>已复制到剪贴板</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
|
||||
<value>已完成</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusForbid" xml:space="preserve">
|
||||
<value>禁止领取</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusInvalid" xml:space="preserve">
|
||||
<value>无效</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusTakenAward" xml:space="preserve">
|
||||
<value>已领取</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusUnfinished" xml:space="preserve">
|
||||
<value>尚未完成</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusWaitTaken" xml:space="preserve">
|
||||
<value>等待领取</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteExpeditionRemainHoursFormat" xml:space="preserve">
|
||||
<value>{0} 時</value>
|
||||
</data>
|
||||
@@ -2522,6 +2750,12 @@
|
||||
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
|
||||
<value>武器活動祈願</value>
|
||||
</data>
|
||||
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
|
||||
<value>下载链接复制成功</value>
|
||||
</data>
|
||||
<data name="WebHoyolabInvalidUid" xml:space="preserve">
|
||||
<value>无效的 UID</value>
|
||||
</data>
|
||||
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
|
||||
<value>验证失败,请手动验证或前往「米游社-我的角色」页面查看</value>
|
||||
</data>
|
||||
|
||||
@@ -153,7 +153,7 @@ internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbSt
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <param name="propertyName">属性名称</param>
|
||||
protected void SetOption(ref string? storage, string key, string value, [CallerMemberName] string? propertyName = null)
|
||||
protected void SetOption(ref string? storage, string key, string? value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (!SetProperty(ref storage, value, propertyName))
|
||||
{
|
||||
|
||||
@@ -19,12 +19,7 @@ namespace Snap.Hutao.Service;
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed partial class AppOptions : DbStoreOptions
|
||||
{
|
||||
private readonly List<NameValue<BackdropType>> supportedBackdropTypesInner = new()
|
||||
{
|
||||
new("Acrylic", BackdropType.Acrylic),
|
||||
new("Mica", BackdropType.Mica),
|
||||
new("MicaAlt", BackdropType.MicaAlt),
|
||||
};
|
||||
private readonly List<NameValue<BackdropType>> supportedBackdropTypesInner = CollectionsNameValue.ListFromEnum<BackdropType>();
|
||||
|
||||
private readonly List<NameValue<string>> supportedCulturesInner = new()
|
||||
{
|
||||
@@ -106,6 +101,8 @@ internal sealed partial class AppOptions : DbStoreOptions
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用高级功能
|
||||
/// DO NOT MOVE TO OTHER CLASS
|
||||
/// We are binding this property in SettingPage
|
||||
/// </summary>
|
||||
public bool IsAdvancedLaunchOptionsEnabled
|
||||
{
|
||||
|
||||
42
src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs
Normal file
42
src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service;
|
||||
|
||||
internal static class AppOptionsExtension
|
||||
{
|
||||
public static bool TryGetGameFolderAndFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
|
||||
{
|
||||
string gamePath = appOptions.GamePath;
|
||||
|
||||
gameFolder = Path.GetDirectoryName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameFolder))
|
||||
{
|
||||
gameFileName = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
gameFileName = Path.GetFileName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameFileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetGameFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFileName)
|
||||
{
|
||||
string gamePath = appOptions.GamePath;
|
||||
|
||||
gameFileName = Path.GetFileName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameFileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -105,8 +105,12 @@ internal static class SummaryHelper
|
||||
/// <returns>分数</returns>
|
||||
public static float GetPercentSubAffixScore(in ReliquarySubAffixId appendId)
|
||||
{
|
||||
// 圣遗物相同类型副词条强化档位一共为 4 档
|
||||
// 恰好为 70% 80% 90% 100%
|
||||
// 圣遗物相同类型副词条强化档位一共为 4/3/2 档
|
||||
// 五星 为 70% 80% 90% 100%
|
||||
// 四星 为 70% 80% 90% 100%
|
||||
// 三星 为 70% 80% 90% 100%
|
||||
// 二星 为 70% 85% 100%
|
||||
// 二星 为 80% 100%
|
||||
// 通过计算与最大属性的 Id 差来决定当前副词条的强化档位
|
||||
uint maxId = GetAffixMaxId(appendId);
|
||||
uint delta = maxId - appendId;
|
||||
@@ -119,7 +123,11 @@ internal static class SummaryHelper
|
||||
(5 or 4 or 3, 3) => 70F,
|
||||
|
||||
(2, 0) => 100F,
|
||||
(2, 1) => 80F,
|
||||
(2, 1) => 85F,
|
||||
(2, 2) => 70F,
|
||||
|
||||
(1, 0) => 100F,
|
||||
(1, 1) => 80F,
|
||||
|
||||
_ => throw Must.NeverHappen($"Unexpected AppendId: {appendId.Value} Delta: {delta}"),
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ internal sealed partial class DailyNoteNotificationOperation
|
||||
private const string ToastAttributionUnknown = "Unknown";
|
||||
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly IGameService gameService;
|
||||
private readonly IGameServiceFacade gameService;
|
||||
private readonly BindingClient bindingClient;
|
||||
private readonly DailyNoteOptions options;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions
|
||||
private NameValue<int>? selectedRefreshTime;
|
||||
private bool? isReminderNotification;
|
||||
private bool? isSilentWhenPlayingGame;
|
||||
private string? webhookUrl;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新时间
|
||||
@@ -76,7 +77,7 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions
|
||||
{
|
||||
if (runtimeOptions.IsElevated)
|
||||
{
|
||||
// leave below untouched if we are running in elevated privilege
|
||||
// leave untouched when we are running in elevated privilege
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -87,7 +88,7 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions
|
||||
{
|
||||
if (runtimeOptions.IsElevated)
|
||||
{
|
||||
// leave below untouched if we are running in elevated privilege
|
||||
// leave untouched when we are running in elevated privilege
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,4 +123,10 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions
|
||||
get => GetOption(ref isSilentWhenPlayingGame, SettingEntry.DailyNoteSilentWhenPlayingGame);
|
||||
set => SetOption(ref isSilentWhenPlayingGame, SettingEntry.DailyNoteSilentWhenPlayingGame, value);
|
||||
}
|
||||
|
||||
public string? WebhookUrl
|
||||
{
|
||||
get => GetOption(ref webhookUrl, SettingEntry.DailyNoteWebhookUrl);
|
||||
set => SetOption(ref webhookUrl, SettingEntry.DailyNoteWebhookUrl, value);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,15 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Message;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.ViewModel.User;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using System.Collections.ObjectModel;
|
||||
using WebDailyNote = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote.DailyNote;
|
||||
|
||||
@@ -108,6 +111,8 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
|
||||
|
||||
private async ValueTask RefreshDailyNotesCoreAsync(bool forceRefresh)
|
||||
{
|
||||
DailyNoteWebhookOperation dailyNoteWebhookOperation = serviceProvider.GetRequiredService<DailyNoteWebhookOperation>();
|
||||
|
||||
foreach (DailyNoteEntry entry in await dailyNoteDbService.GetDailyNoteEntryIncludeUserListAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (!forceRefresh && entry.DailyNote is not null)
|
||||
@@ -144,6 +149,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
|
||||
// database
|
||||
entry.UpdateDailyNote(dailyNote);
|
||||
await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry).ConfigureAwait(false);
|
||||
await dailyNoteWebhookOperation.TryPostDailyNoteToWebhookAsync(dailyNote).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using System.Net.Http;
|
||||
using WebDailyNote = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote.DailyNote;
|
||||
|
||||
namespace Snap.Hutao.Service.DailyNote;
|
||||
|
||||
[ConstructorGenerated(ResolveHttpClient = true)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed partial class DailyNoteWebhookOperation
|
||||
{
|
||||
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
||||
private readonly ILogger<DailyNoteWebhookOperation> logger;
|
||||
private readonly DailyNoteOptions dailyNoteOptions;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
public async ValueTask TryPostDailyNoteToWebhookAsync(WebDailyNote dailyNote, CancellationToken token = default)
|
||||
{
|
||||
string? targetUrl = dailyNoteOptions.WebhookUrl;
|
||||
if (string.IsNullOrEmpty(targetUrl) || !Uri.TryCreate(targetUrl, UriKind.Absolute, out Uri? targetUri))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(targetUri)
|
||||
.PostJson(dailyNote);
|
||||
|
||||
await builder.TryCatchSendAsync(httpClient, logger, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user