mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
564 Commits
l10n_devel
...
feat/IFile
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94fe192581 | ||
|
|
c679032387 | ||
|
|
d119b056c7 | ||
|
|
ab95ce8ce8 | ||
|
|
0ede5b158f | ||
|
|
6c9a98c2c9 | ||
|
|
a725fc0e9e | ||
|
|
5251dd9343 | ||
|
|
0b71053bf9 | ||
|
|
a91499171d | ||
|
|
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 | ||
|
|
7a572631e9 | ||
|
|
41f5a04e5a | ||
|
|
4f26d6efc2 | ||
|
|
0d6b5e595e | ||
|
|
e30bebd1a3 | ||
|
|
ffb34c0e93 | ||
|
|
35b35ab649 | ||
|
|
f067350cd8 | ||
|
|
14648dafe8 | ||
|
|
7a7d81cfee | ||
|
|
30c055e7ba | ||
|
|
6a922d9db6 | ||
|
|
02a6e64a8c | ||
|
|
51c4e66472 | ||
|
|
48875195bf | ||
|
|
a179e0e838 | ||
|
|
00c3e94e97 | ||
|
|
eec7224c07 | ||
|
|
85bcf37b1b | ||
|
|
465c7035c0 | ||
|
|
11620816ec | ||
|
|
c5b75d6f82 | ||
|
|
6241bbc997 | ||
|
|
afbd8ec6ad | ||
|
|
288cc841ac | ||
|
|
a9295c0a37 | ||
|
|
29cd690032 | ||
|
|
78d8539ae2 | ||
|
|
7dec87586b | ||
|
|
d169f355f3 | ||
|
|
e1784e2078 | ||
|
|
32b1b698df | ||
|
|
d6c7df1593 | ||
|
|
904fdf7fc9 | ||
|
|
b3e4ebb5d3 | ||
|
|
09b9af4575 | ||
|
|
8930548f44 | ||
|
|
34dbcc6f5d | ||
|
|
75c25cec53 | ||
|
|
22251ca937 | ||
|
|
57c9531db8 | ||
|
|
fd73743159 | ||
|
|
6594d9032d | ||
|
|
ae1b452697 | ||
|
|
bc3df782e4 | ||
|
|
3ef117e683 | ||
|
|
8b1613d46f | ||
|
|
c2708aaedd | ||
|
|
db3de01eec | ||
|
|
635b52980b | ||
|
|
4988c1bd47 | ||
|
|
fd499e372c | ||
|
|
a6c376cd44 | ||
|
|
425b3789bf | ||
|
|
a6986bc201 | ||
|
|
301a14611a | ||
|
|
59db7d968a | ||
|
|
45f4b46e9e | ||
|
|
82b5d9b12a | ||
|
|
6fb5cfe25d | ||
|
|
1df78345c5 | ||
|
|
fd817af9c3 | ||
|
|
3f6efe2e24 | ||
|
|
ef6d88ba2e | ||
|
|
26748e5885 | ||
|
|
5f180846f6 | ||
|
|
57ea6bb34e | ||
|
|
95024e4107 | ||
|
|
678ec191a6 | ||
|
|
40b26d9d3c | ||
|
|
e5677e7de4 | ||
|
|
9ae8b182e7 | ||
|
|
02fe498cce | ||
|
|
f924f78ee2 | ||
|
|
977e749939 | ||
|
|
f8ad3b98fc | ||
|
|
ee39f7fec5 | ||
|
|
c8c1d21c9b | ||
|
|
f06825f246 | ||
|
|
72395fdb89 | ||
|
|
8954f7e325 | ||
|
|
976441de18 | ||
|
|
60a49971f6 | ||
|
|
fb5de92283 | ||
|
|
046f3ace94 | ||
|
|
4c369ad0ad | ||
|
|
7efaaae3e1 | ||
|
|
446bdb2b49 | ||
|
|
20277b8b79 | ||
|
|
7baf125f88 | ||
|
|
a4e782da78 | ||
|
|
d5551e5cdf | ||
|
|
f016a4a27f | ||
|
|
8b931b6d89 | ||
|
|
b942ceb914 | ||
|
|
f7a49e52e0 | ||
|
|
d4bd610fe2 | ||
|
|
a3dcfd3804 | ||
|
|
592525d149 | ||
|
|
83c4598df5 | ||
|
|
31670953b0 | ||
|
|
aa680388ad | ||
|
|
ba4f59de30 | ||
|
|
8780cf385e | ||
|
|
431cdd1253 | ||
|
|
9a8827fb40 | ||
|
|
d88a6ca301 | ||
|
|
5d401794e5 | ||
|
|
26396443dc | ||
|
|
ae2415dbca | ||
|
|
6b755d934d | ||
|
|
7d5b057269 | ||
|
|
917c173eb2 | ||
|
|
28d702422e | ||
|
|
7612ab5da3 | ||
|
|
ab436ecb2f | ||
|
|
457e3ff4d5 | ||
|
|
224c4e52ea | ||
|
|
2a5c7b21fd | ||
|
|
53f8291a66 | ||
|
|
b621d5406a | ||
|
|
12f4847aea | ||
|
|
39a3d31f38 | ||
|
|
70ac0b13a5 | ||
|
|
a3520a4991 | ||
|
|
aed0284e4b | ||
|
|
96ef07cbe5 | ||
|
|
d1f37f37ac | ||
|
|
1fe09f3069 | ||
|
|
5b109013a0 | ||
|
|
32d9355c3a | ||
|
|
71f170d51e | ||
|
|
60015b6354 | ||
|
|
eecae3ea4f | ||
|
|
1831166f1e | ||
|
|
a98915ea24 | ||
|
|
0d46656f57 | ||
|
|
c814a5c28f | ||
|
|
9c3d59cc6f | ||
|
|
890cf3f3ea | ||
|
|
196bbb54c3 | ||
|
|
0481b9e474 | ||
|
|
c4f3eb68e8 | ||
|
|
c2e9f3a926 | ||
|
|
fb1fe3e40f | ||
|
|
75ed512e4a | ||
|
|
dd4dd33d93 | ||
|
|
1e216e9823 | ||
|
|
f823cb5f1a | ||
|
|
2c6d25f0a3 | ||
|
|
817f768263 | ||
|
|
2998fbb167 | ||
|
|
8f0f94054d | ||
|
|
17a5d4d3a2 | ||
|
|
a1c604e68a | ||
|
|
948ec9a822 | ||
|
|
f83174d690 | ||
|
|
d686debbfb | ||
|
|
279e107919 | ||
|
|
45248d75e1 | ||
|
|
22646cfab2 | ||
|
|
d0237a3c89 | ||
|
|
73c80fad10 | ||
|
|
320bed9fcb | ||
|
|
bb12aca3b4 | ||
|
|
c7b5d98fb1 | ||
|
|
7cc96f94f2 | ||
|
|
b35355f9a3 | ||
|
|
94a5e71130 | ||
|
|
745815657d | ||
|
|
07cdfcea28 | ||
|
|
d93a9f41f3 | ||
|
|
910f099c6d | ||
|
|
e68449ec0c | ||
|
|
e484fbed21 | ||
|
|
88af6d28a9 | ||
|
|
3ab34f0992 | ||
|
|
5e875a7f18 | ||
|
|
89d98748e8 | ||
|
|
d33cd894b9 | ||
|
|
f0c19b419e | ||
|
|
f1d9787e45 | ||
|
|
5f9b4a7cb2 | ||
|
|
8710150897 | ||
|
|
92c1b12764 | ||
|
|
d73bd557f3 | ||
|
|
777d7d1056 | ||
|
|
1a944dae9c | ||
|
|
a26c52ba97 | ||
|
|
5fab03d57e | ||
|
|
e8a459cb41 | ||
|
|
04df5a7bf1 | ||
|
|
a93eb505d6 | ||
|
|
1ebcc2fc89 | ||
|
|
e9917e788d | ||
|
|
9665876d52 | ||
|
|
8921816873 | ||
|
|
2698761594 | ||
|
|
3ae4210ca0 | ||
|
|
2f5e0cbe39 | ||
|
|
d3444a9435 | ||
|
|
8b6f95c3d9 | ||
|
|
88b8335e5b | ||
|
|
061aba715b | ||
|
|
da80631b72 | ||
|
|
97acf872bc | ||
|
|
addaf1a9e3 | ||
|
|
76183901da | ||
|
|
87ee81e7fa | ||
|
|
f2f858de15 | ||
|
|
c434521004 | ||
|
|
27ed2cefc1 | ||
|
|
6dc1e664b0 | ||
|
|
aa46b6531b | ||
|
|
b36399f572 | ||
|
|
51c3dde24b | ||
|
|
2d497faaa5 | ||
|
|
4783934b92 | ||
|
|
03d235876a | ||
|
|
f49e9669af | ||
|
|
533c70caaa | ||
|
|
dd59302bb3 | ||
|
|
96e42f51f0 | ||
|
|
5a19c19759 | ||
|
|
8fb831ef7c | ||
|
|
a30c8d8678 | ||
|
|
2655e835f8 | ||
|
|
ffd74703cd | ||
|
|
584465dc45 | ||
|
|
a1e751160d | ||
|
|
d78d2cf51a | ||
|
|
24709bfbf9 | ||
|
|
9be396b175 | ||
|
|
bb83e76d33 | ||
|
|
1ca24c8a78 | ||
|
|
3d56aef221 | ||
|
|
d43f2e76c4 | ||
|
|
104fb9a3b0 | ||
|
|
d6b79584b6 | ||
|
|
fcd0b65257 | ||
|
|
802951edd7 | ||
|
|
79fc42aa3b | ||
|
|
fb0491dc57 | ||
|
|
b81d088379 | ||
|
|
553d267625 | ||
|
|
199e753103 | ||
|
|
48774960a7 | ||
|
|
7bfea0e090 | ||
|
|
f0f9e387a8 | ||
|
|
f71a34a6be | ||
|
|
e6fd0b833b | ||
|
|
d2c33cf19c | ||
|
|
59a7d6746f | ||
|
|
1d074f5313 | ||
|
|
769a1c3812 | ||
|
|
b54717fa9b | ||
|
|
ffa0b05a12 | ||
|
|
d07a33f3e4 | ||
|
|
b49cd924d0 | ||
|
|
49db3003c9 | ||
|
|
314c771020 | ||
|
|
967f6f76f0 | ||
|
|
5d05c31af5 | ||
|
|
bbd274c391 | ||
|
|
f8a8a929ac | ||
|
|
cf3298dbd0 | ||
|
|
a8b887def2 | ||
|
|
5a937b0838 | ||
|
|
c016ae1cb8 | ||
|
|
c7fdf8001d | ||
|
|
64998453a1 | ||
|
|
9fdedd78d0 | ||
|
|
58e4d1b90e | ||
|
|
e0d11bf9a0 | ||
|
|
51be2c76aa | ||
|
|
686d2378de | ||
|
|
e2d5baffe0 | ||
|
|
4001cc7051 | ||
|
|
b106fe4729 | ||
|
|
d138d856e4 | ||
|
|
91f16c1701 | ||
|
|
54d21b24f7 | ||
|
|
268c2d0543 | ||
|
|
acdcee7558 | ||
|
|
371e469db7 | ||
|
|
22a974408d | ||
|
|
055b343571 | ||
|
|
84e56792b0 | ||
|
|
da95b7837a | ||
|
|
48ddb4c091 | ||
|
|
ea95f2e2b1 | ||
|
|
93077104b8 | ||
|
|
3ffdc901c7 | ||
|
|
0d66c85744 | ||
|
|
b11526761e | ||
|
|
d293149672 | ||
|
|
3784df67a3 | ||
|
|
4aaca4d19f | ||
|
|
e6cf39831d | ||
|
|
24a2a18760 | ||
|
|
d8dce5c062 | ||
|
|
6ee823094a | ||
|
|
ccbb7f76d4 | ||
|
|
857eea61f9 | ||
|
|
d82f416c10 | ||
|
|
b8bcad2107 | ||
|
|
ad240a543d | ||
|
|
e7775b611f | ||
|
|
14894b0b47 | ||
|
|
53d920621c | ||
|
|
55cb346fb4 | ||
|
|
c0f63187cc | ||
|
|
6834073603 | ||
|
|
911fe57fb2 | ||
|
|
7320cf7dd0 | ||
|
|
bc6d03e442 | ||
|
|
884ec87edf | ||
|
|
18d3180bc2 | ||
|
|
4908364e45 | ||
|
|
b7fe16c52c | ||
|
|
0c8646b499 | ||
|
|
f5b0d07d32 | ||
|
|
231635ac89 | ||
|
|
e0a28d0f90 | ||
|
|
22e7942899 | ||
|
|
d81e7f6624 | ||
|
|
92240a27a0 | ||
|
|
c5313c078d | ||
|
|
2c320fe7e6 | ||
|
|
05a8ab990c | ||
|
|
3661822852 | ||
|
|
7519d7b263 | ||
|
|
47d0cbcf31 | ||
|
|
449a5393a9 | ||
|
|
3b636ecd27 | ||
|
|
95531db559 | ||
|
|
eeed58ed71 | ||
|
|
493af0fd4c | ||
|
|
3df70a5feb | ||
|
|
879b930ea6 | ||
|
|
c5e0221a0b | ||
|
|
44fbb56d83 | ||
|
|
1a1bdb7f85 | ||
|
|
52cd505ed0 | ||
|
|
cd16bebee2 | ||
|
|
307a49b346 | ||
|
|
9f8d80ff43 | ||
|
|
2be2d6313b | ||
|
|
bee7e48cb9 | ||
|
|
83cbc9bbe1 | ||
|
|
d34130b6c0 | ||
|
|
2161f12069 | ||
|
|
0bedd1894c | ||
|
|
442db0bae4 |
21
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
21
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
@@ -18,28 +18,28 @@ body:
|
||||
options:
|
||||
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
|
||||
required: true
|
||||
|
||||
|
||||
- label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
|
||||
required: true
|
||||
|
||||
- label: 我的问题不是[已修复](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E4%BF%AE%E5%A4%8D)的问题也不是一个别人已发布的**重复的**问题
|
||||
- label: 我的问题不是[已完成](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90)的问题也不是一个别人已发布的**重复的**问题
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: winver
|
||||
attributes:
|
||||
label: Windows 版本
|
||||
description: |
|
||||
description: |
|
||||
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
|
||||
placeholder: 例:22000.556
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
- type: input
|
||||
id: shver
|
||||
attributes:
|
||||
label: Snap Hutao 版本
|
||||
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
|
||||
description: 在应用标题,应用程序的反馈中心界面中可以找到
|
||||
placeholder: 例:1.4.15.0
|
||||
validations:
|
||||
required: true
|
||||
@@ -48,10 +48,10 @@ body:
|
||||
id: deviceid
|
||||
attributes:
|
||||
label: 设备 ID
|
||||
description: |
|
||||
在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID
|
||||
description: |
|
||||
在胡桃工具箱的反馈中心界面,你可以找到并复制你的设备 ID
|
||||
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
|
||||
如果你的程序已经无法启动,请下载并运行[此PowerShell脚本](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1),它将显示你的设备 ID
|
||||
如果你的程序已经无法启动,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.Diagnostic.Tooling.exe),它将显示你的设备 ID
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -79,7 +79,7 @@ body:
|
||||
- 公告
|
||||
- 其它
|
||||
validations:
|
||||
required: true
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
@@ -87,7 +87,7 @@ body:
|
||||
label: 发生了什么?
|
||||
description: |
|
||||
详细的描述问题发生前后的行为,以便我们解决问题。**如果你的问题涉及程序崩溃,你应当检查 Windows 事件查看器,并将相关的 `.Net 错误`详情附上**
|
||||
如果你无法找到该日志,请下载并运行[此PowerShell脚本](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/dump_log_script/dump_log_zh.ps1),它将输出错误日志
|
||||
如果你无法找到该日志,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将转储问题日志至工具运行目录中的 `Snap.Hutao Error Log.txt`
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -107,4 +107,3 @@ body:
|
||||
options:
|
||||
- label: 我认为上述的描述已经足以详细,以允许开发人员能复现该问题
|
||||
required: true
|
||||
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/ENG-bug-report.yml
vendored
21
.github/ISSUE_TEMPLATE/ENG-bug-report.yml
vendored
@@ -18,23 +18,23 @@ body:
|
||||
options:
|
||||
- label: I have read [FAQ page](https://hut.ao/advanced/FAQ.html) and [Exception page](https://hut.ao/advanced/exceptions.html) in Snap Hutao document, and my issue is not answered
|
||||
required: true
|
||||
|
||||
|
||||
- label: I and tried **search feature** in Snap Hutao document site, and no associated article
|
||||
required: true
|
||||
|
||||
- label: My issue is not a [fixed issue](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E4%BF%AE%E5%A4%8D), and it's not a duplicated issue
|
||||
- label: My issue is not a [finished issue](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90), and it's not a duplicated issue
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: winver
|
||||
attributes:
|
||||
label: Windows Version
|
||||
description: |
|
||||
description: |
|
||||
Use `Win+R` and input `winver`, Windows build version is usually at the second line
|
||||
placeholder: e.g. 22000.556
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
- type: input
|
||||
id: shver
|
||||
attributes:
|
||||
@@ -48,10 +48,10 @@ body:
|
||||
id: deviceid
|
||||
attributes:
|
||||
label: Device ID
|
||||
description: |
|
||||
In Snap Hutao's settings page, you can find and copy your device ID
|
||||
description: |
|
||||
In Snap Hutao's Feedback Center, you can find and copy your device ID
|
||||
If your issue is about program crash, please fill this so we can dump the log and locate the source easier
|
||||
If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID.
|
||||
If your program cannot startup, please download and run [Diagnostic Tooling](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.Diagnostic.Tooling.exe), it will shows your device ID.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -74,12 +74,12 @@ body:
|
||||
- User Interface
|
||||
- Snap Hutao Cloud
|
||||
- Snap Hutao Account
|
||||
- Checkin
|
||||
- Checkin
|
||||
- Wiki
|
||||
- Announcement
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
@@ -87,7 +87,7 @@ body:
|
||||
label: What Happened?
|
||||
description: |
|
||||
Describe your issue in detail to help us identify the issue. **If your issue is about program crash, you should check Windows Event Viewer, and attach associated `.Net Error` details here**If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID.
|
||||
If you cannot find it, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/dump_log_script/dump_log_en.ps1), it will dump the error log.
|
||||
If you cannot find it, please download and run [Diagnosis Tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will dump the error log to `Snap.Hutao Error Log.txt` in the working directory of the tool.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -107,4 +107,3 @@ body:
|
||||
options:
|
||||
- label: I believe the description above is detail enough to allow developers to reproduce the issue
|
||||
required: true
|
||||
|
||||
|
||||
40
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
40
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
@@ -12,50 +12,26 @@ body:
|
||||
value: |
|
||||
|
||||
## 创建版本
|
||||
|
||||
|
||||
- [ ] 同步一次 [Crowdin](https://crowdin.com/project/snap-hutao) 翻译
|
||||
- [ ] 发布 RC 版本(Optional)
|
||||
- [ ] 合并入主分支
|
||||
- [ ] 整理更新内容,等待翻译
|
||||
- [ ] 打包
|
||||
- [ ] 提交微软商店
|
||||
- [ ] 包含更新日志
|
||||
- [ ] 在 [Snap.Hutao.Docs@next-patch](https://github.com/DGP-Studio/Snap.Hutao.Docs/tree/next-patch) 分支更新文档并直接开 PR
|
||||
- [ ] 更新日志
|
||||
- [ ] 功能文档更新
|
||||
|
||||
## 发布版本 [半自动]
|
||||
|
||||
- [ ] 在 GitHub 个人设置中更新 [Publish-Automate Beta PAT](https://github.com/settings/tokens?type=beta),有效期需小于预计发版需要天数
|
||||
- [ ] 将更新的 PAT 更新至 Publish-Automate 库的 [Actions Secrets](https://github.com/DGP-Studio/Publish-Automate/settings/secrets/actions) 中
|
||||
|
||||
***
|
||||
***
|
||||
|
||||
- [ ] 运行 [Auto Publish Action](https://github.com/DGP-Studio/Publish-Automate/actions/workflows/auto-publish.yml)
|
||||
- [ ] 在 https://store.rg-adguard.net/ 下载新版本安装包
|
||||
- [ ] Store URL: https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52
|
||||
- [ ] 命名格式为 `Snap.Hutao x.x.x.msix`
|
||||
- [ ] Merge 文档 PR
|
||||
- [ ] 发布 Release
|
||||
- [ ] 更新日志格式(以 1.6.2 版本为例)
|
||||
|
||||
```jsx
|
||||
## Update log
|
||||
https://hut.ao/en/statements/update-log.html#_1-6-2
|
||||
|
||||
## 更新日志
|
||||
[此处从文档复制]
|
||||
|
||||
## What's Changed
|
||||
**Full Changelog**: https://github.com/DGP-Studio/Snap.Hutao/compare/1.6.0...1.6.2
|
||||
```
|
||||
|
||||
- [ ] 通知用户
|
||||
- [ ] 主分支合并入 release 分支
|
||||
- [ ] 等待 Release 自动发布
|
||||
- [ ] 检查极狐是否同步完成 Release
|
||||
- [ ] 通知用户
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: Final Check
|
||||
description: Understand what you are doing
|
||||
description: Understand what you are doing
|
||||
options:
|
||||
- label: I understand that I will get banned from repository if I don't have permission to use this template
|
||||
required: true
|
||||
required: true
|
||||
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -13,4 +13,8 @@ updates:
|
||||
groups:
|
||||
packages:
|
||||
patterns:
|
||||
- "*"
|
||||
- "*"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/.github/workflows" # GitHub Workflows
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
30
.github/workflows/alpha.yml
vendored
30
.github/workflows/alpha.yml
vendored
@@ -12,16 +12,29 @@ on:
|
||||
- '.gitmodules'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- '**.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
paths-ignore:
|
||||
- '.gitattributes'
|
||||
- '.github/**'
|
||||
- '.gitignore'
|
||||
- '.gitmodules'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- '**.yml'
|
||||
- '**.resx'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4.0.0
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0
|
||||
|
||||
@@ -33,20 +46,21 @@ jobs:
|
||||
VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }}
|
||||
|
||||
- name: Sign Msix
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
shell: pwsh
|
||||
run: |
|
||||
[System.Convert]::FromBase64String("${{ secrets.CERTIFICATE }}") | Set-Content -AsByteStream temp.pfx
|
||||
signtool.exe sign /debug /v /a /fd SHA256 /f temp.pfx /p ${{ secrets.PW }} ${{ github.workspace }}\src\output\Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
|
||||
|
||||
- name: Upload signed msix
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v3
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}
|
||||
path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
|
||||
|
||||
- name: Add summary
|
||||
if: success()
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$summary = "
|
||||
@@ -57,7 +71,9 @@ jobs:
|
||||
> 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 请安装 [Snap.Hutao.CI.cer](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate/Snap.Hutao.CI.cer) 以安装测试版安装包
|
||||
> 请注意,从 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) 以安装测试版安装包
|
||||
"
|
||||
|
||||
echo $summary >> $Env:GITHUB_STEP_SUMMARY
|
||||
echo $summary >> $Env:GITHUB_STEP_SUMMARY
|
||||
|
||||
16
.github/workflows/close_stale.yml
vendored
Normal file
16
.github/workflows/close_stale.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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.'
|
||||
days-before-stale: 7
|
||||
days-before-close: 3
|
||||
close-issue-reason: not_planned
|
||||
20
.github/workflows/issue_similarity.yml
vendored
Normal file
20
.github/workflows/issue_similarity.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Issues Similarity Analysis
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
similarity-analysis:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: analysis
|
||||
uses: actions-cool/issues-similarity-analysis@v1
|
||||
with:
|
||||
filter-threshold: 0.5
|
||||
comment-title: '### Probable Similar Topics'
|
||||
title-excludes: '[Publish]:,[Bug]:,[Feat]:,[Network]:,[ENG]'
|
||||
comment-body: '${index}. ${similarity} #${number}'
|
||||
show-footer: false
|
||||
show-mentioned: true
|
||||
since-days: 365
|
||||
26
.github/workflows/lock_closed_issues.yml
vendored
Normal file
26
.github/workflows/lock_closed_issues.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: 'Lock Threads'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
concurrency:
|
||||
group: lock-threads
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
issue-inactive-days: '30'
|
||||
issue-comment: 'This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related topic.'
|
||||
issue-lock-reason: 'resolved'
|
||||
process-only: 'issues'
|
||||
log-output: false
|
||||
77
.gitlab-ci.yml
Normal file
77
.gitlab-ci.yml
Normal file
@@ -0,0 +1,77 @@
|
||||
stages:
|
||||
- fetch
|
||||
- release
|
||||
- refresh
|
||||
|
||||
Fetch:
|
||||
stage: fetch
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
tags:
|
||||
- us3
|
||||
script:
|
||||
- apt-get update -qy
|
||||
- apt-get install -y curl jq
|
||||
- RELEASE_INFO=$(curl -sSL "https://api.github.com/repos/$CI_PROJECT_PATH/releases/latest")
|
||||
- ASSET_URL=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.name | endswith(".msix")) | .browser_download_url')
|
||||
- SHA256SUMS_URL=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.name == "SHA256SUMS") | .browser_download_url')
|
||||
- curl -LJO "$ASSET_URL"
|
||||
- curl -LJO "$SHA256SUMS_URL"
|
||||
- FILE_NAME=$(basename "$ASSET_URL")
|
||||
- SHA256SUMS_NAME=$(basename "$SHA256SUMS_URL")
|
||||
- echo "File name at script stage is $FILE_NAME"
|
||||
- echo "SHA256SUMS name at script stage is $SHA256SUMS_NAME"
|
||||
- echo "THIS_FILE_NAME=$FILE_NAME" >> next.env
|
||||
- echo "THIS_SHA256SUMS_NAME=$SHA256SUMS_NAME" >> next.env
|
||||
after_script:
|
||||
- echo "Current Job ID is $CI_JOB_ID"
|
||||
- echo "THIS_JOB_ID=$CI_JOB_ID" >> next.env
|
||||
artifacts:
|
||||
paths:
|
||||
- "*.msix"
|
||||
- "SHA256SUMS"
|
||||
expire_in: 180 days
|
||||
reports:
|
||||
dotenv: next.env
|
||||
|
||||
release:
|
||||
stage: release
|
||||
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
needs:
|
||||
- job: Fetch
|
||||
artifacts: true
|
||||
variables:
|
||||
TAG: '$CI_COMMIT_TAG'
|
||||
script:
|
||||
- echo "Create Release $TAG"
|
||||
- echo "$THIS_JOB_ID"
|
||||
- echo "$THIS_FILE_NAME"
|
||||
release:
|
||||
name: '$TAG'
|
||||
tag_name: '$TAG'
|
||||
ref: '$TAG'
|
||||
description: 'Release $TAG by CI'
|
||||
assets:
|
||||
links:
|
||||
- name: "$THIS_FILE_NAME"
|
||||
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_FILE_NAME?inline=false"
|
||||
link_type: package
|
||||
- name: "$THIS_SHA256SUMS_NAME"
|
||||
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_SHA256SUMS_NAME?inline=false"
|
||||
link_type: other
|
||||
- name: "artifact_archive"
|
||||
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/download?file_type=archive"
|
||||
link_type: other
|
||||
|
||||
Refresh:
|
||||
stage: refresh
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
needs:
|
||||
- job: release
|
||||
script:
|
||||
- apt-get install -y curl
|
||||
- curl -X PATCH "$PURGE_URL"
|
||||
- curl -X POST -o /dev/null "$UPLOAD_OSS_URL"
|
||||
@@ -4,13 +4,15 @@
|
||||
|
||||
### Setup Snap.Hutao Project
|
||||
|
||||
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/)
|
||||
2. Open Visual Studio Installer to complete Visual Studio installation
|
||||
- You need to install `.NET desktop development`, `Desktop development with C++` and `Universal Windows Platform development` components
|
||||
3. Install `Single-project MSIX Packaging Tools for VS 2022` provided by Microsoft in Visual Studio marketplace
|
||||
4. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device
|
||||
5. Switch git branch to `develop`
|
||||
6. Open project solution with your Visual Studio and then you are ready to go
|
||||
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/).
|
||||
- No need to select workloads; Visual Studio will handle it automatically.
|
||||
- Close Visual Studio Installer to ensure a smooth installation experience for workloads.
|
||||
- If using Visual Studio 2022 17.9 preview, skip step 5, as automatic extension installation is supported in this version.
|
||||
2. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device.
|
||||
3. Switch to the`develop` branch using git.
|
||||
4. Open the project solution with your Visual Studio. Visual Studio will prompt you to install the necessary workloads, closing and reopening automatically.
|
||||
5. (For Visual Studio 2022 17.8) Install the [Single-project MSIX Packaging Tools for VS 2022](https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17) provided by Microsoft in Visual Studio marketplace.
|
||||
6. Open the project solution with your Visual Studio, and you are ready to go.
|
||||
|
||||
### Start Pull Request
|
||||
|
||||
|
||||
36
README.md
36
README.md
@@ -1,19 +1,36 @@
|
||||

|
||||

|
||||
|
||||
|
||||
胡桃工具箱是一款以 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
|
||||
|
||||
[<img src="https://get.microsoft.com/images/zh-cn%20light.svg" width="30%" height="30%">](https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52)
|
||||
 [](https://github.com/DGP-Studio/Snap.Hutao/releases/latest) []()
|
||||
|
||||
---
|
||||
|
||||
你可以按照[快速开始](https://hut.ao/zh/quick-start.html)文档中提供的流程安装并设置 Snap Hutao。
|
||||
|
||||
You can follow the instructions in the [Quick Start](https://hut.ao/en/quick-start.html) document to install and set up Snap Hutao.
|
||||
|
||||
## 本地化翻译 / Localization
|
||||
|
||||
].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)
|
||||
|
||||
Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。
|
||||
|
||||
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.
|
||||
|
||||
## 社区 / Community
|
||||
|
||||
[](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
|
||||
|
||||
@@ -22,9 +39,7 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
||||
|
||||
### 特定的原神项目 / Specific Genshin-related Projects
|
||||
|
||||
* [biuuu/genshin-wish-export](https://github.com/biuuu/genshin-wish-export)
|
||||
* [xunkong/xunkong](https://github.com/xunkong/xunkong)
|
||||
* [YuehaiTeam/cocogoat](https://github.com/YuehaiTeam/cocogoat)
|
||||
* [Scighost/Starward](https://github.com/Scighost/Starward)
|
||||
|
||||
### 使用的技术栈 / Tech Stack
|
||||
|
||||
@@ -35,7 +50,6 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
||||
* [dotnet/efcore](https://github.com/dotnet/efcore)
|
||||
* [dotnet/runtime](https://github.com/dotnet/runtime)
|
||||
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
|
||||
* [microsoft/CsWin32](https://github.com/microsoft/CsWin32)
|
||||
* [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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
# CI process script for Snap.Hutao
|
||||
# Usage:
|
||||
# 1. Append the script in Pipelines
|
||||
# 2. Upload the pfx and cer certificates to Pipelines Library secrets
|
||||
# 3. Permit the pfx usage
|
||||
# 4. Add a `pw` variable in the script variables, which is pfx password
|
||||
# 5. Connect the GitHub in project settings
|
||||
# 6. Run
|
||||
|
||||
trigger: none
|
||||
pr: none
|
||||
# trigger:
|
||||
# branches:
|
||||
# include:
|
||||
# - main
|
||||
# - develop
|
||||
# paths:
|
||||
# exclude:
|
||||
# - README.md
|
||||
# - azure-pipelines.yml
|
||||
# - .github/ISSUE_TEMPLATE/*.yml
|
||||
# - .github/workflows/*.yml
|
||||
# - src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
|
||||
# pr:
|
||||
# branches:
|
||||
# include:
|
||||
# - main
|
||||
# paths:
|
||||
# exclude:
|
||||
# - README.md
|
||||
# - azure-pipelines.yml
|
||||
# - .github/ISSUE_TEMPLATE/*.yml
|
||||
# - .github/workflows/*.yml
|
||||
# - src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
|
||||
|
||||
|
||||
pool:
|
||||
name: Default
|
||||
demands: agent.name -equals Hutao-Server
|
||||
|
||||
variables:
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
solution: '$(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao.sln'
|
||||
project: $(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj'
|
||||
buildPlatform: 'x64'
|
||||
buildConfiguration: 'Release'
|
||||
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: Install dotNet
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '8.x'
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: dotnet cake
|
||||
inputs:
|
||||
script: dotnet tool restore && dotnet cake
|
||||
|
||||
- task: MsixSigning@1
|
||||
name: signMsix
|
||||
displayName: Sign MSIX package
|
||||
inputs:
|
||||
package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix'
|
||||
certificate: 'DGP_Studio_CI.pfx'
|
||||
passwordVariable: 'pw'
|
||||
condition: succeeded()
|
||||
|
||||
- task: DownloadSecureFile@1
|
||||
name: cerFile
|
||||
displayName: Download Root CA
|
||||
inputs:
|
||||
secureFile: 'Snap.Hutao.CI.cer'
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
targetPath: '$(Build.ArtifactStagingDirectory)'
|
||||
artifact: 'Snap.Hutao.Alpha-$(version).msix'
|
||||
publishLocation: 'pipeline'
|
||||
|
||||
#- task: GitHubRelease@1
|
||||
# inputs:
|
||||
# gitHubConnection: 'github.com_Masterain'
|
||||
# repositoryName: 'DGP-Automation/Hutao-Auto-Release'
|
||||
# action: 'create'
|
||||
# target: '$(Build.SourceVersion)'
|
||||
# tagSource: 'userSpecifiedTag'
|
||||
# tag: '$(version)'
|
||||
# title: '$(version)'
|
||||
# releaseNotesSource: 'inline'
|
||||
# releaseNotesInline: |
|
||||
# ## 普通用户请勿下载
|
||||
# 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
|
||||
#
|
||||
# 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
|
||||
#
|
||||
# assets: |
|
||||
# $(Build.ArtifactStagingDirectory)/*
|
||||
# $(cerFile.secureFilePath)
|
||||
# isPreRelease: true
|
||||
# changeLogCompareToRelease: 'lastFullRelease'
|
||||
# changeLogType: 'commitBased'
|
||||
|
||||
|
||||
- task: rclone@1
|
||||
displayName: Upload CI via Rclone
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
||||
inputs:
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix downloadDGPCN:/releases/Alpha/'
|
||||
configPath: 'C:\agent\_work\_tasks\rclone.conf'
|
||||
|
||||
- task: rclone@1
|
||||
displayName: Upload PR CI via Rclone
|
||||
condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
|
||||
inputs:
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix downloadDGPCN:/releases/PR/'
|
||||
configPath: 'C:\agent\_work\_tasks\rclone.conf'
|
||||
88
build.cake
88
build.cake
@@ -1,5 +1,5 @@
|
||||
#tool "nuget:?package=nuget.commandline&version=6.5.0"
|
||||
#addin nuget:?package=Cake.Http&version=3.0.2
|
||||
#tool "nuget:?package=nuget.commandline&version=6.9.1"
|
||||
#addin nuget:?package=Cake.Http&version=4.0.0
|
||||
|
||||
var target = Argument("target", "Build");
|
||||
var configuration = Argument("configuration", "Release");
|
||||
@@ -28,43 +28,33 @@ string manifest
|
||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Package.appxmanifest");
|
||||
}
|
||||
|
||||
if (AzurePipelines.IsRunningOnAzurePipelines)
|
||||
{
|
||||
repoDir = AzurePipelines.Environment.Build.SourcesDirectory.FullPath;
|
||||
outputPath = AzurePipelines.Environment.Build.ArtifactStagingDirectory.FullPath;
|
||||
|
||||
var versionAuth = HasEnvironmentVariable("VERSION_API_TOKEN") ? EnvironmentVariable("VERSION_API_TOKEN") : throw new Exception("Cannot find VERSION_API_TOKEN");
|
||||
version = HttpGet(
|
||||
"https://internal.snapgenshin.cn/BuildIntergration/RequestNewVersion",
|
||||
new HttpSettings
|
||||
{
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
{ "Authorization", versionAuth }
|
||||
}
|
||||
}
|
||||
);
|
||||
Information($"Version: {version}");
|
||||
|
||||
AzurePipelines.Commands.SetVariable("version", version);
|
||||
}
|
||||
else if (GitHubActions.IsRunningOnGitHubActions)
|
||||
if (GitHubActions.IsRunningOnGitHubActions)
|
||||
{
|
||||
repoDir = GitHubActions.Environment.Workflow.Workspace.FullPath;
|
||||
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
|
||||
|
||||
var versionAuth = HasEnvironmentVariable("VERSION_API_TOKEN") ? EnvironmentVariable("VERSION_API_TOKEN") : throw new Exception("Cannot find VERSION_API_TOKEN");
|
||||
version = HttpGet(
|
||||
"https://internal.snapgenshin.cn/BuildIntergration/RequestNewVersion",
|
||||
new HttpSettings
|
||||
{
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
if (GitHubActions.Environment.PullRequest.IsPullRequest)
|
||||
{
|
||||
version = System.DateTime.Now.ToString("yyyy.M.d.0");
|
||||
|
||||
Information("Is Pull Request. Skip version.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var versionAuth = HasEnvironmentVariable("VERSION_API_TOKEN") ? EnvironmentVariable("VERSION_API_TOKEN") : throw new Exception("Cannot find VERSION_API_TOKEN");
|
||||
version = HttpGet(
|
||||
"https://internal.snapgenshin.cn/BuildIntergration/RequestNewVersion",
|
||||
new HttpSettings
|
||||
{
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
{ "Authorization", versionAuth }
|
||||
}
|
||||
}
|
||||
);
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
|
||||
GitHubActions.Commands.SetOutputParameter("version", version);
|
||||
}
|
||||
@@ -79,6 +69,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")
|
||||
@@ -106,7 +105,7 @@ Task("Generate AppxManifest")
|
||||
|
||||
var content = System.IO.File.ReadAllText(manifest);
|
||||
|
||||
if (AzurePipelines.IsRunningOnAzurePipelines || GitHubActions.IsRunningOnGitHubActions)
|
||||
if (GitHubActions.IsRunningOnGitHubActions)
|
||||
{
|
||||
Information("Using CI configuraion");
|
||||
content = content
|
||||
@@ -114,7 +113,7 @@ Task("Generate AppxManifest")
|
||||
.Replace("胡桃", "胡桃 Alpha")
|
||||
.Replace("DGP Studio", "DGP Studio CI");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"7f0db578-026f-4e0b-a75b-d5d06bb0a74c\"");
|
||||
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=DGP Studio CI\"");
|
||||
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}\"");
|
||||
}
|
||||
else if (AppVeyor.IsRunningOnAppVeyor)
|
||||
@@ -122,6 +121,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);
|
||||
|
||||
@@ -175,7 +185,7 @@ Task("Build MSIX")
|
||||
.Does(() =>
|
||||
{
|
||||
var arguments = "arguments";
|
||||
if (AzurePipelines.IsRunningOnAzurePipelines || GitHubActions.IsRunningOnGitHubActions)
|
||||
if (GitHubActions.IsRunningOnGitHubActions)
|
||||
{
|
||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix");
|
||||
}
|
||||
@@ -183,6 +193,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.
@@ -124,9 +124,6 @@ dotnet_diagnostic.SA1623.severity = none
|
||||
# SA1636: File header copyright text should match
|
||||
dotnet_diagnostic.SA1636.severity = none
|
||||
|
||||
# SA1414: Tuple types in signatures should have element names
|
||||
dotnet_diagnostic.SA1414.severity = none
|
||||
|
||||
# SA0001: XML comment analysis disabled
|
||||
dotnet_diagnostic.SA0001.severity = none
|
||||
csharp_style_prefer_parameter_null_checking = true:suggestion
|
||||
@@ -325,7 +322,6 @@ dotnet_diagnostic.CA2227.severity = suggestion
|
||||
# CA2251: 使用 “string.Equals”
|
||||
dotnet_diagnostic.CA2251.severity = suggestion
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
dotnet_diagnostic.SA1010.severity = none
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
11
src/Snap.Hutao/.vsconfig
Normal file
11
src/Snap.Hutao/.vsconfig
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"components": [
|
||||
"Microsoft.VisualStudio.Workload.ManagedDesktop",
|
||||
"Microsoft.VisualStudio.Workload.NativeDesktop",
|
||||
"Microsoft.VisualStudio.Workload.Universal"
|
||||
],
|
||||
"extensions": [
|
||||
"https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Snap.Hutao.Test.IncomingFeature;
|
||||
|
||||
[TestClass]
|
||||
public class GameRegistryContentTest
|
||||
{
|
||||
private static readonly JsonSerializerOptions RegistryContentSerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
};
|
||||
|
||||
[TestMethod]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void GetRegistryContent()
|
||||
{
|
||||
GetRegistryContentCore(@"Software\miHoYo\原神");
|
||||
GetRegistryContentCore(@"Software\miHoYo\Genshin Impact");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void GetRegistryContentCore(string subkey)
|
||||
{
|
||||
using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64))
|
||||
{
|
||||
RegistryKey? gameKey = key.OpenSubKey(subkey);
|
||||
Assert.IsNotNull(gameKey);
|
||||
|
||||
Dictionary<string, object> data = [];
|
||||
foreach (string valueName in gameKey.GetValueNames())
|
||||
{
|
||||
data[valueName] = gameKey.GetValueKind(valueName) switch
|
||||
{
|
||||
RegistryValueKind.DWord => (int)gameKey.GetValue(valueName)!,
|
||||
RegistryValueKind.Binary => GetStringOrObject((byte[])gameKey.GetValue(valueName)!),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
|
||||
Console.WriteLine($"Subkey: {subkey}");
|
||||
Console.WriteLine(JsonSerializer.Serialize(data, RegistryContentSerializerOptions));
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe object GetStringOrObject(byte[] bytes)
|
||||
{
|
||||
fixed (byte* pByte = bytes)
|
||||
{
|
||||
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
|
||||
string temp = Encoding.UTF8.GetString(span);
|
||||
|
||||
if (temp.AsSpan()[0] is '{' or '[')
|
||||
{
|
||||
return JsonSerializer.Deserialize<JsonElement>(temp);
|
||||
}
|
||||
|
||||
return temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,9 +160,39 @@ public sealed class GeniusInvokationDecoding
|
||||
|
||||
ushort[] testKnownResult =
|
||||
[
|
||||
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
|
||||
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
|
||||
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
|
||||
060,
|
||||
019,
|
||||
001,
|
||||
079,
|
||||
120,
|
||||
120,
|
||||
129,
|
||||
151,
|
||||
151,
|
||||
153,
|
||||
153,
|
||||
181,
|
||||
184,
|
||||
184,
|
||||
185,
|
||||
185,
|
||||
194,
|
||||
194,
|
||||
200,
|
||||
200,
|
||||
201,
|
||||
201,
|
||||
217,
|
||||
217,
|
||||
219,
|
||||
241,
|
||||
241,
|
||||
244,
|
||||
244,
|
||||
245,
|
||||
245,
|
||||
270,
|
||||
270,
|
||||
];
|
||||
|
||||
CollectionAssert.AreEqual(resultArray, testKnownResult);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
@@ -12,10 +12,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
|
||||
"allowMarshaling": true,
|
||||
"useSafeHandles": false,
|
||||
"comInterop": {
|
||||
"preserveSigMethods": [
|
||||
"IFileOpenDialog.Show",
|
||||
"IFileSaveDialog.Show"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// COMCTL32
|
||||
DefSubclassProc
|
||||
RemoveWindowSubclass
|
||||
SetWindowSubclass
|
||||
|
||||
// DWMAPI
|
||||
DwmSetWindowAttribute
|
||||
|
||||
// GDI32
|
||||
GetDeviceCaps
|
||||
|
||||
// KERNEL32
|
||||
AllocConsole
|
||||
CloseHandle
|
||||
CreateEventW
|
||||
CreateRemoteThread
|
||||
FreeConsole
|
||||
GetModuleHandleW
|
||||
GetProcAddress
|
||||
K32EnumProcessModules
|
||||
K32GetModuleBaseNameW
|
||||
K32GetModuleInformation
|
||||
ReadProcessMemory
|
||||
SetConsoleTitle
|
||||
SetEvent
|
||||
VirtualAlloc
|
||||
VirtualAllocEx
|
||||
VirtualFree
|
||||
VirtualFreeEx
|
||||
WaitForSingleObject
|
||||
WriteProcessMemory
|
||||
|
||||
// OLE32
|
||||
CoCreateInstance
|
||||
CoWaitForMultipleObjects
|
||||
|
||||
// SHELL32
|
||||
SHCreateItemFromParsingName
|
||||
|
||||
// USER32
|
||||
AttachThreadInput
|
||||
FindWindowExW
|
||||
GetCursorPos
|
||||
GetDC
|
||||
GetDpiForWindow
|
||||
GetForegroundWindow
|
||||
GetWindowPlacement
|
||||
GetWindowThreadProcessId
|
||||
ReleaseDC
|
||||
RegisterHotKey
|
||||
SendInput
|
||||
SetForegroundWindow
|
||||
UnregisterHotKey
|
||||
|
||||
// COM
|
||||
FileOpenDialog
|
||||
FileSaveDialog
|
||||
IFileOpenDialog
|
||||
IFileSaveDialog
|
||||
IPersistFile
|
||||
IShellLinkW
|
||||
ShellLink
|
||||
SHELL_LINK_DATA_FLAGS
|
||||
|
||||
// WinRT
|
||||
IMemoryBufferByteAccess
|
||||
|
||||
// Const value
|
||||
INFINITE
|
||||
RPC_E_WRONG_THREAD
|
||||
MAX_PATH
|
||||
WM_GETMINMAXINFO
|
||||
WM_HOTKEY
|
||||
WM_NCRBUTTONDOWN
|
||||
WM_NCRBUTTONUP
|
||||
WM_NULL
|
||||
|
||||
// Type & Enum definition
|
||||
HRESULT_FROM_WIN32
|
||||
SLGP_FLAGS
|
||||
|
||||
// System.Threading
|
||||
LPTHREAD_START_ROUTINE
|
||||
|
||||
// UI.WindowsAndMessaging
|
||||
MINMAXINFO
|
||||
|
||||
// System.Com
|
||||
CWMO_FLAGS
|
||||
@@ -1,17 +0,0 @@
|
||||
using System;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Com;
|
||||
|
||||
namespace Windows.Win32;
|
||||
|
||||
internal static partial class PInvoke
|
||||
{
|
||||
/// <inheritdoc cref="CoCreateInstance(Guid*, object, CLSCTX, Guid*, out object)"/>
|
||||
internal static unsafe HRESULT CoCreateInstance<TClass, TInterface>(object? pUnkOuter, CLSCTX dwClsContext, out TInterface ppv)
|
||||
where TInterface : class
|
||||
{
|
||||
HRESULT hr = CoCreateInstance(typeof(TClass).GUID, pUnkOuter, dwClsContext, typeof(TInterface).GUID, out object o);
|
||||
ppv = (TInterface)o;
|
||||
return hr;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="NativeMethods.json" />
|
||||
<None Remove="NativeMethods.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="NativeMethods.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
[assembly: InternalsVisibleTo("Snap.Hutao")]
|
||||
|
||||
namespace Snap.Hutao.Win32;
|
||||
|
||||
/// <summary>
|
||||
/// 结构体封送
|
||||
/// </summary>
|
||||
internal static class StructMarshal
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的 <see cref="Windows.Win32.UI.WindowsAndMessaging.WINDOWPLACEMENT"/>
|
||||
/// </summary>
|
||||
/// <returns>新的实例</returns>
|
||||
public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
|
||||
{
|
||||
return new() { length = unchecked((uint)sizeof(WINDOWPLACEMENT)) };
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Windows.Win32.CsWin32.InteropServices;
|
||||
|
||||
internal class WinRTCustomMarshaler : ICustomMarshaler
|
||||
{
|
||||
private static readonly string? AssemblyFullName = typeof(Windows.Foundation.IMemoryBuffer).Assembly.FullName;
|
||||
|
||||
private readonly string className;
|
||||
private bool lookedForFromAbi;
|
||||
private MethodInfo? fromAbiMethod;
|
||||
|
||||
private WinRTCustomMarshaler(string className)
|
||||
{
|
||||
this.className = className;
|
||||
}
|
||||
|
||||
public static ICustomMarshaler GetInstance(string cookie)
|
||||
{
|
||||
return new WinRTCustomMarshaler(cookie);
|
||||
}
|
||||
|
||||
public void CleanUpManagedData(object ManagedObj)
|
||||
{
|
||||
}
|
||||
|
||||
public void CleanUpNativeData(nint pNativeData)
|
||||
{
|
||||
Marshal.Release(pNativeData);
|
||||
}
|
||||
|
||||
public int GetNativeDataSize()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public nint MarshalManagedToNative(object ManagedObj)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public object MarshalNativeToManaged(nint thisPtr)
|
||||
{
|
||||
return className switch
|
||||
{
|
||||
"Windows.System.DispatcherQueueController" => Windows.System.DispatcherQueueController.FromAbi(thisPtr),
|
||||
_ => MarshalNativeToManagedSlow(thisPtr),
|
||||
};
|
||||
}
|
||||
|
||||
private object MarshalNativeToManagedSlow(nint pNativeData)
|
||||
{
|
||||
if (!lookedForFromAbi)
|
||||
{
|
||||
Type? type = Type.GetType($"{className}, {AssemblyFullName}");
|
||||
|
||||
fromAbiMethod = type?.GetMethod("FromAbi");
|
||||
lookedForFromAbi = true;
|
||||
}
|
||||
|
||||
if (fromAbiMethod is not null)
|
||||
{
|
||||
return fromAbiMethod.Invoke(default, new object[] { pNativeData })!;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Marshal.GetObjectForIUnknown(pNativeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,11 @@ EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A95A964-04B1-477A-BDE7-505525B3CAD8}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.vsconfig = .vsconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Win32", "Snap.Hutao.Win32\Snap.Hutao.Win32.csproj", "{0F7ABEB2-5107-4037-B9DC-84D288FB0801}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -66,22 +65,6 @@ Global
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources/>
|
||||
<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,16 +23,20 @@
|
||||
<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
|
||||
x:Key="LargeGridViewItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
TargetType="GridViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Margin" Value="0,0,12,12"/>
|
||||
</Style>
|
||||
<Style
|
||||
|
||||
@@ -6,8 +6,12 @@ using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Shell;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using static Snap.Hutao.Core.Logging.ConsoleVirtualTerminalSequences;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -20,24 +24,22 @@ 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 const string AppInstanceKey = "main";
|
||||
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IActivation activation;
|
||||
private readonly ILogger<App> logger;
|
||||
@@ -50,7 +52,6 @@ public sealed partial class App : Application
|
||||
{
|
||||
// Load app resource
|
||||
InitializeComponent();
|
||||
|
||||
activation = serviceProvider.GetRequiredService<IActivation>();
|
||||
logger = serviceProvider.GetRequiredService<ILogger<App>>();
|
||||
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
|
||||
@@ -64,25 +65,21 @@ public sealed partial class App : Application
|
||||
try
|
||||
{
|
||||
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
AppInstance firstInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey);
|
||||
|
||||
if (firstInstance.IsCurrent)
|
||||
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
|
||||
{
|
||||
logger.LogInformation(ConsoleBanner);
|
||||
|
||||
// manually invoke
|
||||
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
|
||||
activation.InitializeWith(firstInstance);
|
||||
|
||||
LogDiagnosticInformation();
|
||||
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Redirect the activation (and args) to the "main" instance, and exit.
|
||||
firstInstance.RedirectActivationTo(activatedEventArgs);
|
||||
Process.GetCurrentProcess().Kill();
|
||||
Exit();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow));
|
||||
LogDiagnosticInformation();
|
||||
|
||||
// manually invoke
|
||||
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
|
||||
activation.Initialize();
|
||||
|
||||
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -95,8 +92,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));
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,12 @@ internal static class ControlAnimationConstants
|
||||
/// <summary>
|
||||
/// 图像淡入
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageFadeIn = TimeSpan.FromSeconds(0.3);
|
||||
public static readonly TimeSpan ImageScaleFadeIn = TimeSpan.FromSeconds(0.3);
|
||||
|
||||
/// <summary>
|
||||
/// 图像淡出
|
||||
/// </summary>
|
||||
public static readonly TimeSpan ImageFadeOut = TimeSpan.FromSeconds(0.2);
|
||||
public static readonly TimeSpan ImageScaleFadeOut = TimeSpan.FromSeconds(0.2);
|
||||
|
||||
public static readonly TimeSpan ImageOpacityFadeInOut = TimeSpan.FromSeconds(1);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
[DependencyProperty("Period", typeof(TimeSpan))]
|
||||
[DependencyProperty("Command", typeof(ICommand))]
|
||||
[DependencyProperty("CommandParameter", typeof(object))]
|
||||
internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavior : BehaviorBase<FrameworkElement>, IDisposable
|
||||
{
|
||||
private TaskCompletionSource acutalThemeChangedTaskCompletionSource = new();
|
||||
private CancellationTokenSource periodicTimerCancellationTokenSource = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
periodicTimerCancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
protected override bool Initialize()
|
||||
{
|
||||
AssociatedObject.ActualThemeChanged += OnActualThemeChanged;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
{
|
||||
RunCoreAsync().SafeForget();
|
||||
}
|
||||
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
periodicTimerCancellationTokenSource.Cancel();
|
||||
AssociatedObject.ActualThemeChanged -= OnActualThemeChanged;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
acutalThemeChangedTaskCompletionSource.TrySetResult();
|
||||
periodicTimerCancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
private void TryExecuteCommand()
|
||||
{
|
||||
if (AssociatedObject is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Command.TryExecute(CommandParameter);
|
||||
}
|
||||
|
||||
private async ValueTask RunCoreAsync()
|
||||
{
|
||||
using (PeriodicTimer timer = new(Period))
|
||||
{
|
||||
do
|
||||
{
|
||||
if (!IsAttached)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ITaskContext taskContext = Ioc.Default.GetRequiredService<ITaskContext>();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
TryExecuteCommand();
|
||||
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
try
|
||||
{
|
||||
Task nextTickTask = timer.WaitForNextTickAsync(periodicTimerCancellationTokenSource.Token).AsTask();
|
||||
await Task.WhenAny(nextTickTask, acutalThemeChangedTaskCompletionSource.Task).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
|
||||
acutalThemeChangedTaskCompletionSource = new();
|
||||
periodicTimerCancellationTokenSource = new();
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Brush;
|
||||
|
||||
internal sealed class ColorSegmentCollection : List<IColorSegment>
|
||||
{
|
||||
}
|
||||
@@ -9,5 +9,5 @@ internal interface IColorSegment
|
||||
{
|
||||
Color Color { get; }
|
||||
|
||||
double Value { get; }
|
||||
double Value { get; set; }
|
||||
}
|
||||
@@ -9,36 +9,48 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Control.Brush;
|
||||
|
||||
[DependencyProperty("Source", typeof(List<IColorSegment>), default!, nameof(OnSourceChanged))]
|
||||
[DependencyProperty("Source", typeof(ColorSegmentCollection), default!, nameof(OnSourceChanged))]
|
||||
internal sealed partial class SegmentedBar : ContentControl
|
||||
{
|
||||
private readonly LinearGradientBrush brush = new() { StartPoint = new(0, 0), EndPoint = new(1, 0), };
|
||||
|
||||
public SegmentedBar()
|
||||
{
|
||||
HorizontalContentAlignment = HorizontalAlignment.Stretch;
|
||||
VerticalContentAlignment = VerticalAlignment.Stretch;
|
||||
|
||||
Content = new Rectangle()
|
||||
{
|
||||
Fill = brush,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch,
|
||||
};
|
||||
}
|
||||
|
||||
private static void OnSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
SegmentedBar segmentedBar = (SegmentedBar)obj;
|
||||
UpdateLinearGradientBrush((SegmentedBar)obj);
|
||||
}
|
||||
|
||||
private static void UpdateLinearGradientBrush(SegmentedBar segmentedBar)
|
||||
{
|
||||
GradientStopCollection collection = segmentedBar.brush.GradientStops;
|
||||
collection.Clear();
|
||||
|
||||
if (args.NewValue as List<IColorSegment> is [_, ..] list)
|
||||
ColorSegmentCollection segmentCollection = segmentedBar.Source;
|
||||
|
||||
double total = segmentCollection.Sum(seg => seg.Value);
|
||||
if (total is 0D)
|
||||
{
|
||||
double total = list.Sum(seg => seg.Value);
|
||||
double offset = 0;
|
||||
foreach (ref readonly IColorSegment segment in CollectionsMarshal.AsSpan(list))
|
||||
{
|
||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||
offset += segment.Value / total;
|
||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
double offset = 0;
|
||||
foreach (ref readonly IColorSegment segment in CollectionsMarshal.AsSpan(segmentCollection))
|
||||
{
|
||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||
offset += segment.Value / total;
|
||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Collections;
|
||||
using CommunityToolkit.WinUI.Helpers;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
using NotifyCollectionChangedAction = System.Collections.Specialized.NotifyCollectionChangedAction;
|
||||
|
||||
namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
|
||||
|
||||
internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPropertyChanged, ISupportIncrementalLoading, IComparer<object>
|
||||
where T : class
|
||||
{
|
||||
private readonly List<T> view;
|
||||
private readonly ObservableCollection<SortDescription> sortDescriptions;
|
||||
private readonly Dictionary<string, PropertyInfo?> sortProperties;
|
||||
private readonly bool liveShapingEnabled;
|
||||
private readonly HashSet<string?> observedFilterProperties = [];
|
||||
|
||||
private IList<T> source;
|
||||
private Predicate<T>? filter;
|
||||
private int deferCounter;
|
||||
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
|
||||
|
||||
public AdvancedCollectionView()
|
||||
: this([])
|
||||
{
|
||||
}
|
||||
|
||||
public AdvancedCollectionView(IList<T> source, bool isLiveShaping = false)
|
||||
{
|
||||
liveShapingEnabled = isLiveShaping;
|
||||
view = [];
|
||||
sortDescriptions = [];
|
||||
sortDescriptions.CollectionChanged += SortDescriptionsCollectionChanged;
|
||||
sortProperties = [];
|
||||
Source = source;
|
||||
}
|
||||
|
||||
public event EventHandler<object>? CurrentChanged;
|
||||
|
||||
public event CurrentChangingEventHandler? CurrentChanging;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public event VectorChangedEventHandler<object>? VectorChanged;
|
||||
|
||||
public IList<T> Source
|
||||
{
|
||||
get => source;
|
||||
|
||||
[MemberNotNull(nameof(source))]
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(source, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (source is not null)
|
||||
{
|
||||
DetachPropertyChangedHandler(source);
|
||||
}
|
||||
|
||||
source = value;
|
||||
AttachPropertyChangedHandler(source);
|
||||
|
||||
sourceWeakEventListener?.Detach();
|
||||
|
||||
if (source is INotifyCollectionChanged sourceNotifyCollectionChanged)
|
||||
{
|
||||
sourceWeakEventListener = new WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>(this)
|
||||
{
|
||||
// Call the actual collection changed event
|
||||
OnEventAction = (source, changed, arg3) => SourceNotifyCollectionChangedCollectionChanged(source, arg3),
|
||||
|
||||
// The source doesn't exist anymore
|
||||
OnDetachAction = (listener) =>
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sourceWeakEventListener);
|
||||
sourceNotifyCollectionChanged.CollectionChanged -= sourceWeakEventListener.OnEvent;
|
||||
},
|
||||
};
|
||||
sourceNotifyCollectionChanged.CollectionChanged += sourceWeakEventListener.OnEvent;
|
||||
}
|
||||
|
||||
HandleSourceChanged();
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get => view.Count;
|
||||
}
|
||||
|
||||
public bool IsReadOnly
|
||||
{
|
||||
get => source is null || source.IsReadOnly;
|
||||
}
|
||||
|
||||
public IObservableVector<object> CollectionGroups
|
||||
{
|
||||
get => default!;
|
||||
}
|
||||
|
||||
public T? CurrentItem
|
||||
{
|
||||
get => CurrentPosition > -1 && CurrentPosition < view.Count ? view[CurrentPosition] : default;
|
||||
set => MoveCurrentTo(value);
|
||||
}
|
||||
|
||||
public int CurrentPosition { get; private set; }
|
||||
|
||||
public bool HasMoreItems
|
||||
{
|
||||
get => source is ISupportIncrementalLoading { HasMoreItems: true };
|
||||
}
|
||||
|
||||
public bool IsCurrentAfterLast
|
||||
{
|
||||
get => CurrentPosition >= view.Count;
|
||||
}
|
||||
|
||||
public bool IsCurrentBeforeFirst
|
||||
{
|
||||
get => CurrentPosition < 0;
|
||||
}
|
||||
|
||||
public bool CanFilter
|
||||
{
|
||||
get => true;
|
||||
}
|
||||
|
||||
public Predicate<T>? Filter
|
||||
{
|
||||
get => filter;
|
||||
set
|
||||
{
|
||||
if (filter == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
filter = value;
|
||||
HandleFilterChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanSort
|
||||
{
|
||||
get => true;
|
||||
}
|
||||
|
||||
public IList<SortDescription> SortDescriptions
|
||||
{
|
||||
get => sortDescriptions;
|
||||
}
|
||||
|
||||
public IEnumerable<T> SourceCollection
|
||||
{
|
||||
get => source;
|
||||
}
|
||||
|
||||
public IReadOnlyList<T> View
|
||||
{
|
||||
get => view;
|
||||
}
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get => view[index];
|
||||
set => view[index] = value;
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
HandleSourceChanged();
|
||||
}
|
||||
|
||||
public void RefreshFilter()
|
||||
{
|
||||
HandleFilterChanged();
|
||||
}
|
||||
|
||||
public void RefreshSorting()
|
||||
{
|
||||
HandleSortChanged();
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
return view.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return view.GetEnumerator();
|
||||
}
|
||||
|
||||
public void Add(T item)
|
||||
{
|
||||
source.Add(item);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
source.Clear();
|
||||
}
|
||||
|
||||
public bool Contains(T item)
|
||||
{
|
||||
return view.Contains(item);
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
view.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public bool Remove(T item)
|
||||
{
|
||||
source.Remove(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH007")]
|
||||
public int IndexOf(T? item)
|
||||
{
|
||||
return view.IndexOf(item!);
|
||||
}
|
||||
|
||||
public void Insert(int index, T item)
|
||||
{
|
||||
source.Insert(index, item);
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
Remove(view[index]);
|
||||
}
|
||||
|
||||
public bool MoveCurrentTo(T? item)
|
||||
{
|
||||
return (item is not null && item.Equals(CurrentItem)) || MoveCurrentToIndex(IndexOf(item));
|
||||
}
|
||||
|
||||
public bool MoveCurrentToPosition(int index)
|
||||
{
|
||||
return MoveCurrentToIndex(index);
|
||||
}
|
||||
|
||||
public bool MoveCurrentToFirst()
|
||||
{
|
||||
return MoveCurrentToIndex(0);
|
||||
}
|
||||
|
||||
public bool MoveCurrentToLast()
|
||||
{
|
||||
return MoveCurrentToIndex(view.Count - 1);
|
||||
}
|
||||
|
||||
public bool MoveCurrentToNext()
|
||||
{
|
||||
return MoveCurrentToIndex(CurrentPosition + 1);
|
||||
}
|
||||
|
||||
public bool MoveCurrentToPrevious()
|
||||
{
|
||||
return MoveCurrentToIndex(CurrentPosition - 1);
|
||||
}
|
||||
|
||||
public IAsyncOperation<LoadMoreItemsResult>? LoadMoreItemsAsync(uint count)
|
||||
{
|
||||
return (source as ISupportIncrementalLoading)?.LoadMoreItemsAsync(count);
|
||||
}
|
||||
|
||||
public void ObserveFilterProperty(string propertyName)
|
||||
{
|
||||
observedFilterProperties.Add(propertyName);
|
||||
}
|
||||
|
||||
public void ClearObservedFilterProperties()
|
||||
{
|
||||
observedFilterProperties.Clear();
|
||||
}
|
||||
|
||||
public IDisposable DeferRefresh()
|
||||
{
|
||||
return new NotificationDeferrer(this);
|
||||
}
|
||||
|
||||
int IComparer<object>.Compare(object? x, object? y)
|
||||
{
|
||||
if (sortProperties.Count <= 0)
|
||||
{
|
||||
Type listType = source.GetType();
|
||||
Type? type;
|
||||
|
||||
if (listType.IsGenericType)
|
||||
{
|
||||
type = listType.GetGenericArguments()[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
type = x?.GetType();
|
||||
}
|
||||
|
||||
foreach (SortDescription sd in sortDescriptions)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(sd.PropertyName))
|
||||
{
|
||||
sortProperties[sd.PropertyName] = type?.GetProperty(sd.PropertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (SortDescription sd in sortDescriptions)
|
||||
{
|
||||
object? cx, cy;
|
||||
|
||||
if (string.IsNullOrEmpty(sd.PropertyName))
|
||||
{
|
||||
cx = x;
|
||||
cy = y;
|
||||
}
|
||||
else
|
||||
{
|
||||
PropertyInfo? pi = sortProperties[sd.PropertyName];
|
||||
|
||||
cx = pi?.GetValue(x);
|
||||
cy = pi?.GetValue(y);
|
||||
}
|
||||
|
||||
int cmp = sd.Comparer.Compare(cx, cy);
|
||||
|
||||
if (cmp is not 0)
|
||||
{
|
||||
return sd.Direction is SortDirection.Ascending ? +cmp : -cmp;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
internal void OnPropertyChanged([CallerMemberName] string propertyName = default!)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private void ItemOnPropertyChanged(object? item, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (!liveShapingEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
T typedItem = (T)item;
|
||||
|
||||
bool? filterResult = filter?.Invoke(typedItem);
|
||||
|
||||
if (filterResult.HasValue && observedFilterProperties.Contains(e.PropertyName))
|
||||
{
|
||||
int viewIndex = view.IndexOf(typedItem);
|
||||
if (viewIndex != -1 && !filterResult.Value)
|
||||
{
|
||||
RemoveFromView(viewIndex, typedItem);
|
||||
}
|
||||
else if (viewIndex == -1 && filterResult.Value)
|
||||
{
|
||||
int index = source.IndexOf(typedItem);
|
||||
HandleItemAdded(index, typedItem);
|
||||
}
|
||||
}
|
||||
|
||||
if ((filterResult ?? true) && SortDescriptions.Any(sd => sd.PropertyName == e.PropertyName))
|
||||
{
|
||||
int oldIndex = view.IndexOf(typedItem);
|
||||
|
||||
// Check if item is in view:
|
||||
if (oldIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
view.RemoveAt(oldIndex);
|
||||
int targetIndex = view.BinarySearch(typedItem, this);
|
||||
if (targetIndex < 0)
|
||||
{
|
||||
targetIndex = ~targetIndex;
|
||||
}
|
||||
|
||||
// Only trigger expensive UI updates if the index really changed:
|
||||
if (targetIndex != oldIndex)
|
||||
{
|
||||
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, oldIndex, typedItem));
|
||||
|
||||
view.Insert(targetIndex, typedItem);
|
||||
|
||||
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, targetIndex, typedItem));
|
||||
}
|
||||
else
|
||||
{
|
||||
view.Insert(targetIndex, typedItem);
|
||||
}
|
||||
}
|
||||
else if (string.IsNullOrEmpty(e.PropertyName))
|
||||
{
|
||||
HandleSourceChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void AttachPropertyChangedHandler(IEnumerable items)
|
||||
{
|
||||
if (!liveShapingEnabled || items is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (object item in items)
|
||||
{
|
||||
if (item is INotifyPropertyChanged notifyPropertyChanged)
|
||||
{
|
||||
notifyPropertyChanged.PropertyChanged += ItemOnPropertyChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DetachPropertyChangedHandler(IEnumerable items)
|
||||
{
|
||||
if (!liveShapingEnabled || items is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (object item in items)
|
||||
{
|
||||
if (item is INotifyPropertyChanged notifyPropertyChanged)
|
||||
{
|
||||
notifyPropertyChanged.PropertyChanged -= ItemOnPropertyChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSortChanged()
|
||||
{
|
||||
sortProperties.Clear();
|
||||
view.Sort(this);
|
||||
sortProperties.Clear();
|
||||
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
|
||||
}
|
||||
|
||||
private void HandleFilterChanged()
|
||||
{
|
||||
if (filter is not null)
|
||||
{
|
||||
for (int index = 0; index < view.Count; index++)
|
||||
{
|
||||
T item = view[index];
|
||||
if (filter(item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RemoveFromView(index, item);
|
||||
index--;
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<T> viewHash = new(view);
|
||||
int viewIndex = 0;
|
||||
for (int index = 0; index < source.Count; index++)
|
||||
{
|
||||
T item = source[index];
|
||||
if (viewHash.Contains(item))
|
||||
{
|
||||
viewIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HandleItemAdded(index, item, viewIndex))
|
||||
{
|
||||
viewIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSourceChanged()
|
||||
{
|
||||
sortProperties.Clear();
|
||||
T? currentItem = CurrentItem;
|
||||
view.Clear();
|
||||
foreach (T item in Source)
|
||||
{
|
||||
if (filter is not null && !filter(item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sortDescriptions.Count > 0)
|
||||
{
|
||||
int targetIndex = view.BinarySearch(item, this);
|
||||
if (targetIndex < 0)
|
||||
{
|
||||
targetIndex = ~targetIndex;
|
||||
}
|
||||
|
||||
view.Insert(targetIndex, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
view.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
sortProperties.Clear();
|
||||
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
|
||||
MoveCurrentTo(currentItem);
|
||||
}
|
||||
|
||||
private void SourceNotifyCollectionChangedCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
ArgumentNullException.ThrowIfNull(e.NewItems);
|
||||
AttachPropertyChangedHandler(e.NewItems);
|
||||
if (deferCounter <= 0)
|
||||
{
|
||||
if (e.NewItems?.Count == 1)
|
||||
{
|
||||
object? newItem = e.NewItems[0];
|
||||
ArgumentNullException.ThrowIfNull(newItem);
|
||||
HandleItemAdded(e.NewStartingIndex, (T)newItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
HandleSourceChanged();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
ArgumentNullException.ThrowIfNull(e.OldItems);
|
||||
DetachPropertyChangedHandler(e.OldItems);
|
||||
if (deferCounter <= 0)
|
||||
{
|
||||
if (e.OldItems?.Count == 1)
|
||||
{
|
||||
object? oldItem = e.OldItems[0];
|
||||
ArgumentNullException.ThrowIfNull(oldItem);
|
||||
HandleItemRemoved(e.OldStartingIndex, (T)oldItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
HandleSourceChanged();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
if (deferCounter <= 0)
|
||||
{
|
||||
HandleSourceChanged();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HandleItemAdded(int newStartingIndex, T newItem, int? viewIndex = null)
|
||||
{
|
||||
if (filter is not null && !filter(newItem))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int newViewIndex = view.Count;
|
||||
|
||||
if (sortDescriptions.Count > 0)
|
||||
{
|
||||
sortProperties.Clear();
|
||||
newViewIndex = view.BinarySearch(newItem, this);
|
||||
if (newViewIndex < 0)
|
||||
{
|
||||
newViewIndex = ~newViewIndex;
|
||||
}
|
||||
}
|
||||
else if (filter is not null)
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
HandleSourceChanged();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newStartingIndex == 0 || view.Count == 0)
|
||||
{
|
||||
newViewIndex = 0;
|
||||
}
|
||||
else if (newStartingIndex == source.Count - 1)
|
||||
{
|
||||
newViewIndex = view.Count;
|
||||
}
|
||||
else if (viewIndex.HasValue)
|
||||
{
|
||||
newViewIndex = viewIndex.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0, j = 0; i < source.Count; i++)
|
||||
{
|
||||
if (i == newStartingIndex)
|
||||
{
|
||||
newViewIndex = j;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Equals(view[j], source[i]))
|
||||
{
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view.Insert(newViewIndex, newItem);
|
||||
if (newViewIndex <= CurrentPosition)
|
||||
{
|
||||
CurrentPosition++;
|
||||
}
|
||||
|
||||
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, newViewIndex, newItem));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleItemRemoved(int oldStartingIndex, T oldItem)
|
||||
{
|
||||
if (filter is not null && !filter(oldItem))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldStartingIndex < 0 || oldStartingIndex >= view.Count || !Equals(view[oldStartingIndex], oldItem))
|
||||
{
|
||||
oldStartingIndex = view.IndexOf(oldItem);
|
||||
}
|
||||
|
||||
if (oldStartingIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RemoveFromView(oldStartingIndex, oldItem);
|
||||
}
|
||||
|
||||
private void RemoveFromView(int itemIndex, T item)
|
||||
{
|
||||
view.RemoveAt(itemIndex);
|
||||
if (itemIndex <= CurrentPosition)
|
||||
{
|
||||
CurrentPosition--;
|
||||
}
|
||||
|
||||
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, itemIndex, item));
|
||||
}
|
||||
|
||||
private void SortDescriptionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (deferCounter > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HandleSortChanged();
|
||||
}
|
||||
|
||||
private bool MoveCurrentToIndex(int i)
|
||||
{
|
||||
if (i < -1 || i >= view.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (i == CurrentPosition)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentChangingEventArgs e = new();
|
||||
OnCurrentChanging(e);
|
||||
if (e.Cancel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentPosition = i;
|
||||
OnCurrentChanged(default!);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnCurrentChanging(CurrentChangingEventArgs e)
|
||||
{
|
||||
if (deferCounter > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentChanging?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void OnCurrentChanged(object e)
|
||||
{
|
||||
if (deferCounter > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentChanged?.Invoke(this, e);
|
||||
OnPropertyChanged(nameof(CurrentItem));
|
||||
}
|
||||
|
||||
private void OnVectorChanged(IVectorChangedEventArgs e)
|
||||
{
|
||||
if (deferCounter > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VectorChanged?.Invoke(this, e);
|
||||
OnPropertyChanged(nameof(Count));
|
||||
}
|
||||
|
||||
internal sealed class NotificationDeferrer : IDisposable
|
||||
{
|
||||
private readonly AdvancedCollectionView<T> advancedCollectionView;
|
||||
private readonly T? currentItem;
|
||||
|
||||
public NotificationDeferrer(AdvancedCollectionView<T> acvs)
|
||||
{
|
||||
advancedCollectionView = acvs;
|
||||
currentItem = advancedCollectionView.CurrentItem;
|
||||
advancedCollectionView.deferCounter++;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
advancedCollectionView.MoveCurrentTo(currentItem);
|
||||
advancedCollectionView.deferCounter--;
|
||||
advancedCollectionView.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Collections;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System.Collections;
|
||||
|
||||
namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
|
||||
|
||||
internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
|
||||
where T : class
|
||||
{
|
||||
bool CanFilter { get; }
|
||||
|
||||
bool CanSort { get; }
|
||||
|
||||
object? ICollectionView.CurrentItem
|
||||
{
|
||||
get => CurrentItem;
|
||||
}
|
||||
|
||||
new T? CurrentItem { get; }
|
||||
|
||||
Predicate<T>? Filter { get; set; }
|
||||
|
||||
IList<SortDescription> SortDescriptions { get; }
|
||||
|
||||
IEnumerable<T> SourceCollection { get; }
|
||||
|
||||
object IList<object>.this[int index]
|
||||
{
|
||||
get => this[index];
|
||||
set => this[index] = (T)value;
|
||||
}
|
||||
|
||||
new T this[int index] { get; set; }
|
||||
|
||||
void ICollection<object>.Add(object item)
|
||||
{
|
||||
Add((T)item);
|
||||
}
|
||||
|
||||
void Add(T item);
|
||||
|
||||
void ClearObservedFilterProperties();
|
||||
|
||||
bool ICollection<object>.Contains(object item)
|
||||
{
|
||||
return Contains((T)item);
|
||||
}
|
||||
|
||||
bool Contains(T item);
|
||||
|
||||
void ICollection<object>.CopyTo(object[] array, int arrayIndex)
|
||||
{
|
||||
CopyTo((T[])array, arrayIndex);
|
||||
}
|
||||
|
||||
void CopyTo(T[] array, int arrayIndex);
|
||||
|
||||
IDisposable DeferRefresh();
|
||||
|
||||
IEnumerator<object> IEnumerable<object>.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
new IEnumerator<T> GetEnumerator();
|
||||
|
||||
int IList<object>.IndexOf(object item)
|
||||
{
|
||||
return IndexOf((T)item);
|
||||
}
|
||||
|
||||
int IndexOf(T item);
|
||||
|
||||
void IList<object>.Insert(int index, object item)
|
||||
{
|
||||
Insert(index, (T)item);
|
||||
}
|
||||
|
||||
void Insert(int index, T item);
|
||||
|
||||
bool ICollectionView.MoveCurrentTo(object item)
|
||||
{
|
||||
return MoveCurrentTo((T)item);
|
||||
}
|
||||
|
||||
bool MoveCurrentTo(T item);
|
||||
|
||||
void ObserveFilterProperty(string propertyName);
|
||||
|
||||
void Refresh();
|
||||
|
||||
void RefreshFilter();
|
||||
|
||||
void RefreshSorting();
|
||||
|
||||
bool ICollection<object>.Remove(object item)
|
||||
{
|
||||
return Remove((T)item);
|
||||
}
|
||||
|
||||
bool Remove(T item);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
|
||||
|
||||
internal sealed class VectorChangedEventArgs : IVectorChangedEventArgs
|
||||
{
|
||||
public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = null!)
|
||||
{
|
||||
CollectionChange = cc;
|
||||
Index = (uint)index;
|
||||
}
|
||||
|
||||
public CollectionChange CollectionChange { get; }
|
||||
|
||||
public uint Index { get; }
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Helper;
|
||||
|
||||
[SuppressMessage("", "SH001")]
|
||||
[DependencyProperty("PaneCornerRadius", typeof(CornerRadius), default, nameof(OnPaneCornerRadiusChanged), IsAttached = true, AttachedType = typeof(NavigationView))]
|
||||
public sealed partial class NavigationViewHelper
|
||||
{
|
||||
private static void OnPaneCornerRadiusChanged(DependencyObject dp, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
NavigationView navigationView = (NavigationView)dp;
|
||||
CornerRadius newValue = (CornerRadius)args.NewValue;
|
||||
|
||||
if (navigationView.IsLoaded)
|
||||
{
|
||||
SetNavigationViewPaneCornerRadius(navigationView, newValue);
|
||||
return;
|
||||
}
|
||||
|
||||
navigationView.Loaded += (s, e) =>
|
||||
{
|
||||
NavigationView loadedNavigationView = (NavigationView)s;
|
||||
SetNavigationViewPaneCornerRadius(loadedNavigationView, newValue);
|
||||
};
|
||||
}
|
||||
|
||||
private static void SetNavigationViewPaneCornerRadius(NavigationView navigationView, CornerRadius value)
|
||||
{
|
||||
if (navigationView.FindDescendant("RootSplitView") is SplitView splitView)
|
||||
{
|
||||
splitView.CornerRadius = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ using Microsoft.UI.Xaml.Controls;
|
||||
namespace Snap.Hutao.Control.Helper;
|
||||
|
||||
[SuppressMessage("", "SH001")]
|
||||
[DependencyProperty("LeftPanelMaxWidth", typeof(double), IsAttached = true, AttachedType = typeof(ScrollViewer))]
|
||||
[DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))]
|
||||
public sealed partial class ScrollViewerHelper
|
||||
{
|
||||
|
||||
@@ -20,4 +20,4 @@ public sealed partial class SettingsExpanderHelper
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
internal interface IScopedPageScopeReferenceTracker
|
||||
internal interface IScopedPageScopeReferenceTracker : IDisposable
|
||||
{
|
||||
IServiceScope CreateScope();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
internal interface IXamlElementAccessor;
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
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,6 +21,9 @@ internal sealed class CachedImage : Implementation.ImageEx
|
||||
/// </summary>
|
||||
public CachedImage()
|
||||
{
|
||||
DefaultStyleKey = typeof(CachedImage);
|
||||
DefaultStyleResourceUri = "ms-appx:///Control/Image/CachedImage.xaml".ToUri();
|
||||
|
||||
IsCacheEnabled = true;
|
||||
EnableLazyLoading = false;
|
||||
}
|
||||
@@ -26,12 +31,11 @@ internal sealed class CachedImage : Implementation.ImageEx
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<ImageSource?> 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), HutaoExceptionKind.ImageCacheInvalidUri, 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.
|
||||
@@ -40,12 +44,7 @@ internal sealed class CachedImage : Implementation.ImageEx
|
||||
{
|
||||
// The image is corrupted, remove it.
|
||||
imageCache.Remove(imageUri);
|
||||
return null;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// task was explicitly canceled
|
||||
return null;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Image
|
||||
Name="PlaceholderImage"
|
||||
Margin="{TemplateBinding PlaceholderMargin}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
Source="{TemplateBinding PlaceholderSource}"
|
||||
|
||||
@@ -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);
|
||||
@@ -192,7 +193,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
{
|
||||
await AnimationBuilder
|
||||
.Create()
|
||||
.Opacity(from: 0D, to: 1D, duration: ControlAnimationConstants.ImageFadeIn)
|
||||
.Opacity(from: 0D, to: 1D, duration: ControlAnimationConstants.ImageScaleFadeIn)
|
||||
.StartAsync(this, token)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
@@ -213,7 +214,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
{
|
||||
await AnimationBuilder
|
||||
.Create()
|
||||
.Opacity(from: 1D, to: 0D, duration: ControlAnimationConstants.ImageFadeOut)
|
||||
.Opacity(from: 1D, to: 0D, duration: ControlAnimationConstants.ImageScaleFadeOut)
|
||||
.StartAsync(this, token)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
|
||||
@@ -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,15 +6,12 @@ using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Snap.Hutao.Win32;
|
||||
using System.IO;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
internal delegate void ImageExFailedEventHandler(object sender, ImageExFailedEventArgs e);
|
||||
|
||||
internal delegate void ImageExOpenedEventHandler(object sender, ImageExOpenedEventArgs e);
|
||||
|
||||
[SuppressMessage("", "CA1001")]
|
||||
[SuppressMessage("", "SH003")]
|
||||
[TemplateVisualState(Name = LoadingState, GroupName = CommonGroup)]
|
||||
@@ -22,98 +19,34 @@ internal delegate void ImageExOpenedEventHandler(object sender, ImageExOpenedEve
|
||||
[TemplateVisualState(Name = UnloadedState, GroupName = CommonGroup)]
|
||||
[TemplateVisualState(Name = FailedState, GroupName = CommonGroup)]
|
||||
[TemplatePart(Name = PartImage, Type = typeof(object))]
|
||||
internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
|
||||
[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))]
|
||||
[DependencyProperty("Source", typeof(object), default(object), nameof(SourceChanged))]
|
||||
internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
|
||||
{
|
||||
protected const string PartImage = "Image";
|
||||
protected const string PartPlaceholderImage = "PlaceholderImage";
|
||||
protected const string CommonGroup = "CommonStates";
|
||||
protected const string LoadingState = "Loading";
|
||||
protected const string LoadedState = "Loaded";
|
||||
protected const string UnloadedState = "Unloaded";
|
||||
protected const string FailedState = "Failed";
|
||||
|
||||
private static readonly DependencyProperty StretchProperty = DependencyProperty.Register(nameof(Stretch), typeof(Stretch), typeof(ImageExBase), new PropertyMetadata(Stretch.Uniform));
|
||||
private static readonly DependencyProperty DecodePixelHeightProperty = DependencyProperty.Register(nameof(DecodePixelHeight), typeof(int), typeof(ImageExBase), new PropertyMetadata(0));
|
||||
private static readonly DependencyProperty DecodePixelTypeProperty = DependencyProperty.Register(nameof(DecodePixelType), typeof(int), typeof(ImageExBase), new PropertyMetadata(DecodePixelType.Physical));
|
||||
private static readonly DependencyProperty DecodePixelWidthProperty = DependencyProperty.Register(nameof(DecodePixelWidth), typeof(int), typeof(ImageExBase), new PropertyMetadata(0));
|
||||
private static readonly DependencyProperty IsCacheEnabledProperty = DependencyProperty.Register(nameof(IsCacheEnabled), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false));
|
||||
private static readonly DependencyProperty EnableLazyLoadingProperty = DependencyProperty.Register(nameof(EnableLazyLoading), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false, EnableLazyLoadingChanged));
|
||||
private static readonly DependencyProperty LazyLoadingThresholdProperty = DependencyProperty.Register(nameof(LazyLoadingThreshold), typeof(double), typeof(ImageExBase), new PropertyMetadata(default(double), LazyLoadingThresholdChanged));
|
||||
private static readonly DependencyProperty PlaceholderSourceProperty = DependencyProperty.Register(nameof(PlaceholderSource), typeof(ImageSource), typeof(ImageExBase), new PropertyMetadata(default(ImageSource), PlaceholderSourceChanged));
|
||||
private static readonly DependencyProperty PlaceholderStretchProperty = DependencyProperty.Register(nameof(PlaceholderStretch), typeof(Stretch), typeof(ImageExBase), new PropertyMetadata(default(Stretch)));
|
||||
private static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(ImageExBase), new PropertyMetadata(null, SourceChanged));
|
||||
|
||||
private CancellationTokenSource? tokenSource;
|
||||
private object? lazyLoadingSource;
|
||||
private bool isInViewport;
|
||||
|
||||
public event ImageExFailedEventHandler? ImageExFailed;
|
||||
|
||||
public event ImageExOpenedEventHandler? ImageExOpened;
|
||||
|
||||
public event EventHandler? ImageExInitialized;
|
||||
|
||||
public bool IsInitialized { get; private set; }
|
||||
|
||||
public int DecodePixelHeight
|
||||
{
|
||||
get => (int)GetValue(DecodePixelHeightProperty);
|
||||
set => SetValue(DecodePixelHeightProperty, value);
|
||||
}
|
||||
|
||||
public DecodePixelType DecodePixelType
|
||||
{
|
||||
get => (DecodePixelType)GetValue(DecodePixelTypeProperty);
|
||||
set => SetValue(DecodePixelTypeProperty, value);
|
||||
}
|
||||
|
||||
public int DecodePixelWidth
|
||||
{
|
||||
get => (int)GetValue(DecodePixelWidthProperty);
|
||||
set => SetValue(DecodePixelWidthProperty, value);
|
||||
}
|
||||
|
||||
public Stretch Stretch
|
||||
{
|
||||
get => (Stretch)GetValue(StretchProperty);
|
||||
set => SetValue(StretchProperty, value);
|
||||
}
|
||||
|
||||
public bool IsCacheEnabled
|
||||
{
|
||||
get => (bool)GetValue(IsCacheEnabledProperty);
|
||||
set => SetValue(IsCacheEnabledProperty, value);
|
||||
}
|
||||
|
||||
public bool EnableLazyLoading
|
||||
{
|
||||
get => (bool)GetValue(EnableLazyLoadingProperty);
|
||||
set => SetValue(EnableLazyLoadingProperty, value);
|
||||
}
|
||||
|
||||
public double LazyLoadingThreshold
|
||||
{
|
||||
get => (double)GetValue(LazyLoadingThresholdProperty);
|
||||
set => SetValue(LazyLoadingThresholdProperty, value);
|
||||
}
|
||||
|
||||
public ImageSource PlaceholderSource
|
||||
{
|
||||
get => (ImageSource)GetValue(PlaceholderSourceProperty);
|
||||
set => SetValue(PlaceholderSourceProperty, value);
|
||||
}
|
||||
|
||||
public Stretch PlaceholderStretch
|
||||
{
|
||||
get => (Stretch)GetValue(PlaceholderStretchProperty);
|
||||
set => SetValue(PlaceholderStretchProperty, value);
|
||||
}
|
||||
|
||||
public object Source
|
||||
{
|
||||
get => GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
public bool WaitUntilLoaded
|
||||
{
|
||||
get => true;
|
||||
@@ -121,11 +54,9 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
|
||||
protected object? Image { get; private set; }
|
||||
|
||||
public abstract CompositionBrush GetAlphaMask();
|
||||
protected object? PlaceholderImage { get; private set; }
|
||||
|
||||
protected virtual void OnPlaceholderSourceChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
}
|
||||
public abstract CompositionBrush GetAlphaMask();
|
||||
|
||||
protected virtual Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
@@ -136,61 +67,11 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, LoadedState, true);
|
||||
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
|
||||
}
|
||||
|
||||
protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new FileNotFoundException(e.ErrorMessage)));
|
||||
}
|
||||
|
||||
protected void AttachImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened += handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected void RemoveImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected void AttachImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed += handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected void RemoveImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
@@ -199,11 +80,10 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
RemoveImageFailed(OnImageFailed);
|
||||
|
||||
Image = GetTemplateChild(PartImage);
|
||||
PlaceholderImage = GetTemplateChild(PartPlaceholderImage);
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
ImageExInitialized?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
if (Source is null || !EnableLazyLoading || isInViewport)
|
||||
{
|
||||
lazyLoadingSource = null;
|
||||
@@ -218,42 +98,83 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
AttachImageFailed(OnImageFailed);
|
||||
|
||||
base.OnApplyTemplate();
|
||||
|
||||
void AttachImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened += handler;
|
||||
}
|
||||
}
|
||||
|
||||
void AttachImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed += handler;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed -= handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnableLazyLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ImageExBase control)
|
||||
if (d is not ImageExBase control)
|
||||
{
|
||||
bool value = (bool)e.NewValue;
|
||||
if (value)
|
||||
{
|
||||
control.LayoutUpdated += control.ImageExBase_LayoutUpdated;
|
||||
return;
|
||||
}
|
||||
|
||||
control.InvalidateLazyLoading();
|
||||
}
|
||||
else
|
||||
{
|
||||
control.LayoutUpdated -= control.ImageExBase_LayoutUpdated;
|
||||
}
|
||||
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)
|
||||
if (d is ImageExBase { EnableLazyLoading: true } control)
|
||||
{
|
||||
control.InvalidateLazyLoading();
|
||||
}
|
||||
}
|
||||
|
||||
private static void PlaceholderSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ImageExBase control)
|
||||
{
|
||||
control.OnPlaceholderSourceChanged(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not ImageExBase control)
|
||||
@@ -261,17 +182,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.OldValue is null || e.NewValue is null || !e.OldValue.Equals(e.NewValue))
|
||||
if (e.OldValue is not null && e.NewValue is not null && e.OldValue.Equals(e.NewValue))
|
||||
{
|
||||
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
|
||||
{
|
||||
control.lazyLoadingSource = null;
|
||||
control.SetSource(e.NewValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
control.lazyLoadingSource = e.NewValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
|
||||
{
|
||||
control.lazyLoadingSource = null;
|
||||
control.SetSource(e.NewValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
control.lazyLoadingSource = e.NewValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,11 +224,30 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
|
||||
{
|
||||
VisualStateManager.GoToState(this, LoadedState, true);
|
||||
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "IDE0019")]
|
||||
private void AttachPlaceholderSource(ImageSource? source)
|
||||
{
|
||||
if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.Source = source;
|
||||
}
|
||||
else if (PlaceholderImage is ImageBrush brush)
|
||||
{
|
||||
brush.ImageSource = source;
|
||||
}
|
||||
|
||||
if (source is null)
|
||||
{
|
||||
VisualStateManager.GoToState(this, UnloadedState, true);
|
||||
}
|
||||
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
|
||||
{
|
||||
VisualStateManager.GoToState(this, LoadedState, true);
|
||||
}
|
||||
}
|
||||
|
||||
private async void SetSource(object? source)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
@@ -326,22 +268,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
|
||||
VisualStateManager.GoToState(this, LoadingState, true);
|
||||
|
||||
ImageSource? imageSource = source as ImageSource;
|
||||
if (imageSource is not null)
|
||||
if (source as ImageSource is { } imageSource)
|
||||
{
|
||||
AttachSource(imageSource);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Uri? uri = source as Uri;
|
||||
if (uri is null)
|
||||
if (source as Uri is not { } uri)
|
||||
{
|
||||
string? url = source as string ?? source.ToString();
|
||||
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new UriFormatException("Invalid uri specified.")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -355,61 +294,130 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
{
|
||||
await LoadImageAsync(uri, tokenSource.Token).ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetPlaceholderSource(PlaceholderSource);
|
||||
|
||||
if (ex is OperationCanceledException)
|
||||
{
|
||||
// nothing to do as cancellation has been requested.
|
||||
}
|
||||
else
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void SetPlaceholderSource(object? source)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tokenSource?.Cancel();
|
||||
tokenSource = new();
|
||||
|
||||
AttachPlaceholderSource(null);
|
||||
|
||||
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();
|
||||
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsHttpUri(uri) && !uri.IsAbsoluteUri)
|
||||
{
|
||||
uri = new Uri("ms-appx:///" + uri.OriginalString.TrimStart('/'));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (uri is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImageSource? img = 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);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// nothing to do as cancellation has been requested.
|
||||
}
|
||||
catch (Exception e)
|
||||
catch
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadImageAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
if (imageUri is not null)
|
||||
if (imageUri is null)
|
||||
{
|
||||
if (IsCacheEnabled)
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsCacheEnabled)
|
||||
{
|
||||
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
|
||||
// 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)
|
||||
{
|
||||
// Only attach our image if we still have a valid request.
|
||||
AttachSource(img);
|
||||
AttachSource(bitmap);
|
||||
}
|
||||
}
|
||||
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
|
||||
}
|
||||
else
|
||||
{
|
||||
AttachSource(new BitmapImage(imageUri)
|
||||
{
|
||||
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,
|
||||
});
|
||||
}
|
||||
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ImageExBase_LayoutUpdated(object? sender, object e)
|
||||
private void OnImageExBaseLayoutUpdated(object? sender, object e)
|
||||
{
|
||||
InvalidateLazyLoading();
|
||||
}
|
||||
@@ -440,9 +448,10 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
return;
|
||||
}
|
||||
|
||||
Rect controlRect = TransformToVisual(hostElement)
|
||||
.TransformBounds(new Rect(0, 0, ActualWidth, ActualHeight));
|
||||
Rect controlRect = TransformToVisual(hostElement).TransformBounds(StructMarshal.Rect(ActualSize));
|
||||
double lazyLoadingThreshold = LazyLoadingThreshold;
|
||||
|
||||
// Left/Top 1 Threshold, Right/Bottom 2 Threshold
|
||||
Rect hostRect = new(
|
||||
0 - lazyLoadingThreshold,
|
||||
0 - lazyLoadingThreshold,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
internal sealed class ImageExFailedEventArgs : EventArgs
|
||||
{
|
||||
public ImageExFailedEventArgs(Exception errorException)
|
||||
{
|
||||
ErrorMessage = ErrorException?.Message;
|
||||
ErrorException = errorException;
|
||||
}
|
||||
|
||||
public Exception? ErrorException { get; private set; }
|
||||
|
||||
public string? ErrorMessage { get; private set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
internal sealed class ImageExOpenedEventArgs : EventArgs
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Collections.Specialized;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
@@ -18,14 +19,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/>
|
||||
@@ -82,16 +81,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 +96,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 +162,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 +193,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 +221,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 +253,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Snap.Hutao.Control;
|
||||
|
||||
[TemplateVisualState(Name = "LoadingIn", GroupName = "CommonStates")]
|
||||
[TemplateVisualState(Name = "LoadingOut", GroupName = "CommonStates")]
|
||||
[TemplatePart(Name = "ContentGrid", Type = typeof(FrameworkElement))]
|
||||
internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
|
||||
{
|
||||
public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(Loading), new PropertyMetadata(default(bool), IsLoadingPropertyChanged));
|
||||
@@ -16,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
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:shc="using:Snap.Hutao.Control">
|
||||
|
||||
<Style TargetType="shc:Loading">
|
||||
<Style BasedOn="{StaticResource DefaultLoadingStyle}" TargetType="shc:Loading"/>
|
||||
|
||||
<Style x:Key="DefaultLoadingStyle" TargetType="shc:Loading">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
|
||||
@@ -18,12 +18,15 @@ internal sealed class FontIconExtension : MarkupExtension
|
||||
/// </summary>
|
||||
public string Glyph { get; set; } = default!;
|
||||
|
||||
public double FontSize { get; set; } = 12;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override object ProvideValue()
|
||||
{
|
||||
return new FontIcon()
|
||||
{
|
||||
Glyph = Glyph,
|
||||
FontSize = FontSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,16 @@ internal struct Bgra32
|
||||
/// </summary>
|
||||
public byte A;
|
||||
|
||||
public Bgra32(byte b, byte g, byte r, byte a)
|
||||
{
|
||||
B = b;
|
||||
G = g;
|
||||
R = r;
|
||||
A = a;
|
||||
}
|
||||
|
||||
public readonly double Luminance { get => ((0.299 * R) + (0.587 * G) + (0.114 * B)) / 255; }
|
||||
|
||||
/// <summary>
|
||||
/// 从 Color 转换
|
||||
/// </summary>
|
||||
@@ -44,4 +54,11 @@ internal struct Bgra32
|
||||
*(uint*)&bgra8 = BinaryPrimitives.ReverseEndianness(*(uint*)&color);
|
||||
return bgra8;
|
||||
}
|
||||
|
||||
public static unsafe implicit operator Color(Bgra32 bgra8)
|
||||
{
|
||||
Unsafe.SkipInit(out Color color);
|
||||
*(uint*)&color = BinaryPrimitives.ReverseEndianness(*(uint*)&bgra8);
|
||||
return color;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Control.Media;
|
||||
/// <summary>
|
||||
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
|
||||
/// </summary>
|
||||
internal struct Hsl32
|
||||
internal struct Hsla32
|
||||
{
|
||||
/// <summary>
|
||||
/// The Hue in 0..360 range.
|
||||
@@ -46,13 +46,14 @@ internal struct Rgba32
|
||||
/// <summary>
|
||||
/// 使用 RGBA 代码初始化新的结构
|
||||
/// </summary>
|
||||
/// <param name="code">RGBA 代码</param>
|
||||
public unsafe Rgba32(uint code)
|
||||
/// <param name="xrgbaCode">RGBA 代码</param>
|
||||
public unsafe Rgba32(uint xrgbaCode)
|
||||
{
|
||||
// RRGGBBAA -> AABBGGRR
|
||||
// uint layout: 0xRRGGBBAA is AABBGGRR
|
||||
// AABBGGRR -> RRGGBBAA
|
||||
fixed (Rgba32* pSelf = &this)
|
||||
{
|
||||
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(code);
|
||||
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(xrgbaCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,14 +67,14 @@ internal struct Rgba32
|
||||
|
||||
public static unsafe implicit operator Color(Rgba32 hexColor)
|
||||
{
|
||||
// AABBGGRR -> BBGGRRAA
|
||||
// AABBGGRR -> 000000AA
|
||||
uint a = (*(uint*)&hexColor) >> 24;
|
||||
// Goal : Rgba32:RRGGBBAA(0xAABBGGRR) -> Color: AARRGGBB(0xBBGGRRAA)
|
||||
// Step1: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:AA000000(0x000000AA)
|
||||
uint a = ((*(uint*)&hexColor) >> 24) & 0x000000FF;
|
||||
|
||||
// AABBGGRR -> BBGGRR00
|
||||
uint rgb = (*(uint*)&hexColor) << 8;
|
||||
// Step2: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:00RRGGBB(0xRRGGBB00)
|
||||
uint rgb = ((*(uint*)&hexColor) << 8) & 0xFFFFFF00;
|
||||
|
||||
// BBGGRR00 + 000000AA
|
||||
// Step2: UInt32:00RRGGBB(0xRRGGBB00) + UInt32:AA000000(0x000000AA) -> UInt32:AARRGGBB(0xRRGGBBAA)
|
||||
uint rgba = rgb + a;
|
||||
|
||||
return *(Color*)&rgba;
|
||||
@@ -84,7 +85,7 @@ internal struct Rgba32
|
||||
/// </summary>
|
||||
/// <param name="hsl">HSL 颜色</param>
|
||||
/// <returns>RGBA8颜色</returns>
|
||||
public static Rgba32 FromHsl(Hsl32 hsl)
|
||||
public static Rgba32 FromHsl(Hsla32 hsl)
|
||||
{
|
||||
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
|
||||
double h1 = hsl.H / 60;
|
||||
@@ -141,7 +142,7 @@ internal struct Rgba32
|
||||
/// 转换到 HSL 颜色
|
||||
/// </summary>
|
||||
/// <returns>HSL 颜色</returns>
|
||||
public readonly Hsl32 ToHsl()
|
||||
public readonly Hsla32 ToHsl()
|
||||
{
|
||||
const double toDouble = 1.0 / 255;
|
||||
double r = toDouble * R;
|
||||
@@ -174,7 +175,7 @@ internal struct Rgba32
|
||||
double lightness = 0.5 * (max + min);
|
||||
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
|
||||
|
||||
Hsl32 ret;
|
||||
Hsla32 ret;
|
||||
ret.H = 60 * h1;
|
||||
ret.S = saturation;
|
||||
ret.L = lightness;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32.System.WinRT;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.System.WinRT;
|
||||
using WinRT;
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
@@ -26,8 +25,7 @@ internal static class SoftwareBitmapExtension
|
||||
{
|
||||
using (IMemoryBufferReference reference = buffer.CreateReference())
|
||||
{
|
||||
reference.As<IMemoryBufferByteAccess>().GetBuffer(out byte* data, out uint length);
|
||||
Span<Bgra32> bytes = new(data, unchecked((int)length / sizeof(Bgra32)));
|
||||
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
|
||||
foreach (ref Bgra32 pixel in bytes)
|
||||
{
|
||||
byte baseAlpha = pixel.A;
|
||||
@@ -40,4 +38,25 @@ internal static class SoftwareBitmapExtension
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe Bgra32 GetAccentColor(this SoftwareBitmap softwareBitmap)
|
||||
{
|
||||
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
|
||||
{
|
||||
using (IMemoryBufferReference reference = buffer.CreateReference())
|
||||
{
|
||||
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
|
||||
double b = 0, g = 0, r = 0, a = 0;
|
||||
foreach (ref readonly Bgra32 pixel in bytes)
|
||||
{
|
||||
b += pixel.B;
|
||||
g += pixel.G;
|
||||
r += pixel.R;
|
||||
a += pixel.A;
|
||||
}
|
||||
|
||||
return new((byte)(b / bytes.Length), (byte)(g / bytes.Length), (byte)(r / bytes.Length), (byte)(a / bytes.Length));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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,59 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
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)
|
||||
{
|
||||
foreach (UIElement child in Children)
|
||||
{
|
||||
// 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) / Children.Count;
|
||||
double childMaxAvailableWidth = Math.Max(MinItemWidth, childAvailableWidth);
|
||||
child.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
|
||||
}
|
||||
|
||||
return base.MeasureOverride(availableSize);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
int itemCount = Children.Count;
|
||||
double availableItemWidth = (finalSize.Width - (Spacing * (itemCount - 1))) / itemCount;
|
||||
double actualItemWidth = Math.Max(MinItemWidth, availableItemWidth);
|
||||
|
||||
double offset = 0;
|
||||
foreach (UIElement child in Children)
|
||||
{
|
||||
child.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;
|
||||
panel.MinWidth = (panel.MinItemWidth * panel.Children.Count) + (panel.Spacing * (panel.Children.Count - 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;
|
||||
|
||||
55
src/Snap.Hutao/Snap.Hutao/Control/Panel/UniformPanel.cs
Normal file
55
src/Snap.Hutao/Snap.Hutao/Control/Panel/UniformPanel.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
[DependencyProperty("MinItemWidth", typeof(double))]
|
||||
[DependencyProperty("ColumnSpacing", typeof(double))]
|
||||
[DependencyProperty("RowSpacing", typeof(double))]
|
||||
internal sealed partial class UniformPanel : Microsoft.UI.Xaml.Controls.Panel
|
||||
{
|
||||
private int columns;
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,6 @@ using Snap.Hutao.ViewModel.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
/// <summary>
|
||||
/// 表示支持取消加载的异步页面
|
||||
/// 在被导航到其他页面前触发取消异步通知
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[SuppressMessage("", "CA1001")]
|
||||
internal class ScopedPage : Page
|
||||
@@ -21,9 +17,8 @@ internal class ScopedPage : Page
|
||||
private readonly CancellationTokenSource viewCancellationTokenSource = new();
|
||||
private readonly IServiceScope currentScope;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的页面
|
||||
/// </summary>
|
||||
private bool inFrame = true;
|
||||
|
||||
protected ScopedPage()
|
||||
{
|
||||
unloadEventHandler = OnUnloaded;
|
||||
@@ -31,11 +26,6 @@ internal class ScopedPage : Page
|
||||
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步通知接收器
|
||||
/// </summary>
|
||||
/// <param name="extra">额外内容</param>
|
||||
/// <returns>任务</returns>
|
||||
public async ValueTask NotifyRecipientAsync(INavigationData extra)
|
||||
{
|
||||
if (extra.Data is not null && DataContext is INavigationRecipient recipient)
|
||||
@@ -61,6 +51,36 @@ internal class ScopedPage : Page
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
DisposeViewModel();
|
||||
inFrame = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is INavigationData extra)
|
||||
{
|
||||
NotifyRecipientAsync(extra).SafeForget();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (inFrame)
|
||||
{
|
||||
DisposeViewModel();
|
||||
}
|
||||
|
||||
if (this.IsDisposed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Unloaded -= unloadEventHandler;
|
||||
}
|
||||
|
||||
private void DisposeViewModel()
|
||||
{
|
||||
using (viewCancellationTokenSource)
|
||||
{
|
||||
@@ -79,19 +99,4 @@ internal class ScopedPage : Page
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is INavigationData extra)
|
||||
{
|
||||
NotifyRecipientAsync(extra).SafeForget();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DataContext = null;
|
||||
Unloaded -= unloadEventHandler;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@ using Microsoft.UI.Xaml.Documents;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Control.Media;
|
||||
using Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||
using Snap.Hutao.Control.Theme;
|
||||
using Snap.Hutao.Metadata;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
|
||||
@@ -15,20 +17,12 @@ namespace Snap.Hutao.Control.Text;
|
||||
|
||||
/// <summary>
|
||||
/// 专用于呈现描述文本的文本块
|
||||
/// Some part of this file came from:
|
||||
/// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))]
|
||||
[DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))]
|
||||
internal sealed partial class DescriptionTextBlock : ContentControl
|
||||
{
|
||||
private static readonly int ColorTagFullLength = "<color=#FFFFFFFF></color>".Length;
|
||||
private static readonly int ColorTagLeftLength = "<color=#FFFFFFFF>".Length;
|
||||
|
||||
private static readonly int ItalicTagFullLength = "<i></i>".Length;
|
||||
private static readonly int ItalicTagLeftLength = "<i>".Length;
|
||||
|
||||
private readonly TypedEventHandler<FrameworkElement, object> actualThemeChangedEventHandler;
|
||||
|
||||
/// <summary>
|
||||
@@ -51,9 +45,7 @@ internal sealed partial class DescriptionTextBlock : ContentControl
|
||||
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
|
||||
ReadOnlySpan<char> description = (string)e.NewValue;
|
||||
|
||||
UpdateDescription(textBlock, description);
|
||||
UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
|
||||
}
|
||||
|
||||
private static void OnTextStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
@@ -62,101 +54,106 @@ internal sealed partial class DescriptionTextBlock : ContentControl
|
||||
textBlock.Style = (Style)e.NewValue;
|
||||
}
|
||||
|
||||
private static void UpdateDescription(TextBlock textBlock, in ReadOnlySpan<char> description)
|
||||
private static void UpdateDescription(TextBlock textBlock, MiHoYoSyntaxTree syntaxTree)
|
||||
{
|
||||
textBlock.Inlines.Clear();
|
||||
AppendNode(textBlock, textBlock.Inlines, syntaxTree.Root);
|
||||
}
|
||||
|
||||
int last = 0;
|
||||
for (int i = 0; i < description.Length;)
|
||||
private static void AppendNode(TextBlock textBlock, InlineCollection inlines, MiHoYoSyntaxNode node)
|
||||
{
|
||||
switch (node.Kind)
|
||||
{
|
||||
// newline
|
||||
if (description[i..].StartsWith(@"\n"))
|
||||
{
|
||||
AppendText(textBlock, description[last..i]);
|
||||
AppendLineBreak(textBlock);
|
||||
i += 2;
|
||||
last = i;
|
||||
}
|
||||
case MiHoYoSyntaxKind.Root:
|
||||
foreach (MiHoYoSyntaxNode child in ((MiHoYoRootSyntax)node).Children)
|
||||
{
|
||||
AppendNode(textBlock, inlines, child);
|
||||
}
|
||||
|
||||
// color tag
|
||||
else if (description[i..].StartsWith("<c"))
|
||||
{
|
||||
AppendText(textBlock, description[last..i]);
|
||||
Rgba32 color = new(description.Slice(i + 8, 8).ToString());
|
||||
int length = description[(i + ColorTagLeftLength)..].IndexOf('<');
|
||||
AppendColorText(textBlock, description.Slice(i + ColorTagLeftLength, length), color);
|
||||
|
||||
i += length + ColorTagFullLength;
|
||||
last = i;
|
||||
}
|
||||
|
||||
// italic
|
||||
else if (description[i..].StartsWith("<i"))
|
||||
{
|
||||
AppendText(textBlock, description[last..i]);
|
||||
|
||||
int length = description[(i + ItalicTagLeftLength)..].IndexOf('<');
|
||||
AppendItalicText(textBlock, description.Slice(i + ItalicTagLeftLength, length));
|
||||
|
||||
i += length + ItalicTagFullLength;
|
||||
last = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if (i == description.Length - 1)
|
||||
{
|
||||
AppendText(textBlock, description[last..(i + 1)]);
|
||||
}
|
||||
break;
|
||||
case MiHoYoSyntaxKind.PlainText:
|
||||
AppendPlainText(textBlock, inlines, (MiHoYoPlainTextSyntax)node);
|
||||
break;
|
||||
case MiHoYoSyntaxKind.ColorText:
|
||||
AppendColorText(textBlock, inlines, (MiHoYoColorTextSyntax)node);
|
||||
break;
|
||||
case MiHoYoSyntaxKind.ItalicText:
|
||||
AppendItalicText(textBlock, inlines, (MiHoYoItalicTextSyntax)node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendText(TextBlock text, in ReadOnlySpan<char> slice)
|
||||
private static void AppendPlainText(TextBlock textBlock, InlineCollection inlines, MiHoYoPlainTextSyntax plainText)
|
||||
{
|
||||
text.Inlines.Add(new Run { Text = slice.ToString() });
|
||||
// PlainText doesn't have children
|
||||
inlines.Add(new Run { Text = plainText.Span.ToString() });
|
||||
}
|
||||
|
||||
private static void AppendColorText(TextBlock text, in ReadOnlySpan<char> slice, Rgba32 color)
|
||||
private static void AppendColorText(TextBlock textBlock, InlineCollection inlines, MiHoYoColorTextSyntax colorText)
|
||||
{
|
||||
Rgba32 color = new(colorText.ColorSpan.ToString());
|
||||
Color targetColor;
|
||||
if (ThemeHelper.IsDarkMode(text.ActualTheme))
|
||||
if (ThemeHelper.IsDarkMode(textBlock.ActualTheme))
|
||||
{
|
||||
targetColor = color;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make lighter in light mode
|
||||
Hsl32 hsl = color.ToHsl();
|
||||
Hsla32 hsl = color.ToHsl();
|
||||
hsl.L *= 0.3;
|
||||
targetColor = Rgba32.FromHsl(hsl);
|
||||
}
|
||||
|
||||
text.Inlines.Add(new Run
|
||||
if (colorText.Children.Count > 1)
|
||||
{
|
||||
Text = slice.ToString(),
|
||||
Foreground = new SolidColorBrush(targetColor),
|
||||
});
|
||||
Span span = new()
|
||||
{
|
||||
Foreground = new SolidColorBrush(targetColor),
|
||||
};
|
||||
|
||||
foreach (MiHoYoSyntaxNode child in colorText.Children)
|
||||
{
|
||||
AppendNode(textBlock, span.Inlines, child);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
inlines.Add(new Run
|
||||
{
|
||||
Text = colorText.ContentSpan.ToString(),
|
||||
Foreground = new SolidColorBrush(targetColor),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendItalicText(TextBlock text, in ReadOnlySpan<char> slice)
|
||||
private static void AppendItalicText(TextBlock textBlock, InlineCollection inlines, MiHoYoItalicTextSyntax italicText)
|
||||
{
|
||||
text.Inlines.Add(new Run
|
||||
if (italicText.Children.Count > 1)
|
||||
{
|
||||
Text = slice.ToString(),
|
||||
FontStyle = Windows.UI.Text.FontStyle.Italic,
|
||||
});
|
||||
}
|
||||
Span span = new()
|
||||
{
|
||||
FontStyle = Windows.UI.Text.FontStyle.Italic,
|
||||
};
|
||||
|
||||
private static void AppendLineBreak(TextBlock text)
|
||||
{
|
||||
text.Inlines.Add(new LineBreak());
|
||||
foreach (MiHoYoSyntaxNode child in italicText.Children)
|
||||
{
|
||||
AppendNode(textBlock, span.Inlines, child);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
inlines.Add(new Run
|
||||
{
|
||||
Text = italicText.ContentSpan.ToString(),
|
||||
FontStyle = Windows.UI.Text.FontStyle.Italic,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
// Simply re-apply texts
|
||||
UpdateDescription((TextBlock)Content, Description);
|
||||
UpdateDescription((TextBlock)Content, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle(Description)));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -139,7 +140,7 @@ internal sealed partial class HtmlDescriptionTextBlock : ContentControl
|
||||
else
|
||||
{
|
||||
// Make lighter in light mode
|
||||
Hsl32 hsl = color.ToHsl();
|
||||
Hsla32 hsl = color.ToHsl();
|
||||
hsl.L *= 0.3;
|
||||
targetColor = Rgba32.FromHsl(hsl);
|
||||
}
|
||||
@@ -156,7 +157,7 @@ internal sealed partial class HtmlDescriptionTextBlock : ContentControl
|
||||
text.Inlines.Add(new Run
|
||||
{
|
||||
Text = slice.ToString(),
|
||||
FontWeight = FontWeights.Bold,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||
|
||||
internal enum MiHoYoColorKind
|
||||
{
|
||||
None,
|
||||
Rgba,
|
||||
Rgb,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||
|
||||
internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
|
||||
{
|
||||
public MiHoYoColorTextSyntax(MiHoYoColorKind colorKind, string text, int start, int end)
|
||||
: base(MiHoYoSyntaxKind.ColorText, text, start, end)
|
||||
{
|
||||
ColorKind = colorKind;
|
||||
}
|
||||
|
||||
public MiHoYoColorTextSyntax(MiHoYoColorKind colorKind, string text, in TextPosition position)
|
||||
: base(MiHoYoSyntaxKind.ColorText, text, position)
|
||||
{
|
||||
ColorKind = colorKind;
|
||||
}
|
||||
|
||||
public MiHoYoColorKind ColorKind { get; }
|
||||
|
||||
public override TextPosition ContentPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
return ColorKind switch
|
||||
{
|
||||
MiHoYoColorKind.Rgba => new(Position.Start + 17, Position.End - 8),
|
||||
MiHoYoColorKind.Rgb => new(Position.Start + 15, Position.End - 8),
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public TextPosition ColorPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
return ColorKind switch
|
||||
{
|
||||
MiHoYoColorKind.Rgba => new(Position.Start + 8, Position.Start + 16),
|
||||
MiHoYoColorKind.Rgb => new(Position.Start + 8, Position.Start + 14),
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ReadOnlySpan<char> ColorSpan { get => Text.AsSpan()[ColorPosition.Start..ColorPosition.End]; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||
|
||||
internal sealed class MiHoYoItalicTextSyntax : MiHoYoXmlElementSyntax
|
||||
{
|
||||
public MiHoYoItalicTextSyntax(string text, int start, int end)
|
||||
: base(MiHoYoSyntaxKind.ItalicText, text, start, end)
|
||||
{
|
||||
}
|
||||
|
||||
public MiHoYoItalicTextSyntax(string text, in TextPosition position)
|
||||
: base(MiHoYoSyntaxKind.ItalicText, text, position)
|
||||
{
|
||||
}
|
||||
|
||||
public override TextPosition ContentPosition { get => new(Position.Start + 3, Position.End - 4); }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||
|
||||
internal sealed class MiHoYoPlainTextSyntax : MiHoYoSyntaxNode
|
||||
{
|
||||
public MiHoYoPlainTextSyntax(string text, int start, int end)
|
||||
: base(MiHoYoSyntaxKind.PlainText, text, start, end)
|
||||
{
|
||||
}
|
||||
|
||||
public MiHoYoPlainTextSyntax(string text, in TextPosition position)
|
||||
: base(MiHoYoSyntaxKind.PlainText, text, position)
|
||||
{
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user