Compare commits

..

148 Commits

Author SHA1 Message Date
Lightczx
39831b0ae1 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2023-11-10 16:08:54 +08:00
Lightczx
51c9936018 1.7.17 package 2023-11-10 16:08:35 +08:00
Masterain
3466a98ffb Create MGMT-publish.yml 2023-11-09 23:48:29 -08:00
DismissedLight
a064cc10ee Merge pull request #1085 from DGP-Studio/develop 2023-11-10 15:41:33 +08:00
Masterain
3479a19164 New Crowdin updates (#1078)
Co-authored-by: DismissedLight <1686188646@qq.com>
2023-11-10 15:39:00 +08:00
Lightczx
d4549581c1 fix exception capture 2023-11-10 15:33:11 +08:00
Lightczx
f97bc344d0 fix announcement time incorrectness 2023-11-10 14:20:44 +08:00
Lightczx
26d23fec7f impl #830 in previous commit 2023-11-10 11:39:53 +08:00
Lightczx
7442f7f1ec support UIGF v2.4-preview 2023-11-10 11:37:45 +08:00
DismissedLight
3eb2556393 update gacha info endpoints 2023-11-09 23:39:51 +08:00
DismissedLight
cfff6f39fc adjust server timezone 2023-11-09 23:15:08 +08:00
Lightczx
3005031b39 add basic timezone support for gachaitem 2023-11-09 17:18:56 +08:00
Lightczx
71363f4d8d fix #1081 2023-11-09 15:23:51 +08:00
Lightczx
e833578334 rename jsbridge 2023-11-09 11:51:56 +08:00
Lightczx
d529b3cea6 fix #1079 2023-11-09 11:38:30 +08:00
Lightczx
1c0ce62885 fix gacha item corner radius 2023-11-08 13:34:55 +08:00
DismissedLight
acdf2baa9a improve webviewer & hotkey 2023-11-07 21:02:25 +08:00
DismissedLight
ec007d5d81 add fp to jsbridge 2023-11-07 19:08:48 +08:00
Lightczx
5e734ac689 impl #961 2023-11-07 15:37:53 +08:00
DismissedLight
b0ecd048b6 1.7.16 package 2023-11-06 21:41:04 +08:00
DismissedLight
91010d0d8b Merge pull request #1076 from DGP-Studio/develop 2023-11-06 20:57:23 +08:00
Masterain
fc771eb90a New Crowdin updates (#1063) 2023-11-06 20:56:40 +08:00
DismissedLight
80f2fed722 Merge pull request #1075 from DGP-Studio/develop 2023-11-06 20:56:15 +08:00
DismissedLight
bdb406c451 add copy hint for #1074 2023-11-06 20:53:29 +08:00
DismissedLight
5bc957c6a5 fix spiral abyss crash when using 4.2 metadata 2023-11-06 20:32:29 +08:00
Lightczx
416c6f15a6 partial #1074 2023-11-06 17:07:35 +08:00
Lightczx
9eed633e05 DefaultItemCollectionTransitionProvider 2023-11-06 15:52:50 +08:00
Lightczx
7e30173990 update to 4.2 metadata 2023-11-06 14:57:57 +08:00
Lightczx
2200e2e58e fonticon resources 2023-11-06 14:36:36 +08:00
Lightczx
b8886c5cd3 fix #1072 2023-11-06 13:46:59 +08:00
DismissedLight
43007d8fb4 Merge pull request #1073 from qhy040404/develop 2023-11-06 12:40:53 +08:00
Lightczx
88684bff00 code style 2023-11-06 12:40:19 +08:00
qhy040404
0c7ce7a72f Add files 2023-11-06 12:32:22 +08:00
qhy040404
075d92f754 Set IsEnabled by a new property instead of setting it separately for each SettingsCard 2023-11-06 12:18:05 +08:00
Lightczx
a0cba171cc update feat template 2023-11-06 11:43:57 +08:00
Lightczx
f41185310b adjust wish typename 2023-11-06 11:41:33 +08:00
Lightczx
2a4c93d241 impl #778 all 2023-11-06 11:17:16 +08:00
DismissedLight
c0980fabe8 impl #1071 2023-11-05 16:03:10 +08:00
DismissedLight
f2ba316059 recycle fingerprint 2023-11-04 17:47:45 +08:00
DismissedLight
e4e9dd91f1 impl #1062 2023-11-04 17:21:31 +08:00
DismissedLight
749ef0e138 introducing game service facade 2023-11-04 16:53:08 +08:00
DismissedLight
24086ee4d0 optimize UniformStaggeredColumnLayout 2023-11-03 23:32:52 +08:00
DismissedLight
aeb6962ae4 impl #1015 2023-11-03 22:06:51 +08:00
Lightczx
87e5ede91f impl #1068 2023-11-03 16:20:11 +08:00
Lightczx
91de6d170e add fingerprint fetch & fix #1060 2023-11-03 11:52:52 +08:00
Lightczx
3057673cdb fix #1069 2023-11-03 10:11:47 +08:00
Lightczx
c3ace405ac fix pushpage 2023-11-03 09:26:44 +08:00
DismissedLight
0b48581e65 Merge pull request #1065 from qhy040404/main 2023-11-02 21:16:40 +08:00
DismissedLight
4ab129e4a2 code style 2023-11-02 21:14:39 +08:00
qhy040404
13ad36f5b4 Added a master switch for launchOptions 2023-11-02 18:43:07 +08:00
Lightczx
f026321aa8 1.7.15 package 2023-11-02 15:27:40 +08:00
DismissedLight
d1dfdf107b Merge pull request #1064 from DGP-Studio/develop 2023-11-02 15:10:51 +08:00
Lightczx
59f8895675 MarqueeText 2023-11-02 15:05:06 +08:00
Lightczx
4cb3d5f03f launch scheme renewed 2023-11-02 14:23:34 +08:00
Lightczx
067c7d7c4d fix ci 2023-11-02 12:45:11 +08:00
Lightczx
1cc072ba28 EmailSmtpAddress 2023-11-02 11:35:31 +08:00
Lightczx
0e7afa8efb clear username & password after cancel registration 2023-11-02 11:30:00 +08:00
Lightczx
b753728b7e verify code request set token 2023-11-02 11:12:26 +08:00
Lightczx
df019da891 complete cancel registration 2023-11-02 10:42:55 +08:00
Lightczx
c6435f30eb add verify for cancel registration 2023-11-02 10:26:27 +08:00
Lightczx
3ac0be4220 fix unregister passport 2023-11-02 10:07:20 +08:00
Lightczx
24f6a33256 clear username & password after logout 2023-11-02 09:48:22 +08:00
Lightczx
dc9278eb4f fix verifycode crash 2023-11-02 09:42:08 +08:00
Lightczx
4b2c82db62 fix xaml parsing failed 2023-11-02 09:06:56 +08:00
Lightczx
70f30edd7c xaml style rework 2023-11-01 17:03:00 +08:00
Lightczx
c8e8213df6 code style 2023-11-01 15:56:22 +08:00
Lightczx
7cad996902 process cmdline #1061 2023-11-01 15:28:44 +08:00
Lightczx
eec47b72c7 fix #1061 2023-11-01 15:26:36 +08:00
Lightczx
5943b1a1fb impl #886 2023-11-01 13:46:50 +08:00
Lightczx
9f9a5670bc fix dailynote webhook error 2023-11-01 10:45:47 +08:00
Lightczx
10ba927136 fix #1059 2023-10-31 16:45:24 +08:00
Lightczx
07d42cedd1 page style 2023-10-31 15:19:25 +08:00
Lightczx
07c52019f4 1.7.14 hotfix package 2023-10-31 11:38:51 +08:00
DismissedLight
77cb2fc603 Merge pull request #1056 from DGP-Studio/develop 2023-10-31 11:11:03 +08:00
Masterain
b5c16e2dae New Crowdin updates (#1053) 2023-10-31 11:10:06 +08:00
DismissedLight
29c954b032 fingerprint 2023-10-30 22:03:33 +08:00
DismissedLight
8df5d5d6eb fix #1052 & user account add crash 2023-10-30 19:42:36 +08:00
Lightczx
827d944987 1.7.13 package 2023-10-30 15:38:00 +08:00
DismissedLight
4c47f3c08b Merge pull request #1050 from DGP-Studio/develop 2023-10-30 15:16:10 +08:00
DismissedLight
6540cc4577 Merge pull request #1045 from DGP-Studio/l10n_develop 2023-10-30 15:14:10 +08:00
Lightczx
df22d30a96 ui/ux 2023-10-30 15:12:30 +08:00
Lightczx
5ea9dd533f remove hutao user changed message 2023-10-30 09:14:21 +08:00
DismissedLight
744c1079e1 fix #903 2023-10-29 22:56:10 +08:00
Masterain
1539863415 New translations sh.resx (English) 2023-10-29 07:32:38 -07:00
Masterain
3527e43118 New translations sh.resx (Chinese Traditional) 2023-10-29 07:32:37 -07:00
Masterain
8388def548 New translations sh.resx (Korean) 2023-10-29 07:32:36 -07:00
Masterain
d0bfbfa505 New translations sh.resx (Japanese) 2023-10-29 07:32:35 -07:00
DismissedLight
fa640d27f0 implement #911 2023-10-29 22:08:51 +08:00
DismissedLight
389c1417f7 built in resx generator 2023-10-28 15:00:43 +08:00
Masterain
6dcacb1bf4 New translations sh.resx (Japanese) 2023-10-27 10:45:51 -07:00
Lightczx
4ffd09cce8 roslyn generated resx 2023-10-27 15:50:32 +08:00
Lightczx
0dcbac3ee1 fix #431 input crash 2023-10-27 09:23:05 +08:00
Masterain
b1ea5332fc Merge pull request #1037 from DGP-Studio/l10n_develop
New Crowdin updates
2023-10-26 10:29:22 -07:00
Masterain
b34dab0f99 New translations sh.resx (English) 2023-10-26 10:28:57 -07:00
Masterain
791e517e39 New translations sh.resx (Chinese Traditional) 2023-10-26 10:28:55 -07:00
Masterain
4408e3994e New translations sh.resx (Korean) 2023-10-26 10:28:54 -07:00
Masterain
32cbbefe1a New translations sh.resx (Japanese) 2023-10-26 10:28:52 -07:00
DismissedLight
d754c0d117 ignore designer file 2 2023-10-26 23:04:21 +08:00
DismissedLight
4c75295f2c ignore designer file 1 2023-10-26 23:04:01 +08:00
DismissedLight
34e5312d75 impl #1021 2023-10-26 22:58:40 +08:00
Masterain
465d6b631e Update issue templates 2023-10-26 03:37:04 -07:00
Masterain
0d2d1b8115 New translations sh.resx (English) 2023-10-26 02:34:48 -07:00
Masterain
981949651e New translations sh.resx (English) 2023-10-26 02:28:47 -07:00
Masterain
874dac1119 New translations sh.resx (Chinese Traditional) 2023-10-26 02:28:46 -07:00
Masterain
3f0694b28e New translations sh.resx (Korean) 2023-10-26 02:28:44 -07:00
Masterain
6b166b6aed New translations sh.resx (Japanese) 2023-10-26 02:28:43 -07:00
Lightczx
b351231c84 typo fix 2023-10-26 17:27:55 +08:00
Lightczx
0603b24466 implement #431 2023-10-26 15:31:55 +08:00
Lightczx
f97ad4eac0 fix IsGameRunning 2023-10-26 14:12:45 +08:00
DismissedLight
28fc4558be support server l10n 2023-10-25 23:26:46 +08:00
Masterain
ea3391b112 New translations sh.resx (English) 2023-10-25 02:28:14 -07:00
DismissedLight
ec95e42d7d fix #1041 2023-10-23 19:04:00 +08:00
Masterain
e9f12aeb09 Update .github configurations 2023-10-21 19:52:34 -07:00
Masterain
04850dd136 New translations sh.resx (Japanese) 2023-10-21 11:07:53 -07:00
Masterain
4782d61ed0 New translations sh.resx (English) 2023-10-20 06:16:12 -07:00
Masterain
28ade90926 New translations sh.resx (Chinese Traditional) 2023-10-20 06:16:11 -07:00
Masterain
dde97b6489 New translations sh.resx (Korean) 2023-10-20 06:16:10 -07:00
Masterain
44fe729e1a New translations sh.resx (Japanese) 2023-10-20 06:16:09 -07:00
Lightczx
2a1d814cc5 fix #899 2023-10-20 11:44:42 +08:00
Lightczx
c1ee37bd8f fix #1023 2023-10-20 10:59:30 +08:00
Lightczx
65b81f0ad8 fix #1035 2023-10-20 09:07:40 +08:00
DismissedLight
91fea88623 launch page redo 2023-10-19 22:32:01 +08:00
DismissedLight
026c68229a fix #925 2023-10-19 20:46:29 +08:00
DismissedLight
91b2db886f 1.7.11 hotfix package 2023-10-18 19:53:00 +08:00
DismissedLight
101d316525 Merge pull request #1032 from DGP-Studio/develop 2023-10-18 19:38:01 +08:00
DismissedLight
59d62f931d fix launch args 2023-10-18 19:35:59 +08:00
Lightczx
f0bb19bc07 fix #1028 2023-10-18 17:21:45 +08:00
Masterain
c0b05e2c2f Update FUNDING.yml 2023-10-17 17:15:55 -07:00
Masterain
fc02f833a0 Update FUNDING.yml 2023-10-17 15:36:04 -07:00
DismissedLight
7b8ebd86b1 1.7.10 package 2023-10-17 22:12:58 +08:00
DismissedLight
47b24286b1 Merge pull request #1024 from DGP-Studio/develop 2023-10-17 20:31:47 +08:00
Masterain
0e3e3b9e4a New Crowdin updates (#1019) 2023-10-17 20:31:15 +08:00
DismissedLight
85d7b22e11 auto select existed account when detecting 2023-10-17 20:29:14 +08:00
Lightczx
7caeb17788 use reference in picker factory 2023-10-17 14:51:41 +08:00
Lightczx
b11b90e9f1 re-impl #1020 2023-10-16 11:03:41 +08:00
DismissedLight
58643a60b5 fix bugs 2023-10-15 16:14:22 +08:00
DismissedLight
a29b487c26 impl #1020 2023-10-15 16:14:13 +08:00
DismissedLight
1bd6023e0a fix issues 2023-10-15 12:30:44 +08:00
DismissedLight
579173d464 webview lifetime 2023-10-14 19:29:28 +08:00
DismissedLight
9ed53e8c34 fix spiralabyss damage rank empty crash 2023-10-14 17:26:06 +08:00
DismissedLight
830556a043 fix ICoreWebView2_13 not supported 2023-10-14 00:12:14 +08:00
DismissedLight
7ba27e184f ignore invalid launch schemes 2023-10-13 23:17:08 +08:00
DismissedLight
9aa6a2b57b fix invalid geetest url crash 2023-10-13 22:53:32 +08:00
DismissedLight
5773902f4a fix #1001 2023-10-13 22:36:51 +08:00
DismissedLight
06c5bcad3e fix #1013 #1012 #1011 2023-10-13 21:42:36 +08:00
Lightczx
c0165c57fd support Send on sync context 2023-10-13 15:27:46 +08:00
DismissedLight
4e57520115 Update AvatarIds.cs 2023-10-12 22:55:35 +08:00
Masterain
17c3480dae New Crowdin updates (#1006) 2023-10-12 22:10:54 +08:00
283 changed files with 9784 additions and 11927 deletions

4
.github/FUNDING.yml vendored
View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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/

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
}
}
""");

View 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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>

View File

@@ -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
}

View File

@@ -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"/>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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!;
}
}

View File

@@ -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)
{

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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; }
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -9,11 +9,11 @@
mc:Ignorable="d">
<cwc:SegmentedItem
Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentBulletedList}}"
Tag="List"
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
<cwc:SegmentedItem
Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentGridView}}"
Tag="Grid"
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>

View File

@@ -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}";
}
}

View File

@@ -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>

View 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>

View File

@@ -3,8 +3,18 @@
<x:String x:Key="FontIconContentSetting">&#xE713;</x:String>
<x:String x:Key="FontIconContentRefresh">&#xE72C;</x:String>
<x:String x:Key="FontIconContentDelete">&#xE74D;</x:String>
<x:String x:Key="FontIconContentChevronRight">&#xE76C;</x:String>
<x:String x:Key="FontIconContentWarning">&#xE7BA;</x:String>
<x:String x:Key="FontIconContentGame">&#xE7FC;</x:String>
<x:String x:Key="FontIconContentOpenInNewWindow">&#xE8A7;</x:String>
<x:String x:Key="FontIconContentFolder">&#xE8B7;</x:String>
<x:String x:Key="FontIconContentCopy">&#xE8C8;</x:String>
<x:String x:Key="FontIconContentBulletedList">&#xE8FD;</x:String>
<x:String x:Key="FontIconContentCheckList">&#xE9D5;</x:String>
<x:String x:Key="FontIconContentWebsite">&#xEB41;</x:String>
<x:String x:Key="FontIconContentHomeGroup">&#xEC26;</x:String>
<x:String x:Key="FontIconContentAsteriskBadge12">&#xEDAD;</x:String>
<x:String x:Key="FontIconContentZipFolder">&#xF012;</x:String>
</ResourceDictionary>
<x:String x:Key="FontIconContentGridView">&#xF0E2;</x:String>
<x:String x:Key="FontIconContentGiftboxOpen">&#xF133;</x:String>
</ResourceDictionary>

View File

@@ -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>

View File

@@ -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>

View File

@@ -14,4 +14,10 @@
<TransitionCollection x:Key="ReorderThemeTransitions">
<ReorderThemeTransition/>
</TransitionCollection>
<TransitionCollection x:Key="RepositionThemeTransitions">
<RepositionThemeTransition/>
</TransitionCollection>
<TransitionCollection x:Key="NavigationThemeTransitions">
<NavigationThemeTransition/>
</TransitionCollection>
</ResourceDictionary>

View File

@@ -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/>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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));
}

View File

@@ -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)
{
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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
{

View File

@@ -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);
}
}

View File

@@ -7,5 +7,8 @@ namespace Snap.Hutao.Core.LifeCycle;
internal interface ICurrentWindowReference
{
/// <summary>
/// Only set in WindowController
/// </summary>
public Window Window { get; set; }
}
}

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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
}
}
}

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Threading;
/// </summary>
internal interface ITaskContext
{
IProgress<T> CreateProgressForMainThread<T>(Action<T> handler);
SynchronizationContext GetSynchronizationContext();
/// <summary>
/// 在主线程上同步等待执行操作

View File

@@ -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;
}
}

View File

@@ -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*)&current);
UnregisterForCurrentWindow();
RegisterForCurrentWindow();
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -3,7 +3,7 @@
using Windows.Storage.Pickers;
namespace Snap.Hutao.Factory.Abstraction;
namespace Snap.Hutao.Factory.Picker;
/// <summary>
/// 文件选择器工厂

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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());
}
}

View 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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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");

View 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();
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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,
};
}
}

View File

@@ -14,12 +14,17 @@ internal enum Arkhe
None,
/// <summary>
///
///
/// </summary>
Ousia,
/// <summary>
///
///
/// </summary>
Pneuma,
/// <summary>
/// 圣俗杂座
/// </summary>
Furina,
}

View File

@@ -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,
}

View File

@@ -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>
/// 检查该角色是否为主角

View File

@@ -19,6 +19,8 @@ internal static class MonsterRelationship
5071U => 507U, // 幻形花鼠 · 水 (强化)
5102U => 510U, // 历经百战的浊水粉碎幻灵
5112U => 511U, // 历经百战的浊水喷吐幻灵
30605U => 30603U, // 历经百战的霜剑律从
30606U => 30604U, // 历经百战的幽风铃兰
60402U => 60401U, // (火)岩龙蜥
60403U => 60401U, // (冰)岩龙蜥
60404U => 60401U, // (雷)岩龙蜥

View File

@@ -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]

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 B

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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))
{

View File

@@ -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
{

View 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;
}
}

View File

@@ -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}"),
};

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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