mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
427 Commits
activity-c
...
fix/win10_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f8f2870ae | ||
|
|
7aa4696ba5 | ||
|
|
cd91af8ae9 | ||
|
|
67f6fda900 | ||
|
|
db4b0d3dcb | ||
|
|
ec0abb4b16 | ||
|
|
8cdf2b01f2 | ||
|
|
c1bf3dad52 | ||
|
|
492e867391 | ||
|
|
9cbb36f3e6 | ||
|
|
e15c06a194 | ||
|
|
f71a2a3d40 | ||
|
|
fb89e5f9f0 | ||
|
|
3f97f4207c | ||
|
|
a4ea798ad0 | ||
|
|
80a0daf02d | ||
|
|
c511d27b2f | ||
|
|
c5dcbc9c79 | ||
|
|
91949214ba | ||
|
|
a126a330ff | ||
|
|
055846dfd6 | ||
|
|
b68462b56e | ||
|
|
9d47082f47 | ||
|
|
cd4516d9a7 | ||
|
|
f7e94fe2f2 | ||
|
|
e93802d5a5 | ||
|
|
370f2fe1f7 | ||
|
|
c6f747a89b | ||
|
|
cf431719df | ||
|
|
c36c15f9be | ||
|
|
f80a63f557 | ||
|
|
36376f5af6 | ||
|
|
2c057458a3 | ||
|
|
f0f50e0e30 | ||
|
|
b834daef93 | ||
|
|
ff10543c21 | ||
|
|
a55b25ae53 | ||
|
|
c0fbb823d4 | ||
|
|
4306af94be | ||
|
|
dfc83d4a34 | ||
|
|
c6a47eb7be | ||
|
|
7413a81ff4 | ||
|
|
24d143ea9f | ||
|
|
9f6611cd20 | ||
|
|
784c727a38 | ||
|
|
1bf517f95d | ||
|
|
8b9190d941 | ||
|
|
16e0ab56f6 | ||
|
|
b10df0bed1 | ||
|
|
c4d1f371f1 | ||
|
|
92a151441b | ||
|
|
faefc9c093 | ||
|
|
c6e6d08707 | ||
|
|
4323ced7dc | ||
|
|
8a1781b449 | ||
|
|
72aff568b3 | ||
|
|
8252e43bac | ||
|
|
f15a692f03 | ||
|
|
5868d53cca | ||
|
|
7d7c8d485e | ||
|
|
6edcf97ec9 | ||
|
|
f4593cd325 | ||
|
|
29454b188e | ||
|
|
942181561d | ||
|
|
5d6e1dad01 | ||
|
|
d52aa0d6b2 | ||
|
|
3ee729eacf | ||
|
|
d3acbcde24 | ||
|
|
38e152befd | ||
|
|
0f767f7e77 | ||
|
|
ce58e35a8f | ||
|
|
a10e9e40a2 | ||
|
|
dafd3128c2 | ||
|
|
0556373bcf | ||
|
|
e8d3a065e6 | ||
|
|
be223909d3 | ||
|
|
7da778699b | ||
|
|
5bfc790ea2 | ||
|
|
fc13b85739 | ||
|
|
df999dbf51 | ||
|
|
688562c1dd | ||
|
|
051a115f84 | ||
|
|
b3d75f9fa5 | ||
|
|
0b90bdaa42 | ||
|
|
77067d27d0 | ||
|
|
e04542606e | ||
|
|
5be958ff64 | ||
|
|
a57933388d | ||
|
|
86c6c9574b | ||
|
|
47e451df2f | ||
|
|
c8592c798b | ||
|
|
5fad960b20 | ||
|
|
44ba0a90a6 | ||
|
|
883c1ca95f | ||
|
|
1a29908e5d | ||
|
|
3e9edd2f62 | ||
|
|
f9c18d2555 | ||
|
|
b5afca256a | ||
|
|
a0e79344b1 | ||
|
|
3b8eba3bb1 | ||
|
|
08a3db7dc9 | ||
|
|
c02e7b0db3 | ||
|
|
a51ede5048 | ||
|
|
1f31c946cc | ||
|
|
7dee4a0ea5 | ||
|
|
2fdeaa2557 | ||
|
|
fbffadd546 | ||
|
|
00abfe6695 | ||
|
|
f2a4d2fa53 | ||
|
|
4246fa3d13 | ||
|
|
e0a5898b3a | ||
|
|
71ac87539f | ||
|
|
3959ce1c0a | ||
|
|
a56f382f8b | ||
|
|
04c3498b54 | ||
|
|
f304e0920f | ||
|
|
6fb276af9d | ||
|
|
4bd55c308a | ||
|
|
98a9f5fec9 | ||
|
|
0420568e73 | ||
|
|
45242ff8ce | ||
|
|
074cc1194b | ||
|
|
bd4a0f0d8e | ||
|
|
bbc2d7655c | ||
|
|
588aba1395 | ||
|
|
611469beb3 | ||
|
|
1b80f79189 | ||
|
|
a8cfb7fcc4 | ||
|
|
10445a73b4 | ||
|
|
7d00cec7c6 | ||
|
|
05674fb01a | ||
|
|
2c6682574f | ||
|
|
53a95ddcb9 | ||
|
|
bbfd5096d7 | ||
|
|
24407ecc05 | ||
|
|
ff2521c02c | ||
|
|
5954c1a0ab | ||
|
|
4dc753bf5a | ||
|
|
bd3617c15a | ||
|
|
70da292f21 | ||
|
|
97c5e7d37f | ||
|
|
388f9d5657 | ||
|
|
74e11f3823 | ||
|
|
c1305cda43 | ||
|
|
b0d5051957 | ||
|
|
6a42c36a76 | ||
|
|
7cf106ec50 | ||
|
|
f12cd63c92 | ||
|
|
c441fdb6b0 | ||
|
|
09a880525b | ||
|
|
15212d8f21 | ||
|
|
a60c4bff08 | ||
|
|
e02985926d | ||
|
|
09448b7137 | ||
|
|
6487df776a | ||
|
|
2be11c22df | ||
|
|
9ecb3d5821 | ||
|
|
ca64c3e0ef | ||
|
|
3fe726aa63 | ||
|
|
6b7ffe9fe9 | ||
|
|
5ed5729c4e | ||
|
|
9ba0066f40 | ||
|
|
9c639fbaa4 | ||
|
|
a592816661 | ||
|
|
ec9c5ebee1 | ||
|
|
9475c19b64 | ||
|
|
9bfe7f78ef | ||
|
|
88d7f0bcc7 | ||
|
|
4920da4ea2 | ||
|
|
99b2ccb33b | ||
|
|
9b94a75d6f | ||
|
|
e390ad2839 | ||
|
|
3086d59674 | ||
|
|
cb00fdbda0 | ||
|
|
1702dfcdc6 | ||
|
|
4b54b343f9 | ||
|
|
292b21a759 | ||
|
|
29e9413022 | ||
|
|
7c923aaa5e | ||
|
|
c6618be0fc | ||
|
|
45dd276b89 | ||
|
|
8a9fb38f49 | ||
|
|
1249216491 | ||
|
|
4bf3f4151e | ||
|
|
89d909b04f | ||
|
|
2cec0f5e0e | ||
|
|
bbb97cd802 | ||
|
|
67b058f126 | ||
|
|
b98611ccd9 | ||
|
|
80d6d5eb2b | ||
|
|
f682bb57e8 | ||
|
|
d8310b784f | ||
|
|
486c6eb308 | ||
|
|
c5d04e09da | ||
|
|
0629f7c4c9 | ||
|
|
b49288a98f | ||
|
|
df61aa3968 | ||
|
|
ee99d0b665 | ||
|
|
72b62aa9c6 | ||
|
|
6b031e1866 | ||
|
|
59c03c7f3b | ||
|
|
c03a96b44f | ||
|
|
d5a97903d3 | ||
|
|
7f998dc87f | ||
|
|
2367c4759d | ||
|
|
043e3f07d8 | ||
|
|
cd075c4dab | ||
|
|
65252f1f69 | ||
|
|
f5dd5f4c1d | ||
|
|
27ce55f3f7 | ||
|
|
311941bb89 | ||
|
|
07e2489cab | ||
|
|
699ac60aaf | ||
|
|
dc7bc7e35d | ||
|
|
bf67fcf3a2 | ||
|
|
5093246571 | ||
|
|
94fe192581 | ||
|
|
c679032387 | ||
|
|
d119b056c7 | ||
|
|
ab95ce8ce8 | ||
|
|
0ede5b158f | ||
|
|
6c9a98c2c9 | ||
|
|
a725fc0e9e | ||
|
|
5251dd9343 | ||
|
|
0b71053bf9 | ||
|
|
a91499171d | ||
|
|
b6c474cc12 | ||
|
|
e041def070 | ||
|
|
2e81ddf3f4 | ||
|
|
4d988e5500 | ||
|
|
36aff679c0 | ||
|
|
d490393108 | ||
|
|
1633f2c668 | ||
|
|
241122eadc | ||
|
|
339bb9292b | ||
|
|
4e27d40b76 | ||
|
|
4f42a299d1 | ||
|
|
80aad5f09e | ||
|
|
35370c392b | ||
|
|
115dae8966 | ||
|
|
795e9730b9 | ||
|
|
7dac0d6f73 | ||
|
|
7c42b7e8ed | ||
|
|
3c766f55b5 | ||
|
|
38a07caeab | ||
|
|
5fb55ea645 | ||
|
|
aba568d8c6 | ||
|
|
116bd58a81 | ||
|
|
46cc5f7f1e | ||
|
|
3e5ca2440b | ||
|
|
081a60f3f8 | ||
|
|
4a5ddd872d | ||
|
|
81e5159615 | ||
|
|
d5eec58157 | ||
|
|
0242d3d0ca | ||
|
|
3fcb4caaf1 | ||
|
|
ae2ddee6c4 | ||
|
|
089938c1c0 | ||
|
|
c515f70e95 | ||
|
|
317b68b1ce | ||
|
|
7b5a6ccae0 | ||
|
|
13108b6694 | ||
|
|
c4ff359995 | ||
|
|
70399f7a7d | ||
|
|
3d1a365079 | ||
|
|
2b6d4af5a3 | ||
|
|
80450660e4 | ||
|
|
28b09d56f3 | ||
|
|
995860463f | ||
|
|
474d06457c | ||
|
|
7534729794 | ||
|
|
167d66aa67 | ||
|
|
e20a9f3ff4 | ||
|
|
868232d98f | ||
|
|
ab1b3c02c8 | ||
|
|
8ef5dc9302 | ||
|
|
4c7e02cd5c | ||
|
|
d8e0d74be6 | ||
|
|
655f97353c | ||
|
|
a387e0dbf5 | ||
|
|
8111c1e662 | ||
|
|
9cd9193425 | ||
|
|
d531c81fa2 | ||
|
|
1d0a72493e | ||
|
|
f2b361819b | ||
|
|
41f7245a1a | ||
|
|
889e914f7f | ||
|
|
9f90ec221c | ||
|
|
8fc874fd09 | ||
|
|
f42ec1ea12 | ||
|
|
5cc3cf264c | ||
|
|
e38517a2ad | ||
|
|
cdc0fb8a82 | ||
|
|
d50a6df14c | ||
|
|
4886904530 | ||
|
|
ac34376c13 | ||
|
|
3bcee3d149 | ||
|
|
3d3b03851e | ||
|
|
dc64302424 | ||
|
|
db3a611c6e | ||
|
|
a2586b0ef2 | ||
|
|
eee84a338e | ||
|
|
be30362b52 | ||
|
|
38f36bbb82 | ||
|
|
704866b16a | ||
|
|
ca9783bc1b | ||
|
|
6e8e151fff | ||
|
|
b98dc9f5d3 | ||
|
|
206100d8ef | ||
|
|
1a74c7ca96 | ||
|
|
88528fa28d | ||
|
|
263cea9225 | ||
|
|
2879bd653a | ||
|
|
3d061e3bdb | ||
|
|
022527829e | ||
|
|
0fcf10dfa7 | ||
|
|
7fc6ecc3c3 | ||
|
|
fad5f8ada3 | ||
|
|
238c2036f6 | ||
|
|
e48f740e46 | ||
|
|
8574a3d825 | ||
|
|
08f27e8ee7 | ||
|
|
c8b3779d97 | ||
|
|
54cbd9dfb9 | ||
|
|
65517abcb4 | ||
|
|
87c3043fd9 | ||
|
|
762bc14b88 | ||
|
|
51dfc7020f | ||
|
|
47dda1bebc | ||
|
|
1a300e8b9c | ||
|
|
16e8a17614 | ||
|
|
a5c75f9465 | ||
|
|
0cb59808a1 | ||
|
|
80439ed673 | ||
|
|
d632002e4b | ||
|
|
cf6a972d55 | ||
|
|
e1a976f02d | ||
|
|
dbedc2a00d | ||
|
|
03312f1d52 | ||
|
|
47f29de9c6 | ||
|
|
708da5769a | ||
|
|
8bce2d3b28 | ||
|
|
5252e7b451 | ||
|
|
2f5d884425 | ||
|
|
5aea3695b9 | ||
|
|
462e570bc1 | ||
|
|
511d113c65 | ||
|
|
2a5e7e0136 | ||
|
|
177e7a6233 | ||
|
|
bd09b303ab | ||
|
|
5538b74939 | ||
|
|
fda289421b | ||
|
|
c0abe7f7c5 | ||
|
|
e95732d448 | ||
|
|
9e52a87d11 | ||
|
|
66bce570cc | ||
|
|
a5091134b0 | ||
|
|
59c9845ad0 | ||
|
|
2db829e1e2 | ||
|
|
1a64328270 | ||
|
|
1c847626c7 | ||
|
|
062a09c632 | ||
|
|
7433c1832a | ||
|
|
252649f28c | ||
|
|
75dda65c55 | ||
|
|
a9b165882f | ||
|
|
e41e913558 | ||
|
|
2d9125d369 | ||
|
|
2c7a6d152c | ||
|
|
0c4c509fd6 | ||
|
|
7dba08451c | ||
|
|
a55cb89fe8 | ||
|
|
4aa9557526 | ||
|
|
38577f9813 | ||
|
|
91361ddab5 | ||
|
|
5ced8f6c71 | ||
|
|
55706e68f0 | ||
|
|
63c4bb6a23 | ||
|
|
baa4b88622 | ||
|
|
de2ce0db63 | ||
|
|
6cbf8ca918 | ||
|
|
df125904a6 | ||
|
|
1bad9d0928 | ||
|
|
a762166db4 | ||
|
|
f432e5ba42 | ||
|
|
46dd4db25c | ||
|
|
2e685182e6 | ||
|
|
4f1bbd2e89 | ||
|
|
0f5b6dda16 | ||
|
|
4c38bb528f | ||
|
|
44e7f7482c | ||
|
|
8a2fa3c701 | ||
|
|
6e10819609 | ||
|
|
c1b838bd97 | ||
|
|
268c4c6c71 | ||
|
|
59dabaa5be | ||
|
|
dcac7ac792 | ||
|
|
2ad6dad282 | ||
|
|
4185336556 | ||
|
|
15a627e50a | ||
|
|
2ea53fd39d | ||
|
|
6de3cba550 | ||
|
|
03c1bacfe9 | ||
|
|
d157476a9a | ||
|
|
e35c03ac90 | ||
|
|
9619835cc2 | ||
|
|
6936a30a74 | ||
|
|
506d198167 | ||
|
|
9c2131cb9d | ||
|
|
896e3d49f2 | ||
|
|
7da24790c4 | ||
|
|
ebf163f200 | ||
|
|
26043a6a73 | ||
|
|
ad67001556 | ||
|
|
428cd35a7f | ||
|
|
1cf3450264 | ||
|
|
1849ac16da | ||
|
|
4e9d87af50 | ||
|
|
eb125e547f | ||
|
|
c17798a8c9 | ||
|
|
fa9b39d134 | ||
|
|
8dc85c7a25 | ||
|
|
5845c718e1 | ||
|
|
0c093851ed | ||
|
|
04c187aa16 | ||
|
|
25cf9f5356 | ||
|
|
c30c8b114a |
21
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
21
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
@@ -40,7 +40,7 @@ body:
|
||||
attributes:
|
||||
label: Snap Hutao 版本
|
||||
description: 在应用标题,应用程序的反馈中心界面中可以找到
|
||||
placeholder: 例:1.4.15.0
|
||||
placeholder: 例:1.9.9.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -62,20 +62,19 @@ body:
|
||||
description: 请设置一个你认为合适的分类,这将帮助我们快速定位问题
|
||||
options:
|
||||
- 安装和环境
|
||||
- 成就管理
|
||||
- 角色信息面板
|
||||
- 游戏启动器
|
||||
- 祈愿记录
|
||||
- 成就管理
|
||||
- 我的角色
|
||||
- 实时便笺
|
||||
- 养成计算
|
||||
- 文件缓存
|
||||
- 祈愿记录
|
||||
- 玩家查询
|
||||
- 胡桃数据库
|
||||
- 用户界面
|
||||
- 胡桃云
|
||||
- 胡桃帐号
|
||||
- 签到
|
||||
- 深境螺旋/胡桃数据库
|
||||
- Wiki
|
||||
- 米游社账号面板
|
||||
- 每日签到奖励
|
||||
- 胡桃通行证/胡桃云
|
||||
- 用户界面
|
||||
- 文件缓存
|
||||
- 公告
|
||||
- 其它
|
||||
validations:
|
||||
|
||||
19
.github/ISSUE_TEMPLATE/ENG-bug-report.yml
vendored
19
.github/ISSUE_TEMPLATE/ENG-bug-report.yml
vendored
@@ -40,7 +40,7 @@ body:
|
||||
attributes:
|
||||
label: Snap Hutao Version
|
||||
description: You can find the version in application's title bar
|
||||
placeholder: e.g. 1.4.15.0
|
||||
placeholder: e.g. 1.9.9.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -62,20 +62,19 @@ body:
|
||||
description: Please select the most associated category of your issue
|
||||
options:
|
||||
- Installation and Environment
|
||||
- Game Launcher
|
||||
- Wish Export
|
||||
- Achievement
|
||||
- My Character
|
||||
- Game Launcher
|
||||
- Realtime Note
|
||||
- Develop Plan
|
||||
- File Cache
|
||||
- Wish Export
|
||||
- Game Record
|
||||
- Hutao Database
|
||||
- User Interface
|
||||
- Snap Hutao Cloud
|
||||
- Snap Hutao Account
|
||||
- Checkin
|
||||
- Spiral Abyss
|
||||
- Wiki
|
||||
- MiHoYo Account Panel
|
||||
- Daily Checkin Reward
|
||||
- Hutao Passport/Hutao Cloud
|
||||
- User Interface
|
||||
- File Cache
|
||||
- Announcement
|
||||
- Other
|
||||
validations:
|
||||
|
||||
15
.github/pull_request_template.md
vendored
Normal file
15
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<!--- Hi, thanks for considering make a PR contribution to Snap Hutao, we appreciate your work. -->
|
||||
<!--- Before you create this PR, please fill the following form and checklist -->
|
||||
|
||||
## Description
|
||||
|
||||
<!--- Describe your changes -->
|
||||
|
||||
## Related Issue
|
||||
|
||||
<!--- If there's an associated issue, please use [GitHub Keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests) to link it -->
|
||||
<!-- e.g. fix #999, resolve #999, close #999 -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] The target PR branch is `develop` branch
|
||||
12
.github/workflows/alpha.yml
vendored
12
.github/workflows/alpha.yml
vendored
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- 'feat/*'
|
||||
paths-ignore:
|
||||
- '.gitattributes'
|
||||
- '.github/**'
|
||||
@@ -28,7 +29,14 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: self-hosted
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
runner:
|
||||
- self-hosted
|
||||
- windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -73,7 +81,7 @@ jobs:
|
||||
> [!IMPORTANT]
|
||||
> 请注意,从 Snap Hutao Alpha 2023.12.21.3 开始,我们将使用全新的 CI 证书,原有的 Snap.Hutao.CI.cer 将在几天后过期停止使用。
|
||||
>
|
||||
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 以安装测试版安装包
|
||||
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 到 `受信任的根证书颁发机构` 以安装测试版安装包
|
||||
"
|
||||
|
||||
echo $summary >> $Env:GITHUB_STEP_SUMMARY
|
||||
|
||||
2
.github/workflows/close_stale.yml
vendored
2
.github/workflows/close_stale.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
any-of-labels: 'needs-more-info,需要更多信息'
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 3 days.'
|
||||
stale-issue-message: 'This issue is stale because it has been open 7 days with no activity. Remove stale label or comment or this will be closed in 3 days.'
|
||||
days-before-stale: 7
|
||||
days-before-close: 3
|
||||
close-issue-reason: not_planned
|
||||
49
README.md
49
README.md
@@ -1,41 +1,44 @@
|
||||

|
||||

|
||||
|
||||
|
||||
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新 功能相结合,它提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
|
||||
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新功能相结合,提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
|
||||
|
||||
Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed for modern Windows platform to improve the gaming experience for desktop players. By combining existing official resources with new features designed by the development team, it provides a complete and useful set of tools without the need to rely on mobile devices. Snap Hutao does not take any destructive modification to the game client to ensure the security of the toolkit.
|
||||
|
||||
## 下载使用 / Download
|
||||
## 安装 / Installation
|
||||
|
||||
 [](https://github.com/DGP-Studio/Snap.Hutao/releases/latest) []()
|
||||
|
||||
---
|
||||
|
||||
#### 使用安装器安装 / Install with Snap.Hutao.Depolyment Installer
|
||||
你可以按照[快速开始](https://hut.ao/zh/quick-start.html)文档中提供的流程安装并设置 Snap Hutao。
|
||||
|
||||
Snap.Hutao.Depolyment 是一个由 DGP-Studio 重新包装的 Windows 应用安装器,适用于缺少专业计算机知识的一般用户,可以在安装时同时解决缺少必要系统环境的问题。
|
||||
You can follow the instructions in the [Quick Start](https://hut.ao/en/quick-start.html) document to install and set up Snap Hutao.
|
||||
|
||||
Snap.Hutao.Depolyment is a Windows application installer repackaged by DGP-Studio for the users who lacks computer knowledge and can solve the problem of missing necessary system environment at the same time as the installation.
|
||||
## 本地化翻译 / Localization
|
||||
|
||||
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao.Deployment/releases/latest)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
|
||||
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao.Deployment/-/releases)
|
||||
Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。
|
||||
|
||||
#### 使用 MSIX 包安装 / Install with MSIX Package
|
||||
Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translation platform where you can submit translated text for languages you are familiar with. We are grateful to every community member who has contributed to Snap Hutao and welcome more friends to participate in this project.
|
||||
|
||||
直接使用 Snap Hutao MSIX 安装包,使用 Windows 内置的 App Installer 即可安装。如在安装中出现问题,请查阅我们的[常见问题](https://hut.ao/zh/advanced/FAQ.html)文档
|
||||
## 社区 / Community
|
||||
|
||||
Install with Snap Hutao MSIX package, can be installed with Windows built-in App Installer. If you faced any issue, please check our [FAQ](https://hut.ao/en/advanced/FAQ.html) document.
|
||||
|
||||
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao/releases/latest)
|
||||
|
||||
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao/-/releases)
|
||||
[](https://discord.gg/CcH5XtDtvR) [](https://qm.qq.com/q/WJKykrY9W)
|
||||
|
||||
## 贡献 / Contribute
|
||||
|
||||
* [向我们提交 PR / Make Pull Requests](https://github.com/DGP-Studio/Snap.Hutao/pulls)
|
||||
* [在 Crowdin 上进行本地化 / Translate Project on Crowdin](https://translate.hut.ao/)
|
||||
* [为我们更新文档 / Enhance our Document ](https://github.com/DGP-Studio/Snap.Hutao.Docs)
|
||||
* [向我们提交 PR / Make Pull Requests](https://hut.ao/development/contribute.html)
|
||||
* [为我们更新文档 / Enhance our Document](https://github.com/DGP-Studio/Snap.Hutao.Docs)
|
||||
|
||||
## 特别感谢 / Special Thanks
|
||||
|
||||
@@ -51,13 +54,13 @@ Install with Snap Hutao MSIX package, can be installed with Windows built-in App
|
||||
* [CommunityToolkit/dotnet](https://github.com/CommunityToolkit/dotnet)
|
||||
* [CommunityToolkit/Labs-Windows](https://github.com/CommunityToolkit/Labs-Windows)
|
||||
* [CommunityToolkit/Windows](https://github.com/CommunityToolkit/Windows)
|
||||
* [dahall/taskscheduler](https://github.com/dahall/taskscheduler)
|
||||
* [dotnet/efcore](https://github.com/dotnet/efcore)
|
||||
* [dotnet/runtime](https://github.com/dotnet/runtime)
|
||||
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
|
||||
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
|
||||
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
|
||||
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
|
||||
* [quartznet/quartznet](https://github.com/quartznet/quartznet)
|
||||
|
||||
### 支撑项目 / Supporter Project
|
||||
|
||||
@@ -69,9 +72,9 @@ Install with Snap Hutao MSIX package, can be installed with Windows built-in App
|
||||
Snap Hutao is currently using sponsored software from the following service providers.
|
||||
|
||||
| [](https://www.netlify.com/) | [](https://crowdin.com/) | [](https://gitlab.cn/) |
|
||||
|:----------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|
|
||||
| [](https://about.signpath.io) | [](https://1password.com/) | [](https://about.signpath.io) |
|
||||
|
||||
|:-:|:-:|:-:|
|
||||
|[](https://about.signpath.io)|[](https://1password.com/)|[](https://www.digitalocean.com)|
|
||||
|[](https://www.jetbrains.com/opensource/)|||
|
||||
|
||||
- Netlify provides document and home page hosting service for Snap Hutao
|
||||
|
||||
@@ -85,6 +88,8 @@ Snap Hutao is currently using sponsored software from the following service prov
|
||||
|
||||
- DigitalOcean provides reliable cloud database for Snap Hutao database backup
|
||||
|
||||
- Jetbrains provides powerful IDE for Snap Hutao infrastructure services coding
|
||||
|
||||
## 开发 / Development
|
||||
|
||||

|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| >=1.6.0 | :white_check_mark: |
|
||||
| <1.6.0 | :x: |
|
||||
| >=1.9.0 | :white_check_mark: |
|
||||
| <1.9.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
34
build.cake
34
build.cake
@@ -11,6 +11,15 @@ var version = "version";
|
||||
var repoDir = "repoDir";
|
||||
var outputPath = "outputPath";
|
||||
|
||||
// Extension
|
||||
|
||||
static ProcessArgumentBuilder AppendIf(this ProcessArgumentBuilder builder, string text, bool condition)
|
||||
{
|
||||
return condition ? builder.Append(text) : builder;
|
||||
}
|
||||
|
||||
// Properties
|
||||
|
||||
string solution
|
||||
{
|
||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao.sln");
|
||||
@@ -69,6 +78,15 @@ else if (AppVeyor.IsRunningOnAppVeyor)
|
||||
})[..^2];
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
else // Local
|
||||
{
|
||||
repoDir = System.Environment.CurrentDirectory;
|
||||
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
|
||||
|
||||
version = System.DateTime.Now.ToString("yyyy.M.d.") + ((int)((System.DateTime.Now - System.DateTime.Today).TotalSeconds / 86400 * 65535)).ToString();
|
||||
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
|
||||
Task("Build")
|
||||
.IsDependentOn("Build binary package")
|
||||
@@ -112,6 +130,17 @@ Task("Generate AppxManifest")
|
||||
Information("Using Release configuration");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
Information("Using Local configuration.");
|
||||
content = content
|
||||
.Replace("Snap Hutao", "Snap Hutao Local")
|
||||
.Replace("胡桃", "胡桃 Local")
|
||||
.Replace("DGP Studio", "DGP Studio CI");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"E8B6E2B3-D2A0-4435-A81D-2A16AAF405C7\"");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"E=admin@dgp-studio.cn, CN=DGP Studio CI, OU=CI, O=DGP-Studio, L=San Jose, S=CA, C=US\"");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\"");
|
||||
}
|
||||
|
||||
System.IO.File.WriteAllText(manifest, content);
|
||||
|
||||
@@ -137,6 +166,7 @@ Task("Build binary package")
|
||||
.Append("/p:AppxPackageSigningEnabled=false")
|
||||
.Append("/p:AppxBundle=Never")
|
||||
.Append("/p:AppxPackageOutput=" + outputPath)
|
||||
.AppendIf("/p:AlphaConstants=IS_ALPHA_BUILD", !AppVeyor.IsRunningOnAppVeyor)
|
||||
};
|
||||
|
||||
DotNetBuild(project, settings);
|
||||
@@ -173,6 +203,10 @@ Task("Build MSIX")
|
||||
{
|
||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao-{version}.msix");
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Local-{version}.msix");
|
||||
}
|
||||
var p = StartProcess(
|
||||
"makeappx.exe",
|
||||
new ProcessSettings
|
||||
|
||||
BIN
res/Banner3-large-cn.psd
Normal file
BIN
res/Banner3-large-cn.psd
Normal file
Binary file not shown.
BIN
res/Banner3-large.psd
Normal file
BIN
res/Banner3-large.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/abyss.psd
Normal file
BIN
res/Store/chs/abyss.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/achievement.psd
Normal file
BIN
res/Store/chs/achievement.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/character-data.psd
Normal file
BIN
res/Store/chs/character-data.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/lancher.psd
Normal file
BIN
res/Store/chs/lancher.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/realtime-notes.psd
Normal file
BIN
res/Store/chs/realtime-notes.psd
Normal file
Binary file not shown.
BIN
res/Store/chs/wish.psd
Normal file
BIN
res/Store/chs/wish.psd
Normal file
Binary file not shown.
BIN
res/Store/en/abyss.psd
Normal file
BIN
res/Store/en/abyss.psd
Normal file
Binary file not shown.
BIN
res/Store/en/achievement.psd
Normal file
BIN
res/Store/en/achievement.psd
Normal file
Binary file not shown.
BIN
res/Store/en/character-data.psd
Normal file
BIN
res/Store/en/character-data.psd
Normal file
Binary file not shown.
BIN
res/Store/en/lancher.psd
Normal file
BIN
res/Store/en/lancher.psd
Normal file
Binary file not shown.
BIN
res/Store/en/realtime-notes.psd
Normal file
BIN
res/Store/en/realtime-notes.psd
Normal file
Binary file not shown.
BIN
res/Store/en/wish.psd
Normal file
BIN
res/Store/en/wish.psd
Normal file
Binary file not shown.
@@ -110,7 +110,6 @@ dotnet_diagnostic.SA1642.severity = none
|
||||
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
dotnet_diagnostic.IDE0060.severity = none
|
||||
dotnet_diagnostic.IDE0290.severity = none
|
||||
|
||||
# SA1208: System using directives should be placed before other using directives
|
||||
dotnet_diagnostic.SA1208.severity = none
|
||||
@@ -321,7 +320,8 @@ dotnet_diagnostic.CA2227.severity = suggestion
|
||||
|
||||
# CA2251: 使用 “string.Equals”
|
||||
dotnet_diagnostic.CA2251.severity = suggestion
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
|
||||
csharp_style_prefer_primary_constructors = false:none
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public class UnsafeAccessorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void UnsafeAccessorCanGetInterfaceProperty()
|
||||
{
|
||||
TestClass test = new();
|
||||
int value = InternalGetInterfaceProperty(test);
|
||||
Assert.AreEqual(3, value);
|
||||
}
|
||||
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_TestProperty")]
|
||||
private static extern int InternalGetInterfaceProperty(ITestInterface instance);
|
||||
|
||||
internal interface ITestInterface
|
||||
{
|
||||
internal int TestProperty { get; }
|
||||
}
|
||||
|
||||
internal sealed class TestClass : ITestInterface
|
||||
{
|
||||
public int TestProperty { get; } = 3;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.Test.PlatformExtensions;
|
||||
|
||||
@@ -10,7 +12,10 @@ public sealed class DependencyInjectionTest
|
||||
.AddSingleton<IService, ServiceA>()
|
||||
.AddSingleton<IService, ServiceB>()
|
||||
.AddScoped<IScopedService, ServiceA>()
|
||||
.AddKeyedTransient<IKeyedService, KeyedServiceA>("A")
|
||||
.AddKeyedTransient<IKeyedService, KeyedServiceB>("B")
|
||||
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
|
||||
.AddLogging(builder => builder.AddConsole())
|
||||
.BuildServiceProvider();
|
||||
|
||||
[TestMethod]
|
||||
@@ -41,6 +46,22 @@ public sealed class DependencyInjectionTest
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LoggerWithInterfaceTypeCanBeResolved()
|
||||
{
|
||||
Assert.IsNotNull(services.GetService<ILogger<IScopedService>>());
|
||||
Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void KeyedServicesCanBeResolvedAsEnumerable()
|
||||
{
|
||||
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("A"));
|
||||
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("B"));
|
||||
|
||||
Assert.AreEqual(0, services.GetServices<IKeyedService>().Count());
|
||||
}
|
||||
|
||||
private interface IService
|
||||
{
|
||||
Guid Id { get; }
|
||||
@@ -86,4 +107,14 @@ public sealed class DependencyInjectionTest
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private interface IKeyedService;
|
||||
|
||||
private sealed class KeyedServiceA : IKeyedService
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class KeyedServiceB : IKeyedService
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
@@ -46,7 +47,14 @@ public sealed class UnsafeRuntimeBehaviorTest
|
||||
Assert.AreEqual(1212, testStruct.Value4);
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void UnsafeUtf8StringReference()
|
||||
{
|
||||
void* ptr = Unsafe.AsPointer(ref MemoryMarshal.GetReference("test"u8));
|
||||
GC.Collect(GC.MaxGeneration);
|
||||
ReadOnlySpan<byte> bytes = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr);
|
||||
Console.WriteLine(System.Text.Encoding.UTF8.GetString(bytes));
|
||||
}
|
||||
|
||||
private readonly struct TestStruct
|
||||
{
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources/>
|
||||
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsCard/SettingsCard.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.TokenizingTextBox/TokenizingTextBox.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Loading.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Image/CachedImage.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Card.xaml"/>
|
||||
@@ -22,11 +24,13 @@
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/PageOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/SegmentedOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/SettingsStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Thickness.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///View/Card/Primitive/CardProgressBar.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<Style
|
||||
|
||||
@@ -7,7 +7,8 @@ using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||
using Snap.Hutao.Core.Shell;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
@@ -21,24 +22,24 @@ namespace Snap.Hutao;
|
||||
[SuppressMessage("", "SH001")]
|
||||
public sealed partial class App : Application
|
||||
{
|
||||
private const string ConsoleBanner = """
|
||||
private const string ConsoleBanner = $"""
|
||||
----------------------------------------------------------------
|
||||
_____ _ _ _
|
||||
/ ____| | | | | | |
|
||||
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
|
||||
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
|
||||
_____ _ _ _
|
||||
/ ____| | | | | | |
|
||||
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
|
||||
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
|
||||
____) || | | || (_| || |_) |_ | | | || |_| || |_| (_| || (_) |
|
||||
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
|
||||
| |
|
||||
|_|
|
||||
|
||||
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
|
||||
| |
|
||||
|_|
|
||||
|
||||
Snap.Hutao is a open source software developed by DGP Studio.
|
||||
Copyright (C) 2022 - 2024 DGP Studio, All Rights Reserved.
|
||||
----------------------------------------------------------------
|
||||
""";
|
||||
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IActivation activation;
|
||||
private readonly IAppActivation activation;
|
||||
private readonly ILogger<App> logger;
|
||||
|
||||
/// <summary>
|
||||
@@ -49,13 +50,19 @@ public sealed partial class App : Application
|
||||
{
|
||||
// Load app resource
|
||||
InitializeComponent();
|
||||
activation = serviceProvider.GetRequiredService<IActivation>();
|
||||
activation = serviceProvider.GetRequiredService<IAppActivation>();
|
||||
logger = serviceProvider.GetRequiredService<ILogger<App>>();
|
||||
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public new void Exit()
|
||||
{
|
||||
XamlWindowLifetime.ApplicationExiting = true;
|
||||
base.Exit();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
@@ -69,18 +76,16 @@ public sealed partial class App : Application
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation(ConsoleBanner);
|
||||
logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow));
|
||||
LogDiagnosticInformation();
|
||||
|
||||
// manually invoke
|
||||
// Manually invoke
|
||||
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
|
||||
activation.Initialize();
|
||||
|
||||
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
|
||||
activation.PostInitialization();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// AppInstance.GetCurrent() calls failed
|
||||
Debug.WriteLine(ex);
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
}
|
||||
@@ -89,8 +94,8 @@ public sealed partial class App : Application
|
||||
{
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
|
||||
logger.LogInformation("FamilyName: {name}", runtimeOptions.FamilyName);
|
||||
logger.LogInformation("Version: {version}", runtimeOptions.Version);
|
||||
logger.LogInformation("LocalCache: {folder}", runtimeOptions.LocalCache);
|
||||
logger.LogColorizedInformation(("FamilyName: {Name}", ConsoleColor.Blue), (runtimeOptions.FamilyName, ConsoleColor.Cyan));
|
||||
logger.LogColorizedInformation(("Version: {Version}", ConsoleColor.Blue), (runtimeOptions.Version, ConsoleColor.Cyan));
|
||||
logger.LogColorizedInformation(("LocalCache: {Path}", ConsoleColor.Blue), (runtimeOptions.LocalCache, ConsoleColor.Cyan));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using System.Collections;
|
||||
|
||||
namespace Snap.Hutao.Control.AutoSuggestBox;
|
||||
|
||||
[DependencyProperty("FilterCommand", typeof(ICommand))]
|
||||
[DependencyProperty("FilterCommandParameter", typeof(object))]
|
||||
[DependencyProperty("AvailableTokens", typeof(IReadOnlyDictionary<string, SearchToken>))]
|
||||
internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox
|
||||
{
|
||||
public AutoSuggestTokenBox()
|
||||
{
|
||||
DefaultStyleKey = typeof(TokenizingTextBox);
|
||||
TextChanged += OnFilterSuggestionRequested;
|
||||
QuerySubmitted += OnQuerySubmitted;
|
||||
TokenItemAdding += OnTokenItemAdding;
|
||||
TokenItemAdded += OnTokenItemCollectionChanged;
|
||||
TokenItemRemoved += OnTokenItemCollectionChanged;
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (this.FindDescendant("SuggestionsPopup") is Popup { Child: Border { Child: ListView listView } border })
|
||||
{
|
||||
IAppResourceProvider appResourceProvider = this.ServiceProvider().GetRequiredService<IAppResourceProvider>();
|
||||
|
||||
listView.Background = null;
|
||||
listView.Margin = appResourceProvider.GetResource<Thickness>("AutoSuggestListPadding");
|
||||
|
||||
border.Background = appResourceProvider.GetResource<Microsoft.UI.Xaml.Media.Brush>("AutoSuggestBoxSuggestionsListBackground");
|
||||
CornerRadius overlayCornerRadius = appResourceProvider.GetResource<CornerRadius>("OverlayCornerRadius");
|
||||
CornerRadiusFilterConverter cornerRadiusFilterConverter = new() { Filter = CornerRadiusFilterKind.Bottom };
|
||||
border.CornerRadius = (CornerRadius)cornerRadiusFilterConverter.Convert(overlayCornerRadius, typeof(CornerRadius), default, default);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFilterSuggestionRequested(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Text))
|
||||
{
|
||||
sender.ItemsSource = AvailableTokens
|
||||
.OrderBy(kvp => kvp.Value.Kind)
|
||||
.Select(kvp => kvp.Value);
|
||||
}
|
||||
|
||||
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
|
||||
{
|
||||
sender.ItemsSource = AvailableTokens
|
||||
.Where(kvp => kvp.Value.Value.Contains(Text, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(kvp => kvp.Value.Kind)
|
||||
.ThenBy(kvp => kvp.Value.Order)
|
||||
.Select(kvp => kvp.Value)
|
||||
.DefaultIfEmpty(SearchToken.NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnQuerySubmitted(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
|
||||
{
|
||||
if (args.ChosenSuggestion is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CommandInvocation.TryExecute(FilterCommand, FilterCommandParameter);
|
||||
}
|
||||
|
||||
private void OnTokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(args.TokenText))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (AvailableTokens.GetValueOrDefault(args.TokenText) is { } token)
|
||||
{
|
||||
args.Item = token;
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTokenItemCollectionChanged(TokenizingTextBox sender, object args)
|
||||
{
|
||||
if (args is SearchToken { Kind: SearchTokenKind.None } token)
|
||||
{
|
||||
((IList)sender.ItemsSource).Remove(token);
|
||||
}
|
||||
|
||||
FilterCommand.TryExecute(FilterCommandParameter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.AutoSuggestBox;
|
||||
|
||||
internal sealed class SearchToken
|
||||
{
|
||||
public static readonly SearchToken NotFound = new(SearchTokenKind.None, SH.ControlAutoSuggestBoxNotFoundValue, 0);
|
||||
|
||||
public SearchToken(SearchTokenKind kind, string value, int order, Uri? iconUri = null, Uri? sideIconUri = null, Color? quality = null)
|
||||
{
|
||||
Value = value;
|
||||
Kind = kind;
|
||||
IconUri = iconUri;
|
||||
SideIconUri = sideIconUri;
|
||||
Quality = quality;
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public SearchTokenKind Kind { get; }
|
||||
|
||||
public string Value { get; set; } = default!;
|
||||
|
||||
public Uri? IconUri { get; }
|
||||
|
||||
public Uri? SideIconUri { get; }
|
||||
|
||||
public Color? Quality { get; }
|
||||
|
||||
public int Order { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.AutoSuggestBox;
|
||||
|
||||
internal enum SearchTokenKind
|
||||
{
|
||||
None,
|
||||
ItemQuality,
|
||||
WeaponType,
|
||||
FightProperty,
|
||||
ElementName,
|
||||
AssociationType,
|
||||
BodyType,
|
||||
Avatar,
|
||||
Weapon,
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
@@ -45,10 +46,6 @@ internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIEle
|
||||
return;
|
||||
}
|
||||
|
||||
if (Command is not null && Command.CanExecute(CommandParameter))
|
||||
{
|
||||
Command.Execute(CommandParameter);
|
||||
executed = true;
|
||||
}
|
||||
executed = Command.TryExecute(CommandParameter);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
@@ -32,6 +33,7 @@ internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavio
|
||||
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
periodicTimerCancellationTokenSource.Cancel();
|
||||
AssociatedObject.ActualThemeChanged -= OnActualThemeChanged;
|
||||
return true;
|
||||
}
|
||||
@@ -49,10 +51,7 @@ internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavio
|
||||
return;
|
||||
}
|
||||
|
||||
if (Command is not null && Command.CanExecute(CommandParameter))
|
||||
{
|
||||
Command.Execute(CommandParameter);
|
||||
}
|
||||
Command.TryExecute(CommandParameter);
|
||||
}
|
||||
|
||||
private async ValueTask RunCoreAsync()
|
||||
|
||||
@@ -13,6 +13,4 @@ namespace Snap.Hutao.Control;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("DataContext", typeof(object))]
|
||||
internal sealed partial class BindingProxy : DependencyObject
|
||||
{
|
||||
}
|
||||
internal sealed partial class BindingProxy : DependencyObject;
|
||||
@@ -5,7 +5,6 @@ using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Shapes;
|
||||
using System.Collections.Specialized;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Control.Brush;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal class ButtonBaseBuilder<TButton> : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase, new()
|
||||
{
|
||||
public TButton Button { get; } = new();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction.Extension;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal static class ButtonBaseBuilderExtension
|
||||
{
|
||||
public static TBuilder SetContent<TBuilder, TButton>(this TBuilder builder, object? content)
|
||||
where TBuilder : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
builder.Configure(builder => builder.Button.Content = content);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder SetCommand<TBuilder, TButton>(this TBuilder builder, ICommand command)
|
||||
where TBuilder : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
builder.Configure(builder => builder.Button.Command = command);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal sealed class ButtonBuilder : ButtonBaseBuilder<Button>;
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal static class ButtonBuilderExtension
|
||||
{
|
||||
public static ButtonBuilder SetContent(this ButtonBuilder builder, object? content)
|
||||
{
|
||||
return builder.SetContent<ButtonBuilder, Button>(content);
|
||||
}
|
||||
|
||||
public static ButtonBuilder SetCommand(this ButtonBuilder builder, ICommand command)
|
||||
{
|
||||
return builder.SetCommand<ButtonBuilder, Button>(command);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal interface IButtonBaseBuilder<TButton> : IBuilder
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
TButton Button { get; }
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
using CommunityToolkit.WinUI.Collections;
|
||||
using CommunityToolkit.WinUI.Helpers;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
@@ -31,7 +30,7 @@ internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, IN
|
||||
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
|
||||
|
||||
public AdvancedCollectionView()
|
||||
: this(new List<T>(0))
|
||||
: this([])
|
||||
{
|
||||
}
|
||||
|
||||
@@ -208,13 +207,11 @@ internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, IN
|
||||
|
||||
public void Add(T item)
|
||||
{
|
||||
ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only.");
|
||||
source.Add(item);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only.");
|
||||
source.Clear();
|
||||
}
|
||||
|
||||
@@ -230,7 +227,6 @@ internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, IN
|
||||
|
||||
public bool Remove(T item)
|
||||
{
|
||||
ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only.");
|
||||
source.Remove(item);
|
||||
return true;
|
||||
}
|
||||
@@ -243,7 +239,6 @@ internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, IN
|
||||
|
||||
public void Insert(int index, T item)
|
||||
{
|
||||
ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only.");
|
||||
source.Insert(index, item);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
|
||||
|
||||
internal sealed class VectorChangedEventArgs : IVectorChangedEventArgs
|
||||
{
|
||||
public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = null!)
|
||||
public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = default!)
|
||||
{
|
||||
CollectionChange = cc;
|
||||
Index = (uint)index;
|
||||
|
||||
@@ -6,6 +6,7 @@ using Windows.Foundation.Collections;
|
||||
|
||||
namespace Snap.Hutao.Control.Collection.Alternating;
|
||||
|
||||
[Obsolete("Use SettingsCard instead")]
|
||||
[DependencyProperty("ItemAlternateBackground", typeof(Microsoft.UI.Xaml.Media.Brush))]
|
||||
internal sealed partial class AlternatingItemsControl : ItemsControl
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
namespace Snap.Hutao.Control.Collection.Alternating;
|
||||
|
||||
[Obsolete("Use SettingsCard instead")]
|
||||
internal interface IAlternatingItem
|
||||
{
|
||||
public Microsoft.UI.Xaml.Media.Brush? Background { get; set; }
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
@@ -40,6 +41,6 @@ internal abstract class DependencyValueConverter<TFrom, TTo> : DependencyObject,
|
||||
/// <returns>源</returns>
|
||||
public virtual TFrom ConvertBack(TTo to)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
throw HutaoException.NotSupported();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Extension;
|
||||
|
||||
internal static class CommandInvocation
|
||||
{
|
||||
public static bool TryExecute(this ICommand? command, object? parameter = null)
|
||||
{
|
||||
if (command is not null && command.CanExecute(parameter))
|
||||
{
|
||||
command.Execute(parameter);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Control.Extension;
|
||||
|
||||
internal static class DependencyObjectExtension
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IServiceProvider ServiceProvider(this DependencyObject obj)
|
||||
{
|
||||
return Ioc.Default;
|
||||
|
||||
@@ -28,4 +28,20 @@ internal static class FrameworkElementExtension
|
||||
frameworkElement.IsRightTapEnabled = false;
|
||||
frameworkElement.IsTabStop = false;
|
||||
}
|
||||
|
||||
public static void InitializeDataContext<TDataContext>(this FrameworkElement frameworkElement, IServiceProvider? serviceProvider = default)
|
||||
where TDataContext : class
|
||||
{
|
||||
IServiceProvider service = serviceProvider ?? Ioc.Default;
|
||||
try
|
||||
{
|
||||
frameworkElement.DataContext = service.GetRequiredService<TDataContext>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ILogger? logger = service.GetRequiredService(typeof(ILogger<>).MakeGenericType([frameworkElement.GetType()])) as ILogger;
|
||||
logger?.LogError(ex, "Failed to initialize DataContext");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ namespace Snap.Hutao.Control.Helper;
|
||||
|
||||
[SuppressMessage("", "SH001")]
|
||||
[DependencyProperty("SquareLength", typeof(double), 0D, nameof(OnSquareLengthChanged), IsAttached = true, AttachedType = typeof(FrameworkElement))]
|
||||
[DependencyProperty("IsActualThemeBindingEnabled", typeof(bool), false, nameof(OnIsActualThemeBindingEnabled), IsAttached = true, AttachedType = typeof(FrameworkElement))]
|
||||
[DependencyProperty("ActualTheme", typeof(ElementTheme), ElementTheme.Default, IsAttached = true, AttachedType = typeof(FrameworkElement))]
|
||||
public sealed partial class FrameworkElementHelper
|
||||
{
|
||||
private static void OnSquareLengthChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
|
||||
@@ -15,4 +17,22 @@ public sealed partial class FrameworkElementHelper
|
||||
element.Width = (double)e.NewValue;
|
||||
element.Height = (double)e.NewValue;
|
||||
}
|
||||
|
||||
private static void OnIsActualThemeBindingEnabled(DependencyObject dp, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
FrameworkElement element = (FrameworkElement)dp;
|
||||
if ((bool)e.NewValue)
|
||||
{
|
||||
element.ActualThemeChanged += OnActualThemeChanged;
|
||||
}
|
||||
else
|
||||
{
|
||||
element.ActualThemeChanged -= OnActualThemeChanged;
|
||||
}
|
||||
|
||||
static void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
SetActualTheme(sender, sender.ActualTheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Snap.Hutao/Snap.Hutao/Control/Helper/UIElementHelper.cs
Normal file
24
src/Snap.Hutao/Snap.Hutao/Control/Helper/UIElementHelper.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Helper;
|
||||
|
||||
[SuppressMessage("", "SH001")]
|
||||
[DependencyProperty("VisibilityObject", typeof(object), null, nameof(OnVisibilityObjectChanged), IsAttached = true, AttachedType = typeof(UIElement))]
|
||||
[DependencyProperty("OpacityObject", typeof(object), null, nameof(OnOpacityObjectChanged), IsAttached = true, AttachedType = typeof(UIElement))]
|
||||
public sealed partial class UIElementHelper
|
||||
{
|
||||
private static void OnVisibilityObjectChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
UIElement element = (UIElement)dp;
|
||||
element.Visibility = e.NewValue is null ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
private static void OnOpacityObjectChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
UIElement element = (UIElement)dp;
|
||||
element.Opacity = e.NewValue is null ? 0D : 1D;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
internal interface IXamlElementAccessor;
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
@@ -19,22 +19,21 @@ internal sealed class CachedImage : Implementation.ImageEx
|
||||
/// </summary>
|
||||
public CachedImage()
|
||||
{
|
||||
IsCacheEnabled = true;
|
||||
EnableLazyLoading = false;
|
||||
DefaultStyleKey = typeof(CachedImage);
|
||||
DefaultStyleResourceUri = "ms-appx:///Control/Image/CachedImage.xaml".ToUri();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
protected override async Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
// We can only use Ioc to retrieve IImageCache, no IServiceProvider is available.
|
||||
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
|
||||
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
|
||||
|
||||
try
|
||||
{
|
||||
Verify.Operation(!string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
|
||||
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
|
||||
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
|
||||
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
|
||||
return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.
|
||||
return file.ToUri();
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}"/>
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="LazyLoadingThreshold" Value="256"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="shci:CachedImage">
|
||||
|
||||
@@ -168,6 +168,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
if (surface.DecodedPhysicalSize.Size() <= 0D)
|
||||
{
|
||||
await Task.WhenAny(surfaceLoadTaskCompletionSource.Task, Task.Delay(5000, token)).ConfigureAwait(true);
|
||||
await Task.Delay(50, token).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
LoadImageSurfaceCompleted(surface);
|
||||
|
||||
@@ -7,21 +7,14 @@ using Windows.Media.Casting;
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
internal class ImageEx : ImageExBase
|
||||
[DependencyProperty("NineGrid", typeof(Thickness))]
|
||||
internal partial class ImageEx : ImageExBase
|
||||
{
|
||||
private static readonly DependencyProperty NineGridProperty = DependencyProperty.Register(nameof(NineGrid), typeof(Thickness), typeof(ImageEx), new PropertyMetadata(default(Thickness)));
|
||||
|
||||
public ImageEx()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
|
||||
public Thickness NineGrid
|
||||
{
|
||||
get => (Thickness)GetValue(NineGridProperty);
|
||||
set => SetValue(NineGridProperty, value);
|
||||
}
|
||||
|
||||
public override CompositionBrush GetAlphaMask()
|
||||
{
|
||||
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
|
||||
@@ -6,8 +6,6 @@ using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using System.IO;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
@@ -20,12 +18,6 @@ namespace Snap.Hutao.Control.Image.Implementation;
|
||||
[TemplatePart(Name = PartImage, Type = typeof(object))]
|
||||
[TemplatePart(Name = PartPlaceholderImage, Type = typeof(object))]
|
||||
[DependencyProperty("Stretch", typeof(Stretch), Stretch.Uniform)]
|
||||
[DependencyProperty("DecodePixelHeight", typeof(int), 0)]
|
||||
[DependencyProperty("DecodePixelWidth", typeof(int), 0)]
|
||||
[DependencyProperty("DecodePixelType", typeof(DecodePixelType), DecodePixelType.Physical)]
|
||||
[DependencyProperty("IsCacheEnabled", typeof(bool), false)]
|
||||
[DependencyProperty("EnableLazyLoading", typeof(bool), false, nameof(EnableLazyLoadingChanged))]
|
||||
[DependencyProperty("LazyLoadingThreshold", typeof(double), default(double), nameof(LazyLoadingThresholdChanged))]
|
||||
[DependencyProperty("PlaceholderSource", typeof(object), default(object))]
|
||||
[DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)]
|
||||
[DependencyProperty("PlaceholderMargin", typeof(Thickness))]
|
||||
@@ -41,8 +33,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
protected const string FailedState = "Failed";
|
||||
|
||||
private CancellationTokenSource? tokenSource;
|
||||
private object? lazyLoadingSource;
|
||||
private bool isInViewport;
|
||||
|
||||
public bool IsInitialized { get; private set; }
|
||||
|
||||
@@ -57,10 +47,10 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
public abstract CompositionBrush GetAlphaMask();
|
||||
|
||||
protected virtual Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
protected virtual Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
// By default we just use the built-in UWP image cache provided within the Image control.
|
||||
return Task.FromResult<ImageSource?>(new BitmapImage(imageUri));
|
||||
return Task.FromResult<Uri?>(imageUri);
|
||||
}
|
||||
|
||||
protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
|
||||
@@ -79,19 +69,10 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
RemoveImageFailed(OnImageFailed);
|
||||
|
||||
Image = GetTemplateChild(PartImage);
|
||||
PlaceholderImage = GetTemplateChild(PartPlaceholderImage);
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
if (Source is null || !EnableLazyLoading || isInViewport)
|
||||
{
|
||||
lazyLoadingSource = null;
|
||||
SetSource(Source);
|
||||
}
|
||||
else
|
||||
{
|
||||
lazyLoadingSource = Source;
|
||||
}
|
||||
SetSource(Source);
|
||||
|
||||
AttachImageOpened(OnImageOpened);
|
||||
AttachImageFailed(OnImageFailed);
|
||||
@@ -147,34 +128,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnableLazyLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not ImageExBase control)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool value = (bool)e.NewValue;
|
||||
if (value)
|
||||
{
|
||||
control.LayoutUpdated += control.OnImageExBaseLayoutUpdated;
|
||||
|
||||
control.InvalidateLazyLoading();
|
||||
}
|
||||
else
|
||||
{
|
||||
control.LayoutUpdated -= control.OnImageExBaseLayoutUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
private static void LazyLoadingThresholdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ImageExBase control && control.EnableLazyLoading)
|
||||
{
|
||||
control.InvalidateLazyLoading();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not ImageExBase control)
|
||||
@@ -187,15 +140,7 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
|
||||
{
|
||||
control.lazyLoadingSource = null;
|
||||
control.SetSource(e.NewValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
control.lazyLoadingSource = e.NewValue;
|
||||
}
|
||||
control.SetSource(e.NewValue);
|
||||
}
|
||||
|
||||
private static bool IsHttpUri(Uri uri)
|
||||
@@ -203,11 +148,8 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https");
|
||||
}
|
||||
|
||||
private void AttachSource(ImageSource? source)
|
||||
private void AttachSource(BitmapImage? source, Uri? uri)
|
||||
{
|
||||
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
|
||||
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
|
||||
// We only need to call those methods if we fail in other cases before we get here.
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.Source = source;
|
||||
@@ -221,17 +163,16 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
{
|
||||
VisualStateManager.GoToState(this, UnloadedState, true);
|
||||
}
|
||||
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
|
||||
else
|
||||
{
|
||||
// https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-animations-and-media#optimize-image-resources
|
||||
source.UriSource = uri;
|
||||
VisualStateManager.GoToState(this, LoadedState, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void AttachPlaceholderSource(ImageSource? source)
|
||||
private void AttachPlaceholderSource(BitmapImage? source, Uri? uri)
|
||||
{
|
||||
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
|
||||
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
|
||||
// We only need to call those methods if we fail in other cases before we get here.
|
||||
if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.Source = source;
|
||||
@@ -240,6 +181,17 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
{
|
||||
brush.ImageSource = source;
|
||||
}
|
||||
|
||||
if (source is null)
|
||||
{
|
||||
VisualStateManager.GoToState(this, UnloadedState, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-animations-and-media#optimize-image-resources
|
||||
source.UriSource = uri;
|
||||
VisualStateManager.GoToState(this, LoadedState, true);
|
||||
}
|
||||
}
|
||||
|
||||
private async void SetSource(object? source)
|
||||
@@ -250,10 +202,9 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
|
||||
tokenSource?.Cancel();
|
||||
|
||||
tokenSource = new CancellationTokenSource();
|
||||
|
||||
AttachSource(null);
|
||||
AttachSource(default, default);
|
||||
|
||||
if (source is null)
|
||||
{
|
||||
@@ -262,13 +213,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
|
||||
VisualStateManager.GoToState(this, LoadingState, true);
|
||||
|
||||
if (source as ImageSource is { } imageSource)
|
||||
{
|
||||
AttachSource(imageSource);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (source as Uri is not { } uri)
|
||||
{
|
||||
string? url = source as string ?? source.ToString();
|
||||
@@ -311,23 +255,15 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
}
|
||||
|
||||
tokenSource?.Cancel();
|
||||
tokenSource = new();
|
||||
|
||||
tokenSource = new CancellationTokenSource();
|
||||
|
||||
AttachPlaceholderSource(null);
|
||||
AttachPlaceholderSource(default, default);
|
||||
|
||||
if (source is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (source as ImageSource is { } imageSource)
|
||||
{
|
||||
AttachPlaceholderSource(imageSource);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (source as Uri is not { } uri)
|
||||
{
|
||||
string? url = source as string ?? source.ToString();
|
||||
@@ -349,13 +285,13 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
return;
|
||||
}
|
||||
|
||||
ImageSource? img = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
|
||||
Uri? actualUri = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
// Only attach our image if we still have a valid request.
|
||||
AttachPlaceholderSource(img);
|
||||
AttachPlaceholderSource(new BitmapImage(), actualUri);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -374,98 +310,13 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsCacheEnabled)
|
||||
Uri? actualUri = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
// Only attach our image if we still have a valid request.
|
||||
AttachSource(img);
|
||||
}
|
||||
}
|
||||
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string source = imageUri.OriginalString;
|
||||
const string base64Head = "base64,";
|
||||
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
|
||||
BitmapImage bitmap = new();
|
||||
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
AttachSource(bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AttachSource(new BitmapImage(imageUri)
|
||||
{
|
||||
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnImageExBaseLayoutUpdated(object? sender, object e)
|
||||
{
|
||||
InvalidateLazyLoading();
|
||||
}
|
||||
|
||||
private void InvalidateLazyLoading()
|
||||
{
|
||||
if (!IsLoaded)
|
||||
{
|
||||
isInViewport = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the first ascendant ScrollViewer, if not found, use the root element.
|
||||
FrameworkElement? hostElement = default;
|
||||
IEnumerable<FrameworkElement> ascendants = this.FindAscendants().OfType<FrameworkElement>();
|
||||
foreach (FrameworkElement ascendant in ascendants)
|
||||
{
|
||||
hostElement = ascendant;
|
||||
if (hostElement is Microsoft.UI.Xaml.Controls.ScrollViewer)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hostElement is null)
|
||||
{
|
||||
isInViewport = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Rect controlRect = TransformToVisual(hostElement)
|
||||
.TransformBounds(new Rect(0, 0, ActualWidth, ActualHeight));
|
||||
double lazyLoadingThreshold = LazyLoadingThreshold;
|
||||
Rect hostRect = new(
|
||||
0 - lazyLoadingThreshold,
|
||||
0 - lazyLoadingThreshold,
|
||||
hostElement.ActualWidth + (2 * lazyLoadingThreshold),
|
||||
hostElement.ActualHeight + (2 * lazyLoadingThreshold));
|
||||
|
||||
if (controlRect.IntersectsWith(hostRect))
|
||||
{
|
||||
isInViewport = true;
|
||||
|
||||
if (lazyLoadingSource is not null)
|
||||
{
|
||||
object source = lazyLoadingSource;
|
||||
lazyLoadingSource = null;
|
||||
SetSource(source);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
isInViewport = false;
|
||||
// Only attach our image if we still have a valid request.
|
||||
AttachSource(new BitmapImage(), actualUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using System.Numerics;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class DefaultItemCollectionTransitionProvider : ItemCollectionTransitionProvider
|
||||
{
|
||||
private const double DefaultAnimationDurationInMs = 300.0;
|
||||
|
||||
static DefaultItemCollectionTransitionProvider()
|
||||
{
|
||||
AnimationSlowdownFactor = 1.0;
|
||||
}
|
||||
|
||||
public static double AnimationSlowdownFactor { get; set; }
|
||||
|
||||
protected override bool ShouldAnimateCore(ItemCollectionTransition transition)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void StartTransitions(IList<ItemCollectionTransition> transitions)
|
||||
{
|
||||
List<ItemCollectionTransition> addTransitions = [];
|
||||
List<ItemCollectionTransition> removeTransitions = [];
|
||||
List<ItemCollectionTransition> moveTransitions = [];
|
||||
|
||||
foreach (ItemCollectionTransition transition in addTransitions)
|
||||
{
|
||||
switch (transition.Operation)
|
||||
{
|
||||
case ItemCollectionTransitionOperation.Add:
|
||||
addTransitions.Add(transition);
|
||||
break;
|
||||
case ItemCollectionTransitionOperation.Remove:
|
||||
removeTransitions.Add(transition);
|
||||
break;
|
||||
case ItemCollectionTransitionOperation.Move:
|
||||
moveTransitions.Add(transition);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
StartAddTransitions(addTransitions, removeTransitions.Count > 0, moveTransitions.Count > 0);
|
||||
StartRemoveTransitions(removeTransitions);
|
||||
StartMoveTransitions(moveTransitions, removeTransitions.Count > 0);
|
||||
}
|
||||
|
||||
private static void StartAddTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveTransitions, bool hasMoveTransitions)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
|
||||
ScalarKeyFrameAnimation fadeInAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
fadeInAnimation.InsertKeyFrame(0.0f, 0.0f);
|
||||
|
||||
if (hasMoveTransitions && hasRemoveTransitions)
|
||||
{
|
||||
fadeInAnimation.InsertKeyFrame(0.66f, 0.0f);
|
||||
}
|
||||
else if (hasMoveTransitions || hasRemoveTransitions)
|
||||
{
|
||||
fadeInAnimation.InsertKeyFrame(0.5f, 0.0f);
|
||||
}
|
||||
|
||||
fadeInAnimation.InsertKeyFrame(1.0f, 1.0f);
|
||||
fadeInAnimation.Duration = TimeSpan.FromMilliseconds(
|
||||
DefaultAnimationDurationInMs * ((hasRemoveTransitions ? 1 : 0) + (hasMoveTransitions ? 1 : 0) + 1) * AnimationSlowdownFactor);
|
||||
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
visual.StartAnimation("Opacity", fadeInAnimation);
|
||||
batch.End();
|
||||
batch.Completed += (_, _) => progress.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private static void StartRemoveTransitions(IList<ItemCollectionTransition> transitions)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
|
||||
ScalarKeyFrameAnimation fadeOutAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
fadeOutAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue");
|
||||
fadeOutAnimation.InsertKeyFrame(1.0f, 0.0f);
|
||||
fadeOutAnimation.Duration = TimeSpan.FromMilliseconds(DefaultAnimationDurationInMs * AnimationSlowdownFactor);
|
||||
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
visual.StartAnimation(nameof(Visual.Opacity), fadeOutAnimation);
|
||||
batch.End();
|
||||
batch.Completed += (_, _) =>
|
||||
{
|
||||
visual.Opacity = 1.0f;
|
||||
progress.Complete();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void StartMoveTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveAnimations)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
|
||||
// Animate offset.
|
||||
if (transition.OldBounds.X != transition.NewBounds.X ||
|
||||
transition.OldBounds.Y != transition.NewBounds.Y)
|
||||
{
|
||||
AnimateOffset(visual, compositor, transition.OldBounds, transition.NewBounds, hasRemoveAnimations);
|
||||
}
|
||||
|
||||
batch.End();
|
||||
batch.Completed += (_, _) => progress.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AnimateOffset(Visual visual, Compositor compositor, Rect oldBounds, Rect newBounds, bool hasRemoveAnimations)
|
||||
{
|
||||
Vector2KeyFrameAnimation offsetAnimation = compositor.CreateVector2KeyFrameAnimation();
|
||||
|
||||
offsetAnimation.SetVector2Parameter("delta", new Vector2(
|
||||
(float)(oldBounds.X - newBounds.X),
|
||||
(float)(oldBounds.Y - newBounds.Y)));
|
||||
offsetAnimation.SetVector2Parameter("final", default);
|
||||
offsetAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue + delta");
|
||||
if (hasRemoveAnimations)
|
||||
{
|
||||
offsetAnimation.InsertExpressionKeyFrame(0.5f, "delta");
|
||||
}
|
||||
|
||||
offsetAnimation.InsertExpressionKeyFrame(1.0f, "final");
|
||||
offsetAnimation.Duration = TimeSpan.FromMilliseconds(
|
||||
DefaultAnimationDurationInMs * ((hasRemoveAnimations ? 1 : 0) + 1) * AnimationSlowdownFactor);
|
||||
|
||||
visual.StartAnimation("TransformMatrix._41_42", offsetAnimation);
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,12 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
|
||||
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/>
|
||||
@@ -65,12 +63,12 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
|
||||
/// <inheritdoc/>
|
||||
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
|
||||
{
|
||||
if (context.ItemCount == 0)
|
||||
if (context.ItemCount is 0)
|
||||
{
|
||||
return new Size(availableSize.Width, 0);
|
||||
}
|
||||
|
||||
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
|
||||
if ((context.RealizationRect.Width is 0) && (context.RealizationRect.Height is 0))
|
||||
{
|
||||
return new Size(availableSize.Width, 0.0f);
|
||||
}
|
||||
@@ -82,16 +80,10 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
|
||||
|
||||
(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));
|
||||
double totalWidth = ((state.ColumnWidth + MinColumnSpacing) * numberOfColumns) - MinColumnSpacing;
|
||||
|
||||
if (totalWidth > availableWidth)
|
||||
{
|
||||
numberOfColumns--;
|
||||
@@ -103,7 +95,6 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
|
||||
|
||||
if (numberOfColumns != state.NumberOfColumns)
|
||||
{
|
||||
// The items will not need to be remeasured, but they will need to go into new columns
|
||||
state.ClearColumns();
|
||||
}
|
||||
|
||||
@@ -170,7 +161,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
|
||||
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
|
||||
// this item changed size; we need to recalculate layout for everything after this item
|
||||
state.RemoveFromIndex(i + 1);
|
||||
item.Height = item.Element.DesiredSize.Height;
|
||||
columnHeights[columnIndex] = item.Top + item.Height;
|
||||
@@ -201,16 +192,16 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
|
||||
// Cycle through each column and arrange the items that are within the realization bounds
|
||||
for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
|
||||
{
|
||||
UniformStaggeredColumnLayout layout = state.GetColumnLayout(columnIndex);
|
||||
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(layout))
|
||||
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(state.GetColumnLayout(columnIndex)))
|
||||
{
|
||||
double bottom = item.Top + item.Height;
|
||||
if (bottom < context.RealizationRect.Top)
|
||||
{
|
||||
// element is above the realization bounds
|
||||
// Element is above the realization bounds
|
||||
continue;
|
||||
}
|
||||
|
||||
// Partial or fully in the view
|
||||
if (item.Top <= context.RealizationRect.Bottom)
|
||||
{
|
||||
double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex);
|
||||
@@ -229,21 +220,22 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double minColumnSpacing)
|
||||
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double columnSpacing)
|
||||
{
|
||||
// test if the width can fit in 2 items
|
||||
if ((2 * minItemWidth) + minColumnSpacing > availableWidth)
|
||||
if ((2 * minItemWidth) + columnSpacing > availableWidth)
|
||||
{
|
||||
return (1, availableWidth);
|
||||
}
|
||||
|
||||
int columnCount = Math.Max(1, (int)((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing)));
|
||||
double columnWidthAddSpacing = (availableWidth + minColumnSpacing) / columnCount;
|
||||
return (columnCount, columnWidthAddSpacing - minColumnSpacing);
|
||||
int columnCount = Math.Max(1, (int)((availableWidth + columnSpacing) / (minItemWidth + columnSpacing)));
|
||||
double columnWidthWithSpacing = (availableWidth + columnSpacing) / columnCount;
|
||||
return (columnCount, columnWidthWithSpacing - columnSpacing);
|
||||
}
|
||||
|
||||
private static int GetLowestColumnIndex(in ReadOnlySpan<double> columnHeights)
|
||||
{
|
||||
// We want to find the leftest column with the lowest height
|
||||
int columnIndex = 0;
|
||||
double height = columnHeights[0];
|
||||
for (int j = 1; j < columnHeights.Length; j++)
|
||||
@@ -260,13 +252,11 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
|
||||
|
||||
private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
|
||||
panel.InvalidateMeasure();
|
||||
((UniformStaggeredLayout)d).InvalidateMeasure();
|
||||
}
|
||||
|
||||
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
|
||||
panel.InvalidateMeasure();
|
||||
((UniformStaggeredLayout)d).InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// 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;
|
||||
|
||||
@@ -67,46 +66,6 @@ internal sealed class UniformStaggeredLayoutState
|
||||
return columnLayout[columnIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear everything that has been calculated.
|
||||
/// </summary>
|
||||
internal void Clear()
|
||||
{
|
||||
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
|
||||
// The first element must be force refreshed otherwise
|
||||
// it will use the old one realized
|
||||
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
|
||||
// Now we need to refresh the first element of each column
|
||||
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
|
||||
// Finally we need to refresh the whole layout when we reset
|
||||
if (context.ItemCount > 0)
|
||||
{
|
||||
for (int i = 0; i < context.ItemCount; i++)
|
||||
{
|
||||
RecycleElementAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
columnLayout.Clear();
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the layout columns so they will be recalculated.
|
||||
/// </summary>
|
||||
internal void ClearColumns()
|
||||
{
|
||||
columnLayout.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the estimated height of the layout.
|
||||
/// </summary>
|
||||
/// <returns>The estimated height of the layout.</returns>
|
||||
/// <remarks>
|
||||
/// If all of the items have been calculated then the actual height will be returned.
|
||||
/// If all of the items have not been calculated then an estimated height will be calculated based on the average height of the items.
|
||||
/// </remarks>
|
||||
internal double GetHeight()
|
||||
{
|
||||
double desiredHeight = columnLayout.Values.Max(c => c.Height);
|
||||
@@ -139,10 +98,37 @@ internal sealed class UniformStaggeredLayoutState
|
||||
return desiredHeight;
|
||||
}
|
||||
|
||||
internal void Clear()
|
||||
{
|
||||
RecycleElements();
|
||||
ClearColumns();
|
||||
ClearItems();
|
||||
}
|
||||
|
||||
internal void ClearColumns()
|
||||
{
|
||||
columnLayout.Clear();
|
||||
}
|
||||
|
||||
internal void ClearItems()
|
||||
{
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
internal void RecycleElements()
|
||||
{
|
||||
if (context.ItemCount > 0)
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
RecycleElementAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void RecycleElementAt(int index)
|
||||
{
|
||||
UIElement element = context.GetOrCreateElementAt(index);
|
||||
context.RecycleElement(element);
|
||||
context.RecycleElement(context.GetOrCreateElementAt(index));
|
||||
}
|
||||
|
||||
internal void RemoveFromIndex(int index)
|
||||
@@ -175,7 +161,7 @@ internal sealed class UniformStaggeredLayoutState
|
||||
{
|
||||
for (int i = startIndex; i <= endIndex; i++)
|
||||
{
|
||||
if (i > items.Count)
|
||||
if (i >= items.Count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -184,7 +170,7 @@ internal sealed class UniformStaggeredLayoutState
|
||||
item.Height = 0;
|
||||
item.Top = 0;
|
||||
|
||||
// We must recycle all elements to ensure that it gets the correct context
|
||||
// We must recycle all removed elements to ensure that it gets the correct context
|
||||
RecycleElementAt(i);
|
||||
}
|
||||
|
||||
|
||||
25
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapItem.cs
Normal file
25
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapItem.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// Licensed to the .NET Fou// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class WrapItem
|
||||
{
|
||||
public WrapItem(int index)
|
||||
{
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public static Point EmptyPosition { get; } = new(float.NegativeInfinity, float.NegativeInfinity);
|
||||
|
||||
public int Index { get; }
|
||||
|
||||
public Size Size { get; set; } = Size.Empty;
|
||||
|
||||
public Point Position { get; set; } = EmptyPosition;
|
||||
|
||||
public UIElement? Element { get; set; }
|
||||
}
|
||||
220
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayout.cs
Normal file
220
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayout.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
// 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 Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
[DependencyProperty("HorizontalSpacing", typeof(double), 0D, nameof(LayoutPropertyChanged))]
|
||||
[DependencyProperty("VerticalSpacing", typeof(double), 0D, nameof(LayoutPropertyChanged))]
|
||||
internal sealed partial class WrapLayout : VirtualizingLayout
|
||||
{
|
||||
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
|
||||
{
|
||||
context.LayoutState = new WrapLayoutState(context);
|
||||
}
|
||||
|
||||
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
|
||||
{
|
||||
context.LayoutState = default;
|
||||
}
|
||||
|
||||
protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
state.RemoveFromIndex(args.NewStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
|
||||
state.RemoveFromIndex(minIndex);
|
||||
state.RecycleElementAt(args.OldStartingIndex);
|
||||
state.RecycleElementAt(args.NewStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
state.RemoveFromIndex(args.OldStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
state.RemoveFromIndex(args.NewStartingIndex);
|
||||
state.RecycleElementAt(args.NewStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
state.Clear();
|
||||
break;
|
||||
}
|
||||
|
||||
base.OnItemsChangedCore(context, source, args);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
|
||||
{
|
||||
if (context.ItemCount is 0)
|
||||
{
|
||||
return new Size(availableSize.Width, 0);
|
||||
}
|
||||
|
||||
if ((context.RealizationRect.Width is 0) && (context.RealizationRect.Height is 0))
|
||||
{
|
||||
return new Size(availableSize.Width, 0.0f);
|
||||
}
|
||||
|
||||
Size spacing = new(HorizontalSpacing, VerticalSpacing);
|
||||
|
||||
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
|
||||
|
||||
if (spacing != state.Spacing || state.AvailableWidth != availableSize.Width)
|
||||
{
|
||||
state.ClearPositions();
|
||||
state.Spacing = spacing;
|
||||
state.AvailableWidth = availableSize.Width;
|
||||
}
|
||||
|
||||
double currentHeight = 0;
|
||||
Point itemPosition = default;
|
||||
for (int i = 0; i < context.ItemCount; ++i)
|
||||
{
|
||||
bool itemMeasured = false;
|
||||
WrapItem item = state.GetItemAt(i);
|
||||
if (item.Size == Size.Empty)
|
||||
{
|
||||
item.Element = context.GetOrCreateElementAt(i);
|
||||
item.Element.Measure(availableSize);
|
||||
item.Size = item.Element.DesiredSize;
|
||||
itemMeasured = true;
|
||||
}
|
||||
|
||||
Size itemSize = item.Size;
|
||||
|
||||
if (item.Position == WrapItem.EmptyPosition)
|
||||
{
|
||||
if (availableSize.Width < itemPosition.X + itemSize.Width)
|
||||
{
|
||||
// New Row
|
||||
itemPosition.X = 0;
|
||||
itemPosition.Y += currentHeight + spacing.Height;
|
||||
currentHeight = 0;
|
||||
}
|
||||
|
||||
item.Position = itemPosition;
|
||||
}
|
||||
|
||||
itemPosition = item.Position;
|
||||
|
||||
double bottom = itemPosition.Y + itemSize.Height;
|
||||
if (bottom < context.RealizationRect.Top)
|
||||
{
|
||||
// Item is "above" the bounds
|
||||
if (item.Element is not null)
|
||||
{
|
||||
context.RecycleElement(item.Element);
|
||||
item.Element = default;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (itemPosition.Y > context.RealizationRect.Bottom)
|
||||
{
|
||||
// Item is "below" the bounds.
|
||||
if (item.Element is not null)
|
||||
{
|
||||
context.RecycleElement(item.Element);
|
||||
item.Element = default;
|
||||
}
|
||||
|
||||
// We don't need to measure anything below the bounds
|
||||
break;
|
||||
}
|
||||
else if (!itemMeasured)
|
||||
{
|
||||
// Always measure elements that are within the bounds
|
||||
item.Element = context.GetOrCreateElementAt(i);
|
||||
item.Element.Measure(availableSize);
|
||||
|
||||
itemSize = item.Element.DesiredSize;
|
||||
if (itemSize != item.Size)
|
||||
{
|
||||
// this item changed size; we need to recalculate layout for everything after this
|
||||
state.RemoveFromIndex(i + 1);
|
||||
item.Size = itemSize;
|
||||
|
||||
// did the change make it go into the new row?
|
||||
if (availableSize.Width < itemPosition.X + itemSize.Width)
|
||||
{
|
||||
// New Row
|
||||
itemPosition.X = 0;
|
||||
itemPosition.Y += currentHeight + spacing.Height;
|
||||
currentHeight = 0;
|
||||
}
|
||||
|
||||
item.Position = itemPosition;
|
||||
}
|
||||
}
|
||||
|
||||
itemPosition.X += itemSize.Width + spacing.Width;
|
||||
currentHeight = Math.Max(itemSize.Height, currentHeight);
|
||||
}
|
||||
|
||||
return new Size(double.IsInfinity(availableSize.Width) ? 0 : Math.Ceiling(availableSize.Width), state.GetHeight());
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
|
||||
{
|
||||
if (context.ItemCount > 0)
|
||||
{
|
||||
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
|
||||
|
||||
for (int i = 0; i < context.ItemCount; ++i)
|
||||
{
|
||||
if (!ArrangeItem(context, state.GetItemAt(i)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
|
||||
static bool ArrangeItem(VirtualizingLayoutContext context, WrapItem item)
|
||||
{
|
||||
if (item.Size == Size.Empty || item.Position == WrapItem.EmptyPosition)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Size size = item.Size;
|
||||
Point position = item.Position;
|
||||
|
||||
if (context.RealizationRect.Top <= position.Y + size.Height && position.Y <= context.RealizationRect.Bottom)
|
||||
{
|
||||
// place the item
|
||||
UIElement child = context.GetOrCreateElementAt(item.Index);
|
||||
child.Arrange(new Rect(position, size));
|
||||
}
|
||||
else if (position.Y > context.RealizationRect.Bottom)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WrapLayout layout)
|
||||
{
|
||||
layout.InvalidateMeasure();
|
||||
layout.InvalidateArrange();
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayoutState.cs
Normal file
109
src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayoutState.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class WrapLayoutState
|
||||
{
|
||||
private readonly List<WrapItem> items = [];
|
||||
private readonly VirtualizingLayoutContext context;
|
||||
|
||||
public WrapLayoutState(VirtualizingLayoutContext context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public Orientation Orientation { get; private set; }
|
||||
|
||||
public Size Spacing { get; set; }
|
||||
|
||||
public double AvailableWidth { get; set; }
|
||||
|
||||
public WrapItem GetItemAt(int index)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index);
|
||||
|
||||
if (index <= (items.Count - 1))
|
||||
{
|
||||
return items[index];
|
||||
}
|
||||
else
|
||||
{
|
||||
WrapItem item = new(index);
|
||||
items.Add(item);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
RecycleElementAt(i);
|
||||
}
|
||||
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
public 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);
|
||||
}
|
||||
|
||||
public void ClearPositions()
|
||||
{
|
||||
foreach (ref readonly WrapItem item in CollectionsMarshal.AsSpan(items))
|
||||
{
|
||||
item.Position = WrapItem.EmptyPosition;
|
||||
}
|
||||
}
|
||||
|
||||
public double GetHeight()
|
||||
{
|
||||
if (items.Count is 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
Point? lastPosition = default;
|
||||
double maxHeight = 0;
|
||||
|
||||
Span<WrapItem> itemSpan = CollectionsMarshal.AsSpan(items);
|
||||
for (int i = items.Count - 1; i >= 0; --i)
|
||||
{
|
||||
ref readonly WrapItem item = ref itemSpan[i];
|
||||
|
||||
if (item.Position == WrapItem.EmptyPosition || item.Size == Size.Empty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastPosition is not null && lastPosition.Value.Y > item.Position.Y)
|
||||
{
|
||||
// This is a row above the last item.
|
||||
break;
|
||||
}
|
||||
|
||||
lastPosition = item.Position;
|
||||
maxHeight = Math.Max(maxHeight, item.Size.Height);
|
||||
}
|
||||
|
||||
return lastPosition?.Y + maxHeight ?? 0;
|
||||
}
|
||||
|
||||
public void RecycleElementAt(int index)
|
||||
{
|
||||
context.RecycleElement(context.GetOrCreateElementAt(index));
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
|
||||
public Loading()
|
||||
{
|
||||
DefaultStyleKey = typeof(Loading);
|
||||
DefaultStyleResourceUri = new("ms-appx:///Control/Loading.xaml");
|
||||
DefaultStyleResourceUri = "ms-appx:///Control/Loading.xaml".ToUri();
|
||||
}
|
||||
|
||||
public bool IsLoading
|
||||
|
||||
@@ -12,7 +12,6 @@ internal sealed class UInt32Extension : MarkupExtension
|
||||
|
||||
protected override object ProvideValue()
|
||||
{
|
||||
_ = uint.TryParse(Value, out uint result);
|
||||
return result;
|
||||
return XamlBindingHelper.ConvertValue(typeof(uint), Value);
|
||||
}
|
||||
}
|
||||
@@ -8,45 +8,19 @@ using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
|
||||
/// <summary>
|
||||
/// RGBA 颜色
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal struct Rgba32
|
||||
{
|
||||
/// <summary>
|
||||
/// R
|
||||
/// </summary>
|
||||
public byte R;
|
||||
|
||||
/// <summary>
|
||||
/// G
|
||||
/// </summary>
|
||||
public byte G;
|
||||
|
||||
/// <summary>
|
||||
/// B
|
||||
/// </summary>
|
||||
public byte B;
|
||||
|
||||
/// <summary>
|
||||
/// A
|
||||
/// </summary>
|
||||
public byte A;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的 RGBA8 颜色
|
||||
/// </summary>
|
||||
/// <param name="hex">色值字符串</param>
|
||||
public Rgba32(string hex)
|
||||
: this(hex.Length == 6 ? Convert.ToUInt32($"{hex}FF", 16) : Convert.ToUInt32(hex, 16))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 RGBA 代码初始化新的结构
|
||||
/// </summary>
|
||||
/// <param name="xrgbaCode">RGBA 代码</param>
|
||||
public unsafe Rgba32(uint xrgbaCode)
|
||||
{
|
||||
// uint layout: 0xRRGGBBAA is AABBGGRR
|
||||
@@ -80,11 +54,6 @@ internal struct Rgba32
|
||||
return *(Color*)&rgba;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 HSL 颜色转换
|
||||
/// </summary>
|
||||
/// <param name="hsl">HSL 颜色</param>
|
||||
/// <returns>RGBA8颜色</returns>
|
||||
public static Rgba32 FromHsl(Hsla32 hsl)
|
||||
{
|
||||
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
|
||||
@@ -138,10 +107,6 @@ internal struct Rgba32
|
||||
return new(r, g, b, a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到 HSL 颜色
|
||||
/// </summary>
|
||||
/// <returns>HSL 颜色</returns>
|
||||
public readonly Hsla32 ToHsl()
|
||||
{
|
||||
const double toDouble = 1.0 / 255;
|
||||
|
||||
14
src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba64.cs
Normal file
14
src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba64.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
// Some part of this file came from:
|
||||
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
|
||||
internal struct Rgba64
|
||||
{
|
||||
public Half R;
|
||||
public Half G;
|
||||
public Half B;
|
||||
public Half A;
|
||||
}
|
||||
@@ -39,24 +39,6 @@ internal static class SoftwareBitmapExtension
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe double Luminance(this SoftwareBitmap softwareBitmap)
|
||||
{
|
||||
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
|
||||
{
|
||||
using (IMemoryBufferReference reference = buffer.CreateReference())
|
||||
{
|
||||
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
|
||||
double sum = 0;
|
||||
foreach (ref readonly Bgra32 pixel in bytes)
|
||||
{
|
||||
sum += pixel.Luminance;
|
||||
}
|
||||
|
||||
return sum / bytes.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe Bgra32 GetAccentColor(this SoftwareBitmap softwareBitmap)
|
||||
{
|
||||
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
|
||||
|
||||
89
src/Snap.Hutao/Snap.Hutao/Control/Panel/EqualPanel.cs
Normal file
89
src/Snap.Hutao/Snap.Hutao/Control/Panel/EqualPanel.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using System.Data;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
[DependencyProperty("Spacing", typeof(double), default(double), nameof(OnSpacingChanged))]
|
||||
internal partial class EqualPanel : Microsoft.UI.Xaml.Controls.Panel
|
||||
{
|
||||
private double maxItemWidth;
|
||||
private double maxItemHeight;
|
||||
private int visibleItemsCount;
|
||||
|
||||
public EqualPanel()
|
||||
{
|
||||
RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnHorizontalAlignmentChanged);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
maxItemWidth = 0;
|
||||
maxItemHeight = 0;
|
||||
|
||||
List<UIElement> elements = [.. Children.Where(element => element.Visibility == Visibility.Visible)];
|
||||
visibleItemsCount = elements.Count;
|
||||
|
||||
foreach (ref readonly UIElement child in CollectionsMarshal.AsSpan(elements))
|
||||
{
|
||||
child.Measure(availableSize);
|
||||
maxItemWidth = Math.Max(maxItemWidth, child.DesiredSize.Width);
|
||||
maxItemHeight = Math.Max(maxItemHeight, child.DesiredSize.Height);
|
||||
}
|
||||
|
||||
if (visibleItemsCount > 0)
|
||||
{
|
||||
// Return equal widths based on the widest item
|
||||
// In very specific edge cases the AvailableWidth might be infinite resulting in a crash.
|
||||
if (HorizontalAlignment is not HorizontalAlignment.Stretch || double.IsInfinity(availableSize.Width))
|
||||
{
|
||||
return new Size((maxItemWidth * visibleItemsCount) + (Spacing * (visibleItemsCount - 1)), maxItemHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Equal columns based on the available width, adjust for spacing
|
||||
double totalWidth = availableSize.Width - (Spacing * (visibleItemsCount - 1));
|
||||
maxItemWidth = totalWidth / visibleItemsCount;
|
||||
return new Size(availableSize.Width, maxItemHeight);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Size(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
double x = 0;
|
||||
|
||||
// Check if there's more (little) width available - if so, set max item width to the maximum possible as we have an almost perfect height.
|
||||
if (finalSize.Width > (visibleItemsCount * maxItemWidth) + (Spacing * (visibleItemsCount - 1)))
|
||||
{
|
||||
maxItemWidth = (finalSize.Width - (Spacing * (visibleItemsCount - 1))) / visibleItemsCount;
|
||||
}
|
||||
|
||||
IEnumerable<UIElement> elements = Children.Where(static e => e.Visibility == Visibility.Visible);
|
||||
foreach (UIElement child in elements)
|
||||
{
|
||||
child.Arrange(new Rect(x, 0, maxItemWidth, maxItemHeight));
|
||||
x += maxItemWidth + Spacing;
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
(d as EqualPanel)?.InvalidateMeasure();
|
||||
}
|
||||
|
||||
private static void OnHorizontalAlignmentChanged(DependencyObject d, DependencyProperty dp)
|
||||
{
|
||||
(d as EqualPanel)?.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
[DependencyProperty("MinItemWidth", typeof(double))]
|
||||
[DependencyProperty("Spacing", typeof(double))]
|
||||
internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
|
||||
{
|
||||
public HorizontalEqualPanel()
|
||||
{
|
||||
Loaded += OnLoaded;
|
||||
SizeChanged += OnSizeChanged;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
|
||||
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
|
||||
{
|
||||
// ScrollViewer will always return an Infinity Size, we should use ActualWidth for this situation.
|
||||
double availableWidth = double.IsInfinity(availableSize.Width) ? ActualWidth : availableSize.Width;
|
||||
double childAvailableWidth = (availableWidth + Spacing) / visibleChildren.Count;
|
||||
double childMaxAvailableWidth = Math.Max(MinItemWidth, childAvailableWidth);
|
||||
visibleChild.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
|
||||
}
|
||||
|
||||
return base.MeasureOverride(availableSize);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
|
||||
double availableItemWidth = (finalSize.Width - (Spacing * (visibleChildren.Count - 1))) / visibleChildren.Count;
|
||||
double actualItemWidth = Math.Max(MinItemWidth, availableItemWidth);
|
||||
|
||||
double offset = 0;
|
||||
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
|
||||
{
|
||||
visibleChild.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
|
||||
offset += actualItemWidth + Spacing;
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private static void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
HorizontalEqualPanel panel = (HorizontalEqualPanel)sender;
|
||||
int vivibleChildrenCount = panel.Children.Count(child => child.Visibility is Visibility.Visible);
|
||||
panel.MinWidth = (panel.MinItemWidth * vivibleChildrenCount) + (panel.Spacing * (vivibleChildrenCount - 1));
|
||||
}
|
||||
|
||||
private static void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
((HorizontalEqualPanel)sender).InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
Style="{StaticResource DefaultSegmentedStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<cwc:SegmentedItem
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using System.Collections.Frozen;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
@@ -19,11 +20,11 @@ internal sealed partial class PanelSelector : Segmented
|
||||
public const string List = nameof(List);
|
||||
public const string Grid = nameof(Grid);
|
||||
|
||||
private static readonly Dictionary<int, string> IndexTypeMap = new()
|
||||
{
|
||||
[0] = List,
|
||||
[1] = Grid,
|
||||
};
|
||||
private static readonly FrozenDictionary<int, string> IndexTypeMap = FrozenDictionary.ToFrozenDictionary(
|
||||
[
|
||||
KeyValuePair.Create(0, List),
|
||||
KeyValuePair.Create(1, Grid),
|
||||
]);
|
||||
|
||||
private readonly RoutedEventHandler loadedEventHandler;
|
||||
private readonly RoutedEventHandler unloadedEventHandler;
|
||||
|
||||
@@ -1,21 +1,53 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
[DependencyProperty("MinItemWidth", typeof(double))]
|
||||
internal sealed partial class UniformPanel : UniformGrid
|
||||
[DependencyProperty("ColumnSpacing", typeof(double))]
|
||||
[DependencyProperty("RowSpacing", typeof(double))]
|
||||
internal sealed partial class UniformPanel : Microsoft.UI.Xaml.Controls.Panel
|
||||
{
|
||||
public UniformPanel()
|
||||
private int columns;
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
Columns = 1;
|
||||
SizeChanged += OnSizeChanged;
|
||||
columns = (int)((availableSize.Width + ColumnSpacing) / (MinItemWidth + ColumnSpacing));
|
||||
double availableItemWidth = ((availableSize.Width + ColumnSpacing) / columns) - ColumnSpacing;
|
||||
|
||||
double maxDesiredHeight = 0;
|
||||
foreach (UIElement child in Children)
|
||||
{
|
||||
child.Measure(new Size(availableItemWidth, availableSize.Height));
|
||||
maxDesiredHeight = Math.Max(maxDesiredHeight, child.DesiredSize.Height);
|
||||
}
|
||||
|
||||
int desiredRows = (int)Math.Ceiling(Children.Count / (double)columns);
|
||||
double desiredHeight = ((maxDesiredHeight + RowSpacing) * desiredRows) - RowSpacing;
|
||||
|
||||
return new Size(availableSize.Width, desiredHeight);
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e)
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
Columns = (int)((e.NewSize.Width + ColumnSpacing) / (MinItemWidth + ColumnSpacing));
|
||||
double itemWidth = ((finalSize.Width + ColumnSpacing) / columns) - ColumnSpacing;
|
||||
|
||||
for (int index = 0; index < Children.Count; index++)
|
||||
{
|
||||
UIElement child = Children[index];
|
||||
|
||||
int row = index / columns;
|
||||
int column = index % columns;
|
||||
|
||||
double x = column * (itemWidth + ColumnSpacing);
|
||||
double y = row * (child.DesiredSize.Height + RowSpacing);
|
||||
|
||||
child.Arrange(new Rect(x, y, itemWidth, child.DesiredSize.Height));
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ internal class ScopedPage : Page
|
||||
{
|
||||
private readonly RoutedEventHandler unloadEventHandler;
|
||||
private readonly CancellationTokenSource viewCancellationTokenSource = new();
|
||||
private readonly IServiceScope currentScope;
|
||||
private readonly IServiceScope pageScope;
|
||||
|
||||
private bool inFrame = true;
|
||||
|
||||
@@ -23,7 +23,7 @@ internal class ScopedPage : Page
|
||||
{
|
||||
unloadEventHandler = OnUnloaded;
|
||||
Unloaded += unloadEventHandler;
|
||||
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
|
||||
pageScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
|
||||
}
|
||||
|
||||
public async ValueTask NotifyRecipientAsync(INavigationData extra)
|
||||
@@ -44,9 +44,17 @@ internal class ScopedPage : Page
|
||||
protected void InitializeWith<TViewModel>()
|
||||
where TViewModel : class, IViewModel
|
||||
{
|
||||
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
try
|
||||
{
|
||||
IViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
pageScope.ServiceProvider.GetRequiredService<ILogger<ScopedPage>>().LogError(ex, "Failed to initialize view model.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -72,7 +80,11 @@ internal class ScopedPage : Page
|
||||
DisposeViewModel();
|
||||
}
|
||||
|
||||
DataContext = null;
|
||||
if (this.IsDisposed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Unloaded -= unloadEventHandler;
|
||||
}
|
||||
|
||||
@@ -91,7 +103,8 @@ internal class ScopedPage : Page
|
||||
viewModel.IsViewDisposed = true;
|
||||
|
||||
// Dispose the scope
|
||||
currentScope.Dispose();
|
||||
pageScope.Dispose();
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ internal sealed partial class ScopedPageScopeReferenceTracker : IScopedPageScope
|
||||
|
||||
public IServiceScope CreateScope()
|
||||
{
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
|
||||
IServiceScope currentScope = serviceProvider.CreateScope();
|
||||
|
||||
// In case previous one is not disposed.
|
||||
|
||||
@@ -45,15 +45,7 @@ internal sealed partial class DescriptionTextBlock : ContentControl
|
||||
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
|
||||
|
||||
try
|
||||
{
|
||||
UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ = ex;
|
||||
}
|
||||
UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
|
||||
}
|
||||
|
||||
private static void OnTextStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
|
||||
@@ -14,6 +14,7 @@ using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Text;
|
||||
|
||||
// TODO: change the parsing to syntax tree
|
||||
[DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))]
|
||||
[DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))]
|
||||
internal sealed partial class HtmlDescriptionTextBlock : ContentControl
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||
|
||||
internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
|
||||
@@ -27,7 +29,7 @@ internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
|
||||
{
|
||||
MiHoYoColorKind.Rgba => new(Position.Start + 17, Position.End - 8),
|
||||
MiHoYoColorKind.Rgb => new(Position.Start + 15, Position.End - 8),
|
||||
_ => throw Must.NeverHappen(),
|
||||
_ => throw HutaoException.NotSupported(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -40,7 +42,7 @@ internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
|
||||
{
|
||||
MiHoYoColorKind.Rgba => new(Position.Start + 8, Position.Start + 16),
|
||||
MiHoYoColorKind.Rgb => new(Position.Start + 8, Position.Start + 14),
|
||||
_ => throw Must.NeverHappen(),
|
||||
_ => throw HutaoException.NotSupported(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||
|
||||
// TODO: Pooling syntax nodes to reduce memory allocation
|
||||
internal sealed class MiHoYoSyntaxTree
|
||||
{
|
||||
public MiHoYoSyntaxNode Root { get; set; } = default!;
|
||||
@@ -75,7 +78,7 @@ internal sealed class MiHoYoSyntaxTree
|
||||
{
|
||||
17 => MiHoYoColorKind.Rgba,
|
||||
15 => MiHoYoColorKind.Rgb,
|
||||
_ => throw Must.NeverHappen(),
|
||||
_ => throw HutaoException.NotSupported(),
|
||||
};
|
||||
|
||||
TextPosition position = new(0, endOfXmlColorRightClosingAtUnprocessedContent);
|
||||
|
||||
@@ -4,21 +4,19 @@
|
||||
xmlns:cwm="using:CommunityToolkit.WinUI.Media">
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<cwm:AttachedCardShadow
|
||||
x:Key="CompatCardShadow"
|
||||
BlurRadius="8"
|
||||
Opacity="0.14"
|
||||
Offset="0,4,0"/>
|
||||
<x:Double x:Key="CompatShadowThemeOpacity">0.14</x:Double>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<cwm:AttachedCardShadow
|
||||
x:Key="CompatCardShadow"
|
||||
BlurRadius="8"
|
||||
Opacity="0.28"
|
||||
Offset="0,4,0"/>
|
||||
<x:Double x:Key="CompatShadowThemeOpacity">0.28</x:Double>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<cwm:AttachedCardShadow
|
||||
x:Key="CompatCardShadow"
|
||||
BlurRadius="8"
|
||||
Opacity="{ThemeResource CompatShadowThemeOpacity}"
|
||||
Offset="0,4,0"/>
|
||||
|
||||
<Style x:Key="BorderCardStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing0Template">
|
||||
<cwcont:WrapPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing2Template">
|
||||
<cwcont:WrapPanel HorizontalSpacing="2" VerticalSpacing="2"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing4Template">
|
||||
<cwcont:WrapPanel HorizontalSpacing="4" VerticalSpacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
@@ -17,6 +20,9 @@
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing2Template">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing4Template">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
|
||||
<StackPanel Spacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
|
||||
@@ -11,4 +11,6 @@ internal static class KnownColors
|
||||
public static readonly Color Orange = StructMarshal.Color(0xFFBC6932);
|
||||
public static readonly Color Purple = StructMarshal.Color(0xFFA156E0);
|
||||
public static readonly Color Blue = StructMarshal.Color(0xFF5180CB);
|
||||
public static readonly Color Green = StructMarshal.Color(0xFF2A8F72);
|
||||
public static readonly Color White = StructMarshal.Color(0xFF72778B);
|
||||
}
|
||||
115
src/Snap.Hutao/Snap.Hutao/Control/Theme/SegmentedOverride.xaml
Normal file
115
src/Snap.Hutao/Snap.Hutao/Control/Theme/SegmentedOverride.xaml
Normal file
@@ -0,0 +1,115 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cw="using:CommunityToolkit.WinUI"
|
||||
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:shcp="using:Snap.Hutao.Control.Panel"
|
||||
xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/SegmentedItem/SegmentedItem.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<StaticResource x:Key="SegmentedBackground" ResourceKey="ControlAltFillColorSecondaryBrush"/>
|
||||
<StaticResource x:Key="SegmentedBorderBrush" ResourceKey="ControlStrokeColorDefaultBrush"/>
|
||||
<Thickness x:Key="SegmentedBorderThickness">1</Thickness>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<StaticResource x:Key="SegmentedBackground" ResourceKey="ControlAltFillColorSecondaryBrush"/>
|
||||
<StaticResource x:Key="SegmentedBorderBrush" ResourceKey="ControlStrokeColorDefaultBrush"/>
|
||||
<Thickness x:Key="SegmentedBorderThickness">1</Thickness>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<StaticResource x:Key="SegmentedBackground" ResourceKey="SystemColorButtonFaceColor"/>
|
||||
<StaticResource x:Key="SegmentedBorderBrush" ResourceKey="SystemColorHighlightColorBrush"/>
|
||||
<Thickness x:Key="SegmentedBorderThickness">1</Thickness>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<x:Double x:Key="SegmentedItemSpacing">1</x:Double>
|
||||
<x:Double x:Key="ButtonItemSpacing">2</x:Double>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultSegmentedStyle}" TargetType="cwc:Segmented"/>
|
||||
|
||||
<Style x:Key="DefaultSegmentedStyle" TargetType="cwc:Segmented">
|
||||
<Style.Setters>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
<Setter Property="Background" Value="{ThemeResource SegmentedBackground}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource SegmentedBorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource SegmentedBorderThickness}"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="SelectionMode" Value="Single"/>
|
||||
<Setter Property="IsItemClickEnabled" Value="False"/>
|
||||
<win:Setter Property="SingleSelectionFollowsFocus" Value="False"/>
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="TabNavigation" Value="Once"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<shcp:EqualPanel
|
||||
HorizontalAlignment="{Binding (cw:FrameworkElementExtensions.Ancestor).HorizontalAlignment, RelativeSource={RelativeSource Self}}"
|
||||
cw:FrameworkElementExtensions.AncestorType="cwc:Segmented"
|
||||
Spacing="{ThemeResource SegmentedItemSpacing}"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="cwc:Segmented">
|
||||
<Grid>
|
||||
<Border
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"/>
|
||||
<ItemsPresenter Margin="{TemplateBinding Padding}"/>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="PivotSegmentedStyle"
|
||||
BasedOn="{StaticResource DefaultSegmentedStyle}"
|
||||
TargetType="cwc:Segmented">
|
||||
<Style.Setters>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource PivotSegmentedItemStyle}"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="{ThemeResource SegmentedItemSpacing}"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="ButtonSegmentedStyle"
|
||||
BasedOn="{StaticResource DefaultSegmentedStyle}"
|
||||
TargetType="cwc:Segmented">
|
||||
<Style.Setters>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource ButtonSegmentedItemStyle}"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="{ThemeResource ButtonItemSpacing}"/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
25
src/Snap.Hutao/Snap.Hutao/Control/Theme/SystemColors.cs
Normal file
25
src/Snap.Hutao/Snap.Hutao/Control/Theme/SystemColors.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Theme;
|
||||
|
||||
internal static class SystemColors
|
||||
{
|
||||
public static Color BaseLowColor(bool isDarkMode)
|
||||
{
|
||||
return isDarkMode ? StructMarshal.Color(0x33FFFFFF) : StructMarshal.Color(0x33000000);
|
||||
}
|
||||
|
||||
public static Color BaseMediumLowColor(bool isDarkMode)
|
||||
{
|
||||
return isDarkMode ? StructMarshal.Color(0x66FFFFFF) : StructMarshal.Color(0x66000000);
|
||||
}
|
||||
|
||||
public static Color BaseHighColor(bool isDarkMode)
|
||||
{
|
||||
return isDarkMode ? StructMarshal.Color(0xFFFFFFFF) : StructMarshal.Color(0xFF000000);
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,13 @@
|
||||
<x:String x:Key="UI_Icon_Intee_Explore_1">https://api.snapgenshin.com/static/raw/Bg/UI_Icon_Intee_Explore_1.png</x:String>
|
||||
<x:String x:Key="UI_ImgSign_ItemIcon">https://api.snapgenshin.com/static/raw/Bg/UI_ImgSign_ItemIcon.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_None">https://api.snapgenshin.com/static/raw/Bg/UI_ItemIcon_None.png</x:String>
|
||||
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Bg/UI_MarkQuest_Events_Proce.png</x:String>
|
||||
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Bg/UI_MarkTower.png</x:String>
|
||||
|
||||
<!-- Mark -->
|
||||
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
|
||||
<x:String x:Key="UI_MarkQuest_Events_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
|
||||
<x:String x:Key="UI_MarkQuest_Main_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
|
||||
<x:String x:Key="UI_MarkQuest_Main_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
|
||||
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Mark/UI_MarkTower.png</x:String>
|
||||
|
||||
<!-- ItemIcon -->
|
||||
<x:String x:Key="UI_ItemIcon_201">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_201.png</x:String>
|
||||
@@ -30,6 +35,7 @@
|
||||
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon52">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon89">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon89.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
@@ -39,6 +40,6 @@ internal abstract class ValueConverter<TFrom, TTo> : IValueConverter
|
||||
/// <returns>源</returns>
|
||||
public virtual TFrom ConvertBack(TTo to)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
throw HutaoException.NotSupported();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Web.Request.Builder;
|
||||
namespace Snap.Hutao.Core.Abstraction.Extension;
|
||||
|
||||
internal static class BuilderExtension
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
internal interface IBuilder;
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 可克隆
|
||||
/// </summary>
|
||||
/// <typeparam name="TSelf">自身类型</typeparam>
|
||||
[HighQuality]
|
||||
internal interface ICloneable<out TSelf>
|
||||
{
|
||||
/// <summary>
|
||||
/// 克隆
|
||||
/// </summary>
|
||||
/// <returns>新的克隆</returns>
|
||||
TSelf Clone();
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
using Snap.Hutao.Core.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
@@ -10,7 +9,7 @@ namespace Snap.Hutao.Core.Caching;
|
||||
/// 为图像缓存提供抽象
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal interface IImageCache : ICastService
|
||||
internal interface IImageCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the file path containing cached item for given Uri
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.IO.Hashing;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.ViewModel.Guide;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
|
||||
@@ -25,19 +29,22 @@ namespace Snap.Hutao.Core.Caching;
|
||||
internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
{
|
||||
private const string CacheFolderName = nameof(ImageCache);
|
||||
private const string CacheFailedDownloadTasksName = $"{nameof(ImageCache)}.FailedDownloadTasks";
|
||||
|
||||
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = new Dictionary<int, TimeSpan>()
|
||||
{
|
||||
[0] = TimeSpan.FromSeconds(4),
|
||||
[1] = TimeSpan.FromSeconds(16),
|
||||
[2] = TimeSpan.FromSeconds(64),
|
||||
}.ToFrozenDictionary();
|
||||
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = FrozenDictionary.ToFrozenDictionary(
|
||||
[
|
||||
KeyValuePair.Create(0, TimeSpan.FromSeconds(4)),
|
||||
KeyValuePair.Create(1, TimeSpan.FromSeconds(16)),
|
||||
KeyValuePair.Create(2, TimeSpan.FromSeconds(64)),
|
||||
]);
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
|
||||
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ILogger<ImageCache> logger;
|
||||
private readonly IMemoryCache memoryCache;
|
||||
|
||||
private string? baseFolder;
|
||||
private string? cacheFolder;
|
||||
@@ -104,12 +111,12 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
||||
{
|
||||
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
|
||||
{
|
||||
logger.LogDebug("Begin downloading image file from '{Uri}' to '{File}'", uri, filePath);
|
||||
logger.LogColorizedInformation("Begin to download file from '{Uri}' to '{File}'", (uri, ConsoleColor.Cyan), (filePath, ConsoleColor.Cyan));
|
||||
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
|
||||
}
|
||||
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
|
||||
{
|
||||
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", uri);
|
||||
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", (uri, ConsoleColor.Cyan));
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -132,10 +139,7 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
||||
|
||||
private static string GetCacheFileName(Uri uri)
|
||||
{
|
||||
string url = uri.ToString();
|
||||
byte[] chars = Encoding.UTF8.GetBytes(url);
|
||||
byte[] hash = SHA1.HashData(chars);
|
||||
return System.Convert.ToHexString(hash);
|
||||
return Hash.SHA1HexString(uri.ToString());
|
||||
}
|
||||
|
||||
private static bool IsFileInvalid(string file, bool treatNullFileAsInvalid = true)
|
||||
@@ -172,40 +176,76 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
||||
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
|
||||
while (retryCount < 3)
|
||||
{
|
||||
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
{
|
||||
if (message.RequestMessage is { RequestUri: { } target } && target != uri)
|
||||
{
|
||||
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
|
||||
}
|
||||
HttpRequestMessageBuilder requestMessageBuilder = httpRequestMessageBuilderFactory
|
||||
.Create()
|
||||
.SetRequestUri(uri)
|
||||
|
||||
if (message.IsSuccessStatusCode)
|
||||
// These headers are only available for our own api
|
||||
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase))
|
||||
.Get();
|
||||
|
||||
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
|
||||
{
|
||||
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
{
|
||||
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
|
||||
{
|
||||
using (FileStream fileStream = File.Create(baseFile))
|
||||
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
|
||||
}
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
if (responseMessage.Content.Headers.ContentType?.MediaType is "application/json")
|
||||
{
|
||||
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
#if DEBUG
|
||||
DebugTrack(uri);
|
||||
#endif
|
||||
string raw = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
logger.LogColorizedCritical("Failed to download '{Uri}' with unexpected body '{Raw}'", (uri, ConsoleColor.Red), (raw, ConsoleColor.DarkYellow));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (message.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
break;
|
||||
using (FileStream fileStream = File.Create(baseFile))
|
||||
{
|
||||
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
switch (responseMessage.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
#if DEBUG
|
||||
DebugTrack(uri);
|
||||
#endif
|
||||
logger.LogColorizedCritical("Failed to download '{Uri}' with status code '{StatusCode}'", (uri, ConsoleColor.Red), (responseMessage.StatusCode, ConsoleColor.DarkYellow));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
internal partial class ImageCache
|
||||
{
|
||||
private void DebugTrack(Uri uri)
|
||||
{
|
||||
HashSet<string>? set = memoryCache.GetOrCreate(CacheFailedDownloadTasksName, entry => entry.Value ??= new HashSet<string>()) as HashSet<string>;
|
||||
set?.Add(uri.ToString());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Collection;
|
||||
|
||||
internal sealed class TwoEnumerbleEnumerator<TFirst, TSecond> : IDisposable
|
||||
{
|
||||
private readonly IEnumerator<TFirst> firstEnumerator;
|
||||
private readonly IEnumerator<TSecond> secondEnumerator;
|
||||
|
||||
public TwoEnumerbleEnumerator(IEnumerable<TFirst> firstEnumerable, IEnumerable<TSecond> secondEnumerable)
|
||||
{
|
||||
firstEnumerator = firstEnumerable.GetEnumerator();
|
||||
secondEnumerator = secondEnumerable.GetEnumerator();
|
||||
}
|
||||
|
||||
public (TFirst First, TSecond Second) Current { get => (firstEnumerator.Current, secondEnumerator.Current); }
|
||||
|
||||
public bool MoveNext(ref bool moveFirst, ref bool moveSecond)
|
||||
{
|
||||
moveFirst = moveFirst && firstEnumerator.MoveNext();
|
||||
moveSecond = moveSecond && secondEnumerator.MoveNext();
|
||||
|
||||
return moveFirst || moveSecond;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
firstEnumerator.Dispose();
|
||||
secondEnumerator.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 支持Md5转换
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class Convert
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取字符串的MD5计算结果
|
||||
/// </summary>
|
||||
/// <param name="source">源字符串</param>
|
||||
/// <returns>计算的结果</returns>
|
||||
public static string ToMd5HexString(string source)
|
||||
{
|
||||
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
|
||||
return System.Convert.ToHexString(hash);
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,6 @@ namespace Snap.Hutao.Core.Database;
|
||||
[HighQuality]
|
||||
internal static class DbSetExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static int AddAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -27,27 +20,13 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步添加并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Add(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加列表并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entities">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -55,27 +34,13 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步添加列表并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entities">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
|
||||
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.AddRange(entities);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -83,27 +48,13 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步移除并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Remove(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -111,18 +62,11 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步更新并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Update(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync();
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -136,11 +80,11 @@ internal static class DbSetExtension
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet)
|
||||
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
int count = await dbContext.SaveChangesAsync(token).ConfigureAwait(false);
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceProvider.IsDisposedSlow())
|
||||
if (serviceProvider.IsDisposed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -96,7 +96,7 @@ internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceProvider.IsDisposedSlow())
|
||||
if (serviceProvider.IsDisposed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 对象扩展
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class CastServiceExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// <see langword="as"/> 的链式调用扩展
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标转换类型</typeparam>
|
||||
/// <param name="service">对象</param>
|
||||
/// <returns>转换类型后的对象</returns>
|
||||
[Obsolete("Not useful anymore")]
|
||||
public static T? As<T>(this ICastService service)
|
||||
where T : class
|
||||
{
|
||||
return service as T;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Quartz;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Service;
|
||||
using System.Globalization;
|
||||
@@ -24,9 +25,18 @@ internal static class DependencyInjection
|
||||
ServiceProvider serviceProvider = new ServiceCollection()
|
||||
|
||||
// Microsoft extension
|
||||
.AddLogging(builder => builder.AddDebug().AddConsoleWindow())
|
||||
.AddLogging(builder =>
|
||||
{
|
||||
builder
|
||||
.SetMinimumLevel(LogLevel.Trace)
|
||||
.AddDebug()
|
||||
.AddConsoleWindow();
|
||||
})
|
||||
.AddMemoryCache()
|
||||
|
||||
// Quartz
|
||||
.AddQuartz()
|
||||
|
||||
// Hutao extensions
|
||||
.AddJsonOptions()
|
||||
.AddDatabase()
|
||||
|
||||
@@ -33,17 +33,17 @@ internal static class IocConfiguration
|
||||
return services
|
||||
.AddTransient(typeof(Database.ScopedDbCurrent<,>))
|
||||
.AddTransient(typeof(Database.ScopedDbCurrent<,,>))
|
||||
.AddDbContext<AppDbContext>(AddDbContextCore);
|
||||
.AddDbContextPool<AppDbContext>(AddDbContextCore);
|
||||
}
|
||||
|
||||
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
|
||||
private static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
|
||||
{
|
||||
RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
|
||||
string sqlConnectionString = $"Data Source={dbFile}";
|
||||
|
||||
// Temporarily create a context
|
||||
using (AppDbContext context = AppDbContext.Create(sqlConnectionString))
|
||||
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString))
|
||||
{
|
||||
if (context.Database.GetPendingMigrations().Any())
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
|
||||
{
|
||||
HttpClientHandler clientHandler = (HttpClientHandler)handler;
|
||||
clientHandler.AllowAutoRedirect = true;
|
||||
clientHandler.UseProxy = true;
|
||||
clientHandler.Proxy = provider.GetRequiredService<DynamicHttpProxy>();
|
||||
});
|
||||
|
||||
@@ -18,13 +18,22 @@ internal static class ServiceProviderExtension
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsDisposedSlow(this IServiceProvider? serviceProvider)
|
||||
public static bool IsDisposed(this IServiceProvider? serviceProvider)
|
||||
{
|
||||
if (serviceProvider is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (serviceProvider is ServiceProvider serviceProviderImpl)
|
||||
{
|
||||
return GetPrivateDisposed(serviceProviderImpl);
|
||||
}
|
||||
|
||||
return serviceProvider.GetType().GetField("_disposed")?.GetValue(serviceProvider) is true;
|
||||
}
|
||||
|
||||
// private bool _disposed;
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
|
||||
private static extern ref bool GetPrivateDisposed(ServiceProvider serviceProvider);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user