Compare commits

..

274 Commits

Author SHA1 Message Date
DismissedLight
ab20aa1c64 Merge pull request #1799 from DGP-Studio/develop 2024-07-06 16:55:20 +08:00
Masterain
84c8d8a2e3 New Crowdin updates (#1789) 2024-07-06 16:35:23 +08:00
DismissedLight
364b056e17 impl #1796 2024-07-06 16:34:34 +08:00
DismissedLight
a4913e84e4 bump version 2024-07-06 16:23:52 +08:00
DismissedLight
7f32437ec0 code style 2024-07-06 11:15:13 +08:00
DismissedLight
e230d7a3ef fix #1795 2024-07-06 10:38:58 +08:00
DismissedLight
1cd6aad518 fix game account attach uid crash 2024-07-06 10:09:07 +08:00
DismissedLight
d3fbf35f34 fix #1792 2024-07-06 09:57:29 +08:00
DismissedLight
72a1ec2122 fix #1791 #1794 2024-07-06 09:53:30 +08:00
DismissedLight
497a5fb0f8 Merge pull request #1788 from DGP-Studio/develop 2024-07-05 19:49:31 +08:00
DismissedLight
bfec29504b Merge pull request #1777 from DGP-Studio/l10n_develop 2024-07-05 19:34:05 +08:00
DismissedLight
de77639f57 add string resources 2024-07-05 19:33:41 +08:00
DismissedLight
951ecd19d5 unlocker kind switch 2024-07-05 19:31:10 +08:00
DismissedLight
7f6430fe80 daily card user remove aware 2024-07-05 17:28:33 +08:00
DismissedLight
d133295599 [skip ci] remove using 2024-07-05 16:50:59 +08:00
DismissedLight
b22eead953 #1784 2024-07-05 16:50:17 +08:00
DismissedLight
d75c680a45 role combat spec 2024-07-05 15:36:17 +08:00
DismissedLight
52949e3431 fix monochrome #1784 2024-07-05 11:54:45 +08:00
DismissedLight
eeee9af09d image cache theme aware 2024-07-05 00:38:35 +08:00
qhy040404
3526e73f35 allow duplicate weapons in cultivation 2024-07-05 00:32:21 +08:00
DismissedLight
81c5acb742 [skip ci] image cache 2024-07-04 17:31:03 +08:00
Masterain
48ce0c2761 New translations sh.resx (Vietnamese) 2024-07-04 00:44:15 -07:00
Masterain
c5f8d6bfd5 New translations sh.resx (French) 2024-07-04 00:44:14 -07:00
Masterain
4ef1262d01 New translations sh.resx (Indonesian) 2024-07-04 00:44:13 -07:00
Masterain
c05c62ac91 New translations sh.resx (English) 2024-07-04 00:44:11 -07:00
Masterain
618299b296 New translations sh.resx (Chinese Traditional) 2024-07-04 00:44:10 -07:00
Masterain
0db7aa239e New translations sh.resx (Russian) 2024-07-04 00:44:09 -07:00
Masterain
6a4cc56d32 New translations sh.resx (Portuguese) 2024-07-04 00:44:07 -07:00
Masterain
5784c55d1e New translations sh.resx (Korean) 2024-07-04 00:44:06 -07:00
Masterain
53167952f4 New translations sh.resx (Japanese) 2024-07-04 00:44:04 -07:00
DismissedLight
2a6b386c2c Merge pull request #1640 from DGP-Studio/fix/1633
fix #1633
2024-07-04 15:42:28 +08:00
DismissedLight
4fdb72ca30 code style 2024-07-04 15:41:27 +08:00
qhy040404
9df46ad60e merge ttb into astb 2024-07-04 13:57:07 +08:00
qhy040404
5c49818a2f fix #1633 2024-07-04 13:56:34 +08:00
DismissedLight
4ba819ce3b replace with standardview 2024-07-04 13:42:08 +08:00
DismissedLight
96ed31c09e Update StandardView.cs 2024-07-03 20:52:13 +08:00
DismissedLight
481753da02 launch game view issue 2024-07-03 20:21:07 +08:00
DismissedLight
ec6e1696da make webview2 great again 2024-07-03 17:15:48 +08:00
DismissedLight
417b537de4 unify achievement mvvm 2024-07-02 22:28:01 +08:00
DismissedLight
e837e425c5 fix minor issue 2024-07-02 22:20:02 +08:00
DismissedLight
a3fb0486c2 StandardView 2024-07-02 17:07:46 +08:00
DismissedLight
64d9d04608 card progress bar border 2024-07-01 23:28:22 +08:00
DismissedLight
fe05c8dd04 fix QA issue 2024-07-01 23:17:50 +08:00
DismissedLight
a2f9ff95a4 fix QA issues 2024-07-01 21:45:53 +08:00
DismissedLight
bd5c244eeb fix QA padding margin align issues 2024-07-01 20:44:18 +08:00
Masterain
b619dd5b09 New translations sh.resx (Vietnamese) 2024-07-01 04:05:06 -07:00
Masterain
ee5a94b961 New translations sh.resx (French) 2024-07-01 04:05:04 -07:00
Masterain
e30c97d0aa New translations sh.resx (Indonesian) 2024-07-01 04:05:03 -07:00
Masterain
261377fe0a New translations sh.resx (English) 2024-07-01 04:05:01 -07:00
Masterain
a080938ee2 New translations sh.resx (Chinese Traditional) 2024-07-01 04:05:00 -07:00
Masterain
deb34c2a7b New translations sh.resx (Russian) 2024-07-01 04:04:58 -07:00
Masterain
c0a9e0b301 New translations sh.resx (Portuguese) 2024-07-01 04:04:57 -07:00
Masterain
f2c9b676c9 New translations sh.resx (Korean) 2024-07-01 04:04:55 -07:00
Masterain
a02ce183eb New translations sh.resx (Japanese) 2024-07-01 04:04:54 -07:00
DismissedLight
06cd462f01 fix card progress bar corner radius 2024-07-01 17:31:37 +08:00
DismissedLight
17d27f9535 make users great again 2024-07-01 17:00:18 +08:00
DismissedLight
d97bd4fd79 [skip ci] broken 2024-06-30 22:50:19 +08:00
Masterain
0c7c25f303 New translations shregex.resx (Indonesian) 2024-06-30 02:27:06 -07:00
Masterain
2b8eed0ccc New translations shregex.resx (Vietnamese) 2024-06-30 02:27:05 -07:00
Masterain
154c31e8f7 New translations shregex.resx (English) 2024-06-30 02:27:04 -07:00
Masterain
c2116af19f New translations shregex.resx (Chinese Traditional) 2024-06-30 02:27:03 -07:00
Masterain
3144ed4ecb New translations shregex.resx (Russian) 2024-06-30 02:27:02 -07:00
Masterain
b6c68c69d6 New translations shregex.resx (Portuguese) 2024-06-30 02:27:01 -07:00
Masterain
12d2f2235e New translations shregex.resx (Korean) 2024-06-30 02:27:00 -07:00
Masterain
e89c5488d9 New translations shregex.resx (Japanese) 2024-06-30 02:27:00 -07:00
Masterain
489bb6bab3 New translations shregex.resx (French) 2024-06-30 02:26:58 -07:00
Masterain
423b220bb3 New translations sh.resx (Indonesian) 2024-06-30 02:26:56 -07:00
Masterain
08a082ae65 New translations sh.resx (English) 2024-06-30 02:26:55 -07:00
Masterain
b99bcb53f2 New translations sh.resx (Chinese Traditional) 2024-06-30 02:26:54 -07:00
Masterain
0160e96837 New translations sh.resx (Russian) 2024-06-30 02:26:53 -07:00
Masterain
ca29320139 New translations sh.resx (Portuguese) 2024-06-30 02:26:52 -07:00
Masterain
e8a6acd2d8 New translations sh.resx (Korean) 2024-06-30 02:26:50 -07:00
Masterain
3bca3a8148 New translations sh.resx (Japanese) 2024-06-30 02:26:49 -07:00
Masterain
e1bbaf5dc9 New translations sh.resx (Vietnamese) 2024-06-30 01:30:30 -07:00
Masterain
b1774e8365 New translations sh.resx (French) 2024-06-30 01:30:29 -07:00
Masterain
f5a81e2f57 New translations sh.resx (Indonesian) 2024-06-30 01:30:28 -07:00
Masterain
cfb72755a0 New translations sh.resx (English) 2024-06-30 01:30:27 -07:00
Masterain
0a7e9afcaf New translations sh.resx (Chinese Traditional) 2024-06-30 01:30:26 -07:00
Masterain
743e8d8069 New translations sh.resx (Russian) 2024-06-30 01:30:24 -07:00
Masterain
4c64fac354 New translations sh.resx (Portuguese) 2024-06-30 01:30:23 -07:00
Masterain
bf50d6b9b3 New translations sh.resx (Korean) 2024-06-30 01:30:22 -07:00
Masterain
471260de59 New translations sh.resx (Japanese) 2024-06-30 01:30:20 -07:00
DismissedLight
dc6dc94b45 make gachalog great again 2024-06-30 16:11:47 +08:00
DismissedLight
c0f7293921 make cultivation great again 2024-06-29 22:16:08 +08:00
DismissedLight
98b5436828 make achievement great again 2024-06-29 20:35:45 +08:00
DismissedLight
ff785387dc fix accessor unreachable issue 2024-06-28 23:39:30 +08:00
DismissedLight
5875147bd3 [skip ci] left inset 2024-06-28 17:31:10 +08:00
DismissedLight
cd80250fd0 [skip ci] refactor 2024-06-28 17:15:45 +08:00
DismissedLight
f1bcef4869 refactor 2024-06-28 13:33:23 +08:00
DismissedLight
2c58f34c5d refactor 2024-06-28 11:58:16 +08:00
DismissedLight
b90e8d062c refactor 2024-06-27 23:34:21 +08:00
DismissedLight
00c9417997 refactor 2024-06-27 17:24:34 +08:00
Masterain
514edd97c8 New translations sh.resx (English) 2024-06-26 21:10:45 -07:00
qhy040404
7f06b0a07c fix #1774 2024-06-27 10:54:53 +08:00
DismissedLight
3211bfbbd6 refactor 2024-06-27 10:44:39 +08:00
Masterain
8067665026 Update templates 2024-06-26 19:44:07 -07:00
qhy040404
0c1968ff49 fix #1771 2024-06-26 11:27:54 +08:00
DismissedLight
0075d79b0c refactor 2024-06-25 22:39:53 +08:00
qhy040404
1f8a70da0d refine ui 2024-06-25 19:07:43 +08:00
qhy040404
034655dc26 extract island to data folder 2024-06-25 18:35:10 +08:00
qhy040404
fb293cfc18 fix broken island state 2024-06-25 18:01:35 +08:00
Lightczx
0433ecbce8 refactor 2024-06-25 17:24:36 +08:00
qhy040404
077243fa38 bump package 2024-06-25 00:38:31 +08:00
Lightczx
83347dfafb refactor 2024-06-24 16:04:50 +08:00
Lightczx
c9df6ac77b refactor 2024-06-24 14:41:05 +08:00
DismissedLight
b626bbe443 fix default unlocker logging error 2024-06-23 19:39:06 +08:00
DismissedLight
ea8685523d refactor 2024-06-23 19:29:46 +08:00
DismissedLight
b02f2b47c8 fix #1763 2024-06-23 17:18:11 +08:00
DismissedLight
b4f7bf934e bump packages 2024-06-23 12:20:54 +08:00
DismissedLight
eeffa446a2 fix unlocking fps
Opening any OSD can kill process
2024-06-23 00:26:36 +08:00
DismissedLight
6746610ab6 refactor 2024-06-22 15:11:53 +08:00
Lightczx
f8c224048e refactor 2024-06-21 17:21:27 +08:00
qhy040404
3f110fd4d3 fix unmatched announcement 2024-06-20 22:50:43 +08:00
Lightczx
44ddae602d refactor 2024-06-20 17:15:30 +08:00
Lightczx
ab91f4e738 fix #1750 2024-06-20 15:42:49 +08:00
Lightczx
5bf1cf0530 refactor 2024-06-20 15:27:49 +08:00
Lightczx
70f4dcb2c9 code style 2024-06-20 10:11:19 +08:00
DismissedLight
f490805875 fix settings right panel padding 2024-06-19 22:25:58 +08:00
DismissedLight
681bf08047 adjust spiral abyss padding 2024-06-19 22:19:29 +08:00
DismissedLight
7b11215551 Add logger in unlocking 2024-06-19 18:35:29 +08:00
DismissedLight
8b20f3beca Fix Desc and All Access 2024-06-19 18:02:46 +08:00
Lightczx
18a088d83b Use All Access Handle 2024-06-19 17:14:23 +08:00
DismissedLight
a6971042dc Merge pull request #1746 from DGP-Studio/develop 2024-06-19 15:46:10 +08:00
Masterain
87f1f2c46b New Crowdin updates (#1743) 2024-06-19 14:54:26 +08:00
Lightczx
c576d8f7c4 bump version 2024-06-19 14:54:03 +08:00
Lightczx
a0c1241b32 spiral abyss delta view 2024-06-18 16:34:28 +08:00
DismissedLight
a3ab24554a add delta rate 2024-06-18 00:07:31 +08:00
DismissedLight
9ae45a4cc4 Merge pull request #1735 from DGP-Studio/qa 2024-06-17 21:39:01 +08:00
DismissedLight
f700faae14 Merge pull request #1737 from DGP-Studio/feat/hypapi 2024-06-17 21:33:06 +08:00
DismissedLight
57b51ed5ee code style 2024-06-17 21:32:51 +08:00
qhy040404
5dfb7fbb63 rename 2024-06-17 18:42:07 +08:00
qhy040404
046823245c refactor package converter 2024-06-17 18:42:06 +08:00
qhy040404
0497d89559 refactor launchgame resources 2024-06-17 18:42:06 +08:00
qhy040404
9d364a291c minor change 2024-06-17 18:42:06 +08:00
qhy040404
c342147809 Add HoyoPlay API 2024-06-17 18:42:05 +08:00
Lightczx
a86caaf229 fix build 2024-06-17 17:32:01 +08:00
Lightczx
d0b07f1308 spiral abyss last data 2024-06-17 17:07:03 +08:00
qhy040404
409a223213 fix inventory items wrong position 2024-06-17 10:36:42 +08:00
Lightczx
75ea2b807f Update AsyncBarrier.cs 2024-06-17 09:57:21 +08:00
qhy040404
719d934222 fix part of qa issues 2024-06-17 00:26:45 +08:00
DismissedLight
e8eed46d82 Merge pull request #1725 from DGP-Studio/feat/gamerole_profilepicture 2024-06-16 22:46:47 +08:00
DismissedLight
ff9b553a19 code style 2024-06-16 22:46:24 +08:00
qhy040404
95d64c2895 make UserGameRole observable 2024-06-16 20:41:19 +08:00
qhy040404
558551c8ad rename 2024-06-16 19:14:33 +08:00
qhy040404
d05c196b7c apply changes 2024-06-16 19:04:51 +08:00
DismissedLight
502fb6dbed fix notification activation 2024-06-16 17:27:20 +08:00
qhy040404
4fa5270070 fix failed notification activate 2024-06-16 01:43:43 +08:00
DismissedLight
94fda223fc drop notification 2024-06-16 01:25:52 +08:00
Mikachu2333
18103b4deb modify alpha's tip (#1730)
Co-authored-by: LinkChou <linkchou@yandex.com>
2024-06-16 01:21:36 +08:00
qhy040404
16ac52e71d use list instead of mappppps 2024-06-16 01:19:56 +08:00
qhy040404
73825d391e fix build 2024-06-16 00:47:02 +08:00
qhy040404
3b2eeb84a7 add profile picture for each game role 2024-06-16 00:46:55 +08:00
DismissedLight
3e8655fd55 Merge pull request #1722 from DGP-Studio/feat/window
make windows transient
2024-06-15 16:04:58 +08:00
DismissedLight
fe38e14ae8 code style 2024-06-15 15:54:04 +08:00
DismissedLight
a174493819 Merge branch 'feat/window' of https://github.com/DGP-Studio/Snap.Hutao into feat/window 2024-06-15 14:32:03 +08:00
qhy040404
3a57d55c62 make windows transient 2024-06-15 14:31:29 +08:00
DismissedLight
99f35ca6db avatar property grid view rework 2024-06-15 00:43:59 +08:00
DismissedLight
c423e8b72d ProfilePicture add unlock type 2024-06-14 23:18:11 +08:00
DismissedLight
7ff78def46 fix hotkey can't register 2024-06-14 11:05:56 +08:00
qhy040404
bc9018f4bf make windows transient 2024-06-13 19:48:06 +08:00
qhy040404
3513268ad9 Update issue template 2024-06-13 18:39:29 +08:00
qhy040404
107963b7ac Update issue template 2024-06-13 18:39:03 +08:00
DismissedLight
4e89406f2f Merge pull request #1721 from DGP-Studio/feat/1715 2024-06-13 16:15:21 +08:00
Lightczx
8119de3fa9 code style 2024-06-13 16:15:08 +08:00
qhy040404
7a8c233b10 review requests 2024-06-13 15:36:50 +08:00
qhy040404
cc71aa9c82 impl #1715 2024-06-13 12:51:22 +08:00
Masterain
850ea7ed4b Update README.md (#1717) 2024-06-11 20:50:44 -07:00
DismissedLight
4276481284 Add CachedImage Debug Layer 2024-06-11 21:05:24 +08:00
Lightczx
6f3159ae0c [skip ci] QA announcement name 2024-06-11 17:01:14 +08:00
Lightczx
c1b3412ba1 fix QA ComboBox width issue 2024-06-11 16:56:33 +08:00
Lightczx
99b3613319 fix #1688 2024-06-11 15:42:23 +08:00
Lightczx
069407abbc use weapon sort 2024-06-11 15:06:15 +08:00
DismissedLight
98c8df5c8e Merge pull request #1712 from DGP-Studio/feat/v3_cultivation 2024-06-11 14:04:49 +08:00
Lightczx
7cfcc17763 refactor 2024-06-11 14:00:48 +08:00
qhy040404
23741c4e48 exclude unavailable avatars 2024-06-11 13:12:37 +08:00
qhy040404
5f4b68d538 add cache to minimal deltas 2024-06-11 12:55:54 +08:00
Lightczx
9ef0d8c57d add SCIP solver 2024-06-11 12:31:51 +08:00
qhy040404
f0bfea51cf move to inventory service 2024-06-11 00:06:01 +08:00
DismissedLight
905454eb02 refactor 2024-06-10 23:31:38 +08:00
DismissedLight
05c3a575bc adjust db service parameter 2024-06-10 23:03:23 +08:00
DismissedLight
3e26e247cd refactor metadata abstraction 2024-06-10 22:43:50 +08:00
qhy040404
293b1e214d migrate all v2 api to v3 api 2024-06-10 22:37:56 +08:00
qhy040404
063665e77e refresh inventory 2024-06-10 22:37:55 +08:00
DismissedLight
50389ac06c Merge pull request #1713 from DGP-Studio/fix/dailynote 2024-06-10 22:12:57 +08:00
qhy040404
b99b34945e fix #1711 2024-06-10 11:05:58 +08:00
qhy040404
94a96c76bc fix #1710 2024-06-10 10:57:29 +08:00
DismissedLight
5cf3046257 Merge pull request #1694 from Mikachu2333/develop 2024-06-06 15:16:22 +08:00
Lightczx
89f8dedb57 fix url protocol launch lock 2024-06-06 13:11:24 +08:00
LinkChou
3c1e9237aa replace Uid to UID 2024-06-06 11:54:11 +08:00
LinkChou
e7cb01b302 Merge branch 'develop' of https://github.com/Mikachu2333/Snap.Hutao into develop 2024-06-06 11:48:03 +08:00
LinkChou
4cd971e166 Add some 2024-06-06 11:47:37 +08:00
Mikachu2333
7a9657f0cb Merge branch 'DGP-Studio:develop' into develop 2024-06-06 11:44:31 +08:00
Lightczx
82e6b62231 correctly free library 2024-06-06 09:29:50 +08:00
LinkChou
374c4d796d reformat 2024-06-06 07:25:11 +08:00
Mikachu2333
6e149a5be3 Update SH.resx 2024-06-06 01:44:32 +08:00
Mikachu2333
00ad0ef346 correct format 2024-06-06 01:39:10 +08:00
Mikachu2333
f22f165592 Merge branch 'develop' into develop 2024-06-06 01:33:09 +08:00
DismissedLight
fd59b471cb Merge pull request #1701 from DGP-Studio/develop 2024-06-05 22:12:35 +08:00
DismissedLight
5d8a39fe43 bump version 2024-06-05 21:29:28 +08:00
DismissedLight
521534be05 Merge pull request #1667 from DGP-Studio/l10n_develop 2024-06-05 21:22:17 +08:00
DismissedLight
b1364db3ac Merge pull request #1697 from DGP-Studio/opt/launch_game_activation 2024-06-05 20:35:18 +08:00
qhy040404
031cf77c27 refine LaunchGameAction 2024-06-05 19:09:57 +08:00
LinkChou
49c75dde2a Chinese text improve 2024-06-05 18:43:45 +08:00
Lightczx
3200c5e60b fix NTHeader offset 2024-06-05 17:22:45 +08:00
Lightczx
b392a6f8e5 Align HMODULE ptr 2024-06-05 17:17:52 +08:00
Lightczx
3e8e109123 use image header to fetch image size 2024-06-05 16:51:59 +08:00
Lightczx
91c886befb code style 2024-06-05 16:15:46 +08:00
Lightczx
32bdfe12af Fix Unlock Fps Attempt 2 2024-06-05 16:05:51 +08:00
Lightczx
eac67b6f44 Fix Unlock Fps Attempt 1 2024-06-05 15:28:50 +08:00
Lightczx
0dcba220c5 fix Launch Game ViewModel scope 2024-06-05 13:42:31 +08:00
Masterain
a204eaa95c New translations sh.resx (Vietnamese) 2024-06-04 18:31:38 -07:00
Masterain
35491c4eb1 New translations sh.resx (French) 2024-06-04 18:31:36 -07:00
Masterain
706401350c New translations sh.resx (Indonesian) 2024-06-04 18:31:36 -07:00
Masterain
c8ba04ee11 New translations sh.resx (English) 2024-06-04 18:31:34 -07:00
Masterain
b080a553c3 New translations sh.resx (Chinese Traditional) 2024-06-04 18:31:33 -07:00
Masterain
baf5612333 New translations sh.resx (Russian) 2024-06-04 18:31:32 -07:00
Masterain
eacd697cfe New translations sh.resx (Portuguese) 2024-06-04 18:31:31 -07:00
Masterain
11dc8e60bb New translations sh.resx (Korean) 2024-06-04 18:31:29 -07:00
Masterain
bba62996a0 New translations sh.resx (Japanese) 2024-06-04 18:31:28 -07:00
DismissedLight
db15b6a30c Merge pull request #1673 from DGP-Studio/ref/disable_web_login 2024-06-05 09:26:14 +08:00
Lightczx
1b0356b5ef fix #1669 2024-06-05 09:24:29 +08:00
qhy040404
6e498f5ede disable web login temporarily
maybe temporarily...
2024-06-04 22:30:32 +08:00
qhy040404
3117aefd54 try to fix permission 2024-06-04 21:00:27 +08:00
qhy040404
34ea240272 inline sign 2024-06-04 20:51:03 +08:00
DismissedLight
6b23ae5332 fix viewmode locker scope 2024-06-04 20:40:52 +08:00
qhy040404
c197d8a35a fix unresolved makeappx.exe 2024-06-04 20:22:47 +08:00
Lightczx
b0fa05283a code style 2024-06-04 17:19:45 +08:00
Masterain
c85a74dfc3 New translations sh.resx (Chinese Traditional) 2024-06-03 07:31:29 -07:00
DismissedLight
f7e53399b4 4.7 compatible 2024-06-03 21:49:06 +08:00
DismissedLight
52ac588a3a Merge pull request #1670 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-7c1ae0f6e4 2024-06-03 16:21:21 +08:00
Lightczx
cd6c1f6b59 batch compute api endpoint 2024-06-03 16:18:12 +08:00
dependabot[bot]
7c734ce4aa Bump the packages group in /src/Snap.Hutao with 2 updates
Bumps the packages group in /src/Snap.Hutao with 2 updates: [MSTest.TestAdapter](https://github.com/microsoft/testfx) and [MSTest.TestFramework](https://github.com/microsoft/testfx).


Updates `MSTest.TestAdapter` from 3.4.0 to 3.4.3
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.4.0...v3.4.3)

Updates `MSTest.TestFramework` from 3.4.0 to 3.4.3
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.4.0...v3.4.3)

---
updated-dependencies:
- dependency-name: MSTest.TestAdapter
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: packages
- dependency-name: MSTest.TestFramework
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: packages
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 08:00:29 +00:00
DismissedLight
a640374b62 Merge pull request #1665 from DGP-Studio/feat/1663
Co-authored-by: Lightczx <1686188646@qq.com>
2024-06-03 14:11:28 +08:00
Lightczx
ca66176d64 code style 2024-06-03 14:11:00 +08:00
Lightczx
0f3a85e35c code style 2024-06-03 11:23:22 +08:00
qhy040404
4bb7316ce5 apply suggestion 2024-06-03 11:11:20 +08:00
qhy040404
7d6a9691a2 code style 2024-06-02 09:15:39 +08:00
qhy040404
1d4409aa43 code style 2024-06-02 09:10:31 +08:00
qhy040404
ea345f4854 use client to determine whether to redirect 2024-06-02 09:10:31 +08:00
qhy040404
72e163f613 auto constructor 2024-06-02 09:10:31 +08:00
qhy040404
86b04bb5a3 impl #1663 2024-06-02 09:10:31 +08:00
Masterain
5859ca3c12 New translations sh.resx (Chinese Traditional) 2024-06-01 11:19:36 -07:00
Masterain
e34e87359f New translations sh.resx (Chinese Traditional) 2024-06-01 09:51:13 -07:00
DismissedLight
53cda02071 screen capture preview 2024-06-01 22:53:56 +08:00
Masterain
ff6c682e1b New translations sh.resx (Chinese Traditional) 2024-05-31 10:16:41 -07:00
Lightczx
bae9c8a46a code style 2024-05-31 17:31:53 +08:00
Lightczx
a8baef99d7 use high performance gpu 2024-05-31 15:46:56 +08:00
Lightczx
2c47e7d1da IDXGIFactory6 2024-05-31 14:43:38 +08:00
Lightczx
2cee94a529 rename 2024-05-31 11:38:36 +08:00
Lightczx
b8b9bb2436 make launch game view model singleton 2024-05-31 10:19:41 +08:00
Lightczx
5511863d7f optimize home page load speed 2024-05-31 10:12:57 +08:00
DismissedLight
adf3f7e7b1 fix QA issues 2024-05-30 22:13:44 +08:00
DismissedLight
2232772110 adjust daily note settings to flyout 2024-05-30 21:59:49 +08:00
DismissedLight
cd343843b3 fix elevated pipe access rights 2024-05-30 21:48:04 +08:00
Lightczx
f5982f81c0 defer content load 2024-05-30 16:40:42 +08:00
Lightczx
1e38c43727 remove AlternatingItemsControl 2024-05-30 14:29:50 +08:00
Lightczx
7879f1278b Update InfoBarOverride.xaml 2024-05-30 12:59:20 +08:00
Lightczx
f8e9b4a1b3 fix pipe access control 2024-05-30 12:36:30 +08:00
Lightczx
c9ea4b358a fix #1650 2024-05-30 11:28:06 +08:00
Lightczx
75287473c5 adjust notifyicon logic and impl #1656 2024-05-30 10:39:20 +08:00
Lightczx
3948b81a48 fix #1657 2024-05-29 14:29:33 +08:00
DismissedLight
e5c751771c avoid exception when Shell_TrayWnd not ready 2024-05-28 22:34:32 +08:00
qhy040404
f7723d21a3 [skip ci] fix unexpected canceled workflow 2024-05-28 17:50:48 +08:00
Lightczx
4ce064a71a Add Vietnamese and fix guide maximized state 2024-05-28 17:28:53 +08:00
Masterain
b07c569a9e New Crowdin updates (#1634) 2024-05-28 17:07:00 +08:00
DismissedLight
c81c0c33d8 Merge pull request #1649 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-3211eb08b6 2024-05-28 16:03:01 +08:00
DismissedLight
2274445303 Merge pull request #1645 from DGP-Studio/fix/jumplist 2024-05-28 16:02:13 +08:00
DismissedLight
271cac9a02 Merge pull request #1644 from DGP-Studio/fix/win10_notifyicon 2024-05-28 16:01:19 +08:00
Lightczx
9f8f2870ae remove blank spaces 2024-05-28 16:01:13 +08:00
DismissedLight
0cc4897354 deferload experiment 2024-05-27 23:24:03 +08:00
qhy040404
7aa4696ba5 drop else 2024-05-27 22:11:38 +08:00
dependabot[bot]
9e3ec32ae6 Bump the packages group in /src/Snap.Hutao with 3 updates
Bumps the packages group in /src/Snap.Hutao with 3 updates: [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest), [MSTest.TestAdapter](https://github.com/microsoft/testfx) and [MSTest.TestFramework](https://github.com/microsoft/testfx).


Updates `Microsoft.NET.Test.Sdk` from 17.9.0 to 17.10.0
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md)
- [Commits](https://github.com/microsoft/vstest/compare/v17.9.0...v17.10.0)

Updates `MSTest.TestAdapter` from 3.3.1 to 3.4.0
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.3.1...v3.4.0)

Updates `MSTest.TestFramework` from 3.3.1 to 3.4.0
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.3.1...v3.4.0)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: packages
- dependency-name: MSTest.TestAdapter
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: packages
- dependency-name: MSTest.TestFramework
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: packages
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 07:57:43 +00:00
qhy040404
cd91af8ae9 use ref readonly 2024-05-27 09:29:58 +08:00
qhy040404
67f6fda900 another impl of hint when icon promoted for win10 2024-05-26 21:53:22 +08:00
1081 changed files with 23781 additions and 14180 deletions

View File

@@ -19,7 +19,7 @@ body:
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
required: true
- label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
- label: 我知道[文档站](https://hut.ao/zh/menu.html)的导航栏中有**搜索功能**,且已经搜索过相关关键词
required: true
- 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)的问题也不是一个别人已发布的**重复的**问题

View File

@@ -1,9 +1,7 @@
name: 功能请求
name: 功能请求
description: 通过这个议题来向开发团队分享你的想法
title: "[Feat]: 在这里填写一个合适的标题"
labels: ["功能", "needs-triage", "priority:none"]
assignees:
- Lightczx
labels: ["feature request", "needs-triage", "priority:none"]
body:
- type: markdown
attributes:
@@ -24,4 +22,4 @@ body:
label: 想要实现或优化的功能
description: 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高
validations:
required: true
required: true

View File

@@ -1,9 +1,7 @@
name: Feature Request [English Form]
description: Tell us about your thought
title: "[Feat]: Place your title here"
labels: ["功能", "needs-triage", "priority:none"]
assignees:
- Lightczx
labels: ["feature request", "needs-triage", "priority:none"]
body:
- type: markdown
attributes:
@@ -22,6 +20,6 @@ body:
id: req
attributes:
label: Detail of the Feature
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted.
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted.
validations:
required: true
required: true

View File

@@ -1,5 +1,5 @@
<!--- Hi, thanks for considering make a PR contribution to Snap Hutao, we appreciate your work. -->
<!--- Before you create this PR, please fill the following form and checklist -->
<!--- Before you create this PR, please check our contribution guide (https://hut.ao/en/development/contribute.html) and fill out the following form and checklist -->
## Description

View File

@@ -29,14 +29,7 @@ on:
jobs:
build:
runs-on: ${{ matrix.runner }}
strategy:
matrix:
runner:
- self-hosted
- windows-latest
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -52,13 +45,8 @@ jobs:
run: dotnet tool restore && dotnet cake
env:
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
CERTIFICATE: ${{ secrets.CERTIFICATE }}
PW: ${{ secrets.PW }}
- name: Upload signed msix
if: success() && github.event_name != 'pull_request'
@@ -76,12 +64,55 @@ jobs:
> 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
> [!TIP]
> 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
> 普通用户请 [点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/) 下载最新的稳定版本
> [!IMPORTANT]
> 请注意,从 Snap Hutao Alpha 2023.12.21.3 开始,我们将使用全新的 CI 证书,原有的 Snap.Hutao.CI.cer 将在几天后过期停止使用。
>
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 到 `受信任的根证书颁发机构` 以安装测试版安装包
> 请先安装 **[DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt)** 到 **受信任的根证书颁发机构** 以安装测试版安装包
"
echo $summary >> $Env:GITHUB_STEP_SUMMARY
fallback_build:
runs-on: windows-latest
needs: build
if: failure()
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0
- name: Cake
id: cake
shell: pwsh
run: dotnet tool restore && dotnet cake
env:
VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }}
CERTIFICATE: ${{ secrets.CERTIFICATE }}
PW: ${{ secrets.PW }}
- name: Upload signed msix
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() && github.event_name != 'pull_request'
shell: pwsh
run: |
$summary = "
> [!WARNING]
> 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
> [!TIP]
> 普通用户请 [点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/) 下载最新的稳定版本
> [!IMPORTANT]
> 请先安装 **[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

View File

@@ -72,9 +72,9 @@ Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translatio
Snap Hutao is currently using sponsored software from the following service providers.
| [![](https://www.netlify.com/v3/img/components/netlify-light.svg)](https://www.netlify.com/) | [![](https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg)](https://crowdin.com/) | [![](https://gitlab.cn/images/icons/logos/logo-121-75.svg)](https://gitlab.cn/) |
|:-:|:-:|:-:|
|[![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/73ae8b90-f3c7-4033-b2b7-f4126331ce66)](https://about.signpath.io)|[![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/49aed8ee-9f19-4a8a-998c-7b93ee286d65)](https://1password.com/)|[![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/ad121220-d2d3-4f49-b215-b6d063dc229d)](https://www.digitalocean.com)|
|[![jetbrains](https://github.com/DGP-Studio/Snap.Hutao/assets/36357191/4105772a-728a-4a84-9c6e-d713a5698a20)](https://www.jetbrains.com/opensource/)|||
| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
| [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/73ae8b90-f3c7-4033-b2b7-f4126331ce66)](https://about.signpath.io) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/49aed8ee-9f19-4a8a-998c-7b93ee286d65)](https://1password.com/) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/ad121220-d2d3-4f49-b215-b6d063dc229d)](https://www.digitalocean.com) |
| [![ducalis](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/ducalis.svg)](https://hi.ducalis.io/) | [![jetbrains](https://github.com/DGP-Studio/Snap.Hutao/assets/36357191/4105772a-728a-4a84-9c6e-d713a5698a20)](https://www.jetbrains.com/opensource/) | |
- Netlify provides document and home page hosting service for Snap Hutao
@@ -88,6 +88,8 @@ Snap Hutao is currently using sponsored software from the following service prov
- DigitalOcean provides reliable cloud database for Snap Hutao database backup
- [Ducalis.io](https://hi.ducalis.io/) provides Snap Hutao project with a complete decision-making toolkit for project management
- Jetbrains provides powerful IDE for Snap Hutao infrastructure services coding
## 开发 / Development

View File

@@ -11,6 +11,9 @@ var version = "version";
var repoDir = "repoDir";
var outputPath = "outputPath";
var pfxPath = "pfxPath";
var pw = "pw";
// Extension
static ProcessArgumentBuilder AppendIf(this ProcessArgumentBuilder builder, string text, bool condition)
@@ -62,6 +65,11 @@ if (GitHubActions.IsRunningOnGitHubActions)
}
);
var certificateBase64 = HasEnvironmentVariable("CERTIFICATE") ? EnvironmentVariable("CERTIFICATE") : throw new Exception("Cannot find CERTIFICATE");
pw = HasEnvironmentVariable("PW") ? EnvironmentVariable("PW") : throw new Exception("Cannot find PW");
pfxPath = System.IO.Path.Combine(repoDir, "temp.pfx");
System.IO.File.WriteAllBytes(pfxPath, System.Convert.FromBase64String(certificateBase64));
Information($"Version: {version}");
}
@@ -88,10 +96,19 @@ else // Local
Information($"Version: {version}");
}
// Windows SDK
var registry = new WindowsRegistry();
var winsdkRegistry = registry.LocalMachine.OpenKey(@"SOFTWARE\Microsoft\Windows Kits\Installed Roots");
var winsdkVersion = winsdkRegistry.GetSubKeyNames().MaxBy(key => int.Parse(key.Split(".")[2]));
var winsdkPath = (string)winsdkRegistry.GetValue("KitsRoot10");
var winsdkBinPath = System.IO.Path.Combine(winsdkPath, "bin", winsdkVersion, "x64");
Information($"Windows SDK: {winsdkPath}");
Task("Build")
.IsDependentOn("Build binary package")
.IsDependentOn("Copy files")
.IsDependentOn("Build MSIX");
.IsDependentOn("Build MSIX")
.IsDependentOn("Sign");
Task("NuGet Restore")
.Does(() =>
@@ -207,8 +224,11 @@ Task("Build MSIX")
{
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Local-{version}.msix");
}
var makeappxPath = System.IO.Path.Combine(winsdkBinPath, "makeappx.exe");
var p = StartProcess(
"makeappx.exe",
makeappxPath,
new ProcessSettings
{
Arguments = arguments
@@ -216,7 +236,46 @@ Task("Build MSIX")
);
if (p != 0)
{
throw new InvalidOperationException("Build failed with exit code " + p);
throw new InvalidOperationException("Build MSIX failed with exit code " + p);
}
});
Task("Sign")
.IsDependentOn("Build MSIX")
.Does(() =>
{
if (AppVeyor.IsRunningOnAppVeyor)
{
Information("Move to SignPath. Skip signing.");
return;
}
else if (GitHubActions.IsRunningOnGitHubActions)
{
if (GitHubActions.Environment.PullRequest.IsPullRequest)
{
Information("Is Pull Request. Skip signing.");
return;
}
var signPath = System.IO.Path.Combine(winsdkBinPath, "signtool.exe");
var arguments = $"sign /debug /v /a /fd SHA256 /f {pfxPath} /p {pw} {System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix")}";
var p = StartProcess(
signPath,
new ProcessSettings
{
Arguments = arguments
}
);
if (p != 0)
{
throw new InvalidOperationException("Sign failed with exit code " + p);
}
}
else
{
Information("Local configuration. Skip signing.");
return;
}
});

View File

@@ -1,3 +1,5 @@
files:
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.%osx_locale%.resx
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.resx
translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.%osx_locale%.resx

View File

@@ -322,6 +322,7 @@ dotnet_diagnostic.CA2227.severity = suggestion
dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = false:none
dotnet_diagnostic.SA1124.severity = none
[*.vb]
#### 命名样式 ####

View File

@@ -0,0 +1,31 @@
using System.Net.Http;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public sealed class HttpClientTest
{
[TestMethod]
public void RedirectionHeaderTest()
{
HttpClientHandler handler = new()
{
UseCookies = false,
AllowAutoRedirect = false,
};
using (handler)
{
using (HttpClient httpClient = new(handler))
{
using (HttpRequestMessage request = new(HttpMethod.Get, "https://api.snapgenshin.com/patch/hutao/download"))
{
using (HttpResponseMessage response = httpClient.Send(request))
{
_ = 1;
}
}
}
}
}
}

View File

@@ -13,19 +13,19 @@ public sealed class JsonSerializeTest
NumberHandling = JsonNumberHandling.AllowReadingFromString,
};
private const string SmapleObjectJson = """
private const string SampleObjectJson = """
{
"A" :1
}
""";
private const string SmapleEmptyStringObjectJson = """
private const string SampleEmptyStringObjectJson = """
{
"A" : ""
}
""";
private const string SmapleNumberKeyDictionaryJson = """
private const string SampleNumberKeyDictionaryJson = """
{
"111" : "12",
"222" : "34"
@@ -35,7 +35,7 @@ public sealed class JsonSerializeTest
[TestMethod]
public void DelegatePropertyCanSerialize()
{
SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SmapleObjectJson)!;
SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SampleObjectJson)!;
Assert.AreEqual(sample.B, 1);
}
@@ -43,14 +43,23 @@ public sealed class JsonSerializeTest
[ExpectedException(typeof(JsonException))]
public void EmptyStringCannotSerializeAsNumber()
{
SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SmapleEmptyStringObjectJson)!;
SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SampleEmptyStringObjectJson)!;
Assert.AreEqual(sample.A, 0);
}
[TestMethod]
public void EmptyStringCanSerializeAsUri()
{
SampleEmptyUriClass sample = JsonSerializer.Deserialize<SampleEmptyUriClass>(SampleEmptyStringObjectJson)!;
Uri.TryCreate("", UriKind.RelativeOrAbsolute, out Uri? value);
Console.WriteLine(value);
Assert.AreEqual(sample.A, value);
}
[TestMethod]
public void NumberStringKeyCanSerializeAsKey()
{
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, AlowStringNumberOptions)!;
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SampleNumberKeyDictionaryJson, AlowStringNumberOptions)!;
Assert.AreEqual(sample[111], "12");
}
@@ -92,6 +101,11 @@ public sealed class JsonSerializeTest
public int A { get; set; }
}
private sealed class SampleEmptyUriClass
{
public Uri A { get; set; } = default!;
}
private sealed class SampleByteArrayPropertyClass
{
public byte[]? Array { get; set; }

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public sealed class ListTest
{
[TestMethod]
public void IndexOfNullIsNegativeOne()
{
List<object> list = [new()];
Assert.AreEqual(-1, list.IndexOf(default!));
}
}

View File

@@ -6,14 +6,25 @@ namespace Snap.Hutao.Test.IncomingFeature;
public class SpiralAbyssScheduleIdTest
{
private static readonly TimeSpan Utc8 = new(8, 0, 0);
private static readonly DateTimeOffset AcrobaticsBattleIntroducedTime = new(2024, 7, 1, 4, 0, 0, Utc8);
[TestMethod]
public void Test()
{
Console.WriteLine($"当前第 {GetForDateTimeOffset(DateTimeOffset.Now)} 期");
DateTimeOffset dateTimeOffset = new(2020, 7, 1, 4, 0, 0, Utc8);
Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(dateTimeOffset)} 期");
// 2020-07-01 04:00:00 为第 1 期
// 2024-06-16 04:00:00 为第 96 期
// 2024-07-01 04:00:00 为第 97 期
// 2024-07-16 04:00:00 为第 98 期
// 2024-08-01 04:00:00 为第 99 期
Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(new(2020, 07, 01, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-06-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 06, 16, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-07-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 07, 01, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-07-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 07, 16, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-08-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 08, 01, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-08-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 08, 16, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-09-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 09, 01, 4, 0, 0, Utc8))} 期");
}
public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset)
@@ -38,6 +49,12 @@ public class SpiralAbyssScheduleIdTest
periodNum--;
}
if (dateTimeOffset >= AcrobaticsBattleIntroducedTime)
{
// 当超过 96 期时,每一个月一期
periodNum = (4 * 12 * 2) + ((periodNum - (4 * 12 * 2)) / 2);
}
return periodNum;
}
}

View File

@@ -0,0 +1,45 @@
using System.Drawing;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Snap.Hutao.Test.RuntimeBehavior;
[TestClass]
public sealed class HttpClientBehaviorTest
{
private const int MessageNotYetSent = 0;
[TestMethod]
public async Task RetrySendHttpRequestMessage()
{
using (HttpClient httpClient = new())
{
HttpRequestMessage requestMessage = new(HttpMethod.Post, "https://jsonplaceholder.typicode.com/posts");
JsonContent content = JsonContent.Create(new Point(12, 34));
requestMessage.Content = content;
using (requestMessage)
{
await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
}
Interlocked.Exchange(ref GetPrivateSendStatus(requestMessage), MessageNotYetSent);
Volatile.Write(ref GetPrivateDisposed(content), false);
await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
}
}
// private int _sendStatus
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_sendStatus")]
private static extern ref int GetPrivateSendStatus(HttpRequestMessage message);
// private bool _disposed
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
private static extern ref bool GetPrivateDisposed(HttpRequestMessage message);
// private bool _disposed
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
private static extern ref bool GetPrivateDisposed(HttpContent content);
}

View File

@@ -56,6 +56,51 @@ public sealed class UnsafeRuntimeBehaviorTest
Console.WriteLine(System.Text.Encoding.UTF8.GetString(bytes));
}
[TestMethod]
public unsafe void UnsafeSizeInt32ToRectInt32Test()
{
RectInt32 rectInt32 = ToRectInt32(new(100, 200));
Assert.AreEqual(rectInt32.X, 0);
Assert.AreEqual(rectInt32.Y, 0);
Assert.AreEqual(rectInt32.Width, 100);
Assert.AreEqual(rectInt32.Height, 200);
unsafe RectInt32 ToRectInt32(SizeInt32 sizeInt32)
{
byte* pBytes = stackalloc byte[sizeof(RectInt32)];
*(SizeInt32*)(pBytes + 8) = sizeInt32;
return *(RectInt32*)pBytes;
}
}
private struct RectInt32
{
public int X;
public int Y;
public int Width;
public int Height;
public RectInt32(int x, int y, int width, int height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
}
private struct SizeInt32
{
public int Width;
public int Height;
public SizeInt32(int width, int height)
{
Width = width;
Height = height;
}
}
private readonly struct TestStruct
{
public readonly int Value1;

View File

@@ -13,9 +13,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.4.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.4.3" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -7,30 +7,38 @@
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources/>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsCard/SettingsCard.xaml"/>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.TokenizingTextBox/TokenizingTextBox.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Loading.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Image/CachedImage.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Card.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Color.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/ComboBox.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Converter.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/CornerRadius.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/FlyoutStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/FontStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Glyph.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/InfoBarOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/ItemsPanelTemplate.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/NumericValue.xaml"/>
<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 Source="ms-appx:///CommunityToolkit.Labs.WinUI.TokenView/TokenItem/TokenItem.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Elevation.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/ItemIcon.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Loading.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/StandardView.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/AutoSuggestBox/AutoSuggestTokenBox.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/CardBlock.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/CardProgressBar.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/HorizontalCard.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/VerticalCard.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Image/CachedImage.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/TextBlock/RateDeltaTextBlock.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Card.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Color.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/ComboBox.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Converter.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/CornerRadius.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/FlyoutStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/FontStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Glyph.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/InfoBarOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/ItemsPanelTemplate.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/NumericValue.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/PageOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/PivotOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/ScrollViewer.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/SegmentedOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/SettingsStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Thickness.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/TransitionCollection.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Uri.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/WindowOverride.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style
@@ -44,15 +52,15 @@
x:Name="NoneSelectionListViewItemStyle"
BasedOn="{StaticResource DefaultListViewItemStyle}"
TargetType="ListViewItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0,4,0,0"/>
<Setter Property="Padding" Value="0"/>
</Style>
<Style
x:Name="NoneSelectionGridViewItemStyle"
BasedOn="{StaticResource DefaultGridViewItemStyle}"
TargetType="GridViewItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0,0,2,4"/>
<Setter Property="Padding" Value="0"/>
</Style>
</ResourceDictionary>
</Application.Resources>

View File

@@ -3,12 +3,13 @@
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Microsoft.Windows.AppNotifications;
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.Windowing;
using Snap.Hutao.UI.Xaml;
using System.Diagnostics;
namespace Snap.Hutao;
@@ -59,7 +60,7 @@ public sealed partial class App : Application
public new void Exit()
{
XamlWindowLifetime.ApplicationExiting = true;
XamlApplicationLifetime.Exiting = true;
base.Exit();
}
@@ -68,10 +69,15 @@ public sealed partial class App : Application
{
try
{
// Important: You must call AppNotificationManager::Default().Register
// before calling AppInstance.GetCurrent.GetActivatedEventArgs.
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
AppNotificationManager.Default.Register();
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
{
logger.LogDebug("Application exiting on RedirectActivationTo");
Exit();
return;
}
@@ -85,7 +91,7 @@ public sealed partial class App : Application
}
catch (Exception ex)
{
Debug.WriteLine(ex);
logger.LogError(ex, "Application failed in App.OnLaunched");
Process.GetCurrentProcess().Kill();
}
}

View File

@@ -1,102 +0,0 @@
// 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);
}
}

View File

@@ -1,46 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Labs.WinUI.MarqueeTextRns;
using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml.Input;
namespace Snap.Hutao.Control.Behavior;
internal sealed class MarqueeTextBehavior : BehaviorBase<MarqueeText>
{
private readonly PointerEventHandler pointerEnteredEventHandler;
private readonly PointerEventHandler pointerExitedEventHandler;
public MarqueeTextBehavior()
{
pointerEnteredEventHandler = OnPointerEntered;
pointerExitedEventHandler = OnPointerExited;
}
protected override bool Initialize()
{
AssociatedObject.PointerEntered += pointerEnteredEventHandler;
AssociatedObject.PointerExited += pointerExitedEventHandler;
return true;
}
protected override bool Uninitialize()
{
AssociatedObject.PointerEntered -= pointerEnteredEventHandler;
AssociatedObject.PointerExited -= pointerExitedEventHandler;
return true;
}
private void OnPointerEntered(object sender, PointerRoutedEventArgs e)
{
AssociatedObject.StartMarquee();
}
private void OnPointerExited(object sender, PointerRoutedEventArgs e)
{
AssociatedObject.StopMarquee();
}
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.UI;
namespace Snap.Hutao.Control.Brush;
internal sealed class ColorSegment : IColorSegment
{
public ColorSegment(Color color, double value)
{
Color = color;
Value = value;
}
public Color Color { get; set; }
public double Value { get; set; }
}

View File

@@ -1,8 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Brush;
internal sealed class ColorSegmentCollection : List<IColorSegment>
{
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.UI;
namespace Snap.Hutao.Control.Brush;
internal interface IColorSegment
{
Color Color { get; }
double Value { get; set; }
}

View File

@@ -1,56 +0,0 @@
// 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.Media;
using Microsoft.UI.Xaml.Shapes;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Brush;
[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)
{
UpdateLinearGradientBrush((SegmentedBar)obj);
}
private static void UpdateLinearGradientBrush(SegmentedBar segmentedBar)
{
GradientStopCollection collection = segmentedBar.brush.GradientStops;
collection.Clear();
ColorSegmentCollection segmentCollection = segmentedBar.Source;
double total = segmentCollection.Sum(seg => seg.Value);
if (total is 0D)
{
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, });
}
}
}

View File

@@ -1,10 +0,0 @@
// 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();
}

View File

@@ -1,25 +0,0 @@
// 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;
}
}

View File

@@ -1,8 +0,0 @@
// 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>;

View File

@@ -1,19 +0,0 @@
// 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);
}
}

View File

@@ -1,12 +0,0 @@
// 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; }
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
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
{
private readonly VectorChangedEventHandler<object> itemsVectorChangedEventHandler;
public AlternatingItemsControl()
{
itemsVectorChangedEventHandler = OnItemsVectorChanged;
Items.VectorChanged += itemsVectorChangedEventHandler;
}
private void OnItemsVectorChanged(IObservableVector<object> items, IVectorChangedEventArgs args)
{
if (args.CollectionChange is CollectionChange.Reset)
{
int index = (int)args.Index;
for (int i = index; i < items.Count; i++)
{
if (items[i] is IAlternatingItem item)
{
item.Background = i % 2 is 0 ? default : ItemAlternateBackground;
}
else
{
break;
}
}
}
}
}

View File

@@ -1,10 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Collection.Alternating;
[Obsolete("Use SettingsCard instead")]
internal interface IAlternatingItem
{
public Microsoft.UI.Xaml.Media.Brush? Background { get; set; }
}

View File

@@ -1,38 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("SquareLength", typeof(double), 0D, nameof(OnSquareLengthChanged), IsAttached = true, AttachedType = typeof(FrameworkElement))]
[DependencyProperty("IsActualThemeBindingEnabled", typeof(bool), false, nameof(OnIsActualThemeBindingEnabled), IsAttached = true, AttachedType = typeof(FrameworkElement))]
[DependencyProperty("ActualTheme", typeof(ElementTheme), ElementTheme.Default, IsAttached = true, AttachedType = typeof(FrameworkElement))]
public sealed partial class FrameworkElementHelper
{
private static void OnSquareLengthChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = (FrameworkElement)dp;
element.Width = (double)e.NewValue;
element.Height = (double)e.NewValue;
}
private static void OnIsActualThemeBindingEnabled(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = (FrameworkElement)dp;
if ((bool)e.NewValue)
{
element.ActualThemeChanged += OnActualThemeChanged;
}
else
{
element.ActualThemeChanged -= OnActualThemeChanged;
}
static void OnActualThemeChanged(FrameworkElement sender, object args)
{
SetActualTheme(sender, sender.ActualTheme);
}
}
}

View File

@@ -1,45 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.ExceptionService;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Image;
/// <summary>
/// 缓存图像
/// </summary>
[HighQuality]
internal sealed class CachedImage : Implementation.ImageEx
{
/// <summary>
/// 构造一个新的缓存图像
/// </summary>
public CachedImage()
{
DefaultStyleKey = typeof(CachedImage);
DefaultStyleResourceUri = "ms-appx:///Control/Image/CachedImage.xaml".ToUri();
}
/// <inheritdoc/>
protected override async Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
try
{
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
return file.ToUri();
}
catch (COMException)
{
// The image is corrupted, remove it.
imageCache.Remove(imageUri);
return default;
}
}
}

View File

@@ -1,37 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Windows.Media.Casting;
namespace Snap.Hutao.Control.Image.Implementation;
[DependencyProperty("NineGrid", typeof(Thickness))]
internal partial class ImageEx : ImageExBase
{
public ImageEx()
: base()
{
}
public override CompositionBrush GetAlphaMask()
{
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)
{
return image.GetAlphaMask();
}
return default!;
}
public CastingSource GetAsCastingSource()
{
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)
{
return image.GetAsCastingSource();
}
return default!;
}
}

View File

@@ -1,67 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Theme;
using Windows.Foundation;
namespace Snap.Hutao.Control.Image;
/// <summary>
/// 支持单色的图像
/// </summary>
[HighQuality]
internal sealed class MonoChrome : CompositionImage
{
private readonly TypedEventHandler<FrameworkElement, object> actualThemeChangedEventHandler;
private CompositionColorBrush? backgroundBrush;
/// <summary>
/// 构造一个新的单色图像
/// </summary>
public MonoChrome()
{
actualThemeChangedEventHandler = OnActualThemeChanged;
ActualThemeChanged += actualThemeChangedEventHandler;
}
/// <inheritdoc/>
protected override SpriteVisual CompositeSpriteVisual(Compositor compositor, LoadedImageSurface imageSurface)
{
CompositionColorBrush blackLayerBrush = compositor.CreateColorBrush(Colors.Black);
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.Uniform, vRatio: 0f);
CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBrush, imageSurfaceBrush, BlendEffectMode.Overlay);
CompositionEffectBrush opacityBrush = compositor.CompositeLuminanceToAlphaEffectBrush(overlayBrush);
backgroundBrush = compositor.CreateColorBrush();
SetBackgroundColor(backgroundBrush);
CompositionEffectBrush alphaMaskEffectBrush = compositor.CompositeAlphaMaskEffectBrush(backgroundBrush, opacityBrush);
return compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
}
private void OnActualThemeChanged(FrameworkElement sender, object args)
{
if (backgroundBrush is not null)
{
SetBackgroundColor(backgroundBrush);
}
}
private void SetBackgroundColor(CompositionColorBrush backgroundBrush)
{
ApplicationTheme theme = ThemeHelper.ElementToApplication(ActualTheme);
backgroundBrush.Color = theme switch
{
ApplicationTheme.Light => Colors.Black,
ApplicationTheme.Dark => Colors.White,
_ => Colors.Transparent,
};
}
}

View File

@@ -1,64 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using Windows.UI;
namespace Snap.Hutao.Control.Media;
/// <summary>
/// BGRA 结构
/// </summary>
[HighQuality]
internal struct Bgra32
{
/// <summary>
/// B
/// </summary>
public byte B;
/// <summary>
/// G
/// </summary>
public byte G;
/// <summary>
/// R
/// </summary>
public byte R;
/// <summary>
/// A
/// </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>
/// <param name="color">颜色</param>
/// <returns>新的 BGRA8 结构</returns>
public static unsafe implicit operator Bgra32(Color color)
{
Unsafe.SkipInit(out Bgra32 bgra8);
*(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;
}
}

View File

@@ -1,32 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// Some part of this file came from:
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
namespace Snap.Hutao.Control.Media;
/// <summary>
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
/// </summary>
internal struct Hsla32
{
/// <summary>
/// The Hue in 0..360 range.
/// </summary>
public double H;
/// <summary>
/// The Saturation in 0..1 range.
/// </summary>
public double S;
/// <summary>
/// The Lightness in 0..1 range.
/// </summary>
public double L;
/// <summary>
/// The Alpha/opacity in 0..1 range.
/// </summary>
public double A;
}

View File

@@ -1,150 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// Some part of this file came from:
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
using System.Buffers.Binary;
using Windows.UI;
namespace Snap.Hutao.Control.Media;
[HighQuality]
internal struct Rgba32
{
public byte R;
public byte G;
public byte B;
public byte A;
public Rgba32(string hex)
: this(hex.Length == 6 ? Convert.ToUInt32($"{hex}FF", 16) : Convert.ToUInt32(hex, 16))
{
}
public unsafe Rgba32(uint xrgbaCode)
{
// uint layout: 0xRRGGBBAA is AABBGGRR
// AABBGGRR -> RRGGBBAA
fixed (Rgba32* pSelf = &this)
{
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(xrgbaCode);
}
}
private Rgba32(byte r, byte g, byte b, byte a)
{
R = r;
G = g;
B = b;
A = a;
}
public static unsafe implicit operator Color(Rgba32 hexColor)
{
// Goal : Rgba32:RRGGBBAA(0xAABBGGRR) -> Color: AARRGGBB(0xBBGGRRAA)
// Step1: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:AA000000(0x000000AA)
uint a = ((*(uint*)&hexColor) >> 24) & 0x000000FF;
// Step2: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:00RRGGBB(0xRRGGBB00)
uint rgb = ((*(uint*)&hexColor) << 8) & 0xFFFFFF00;
// Step2: UInt32:00RRGGBB(0xRRGGBB00) + UInt32:AA000000(0x000000AA) -> UInt32:AARRGGBB(0xRRGGBBAA)
uint rgba = rgb + a;
return *(Color*)&rgba;
}
public static Rgba32 FromHsl(Hsla32 hsl)
{
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
double h1 = hsl.H / 60;
double x = chroma * (1 - Math.Abs((h1 % 2) - 1));
double m = hsl.L - (0.5 * chroma);
double r1, g1, b1;
if (h1 < 1)
{
r1 = chroma;
g1 = x;
b1 = 0;
}
else if (h1 < 2)
{
r1 = x;
g1 = chroma;
b1 = 0;
}
else if (h1 < 3)
{
r1 = 0;
g1 = chroma;
b1 = x;
}
else if (h1 < 4)
{
r1 = 0;
g1 = x;
b1 = chroma;
}
else if (h1 < 5)
{
r1 = x;
g1 = 0;
b1 = chroma;
}
else
{
r1 = chroma;
g1 = 0;
b1 = x;
}
byte r = (byte)(255 * (r1 + m));
byte g = (byte)(255 * (g1 + m));
byte b = (byte)(255 * (b1 + m));
byte a = (byte)(255 * hsl.A);
return new(r, g, b, a);
}
public readonly Hsla32 ToHsl()
{
const double toDouble = 1.0 / 255;
double r = toDouble * R;
double g = toDouble * G;
double b = toDouble * B;
double max = Math.Max(Math.Max(r, g), b);
double min = Math.Min(Math.Min(r, g), b);
double chroma = max - min;
double h1;
if (chroma == 0)
{
h1 = 0;
}
else if (max == r)
{
// The % operator doesn't do proper modulo on negative
// numbers, so we'll add 6 before using it
h1 = (((g - b) / chroma) + 6) % 6;
}
else if (max == g)
{
h1 = 2 + ((b - r) / chroma);
}
else
{
h1 = 4 + ((r - g) / chroma);
}
double lightness = 0.5 * (max + min);
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
Hsla32 ret;
ret.H = 60 * h1;
ret.S = saturation;
ret.L = lightness;
ret.A = toDouble * A;
return ret;
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32;
using Windows.UI;
namespace Snap.Hutao.Control.Theme;
internal static class KnownColors
{
public static readonly Color Orange = StructMarshal.Color(0xFFBC6932);
public static readonly Color Purple = StructMarshal.Color(0xFFA156E0);
public static readonly Color Blue = StructMarshal.Color(0xFF5180CB);
public static readonly Color Green = StructMarshal.Color(0xFF2A8F72);
public static readonly Color White = StructMarshal.Color(0xFF72778B);
}

View File

@@ -3,7 +3,7 @@
using System.Diagnostics;
namespace Snap.Hutao.Core.Abstraction.Extension;
namespace Snap.Hutao.Core.Abstraction;
internal static class BuilderExtension
{

View File

@@ -5,5 +5,5 @@ namespace Snap.Hutao.Core.Abstraction;
internal interface IPinnable<TData>
{
ref readonly TData GetPinnableReference();
ref TData GetPinnableReference();
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
internal interface IResurrectable
{
void Resurrect();
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.IO;
namespace Snap.Hutao.Core.Caching;
@@ -11,27 +12,11 @@ namespace Snap.Hutao.Core.Caching;
[HighQuality]
internal interface IImageCache
{
/// <summary>
/// Gets the file path containing cached item for given Uri
/// </summary>
/// <param name="uri">Uri of the item.</param>
/// <returns>a string path</returns>
ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri);
/// <summary>
/// Removed items based on uri list passed
/// </summary>
/// <param name="uriForCachedItems">Enumerable uri list</param>
ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri, ElementTheme theme);
void Remove(in ReadOnlySpan<Uri> uriForCachedItems);
/// <summary>
/// Removed item based on uri passed
/// </summary>
/// <param name="uriForCachedItem">uri</param>
void Remove(Uri uriForCachedItem);
/// <summary>
/// Removes invalid cached files
/// </summary>
void RemoveInvalid();
}

View File

@@ -2,25 +2,30 @@
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.UI;
using Snap.Hutao.ViewModel.Guide;
using Snap.Hutao.Web;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using Snap.Hutao.Win32.System.WinRT;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Http;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using WinRT;
namespace Snap.Hutao.Core.Caching;
/// <summary>
/// Provides methods and tools to cache files in a folder
/// The class's name will become the cache folder's name
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IImageCache))]
@@ -28,17 +33,17 @@ namespace Snap.Hutao.Core.Caching;
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string CacheFolderName = nameof(ImageCache);
private const string CacheFailedDownloadTasksName = $"{nameof(ImageCache)}.FailedDownloadTasks";
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = FrozenDictionary.ToFrozenDictionary(
private static readonly FrozenDictionary<int, TimeSpan> DelayFromRetryCount = FrozenDictionary.ToFrozenDictionary(
[
KeyValuePair.Create(0, TimeSpan.FromSeconds(4)),
KeyValuePair.Create(1, TimeSpan.FromSeconds(16)),
KeyValuePair.Create(2, TimeSpan.FromSeconds(64)),
]);
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
private readonly ConcurrentDictionary<ElementThemeValueFile, Task> themefileTasks = [];
private readonly ConcurrentDictionary<string, Task> downloadTasks = [];
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly IHttpClientFactory httpClientFactory;
@@ -46,32 +51,24 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
private readonly ILogger<ImageCache> logger;
private readonly IMemoryCache memoryCache;
private string? baseFolder;
private string? cacheFolder;
private string CacheFolder
{
get => LazyInitializer.EnsureInitialized(ref cacheFolder, () =>
{
baseFolder ??= serviceProvider.GetRequiredService<RuntimeOptions>().LocalCache;
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
return info.FullName;
string folder = serviceProvider.GetRequiredService<RuntimeOptions>().GetLocalCacheImageCacheFolder();
Directory.CreateDirectory(Path.Combine(folder, "Light"));
Directory.CreateDirectory(Path.Combine(folder, "Dark"));
return folder;
});
}
/// <inheritdoc/>
public void RemoveInvalid()
{
RemoveCore(Directory.GetFiles(CacheFolder).Where(file => IsFileInvalid(file, false)));
}
/// <inheritdoc/>
public void Remove(Uri uriForCachedItem)
{
Remove([uriForCachedItem]);
}
/// <inheritdoc/>
public void Remove(in ReadOnlySpan<Uri> uriForCachedItems)
{
if (uriForCachedItems.Length <= 0)
@@ -95,45 +92,87 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
RemoveCore(filesToDelete);
}
/// <inheritdoc/>
public async ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri)
public ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri)
{
return GetFileFromCacheAsync(uri, ElementTheme.Default);
}
public async ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri, ElementTheme theme)
{
string fileName = GetCacheFileName(uri);
string filePath = Path.Combine(CacheFolder, fileName);
string defaultFilePath = Path.Combine(CacheFolder, fileName);
string themeOrDefaultFilePath = theme is ElementTheme.Dark or ElementTheme.Light
? Path.Combine(CacheFolder, $"{theme}", fileName)
: defaultFilePath;
if (!IsFileInvalid(filePath))
if (!IsFileInvalid(themeOrDefaultFilePath))
{
return filePath;
return themeOrDefaultFilePath;
}
TaskCompletionSource taskCompletionSource = new();
try
ElementThemeValueFile key = new(fileName, theme);
// To prevent re-entrancy, always try add first, and if add failed, we try to get the task
TaskCompletionSource themeFileTcs = new();
if (themefileTasks.TryAdd(key, themeFileTcs.Task))
{
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
try
{
logger.LogColorizedInformation("Begin to download file from '{Uri}' to '{File}'", (uri, ConsoleColor.Cyan), (filePath, ConsoleColor.Cyan));
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
if (!IsFileInvalid(defaultFilePath))
{
await ConvertAndSaveFileToMonoChromeAsync(defaultFilePath, themeOrDefaultFilePath, theme).ConfigureAwait(false);
return themeOrDefaultFilePath;
}
TaskCompletionSource downloadTcs = new();
if (downloadTasks.TryAdd(fileName, downloadTcs.Task))
{
try
{
logger.LogColorizedInformation("Begin to download file from '{Uri}' to '{File}'", (uri, ConsoleColor.Cyan), (defaultFilePath, ConsoleColor.Cyan));
await DownloadFileAsync(uri, defaultFilePath).ConfigureAwait(false);
}
finally
{
downloadTcs.TrySetResult();
downloadTasks.TryRemove(fileName, out _);
}
}
else if (downloadTasks.TryGetValue(fileName, out Task? task))
{
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", (uri, ConsoleColor.Cyan));
await task.ConfigureAwait(false);
}
if (!IsFileInvalid(defaultFilePath))
{
await ConvertAndSaveFileToMonoChromeAsync(defaultFilePath, themeOrDefaultFilePath, theme).ConfigureAwait(false);
return themeOrDefaultFilePath;
}
return themeOrDefaultFilePath;
}
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
finally
{
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", (uri, ConsoleColor.Cyan));
await task.ConfigureAwait(false);
themeFileTcs.TrySetResult();
themefileTasks.TryRemove(key, out _);
}
concurrentTasks.TryRemove(fileName, out _);
}
finally
else if (themefileTasks.TryGetValue(key, out Task? themeTask))
{
taskCompletionSource.TrySetResult();
await themeTask.ConfigureAwait(false);
return themeOrDefaultFilePath;
}
else
{
throw HutaoException.NotSupported("The task should not be null.");
}
return filePath;
}
/// <inheritdoc/>
public ValueFile GetFileFromCategoryAndName(string category, string fileName)
{
Uri dummyUri = Web.HutaoEndpoints.StaticRaw(category, fileName).ToUri();
Uri dummyUri = HutaoEndpoints.StaticRaw(category, fileName).ToUri();
return Path.Combine(CacheFolder, GetCacheFileName(dummyUri));
}
@@ -149,8 +188,51 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
return treatNullFileAsInvalid;
}
FileInfo fileInfo = new(file);
return fileInfo.Length == 0;
return new FileInfo(file).Length == 0;
}
private static async ValueTask ConvertAndSaveFileToMonoChromeAsync(string sourceFile, string themeFile, ElementTheme theme)
{
if (string.Equals(sourceFile, themeFile, StringComparison.OrdinalIgnoreCase))
{
return;
}
using (FileStream sourceStream = File.OpenRead(sourceFile))
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(sourceStream.AsRandomAccessStream());
// Always premultiplied to prevent some channels have a non-zero value when the alpha channel is zero
using (SoftwareBitmap sourceBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied))
{
using (BitmapBuffer sourceBuffer = sourceBitmap.LockBuffer(BitmapBufferAccessMode.ReadWrite))
{
using (IMemoryBufferReference reference = sourceBuffer.CreateReference())
{
IMemoryBufferByteAccess byteAccess = reference.As<IMemoryBufferByteAccess>();
byte value = theme is ElementTheme.Light ? (byte)0x00 : (byte)0xFF;
ConvertToMonoChrome(byteAccess, value);
}
}
using (FileStream themeStream = File.Create(themeFile))
{
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, themeStream.AsRandomAccessStream());
encoder.SetSoftwareBitmap(sourceBitmap);
await encoder.FlushAsync();
}
}
}
static void ConvertToMonoChrome(IMemoryBufferByteAccess byteAccess, byte background)
{
byteAccess.GetBuffer(out Span<Rgba32> span);
foreach (ref Rgba32 pixel in span)
{
pixel.A = (byte)pixel.Luminance255;
pixel.R = pixel.G = pixel.B = background;
}
}
}
private void RemoveCore(IEnumerable<string> filePaths)
@@ -172,80 +254,93 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
[SuppressMessage("", "SH003")]
private async Task DownloadFileAsync(Uri uri, string baseFile)
{
int retryCount = 0;
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
while (retryCount < 3)
using (HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache)))
{
int retryCount = 0;
HttpRequestMessageBuilder requestMessageBuilder = httpRequestMessageBuilderFactory
.Create()
.SetRequestUri(uri)
// These headers are only available for our own api
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase))
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase)) // These headers are only available for our own api
.Get();
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
while (retryCount < 3)
{
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
{
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
}
requestMessageBuilder.Resurrect();
if (responseMessage.IsSuccessStatusCode)
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
{
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
if (responseMessage.Content.Headers.ContentType?.MediaType is "application/json")
// Redirect detection
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
{
#if DEBUG
DebugTrack(uri);
#endif
string raw = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
logger.LogColorizedCritical("Failed to download '{Uri}' with unexpected body '{Raw}'", (uri, ConsoleColor.Red), (raw, ConsoleColor.DarkYellow));
return;
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
}
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
if (responseMessage.IsSuccessStatusCode)
{
using (FileStream fileStream = File.Create(baseFile))
if (responseMessage.Content.Headers.ContentType?.MediaType is "application/json")
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
DebugTrackFailedUri(uri);
string raw = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
logger.LogColorizedCritical("Failed to download '{Uri}' with unexpected body '{Raw}'", (uri, ConsoleColor.Red), (raw, ConsoleColor.DarkYellow));
return;
}
}
}
switch (responseMessage.StatusCode)
{
case HttpStatusCode.TooManyRequests:
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
retryCount++;
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;
using (FileStream fileStream = File.Create(baseFile))
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
}
}
default:
#if DEBUG
DebugTrack(uri);
#endif
logger.LogColorizedCritical("Failed to download '{Uri}' with status code '{StatusCode}'", (uri, ConsoleColor.Red), (responseMessage.StatusCode, ConsoleColor.DarkYellow));
return;
switch (responseMessage.StatusCode)
{
case HttpStatusCode.TooManyRequests:
{
retryCount++;
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? DelayFromRetryCount[retryCount];
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;
}
default:
DebugTrackFailedUri(uri);
logger.LogColorizedCritical("Failed to download '{Uri}' with status code '{StatusCode}'", (uri, ConsoleColor.Red), (responseMessage.StatusCode, ConsoleColor.DarkYellow));
return;
}
}
}
}
}
}
}
#if DEBUG
internal partial class ImageCache
{
private void DebugTrack(Uri uri)
[Conditional("DEBUG")]
private void DebugTrackFailedUri(Uri uri)
{
HashSet<string>? set = memoryCache.GetOrCreate(CacheFailedDownloadTasksName, entry => entry.Value ??= new HashSet<string>()) as HashSet<string>;
HashSet<string>? set = memoryCache.GetOrCreate(CacheFailedDownloadTasksName, entry => new HashSet<string>());
set?.Add(uri.ToString());
}
}
#endif
private readonly struct ElementThemeValueFile
{
public readonly ValueFile File;
public readonly ElementTheme Theme;
public ElementThemeValueFile(ValueFile file, ElementTheme theme)
{
File = file;
Theme = theme;
}
public override int GetHashCode()
{
return HashCode.Combine(File, Theme);
}
}
}

View File

@@ -4,7 +4,7 @@
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Streams;
namespace Snap.Hutao.Core.IO.DataTransfer;
namespace Snap.Hutao.Core.DataTransfer;
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IClipboardProvider))]
@@ -13,7 +13,6 @@ internal sealed partial class ClipboardProvider : IClipboardProvider
private readonly JsonSerializerOptions options;
private readonly ITaskContext taskContext;
/// <inheritdoc/>
public async ValueTask<T?> DeserializeFromJsonAsync<T>()
where T : class
{
@@ -31,7 +30,6 @@ internal sealed partial class ClipboardProvider : IClipboardProvider
return JsonSerializer.Deserialize<T>(json, options);
}
/// <inheritdoc/>
public bool SetText(string text)
{
try
@@ -48,7 +46,23 @@ internal sealed partial class ClipboardProvider : IClipboardProvider
}
}
/// <inheritdoc/>
public async ValueTask<bool> SetTextAsync(string text)
{
try
{
await taskContext.SwitchToMainThreadAsync();
DataPackage content = new() { RequestedOperation = DataPackageOperation.Copy };
content.SetText(text);
Clipboard.SetContent(content);
Clipboard.Flush();
return true;
}
catch
{
return false;
}
}
public bool SetBitmap(IRandomAccessStream stream)
{
try
@@ -65,4 +79,22 @@ internal sealed partial class ClipboardProvider : IClipboardProvider
return false;
}
}
public async ValueTask<bool> SetBitmapAsync(IRandomAccessStream stream)
{
try
{
await taskContext.SwitchToMainThreadAsync();
RandomAccessStreamReference reference = RandomAccessStreamReference.CreateFromStream(stream);
DataPackage content = new() { RequestedOperation = DataPackageOperation.Copy };
content.SetBitmap(reference);
Clipboard.SetContent(content);
Clipboard.Flush();
return true;
}
catch
{
return false;
}
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Storage.Streams;
namespace Snap.Hutao.Core.DataTransfer;
internal interface IClipboardProvider
{
ValueTask<T?> DeserializeFromJsonAsync<T>()
where T : class;
bool SetBitmap(IRandomAccessStream stream);
ValueTask<bool> SetBitmapAsync(IRandomAccessStream stream);
bool SetText(string text);
ValueTask<bool> SetTextAsync(string text);
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Database;
namespace Snap.Hutao.Core.Database.Abstraction;
internal interface IReorderable
{

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity.Abstraction;
namespace Snap.Hutao.Core.Database.Abstraction;
internal interface ISelectable : IAppDbEntity
{
bool IsSelected { get; set; }
}

View File

@@ -0,0 +1,142 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database.Abstraction;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.UI.Xaml.Data;
namespace Snap.Hutao.Core.Database;
// The scope of the view follows the scope of the service provider.
internal sealed class AdvancedDbCollectionView<TEntity> : AdvancedCollectionView<TEntity>, IAdvancedDbCollectionView<TEntity>
where TEntity : class, IAdvancedCollectionViewItem, ISelectable
{
private readonly IServiceProvider serviceProvider;
private bool savingToDatabase = true;
public AdvancedDbCollectionView(IList<TEntity> source, IServiceProvider serviceProvider)
: base(source, true)
{
this.serviceProvider = serviceProvider;
}
public IDisposable SuppressChangeCurrentItem()
{
return new CurrentItemSuppression(this);
}
protected override void OnCurrentChangedOverride()
{
if (serviceProvider is null || !savingToDatabase)
{
return;
}
TEntity? currentItem = CurrentItem;
foreach (TEntity item in Source)
{
item.IsSelected = ReferenceEquals(item, currentItem);
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
dbContext.Set<TEntity>().ExecuteUpdate(update => update.SetProperty(entity => entity.IsSelected, false));
if (currentItem is not null)
{
dbContext.Set<TEntity>().UpdateAndSave(currentItem);
}
}
}
private sealed class CurrentItemSuppression : IDisposable
{
private readonly AdvancedDbCollectionView<TEntity> view;
private readonly TEntity? currentItem;
public CurrentItemSuppression(AdvancedDbCollectionView<TEntity> view)
{
this.view = view;
currentItem = view.CurrentItem;
view.savingToDatabase = false;
}
public void Dispose()
{
view.MoveCurrentTo(currentItem);
view.savingToDatabase = true;
}
}
}
// The scope of the view follows the scope of the service provider.
[SuppressMessage("", "SA1402")]
internal sealed class AdvancedDbCollectionView<TEntityAccess, TEntity> : AdvancedCollectionView<TEntityAccess>, IAdvancedDbCollectionView<TEntityAccess>
where TEntityAccess : class, IEntityAccess<TEntity>, IAdvancedCollectionViewItem
where TEntity : class, ISelectable
{
private readonly IServiceProvider serviceProvider;
private bool savingToDatabase = true;
public AdvancedDbCollectionView(IList<TEntityAccess> source, IServiceProvider serviceProvider)
: base(source, true)
{
this.serviceProvider = serviceProvider;
}
public IDisposable SuppressChangeCurrentItem()
{
return new CurrentItemSuppression(this);
}
protected override void OnCurrentChangedOverride()
{
if (serviceProvider is null || !savingToDatabase)
{
return;
}
TEntityAccess? currentItem = CurrentItem;
foreach (TEntityAccess item in Source)
{
item.Entity.IsSelected = ReferenceEquals(item, currentItem);
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
dbContext.Set<TEntity>().ExecuteUpdate(update => update.SetProperty(entity => entity.IsSelected, false));
if (currentItem is not null)
{
dbContext.Set<TEntity>().UpdateAndSave(currentItem.Entity);
}
}
}
private sealed class CurrentItemSuppression : IDisposable
{
private readonly AdvancedDbCollectionView<TEntityAccess, TEntity> view;
private readonly TEntityAccess? currentItem;
public CurrentItemSuppression(AdvancedDbCollectionView<TEntityAccess, TEntity> view)
{
this.view = view;
currentItem = view.CurrentItem;
view.savingToDatabase = false;
}
public void Dispose()
{
view.MoveCurrentTo(currentItem);
view.savingToDatabase = true;
}
}
}

View File

@@ -20,13 +20,6 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
where TEntity : class
{
dbSet.Add(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
@@ -34,13 +27,6 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities, CancellationToken token = default)
where TEntity : class
{
dbSet.AddRange(entities);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -48,13 +34,6 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
where TEntity : class
{
dbSet.Remove(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -62,13 +41,6 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
where TEntity : class
{
dbSet.Update(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int SaveChangesAndClearChangeTracker<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class
@@ -79,16 +51,6 @@ internal static class DbSetExtension
return count;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet, CancellationToken token = default)
where TEntity : class
{
DbContext dbContext = dbSet.Context();
int count = await dbContext.SaveChangesAsync(token).ConfigureAwait(false);
dbContext.ChangeTracker.Clear();
return count;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static DbContext Context<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.UI.Xaml.Data;
namespace Snap.Hutao.Core.Database;
internal interface IAdvancedDbCollectionView<TEntity> : IAdvancedCollectionView<TEntity>
where TEntity : class
{
IDisposable SuppressChangeCurrentItem();
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 可选择的项
/// 若要使用 <see cref="ScopedDbCurrent{TEntity, TMessage}"/>
/// 必须实现该接口
/// </summary>
[HighQuality]
internal interface ISelectable
{
/// <summary>
/// 数据库内部Id
/// </summary>
Guid InnerId { get; }
/// <summary>
/// 获取或设置当前项的选中状态
/// </summary>
bool IsSelected { get; set; }
}

View File

@@ -1,8 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database.Abstraction;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.ObjectModel;
@@ -22,8 +22,6 @@ internal sealed class ObservableReorderableDbCollection<TEntity> : ObservableCol
this.serviceProvider = serviceProvider;
}
public IAdvancedCollectionView? View { get; set; }
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
@@ -51,38 +49,33 @@ internal sealed class ObservableReorderableDbCollection<TEntity> : ObservableCol
private void OnReorder()
{
using (View?.DeferRefresh())
{
AdjustIndex((List<TEntity>)Items);
AdjustIndex((List<TEntity>)Items);
using (IServiceScope scope = serviceProvider.CreateScope())
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
foreach (ref readonly TEntity item in CollectionsMarshal.AsSpan((List<TEntity>)Items))
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
foreach (ref readonly TEntity item in CollectionsMarshal.AsSpan((List<TEntity>)Items))
{
dbSet.UpdateAndSave(item);
}
dbSet.UpdateAndSave(item);
}
}
}
}
[SuppressMessage("", "SA1402")]
internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> : ObservableCollection<TEntityOnly>
where TEntityOnly : class, IEntityOnly<TEntity>
internal sealed class ObservableReorderableDbCollection<TEntityAccess, TEntity> : ObservableCollection<TEntityAccess>
where TEntityAccess : class, IEntityAccess<TEntity>
where TEntity : class, IReorderable
{
private readonly IServiceProvider serviceProvider;
public ObservableReorderableDbCollection(List<TEntityOnly> items, IServiceProvider serviceProvider)
public ObservableReorderableDbCollection(List<TEntityAccess> items, IServiceProvider serviceProvider)
: base(AdjustIndex(items.SortBy(x => x.Entity.Index)))
{
this.serviceProvider = serviceProvider;
}
public IAdvancedCollectionView? View { get; set; }
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
@@ -96,12 +89,12 @@ internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> :
}
}
private static List<TEntityOnly> AdjustIndex(List<TEntityOnly> list)
private static List<TEntityAccess> AdjustIndex(List<TEntityAccess> list)
{
Span<TEntityOnly> span = CollectionsMarshal.AsSpan(list);
Span<TEntityAccess> span = CollectionsMarshal.AsSpan(list);
for (int i = 0; i < list.Count; i++)
{
ref readonly TEntityOnly item = ref span[i];
ref readonly TEntityAccess item = ref span[i];
item.Entity.Index = i;
}
@@ -110,19 +103,16 @@ internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> :
private void OnReorder()
{
using (View?.DeferRefresh())
AdjustIndex((List<TEntityAccess>)Items);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AdjustIndex((List<TEntityOnly>)Items);
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
using (IServiceScope scope = serviceProvider.CreateScope())
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
foreach (ref readonly TEntityAccess item in CollectionsMarshal.AsSpan((List<TEntityAccess>)Items))
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
foreach (ref readonly TEntityOnly item in CollectionsMarshal.AsSpan((List<TEntityOnly>)Items))
{
dbSet.UpdateAndSave(item.Entity);
}
dbSet.UpdateAndSave(item.Entity);
}
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database.Abstraction;
using Snap.Hutao.Model;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Database;
internal static class ObservableReorderableDbCollectionExtension
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ObservableReorderableDbCollection<TEntity> ToObservableReorderableDbCollection<TEntity>(this IEnumerable<TEntity> source, IServiceProvider serviceProvider)
where TEntity : class, IReorderable
{
return source is List<TEntity> list
? new ObservableReorderableDbCollection<TEntity>(list, serviceProvider)
: new ObservableReorderableDbCollection<TEntity>([.. source], serviceProvider);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ObservableReorderableDbCollection<TEntityOnly, TEntity> ToObservableReorderableDbCollection<TEntityOnly, TEntity>(this IEnumerable<TEntityOnly> source, IServiceProvider serviceProvider)
where TEntityOnly : class, IEntityAccess<TEntity>
where TEntity : class, IReorderable
{
return source is List<TEntityOnly> list
? new ObservableReorderableDbCollection<TEntityOnly, TEntity>(list, serviceProvider)
: new ObservableReorderableDbCollection<TEntityOnly, TEntity>([.. source], serviceProvider);
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 可查询扩展
/// </summary>
[HighQuality]
internal static class QueryableExtension
{
/// <summary>
/// <code>source.Where(predicate).ExecuteDelete()</code>
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <param name="predicate">条件</param>
/// <returns>SQL返回个数</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ExecuteDeleteWhere<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
{
return source.Where(predicate).ExecuteDelete();
}
/// <summary>
/// <code>source.Where(predicate).ExecuteDeleteAsync(token)</code>
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <param name="predicate">条件</param>
/// <param name="token">取消令牌</param>
/// <returns>SQL返回个数</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ValueTask<int> ExecuteDeleteWhereAsync<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate, CancellationToken token = default)
{
return source.Where(predicate).ExecuteDeleteAsync(token).AsValueTask();
}
}

View File

@@ -1,133 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity.Database;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 范围化的数据库当前项
/// 简化对数据库中选中项的管理
/// </summary>
/// <typeparam name="TEntity">实体的类型</typeparam>
/// <typeparam name="TMessage">消息的类型</typeparam>
[ConstructorGenerated]
internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntity>, new()
{
private readonly IServiceProvider serviceProvider;
private readonly IMessenger messenger;
private TEntity? current;
/// <summary>
/// 当前选中的项
/// </summary>
public TEntity? Current
{
get => current;
set
{
// prevent useless sets
if (current == value)
{
return;
}
if (serviceProvider.IsDisposed())
{
return;
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
// only update when not processing a deletion
if (value is not null && current is not null)
{
current.IsSelected = false;
dbSet.UpdateAndSave(current);
}
TMessage message = new() { OldValue = current, NewValue = value };
current = value;
if (current is not null)
{
current.IsSelected = true;
dbSet.UpdateAndSave(current);
}
messenger.Send(message);
}
}
}
}
[ConstructorGenerated]
internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
where TEntityOnly : class, IEntityOnly<TEntity>
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntityOnly>, new()
{
private readonly IServiceProvider serviceProvider;
private readonly IMessenger messenger;
private TEntityOnly? current;
/// <summary>
/// 当前选中的项
/// </summary>
public TEntityOnly? Current
{
get => current;
set
{
// prevent useless sets
if (current == value)
{
return;
}
if (serviceProvider.IsDisposed())
{
return;
}
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
// only update when not processing a deletion
if (value is not null)
{
if (current is not null)
{
current.Entity.IsSelected = false;
dbSet.UpdateAndSave(current.Entity);
}
}
TMessage message = new() { OldValue = current, NewValue = value };
current = value;
if (current is not null)
{
current.Entity.IsSelected = true;
dbSet.UpdateAndSave(current.Entity);
}
messenger.Send(message);
}
}
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database.Abstraction;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Database;
@@ -11,13 +12,6 @@ namespace Snap.Hutao.Core.Database;
[HighQuality]
internal static class SelectableExtension
{
/// <summary>
/// 获取选中的值或默认值
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>选中的值或默认值</returns>
/// <exception cref="InvalidOperationException">存在多个选中的值</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TSource? SelectedOrDefault<TSource>(this IEnumerable<TSource> source)
where TSource : ISelectable

View File

@@ -1,12 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
/// <summary>
/// 可转换类型服务
/// </summary>
[Obsolete("Not useful anymore")]
internal interface ICastService
{
}

View File

@@ -20,7 +20,7 @@ internal abstract class OverseaSupportFactory<TClient, TClientCN, TClientOS> : I
this.serviceProvider = serviceProvider;
}
public TClient Create(bool isOversea)
public virtual TClient Create(bool isOversea)
{
return isOversea
? serviceProvider.GetRequiredService<TClientOS>()

View File

@@ -41,7 +41,7 @@ internal static class DependencyInjection
.AddJsonOptions()
.AddDatabase()
.AddInjections()
.AddAllHttpClients()
.AddConfiguredHttpClients()
// Discrete services
.AddSingleton<IMessenger, WeakReferenceMessenger>()

View File

@@ -30,31 +30,28 @@ internal static class IocConfiguration
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddDatabase(this IServiceCollection services)
{
return services
.AddTransient(typeof(Database.ScopedDbCurrent<,>))
.AddTransient(typeof(Database.ScopedDbCurrent<,,>))
.AddDbContextPool<AppDbContext>(AddDbContextCore);
}
return services.AddDbContextPool<AppDbContext>(AddDbContextCore);
private static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
{
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
// Temporarily create a context
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString))
static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
{
if (context.Database.GetPendingMigrations().Any())
{
System.Diagnostics.Debug.WriteLine("[Database] Performing AppDbContext Migrations");
context.Database.Migrate();
}
}
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
builder
.EnableSensitiveDataLogging()
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
.UseSqlite(sqlConnectionString);
// Temporarily create a context
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString))
{
if (context.Database.GetPendingMigrations().Any())
{
System.Diagnostics.Debug.WriteLine("[Database] Performing AppDbContext Migrations");
context.Database.Migrate();
}
}
builder
.EnableSensitiveDataLogging()
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
.UseSqlite(sqlConnectionString);
}
}
}

View File

@@ -15,7 +15,7 @@ internal static partial class IocHttpClientConfiguration
{
private const string ApplicationJson = "application/json";
public static IServiceCollection AddAllHttpClients(this IServiceCollection services)
public static IServiceCollection AddConfiguredHttpClients(this IServiceCollection services)
{
services
.ConfigureHttpClientDefaults(clientBuilder =>
@@ -27,7 +27,7 @@ internal static partial class IocHttpClientConfiguration
HttpClientHandler clientHandler = (HttpClientHandler)handler;
clientHandler.AllowAutoRedirect = true;
clientHandler.UseProxy = true;
clientHandler.Proxy = provider.GetRequiredService<DynamicHttpProxy>();
clientHandler.Proxy = provider.GetRequiredService<HttpProxyUsingSystemProxy>();
});
})
.AddHttpClients();
@@ -38,11 +38,6 @@ internal static partial class IocHttpClientConfiguration
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial IServiceCollection AddHttpClients(this IServiceCollection services);
/// <summary>
/// 默认配置
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="client">配置后的客户端</param>
private static void DefaultConfiguration(IServiceProvider serviceProvider, HttpClient client)
{
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
@@ -51,10 +46,6 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.UserAgent.ParseAdd(runtimeOptions.UserAgent);
}
/// <summary>
/// 对于需要添加动态密钥1的客户端使用此配置
/// </summary>
/// <param name="client">配置后的客户端</param>
private static void XRpcConfiguration(HttpClient client)
{
client.Timeout = Timeout.InfiniteTimeSpan;
@@ -65,10 +56,6 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
}
/// <summary>
/// 对于需要添加动态密钥2的客户端使用此配置
/// </summary>
/// <param name="client">配置后的客户端</param>
private static void XRpc2Configuration(HttpClient client)
{
client.Timeout = Timeout.InfiniteTimeSpan;
@@ -84,11 +71,6 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0");
}
/// <summary>
/// 对于需要添加动态密钥1的客户端使用此配置
/// HoYoLAB app
/// </summary>
/// <param name="client">配置后的客户端</param>
private static void XRpc3Configuration(HttpClient client)
{
client.Timeout = Timeout.InfiniteTimeSpan;
@@ -100,11 +82,6 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
}
/// <summary>
/// 对于需要添加动态密钥2的客户端使用此配置
/// HoYoLAB web
/// </summary>
/// <param name="client">配置后的客户端</param>
[SuppressMessage("", "IDE0051")]
private static void XRpc4Configuration(HttpClient client)
{

View File

@@ -10,11 +10,6 @@ namespace Snap.Hutao.Core.DependencyInjection;
[HighQuality]
internal static partial class ServiceCollectionExtension
{
/// <summary>
/// 向容器注册服务
/// 此方法将会自动生成
/// </summary>
/// <param name="services">容器</param>
/// <returns>可继续操作的服务集合</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial IServiceCollection AddInjections(this IServiceCollection services);
}

View File

@@ -10,30 +10,22 @@ namespace Snap.Hutao.Core.DependencyInjection;
/// </summary>
internal static class ServiceProviderExtension
{
/// <inheritdoc cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T CreateInstance<T>(this IServiceProvider serviceProvider, params object[] parameters)
{
return ActivatorUtilities.CreateInstance<T>(serviceProvider, parameters);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDisposed(this IServiceProvider? serviceProvider)
public static bool IsDisposed(this IServiceProvider? serviceProvider, bool treatNullAsDisposed = true)
{
if (serviceProvider is null)
{
return treatNullAsDisposed;
}
try
{
_ = serviceProvider.GetRequiredService<IServiceScopeFactory>();
return false;
}
catch (ObjectDisposedException)
{
return true;
}
if (serviceProvider is ServiceProvider serviceProviderImpl)
{
return GetPrivateDisposed(serviceProviderImpl);
}
return serviceProvider.GetType().GetField("_disposed")?.GetValue(serviceProvider) is true;
}
// private bool _disposed;
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
private static extern ref bool GetPrivateDisposed(ServiceProvider serviceProvider);
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
/// <summary>
/// 指示此特性附加的属性会在属性改变后会异步地设置的其他属性
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
internal sealed class AlsoAsyncSetsAttribute : Attribute
{
public AlsoAsyncSetsAttribute(string propertyName)
{
}
public AlsoAsyncSetsAttribute(string propertyName1, string propertyName2)
{
}
public AlsoAsyncSetsAttribute(params string[] propertyNames)
{
}
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
/// <summary>
/// 指示此特性附加的属性会在属性改变后会设置的其他属性
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
internal sealed class AlsoSetsAttribute : Attribute
{
public AlsoSetsAttribute(string propertyName)
{
}
public AlsoSetsAttribute(string propertyName1, string propertyName2)
{
}
public AlsoSetsAttribute(params string[] propertyNames)
{
}
}

View File

@@ -2,13 +2,10 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Diagnostics;
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 异常记录器
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Singleton)]
internal sealed partial class ExceptionRecorder
@@ -16,13 +13,15 @@ internal sealed partial class ExceptionRecorder
private readonly ILogger<ExceptionRecorder> logger;
private readonly IServiceProvider serviceProvider;
/// <summary>
/// 记录应用程序异常
/// </summary>
/// <param name="app">应用程序</param>
public void Record(Application app)
{
app.UnhandledException += OnAppUnhandledException;
ConfigureDebugSettings(app);
}
[Conditional("DEBUG")]
private void ConfigureDebugSettings(Application app)
{
app.DebugSettings.FailFastOnErrors = false;
app.DebugSettings.IsBindingTracingEnabled = true;
@@ -35,19 +34,13 @@ internal sealed partial class ExceptionRecorder
app.DebugSettings.LayoutCycleDebugBreakLevel = LayoutCycleDebugBreakLevel.High;
}
[SuppressMessage("", "CA2012")]
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
ValueTask<string?> task = serviceProvider
.GetRequiredService<Web.Hutao.Log.HutaoLogUploadClient>()
.UploadLogAsync(e.Exception);
if (!task.IsCompleted)
{
task.GetAwaiter().GetResult();
}
logger.LogError("未经处理的全局异常:\r\n{Detail}", ExceptionFormat.Format(e.Exception));
serviceProvider
.GetRequiredService<Web.Hutao.Log.HutaoLogUploadClient>()
.UploadLog(e.Exception);
}
private void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e)

View File

@@ -51,13 +51,6 @@ internal sealed class HutaoException : Exception
throw new HutaoException(SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(id), innerException);
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static HutaoException UserdataCorrupted(string message, Exception? innerException = default)
{
throw new HutaoException(message, innerException);
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static InvalidCastException InvalidCast<TFrom, TTo>(string name, Exception? innerException = default)

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 帮助更好的抛出异常
/// </summary>
[HighQuality]
[System.Diagnostics.StackTraceHidden]
[Obsolete("Use HutaoException instead")]
internal static class ThrowHelper
{
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static ArgumentException Argument(string message, string? paramName)
{
throw new ArgumentException(message, paramName);
}
}

View File

@@ -1,24 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.UI;
using Snap.Hutao.Win32.System.WinRT;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using WinRT;
namespace Snap.Hutao.Control.Media;
namespace Snap.Hutao.Core.Graphics.Imaging;
/// <summary>
/// 软件位图拓展
/// </summary>
[HighQuality]
internal static class SoftwareBitmapExtension
{
/// <summary>
/// 混合模式 正常
/// </summary>
/// <param name="softwareBitmap">软件位图</param>
/// <param name="tint">底色</param>
public static unsafe void NormalBlend(this SoftwareBitmap softwareBitmap, Bgra32 tint)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.ReadWrite))
@@ -39,7 +31,7 @@ internal static class SoftwareBitmapExtension
}
}
public static unsafe Bgra32 GetAccentColor(this SoftwareBitmap softwareBitmap)
public static unsafe Bgra32 GetBgra32AccentColor(this SoftwareBitmap softwareBitmap)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
{
@@ -59,4 +51,25 @@ internal static class SoftwareBitmapExtension
}
}
}
public static unsafe Rgba32 GetRgba32AccentColor(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)(r / bytes.Length), (byte)(g / bytes.Length), (byte)(b / bytes.Length), (byte)(a / bytes.Length));
}
}
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Graphics;
internal enum PointInt32Kind
{
TopLeft,
TopCenter,
TopRight,
CenterLeft,
Center,
CenterRight,
BottomLeft,
BottomCenter,
BottomRight,
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Windowing.NotifyIcon;
namespace Snap.Hutao.Core.Graphics;
internal readonly struct PointUInt16
{

View File

@@ -1,19 +1,18 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using Windows.Graphics;
namespace Snap.Hutao.Core.Windowing;
namespace Snap.Hutao.Core.Graphics;
internal readonly struct CompactRect
internal readonly struct RectInt16
{
private readonly short x;
private readonly short y;
private readonly short width;
private readonly short height;
private CompactRect(int x, int y, int width, int height)
private RectInt16(int x, int y, int width, int height)
{
this.x = (short)x;
this.y = (short)y;
@@ -21,24 +20,22 @@ internal readonly struct CompactRect
this.height = (short)height;
}
public static implicit operator RectInt32(CompactRect rect)
public static implicit operator RectInt32(RectInt16 rect)
{
return new(rect.x, rect.y, rect.width, rect.height);
}
public static explicit operator CompactRect(RectInt32 rect)
public static explicit operator RectInt16(RectInt32 rect)
{
return new(rect.X, rect.Y, rect.Width, rect.Height);
}
public static unsafe explicit operator CompactRect(ulong value)
public static unsafe explicit operator RectInt16(ulong value)
{
Unsafe.SkipInit(out CompactRect rect);
*(ulong*)&rect = value;
return rect;
return *(RectInt16*)&value;
}
public static unsafe implicit operator ulong(CompactRect rect)
public static unsafe implicit operator ulong(RectInt16 rect)
{
return *(ulong*)&rect;
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.Foundation;
using System.Numerics;
using Windows.Foundation;
using Windows.Graphics;
namespace Snap.Hutao.Core.Graphics;
internal static class RectInt32Convert
{
public static RectInt32 RectInt32(RECT rect)
{
return new(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
}
public static RectInt32 RectInt32(Point position, Vector2 size)
{
return new((int)position.X, (int)position.Y, (int)size.X, (int)size.Y);
}
public static RectInt32 RectInt32(int x, int y, Vector2 size)
{
return new(x, y, (int)size.X, (int)size.Y);
}
public static unsafe RectInt32 RectInt32(PointInt32 position, SizeInt32 size)
{
RectInt32View view = default;
view.Position = position;
view.Size = size;
return *(RectInt32*)&view;
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.Foundation;
using Windows.Graphics;
namespace Snap.Hutao.Core.Graphics;
internal static class RectInt32Extension
{
public static RectInt32 Scale(this RectInt32 rectInt32, double scale)
{
return new((int)(rectInt32.X * scale), (int)(rectInt32.Y * scale), (int)(rectInt32.Width * scale), (int)(rectInt32.Height * scale));
}
public static int Size(this RectInt32 rectInt32)
{
return rectInt32.Width * rectInt32.Height;
}
public static unsafe SizeInt32 GetSizeInt32(this RectInt32 rectInt32)
{
return ((RectInt32View*)&rectInt32)->Size;
}
public static unsafe PointInt32 GetPointInt32(this RectInt32 rectInt32, PointInt32Kind kind)
{
RectInt32View* pView = (RectInt32View*)&rectInt32;
PointInt32 topLeft = pView->Position;
SizeInt32 size = pView->Size;
return kind switch
{
PointInt32Kind.TopLeft => topLeft,
PointInt32Kind.TopCenter => new PointInt32(topLeft.X + (size.Width / 2), topLeft.Y),
PointInt32Kind.TopRight => new PointInt32(topLeft.X + size.Width, topLeft.Y),
PointInt32Kind.CenterLeft => new PointInt32(topLeft.X, topLeft.Y + (size.Height / 2)),
PointInt32Kind.Center => new PointInt32(topLeft.X + (size.Width / 2), topLeft.Y + (size.Height / 2)),
PointInt32Kind.CenterRight => new PointInt32(topLeft.X + size.Width, topLeft.Y + (size.Height / 2)),
PointInt32Kind.BottomLeft => new PointInt32(topLeft.X, topLeft.Y + size.Height),
PointInt32Kind.BottomCenter => new PointInt32(topLeft.X + (size.Width / 2), topLeft.Y + size.Height),
PointInt32Kind.BottomRight => new PointInt32(topLeft.X + size.Width, topLeft.Y + size.Height),
_ => default,
};
}
public static RECT ToRECT(this RectInt32 rect)
{
return new(rect.X, rect.Y, rect.X + rect.Width, rect.Y + rect.Height);
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Graphics;
namespace Snap.Hutao.Core.Graphics;
internal struct RectInt32View
{
public PointInt32 Position;
public SizeInt32 Size;
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Graphics;
namespace Snap.Hutao.Core.Graphics;
internal static class SizeInt32Extension
{
public static SizeInt32 Scale(this SizeInt32 sizeInt32, double scale)
{
return new((int)(sizeInt32.Width * scale), (int)(sizeInt32.Height * scale));
}
public static int Size(this SizeInt32 sizeInt32)
{
return sizeInt32.Width * sizeInt32.Height;
}
public static unsafe RectInt32 ToRectInt32(this SizeInt32 sizeInt32)
{
RectInt32View view = default;
view.Size = sizeInt32;
return *(RectInt32*)&view;
}
}

View File

@@ -1,34 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Storage.Streams;
namespace Snap.Hutao.Core.IO.DataTransfer;
/// <summary>
/// 剪贴板互操作
/// </summary>
internal interface IClipboardProvider
{
/// <summary>
/// 从剪贴板文本中反序列化
/// </summary>
/// <typeparam name="T">目标类型</typeparam>
/// <returns>实例</returns>
ValueTask<T?> DeserializeFromJsonAsync<T>()
where T : class;
/// <summary>
/// 设置位图
/// </summary>
/// <param name="stream">图片流</param>
/// <returns>是否设置成功</returns>
bool SetBitmap(IRandomAccessStream stream);
/// <summary>
/// 设置文本
/// </summary>
/// <param name="text">文本</param>
/// <returns>是否设置成功</returns>
bool SetText(string text);
}

View File

@@ -2,20 +2,57 @@
// Licensed under the MIT license.
using Microsoft.VisualBasic.FileIO;
using Snap.Hutao.Win32.System.Com;
using Snap.Hutao.Win32.UI.Shell;
using System.IO;
using static Snap.Hutao.Win32.Macros;
using static Snap.Hutao.Win32.Ole32;
using static Snap.Hutao.Win32.Shell32;
namespace Snap.Hutao.Core.IO;
internal static class DirectoryOperation
{
public static bool Move(string sourceDirName, string destDirName)
public static bool TryMove(string sourceDirName, string destDirName)
{
if (!Directory.Exists(sourceDirName))
{
return false;
}
FileSystem.MoveDirectory(sourceDirName, destDirName, true);
return true;
try
{
FileSystem.MoveDirectory(sourceDirName, destDirName, true);
return true;
}
catch
{
return false;
}
}
public static unsafe bool UnsafeRename(string path, string name, FILEOPERATION_FLAGS flags = FILEOPERATION_FLAGS.FOF_ALLOWUNDO | FILEOPERATION_FLAGS.FOF_NOCONFIRMMKDIR)
{
bool result = false;
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(path, default, in IShellItem.IID, out IShellItem* pShellItem)))
{
pFileOperation->SetOperationFlags(flags);
pFileOperation->RenameItem(pShellItem, name, default);
if (SUCCEEDED(pFileOperation->PerformOperations()))
{
result = true;
}
IUnknownMarshal.Release(pShellItem);
}
IUnknownMarshal.Release(pFileOperation);
}
return result;
}
}

View File

@@ -32,8 +32,15 @@ internal static class FileOperation
if (overwrite)
{
File.Move(sourceFileName, destFileName, true);
return true;
try
{
File.Move(sourceFileName, destFileName, true);
return true;
}
catch
{
return false;
}
}
if (File.Exists(destFileName))
@@ -41,10 +48,52 @@ internal static class FileOperation
return false;
}
File.Move(sourceFileName, destFileName, false);
try
{
File.Move(sourceFileName, destFileName, false);
return true;
}
catch
{
return false;
}
}
public static bool Delete(string path)
{
if (!File.Exists(path))
{
return false;
}
File.Delete(path);
return true;
}
public static unsafe bool UnsafeDelete(string path)
{
bool result = false;
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(path, default, in IShellItem.IID, out IShellItem* pShellItem)))
{
pFileOperation->DeleteItem(pShellItem, default);
if (SUCCEEDED(pFileOperation->PerformOperations()))
{
result = true;
}
IUnknownMarshal.Release(pShellItem);
}
IUnknownMarshal.Release(pFileOperation);
}
return result;
}
public static unsafe bool UnsafeMove(string sourceFileName, string destFileName)
{
bool result = false;
@@ -73,28 +122,4 @@ internal static class FileOperation
return result;
}
public static unsafe bool UnsafeDelete(string path)
{
bool result = false;
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(path, default, in IShellItem.IID, out IShellItem* pShellItem)))
{
pFileOperation->DeleteItem(pShellItem, default);
if (SUCCEEDED(pFileOperation->PerformOperations()))
{
result = true;
}
IUnknownMarshal.Release(pShellItem);
}
IUnknownMarshal.Release(pFileOperation);
}
return result;
}
}

View File

@@ -6,6 +6,9 @@ using System.Text;
namespace Snap.Hutao.Core.IO.Hashing;
#if NET9_0_OR_GREATER
[Obsolete("Use CryptographicOperations.HashData()")]
#endif
internal static class Hash
{
public static unsafe string SHA1HexString(string input)

View File

@@ -8,7 +8,9 @@ namespace Snap.Hutao.Core.IO.Hashing;
/// <summary>
/// 摘要
/// </summary>
[HighQuality]
#if NET9_0_OR_GREATER
[Obsolete("Use CryptographicOperations.HashData()")]
#endif
internal static class MD5
{
/// <summary>

View File

@@ -5,6 +5,9 @@ using System.IO;
namespace Snap.Hutao.Core.IO.Hashing;
#if NET9_0_OR_GREATER
[Obsolete("Use CryptographicOperations.HashData()")]
#endif
internal static class SHA256
{
public static async ValueTask<string> HashFileAsync(string filePath, CancellationToken token = default)

View File

@@ -9,6 +9,9 @@ namespace Snap.Hutao.Core.IO.Hashing;
/// <summary>
/// XXH64 摘要
/// </summary>
#if NET9_0_OR_GREATER
[Obsolete("Use CryptographicOperations.HashData()")]
#endif
internal static class XXH64
{
/// <summary>

View File

@@ -8,6 +8,7 @@ using Snap.Hutao.Win32.Security;
using System.Runtime.InteropServices;
using static Snap.Hutao.Win32.AdvApi32;
using static Snap.Hutao.Win32.FirewallApi;
using static Snap.Hutao.Win32.Kernel32;
using static Snap.Hutao.Win32.Macros;
namespace Snap.Hutao.Core.IO.Http.Loopback;
@@ -26,45 +27,7 @@ internal sealed unsafe class LoopbackManager : ObservableObject
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
INET_FIREWALL_APP_CONTAINER* pContainers = default;
try
{
{
WIN32_ERROR error = NetworkIsolationEnumAppContainers(NETISO_FLAG.NETISO_FLAG_MAX, out uint acCount, out pContainers);
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
for (uint i = 0; i < acCount; i++)
{
INET_FIREWALL_APP_CONTAINER* pContainer = pContainers + i;
ReadOnlySpan<char> appContainerName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pContainer->appContainerName);
if (appContainerName.Equals(runtimeOptions.FamilyName, StringComparison.Ordinal))
{
ConvertSidToStringSidW(pContainer->appContainerSid, out PWSTR stringSid);
hutaoContainerStringSID = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid).ToString();
break;
}
}
}
}
finally
{
// This function returns 1 rather than 0 specfied in the document.
_ = NetworkIsolationFreeAppContainers(pContainers);
}
{
WIN32_ERROR error = NetworkIsolationGetAppContainerConfig(out uint accCount, out SID_AND_ATTRIBUTES* pSids);
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
for (uint i = 0; i < accCount; i++)
{
ConvertSidToStringSidW((pSids + i)->Sid, out PWSTR stringSid);
ReadOnlySpan<char> stringSidSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid);
if (stringSidSpan.Equals(hutaoContainerStringSID, StringComparison.Ordinal))
{
IsLoopbackEnabled = true;
break;
}
}
}
Initialize(out hutaoContainerStringSID);
}
public bool IsLoopbackEnabled { get => isLoopbackEnabled; private set => SetProperty(ref isLoopbackEnabled, value); }
@@ -84,4 +47,60 @@ internal sealed unsafe class LoopbackManager : ObservableObject
sids.Add(sidAndAttributes);
IsLoopbackEnabled = NetworkIsolationSetAppContainerConfig(CollectionsMarshal.AsSpan(sids)) is WIN32_ERROR.ERROR_SUCCESS;
}
private void Initialize(out string hutaoContainerStringSID)
{
hutaoContainerStringSID = string.Empty;
INET_FIREWALL_APP_CONTAINER* pContainers = default;
try
{
WIN32_ERROR error = NetworkIsolationEnumAppContainers(NETISO_FLAG.NETISO_FLAG_MAX, out uint acCount, out pContainers);
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
for (uint i = 0; i < acCount; i++)
{
INET_FIREWALL_APP_CONTAINER* pContainer = pContainers + i;
ReadOnlySpan<char> appContainerName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pContainer->appContainerName);
if (appContainerName.Equals(runtimeOptions.FamilyName, StringComparison.Ordinal))
{
ConvertSidToStringSidW(pContainer->appContainerSid, out PWSTR stringSid);
hutaoContainerStringSID = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid).ToString();
break;
}
}
}
finally
{
// This function returns 1 rather than 0 specfied in the document.
_ = NetworkIsolationFreeAppContainers(pContainers);
}
SID_AND_ATTRIBUTES* pSids = default;
uint count = default;
try
{
WIN32_ERROR error = NetworkIsolationGetAppContainerConfig(out count, out pSids);
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
for (uint i = 0; i < count; i++)
{
ConvertSidToStringSidW((pSids + i)->Sid, out PWSTR stringSid);
ReadOnlySpan<char> stringSidSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid);
if (stringSidSpan.Equals(hutaoContainerStringSID, StringComparison.Ordinal))
{
IsLoopbackEnabled = true;
break;
}
}
}
finally
{
for (uint index = 0; index < count; index++)
{
HeapFree(GetProcessHeap(), 0, pSids[index].Sid);
}
HeapFree(GetProcessHeap(), 0, pSids);
}
}
}

View File

@@ -9,7 +9,7 @@ using System.Reflection;
namespace Snap.Hutao.Core.IO.Http.Proxy;
[Injection(InjectAs.Singleton)]
internal sealed partial class DynamicHttpProxy : ObservableObject, IWebProxy, IDisposable
internal sealed partial class HttpProxyUsingSystemProxy : ObservableObject, IWebProxy, IDisposable
{
private const string ProxySettingPath = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections";
@@ -20,13 +20,13 @@ internal sealed partial class DynamicHttpProxy : ObservableObject, IWebProxy, ID
private IWebProxy innerProxy = default!;
public DynamicHttpProxy(IServiceProvider serviceProvider)
public HttpProxyUsingSystemProxy(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
UpdateInnerProxy();
watcher = new(ProxySettingPath, OnSystemProxySettingsChanged);
watcher.Start();
watcher.Start(serviceProvider.GetRequiredService<ILogger<HttpProxyUsingSystemProxy>>());
}
public string CurrentProxyUri
@@ -75,8 +75,8 @@ internal sealed partial class DynamicHttpProxy : ObservableObject, IWebProxy, ID
public void Dispose()
{
(innerProxy as IDisposable)?.Dispose();
watcher.Dispose();
(innerProxy as IDisposable)?.Dispose();
}
public void OnSystemProxySettingsChanged()

View File

@@ -9,6 +9,7 @@ using System.Net.Http;
namespace Snap.Hutao.Core.IO.Http.Sharding;
// TODO: refactor to use tree structure to calculate shards
internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
{
private const int ShardSize = 4 * 1024 * 1024;

View File

@@ -3,7 +3,15 @@
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 流复制状态
/// </summary>
internal sealed record StreamCopyStatus(long BytesCopied, long TotalBytes);
internal sealed class StreamCopyStatus
{
public StreamCopyStatus(long bytesCopied, long totalBytes)
{
BytesCopied = bytesCopied;
TotalBytes = totalBytes;
}
public long BytesCopied { get; }
public long TotalBytes { get; }
}

View File

@@ -12,15 +12,8 @@ namespace Snap.Hutao.Core.IO;
[HighQuality]
internal readonly struct TempFile : IDisposable
{
/// <summary>
/// 路径
/// </summary>
public readonly string Path;
/// <summary>
/// 构造一个新的临时文件
/// </summary>
/// <param name="delete">是否在创建时删除文件</param>
private TempFile(bool delete)
{
try
@@ -38,11 +31,6 @@ internal readonly struct TempFile : IDisposable
}
}
/// <summary>
/// 创建临时文件并复制内容
/// </summary>
/// <param name="file">源文件</param>
/// <returns>临时文件</returns>
public static TempFile? CopyFrom(string file)
{
TempFile temporaryFile = new(false);
@@ -57,9 +45,6 @@ internal readonly struct TempFile : IDisposable
}
}
/// <summary>
/// 删除临时文件
/// </summary>
public void Dispose()
{
try

View File

@@ -5,76 +5,60 @@ using System.IO;
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 临时文件流
/// </summary>
internal sealed class TempFileStream : Stream
{
private readonly string path;
private readonly FileStream stream;
/// <summary>
/// 构造一个新的临时的文件流
/// </summary>
/// <param name="mode">文件模式</param>
/// <param name="access">访问方式</param>
public TempFileStream(FileMode mode, FileAccess access)
{
path = Path.GetTempFileName();
stream = File.Open(path, mode, access);
}
/// <inheritdoc/>
public override bool CanRead { get => stream.CanRead; }
/// <inheritdoc/>
public override bool CanSeek { get => stream.CanSeek; }
/// <inheritdoc/>
public override bool CanWrite { get => stream.CanWrite; }
/// <inheritdoc/>
public override long Length { get => stream.Length; }
/// <inheritdoc/>
public override long Position { get => stream.Position; set => stream.Position = value; }
/// <inheritdoc/>
public override void Flush()
{
stream.Flush();
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
return stream.Read(buffer, offset, count);
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
return stream.Seek(offset, origin);
}
/// <inheritdoc/>
public override void SetLength(long value)
{
stream.SetLength(value);
}
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count)
{
stream.Write(buffer, offset, count);
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
stream.Dispose();
File.Delete(path);
}
stream.Dispose();
File.Delete(path);
base.Dispose(disposing);
}
}

View File

@@ -1,21 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 文件路径
/// </summary>
internal readonly struct ValueFile
{
private readonly string value;
/// <summary>
/// Initializes a new instance of the <see cref="ValueFile"/> struct.
/// </summary>
/// <param name="value">value</param>
private ValueFile(string value)
{
this.value = value;
@@ -31,55 +22,6 @@ internal readonly struct ValueFile
return new(value);
}
/// <summary>
/// 异步反序列化文件中的内容
/// </summary>
/// <typeparam name="T">内容的类型</typeparam>
/// <param name="options">序列化选项</param>
/// <returns>操作是否成功,反序列化后的内容</returns>
public async ValueTask<ValueResult<bool, T?>> DeserializeFromJsonAsync<T>(JsonSerializerOptions options)
where T : class
{
try
{
using (FileStream stream = File.OpenRead(value))
{
T? t = await JsonSerializer.DeserializeAsync<T>(stream, options).ConfigureAwait(false);
return new(true, t);
}
}
catch (Exception ex)
{
_ = ex;
return new(false, null);
}
}
/// <summary>
/// 将对象异步序列化入文件
/// </summary>
/// <typeparam name="T">对象的类型</typeparam>
/// <param name="obj">对象</param>
/// <param name="options">序列化选项</param>
/// <returns>操作是否成功</returns>
public async ValueTask<bool> SerializeToJsonAsync<T>(T obj, JsonSerializerOptions options)
{
try
{
using (FileStream stream = File.Create(value))
{
await JsonSerializer.SerializeAsync(stream, obj, options).ConfigureAwait(false);
}
return true;
}
catch (Exception)
{
return false;
}
}
/// <inheritdoc/>
[SuppressMessage("", "CA1307")]
public override int GetHashCode()
{

View File

@@ -0,0 +1,44 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO;
internal static class ValueFileExtension
{
public static async ValueTask<ValueResult<bool, T?>> DeserializeFromJsonAsync<T>(this ValueFile file, JsonSerializerOptions options)
where T : class
{
try
{
using (FileStream stream = File.OpenRead(file))
{
T? t = await JsonSerializer.DeserializeAsync<T>(stream, options).ConfigureAwait(false);
return new(true, t);
}
}
catch (Exception ex)
{
_ = ex;
return new(false, null);
}
}
public static async ValueTask<bool> SerializeToJsonAsync<T>(this ValueFile file, T obj, JsonSerializerOptions options)
{
try
{
using (FileStream stream = File.Create(file))
{
await JsonSerializer.SerializeAsync(stream, obj, options).ConfigureAwait(false);
}
return true;
}
catch (Exception)
{
return false;
}
}
}

View File

@@ -5,17 +5,11 @@ using System.Globalization;
namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 实现日期的转换
/// 此转换器无法实现无损往返
/// 必须在反序列化后调整 Offset
/// </summary>
[HighQuality]
// 此转换器无法实现无损往返 必须在反序列化后调整 Offset
internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
private const string Format = "yyyy-MM-dd HH:mm:ss";
/// <inheritdoc/>
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.GetString() is { } dataTimeString)
@@ -29,7 +23,6 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
return default;
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.DateTime.ToString(Format, CultureInfo.InvariantCulture));

View File

@@ -6,15 +6,11 @@ using System.Globalization;
namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 逗号分隔列表转换器
/// </summary>
[HighQuality]
internal sealed class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEnumerable<int>>
{
private const char Comma = ',';
/// <inheritdoc/>
public override IEnumerable<int> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.GetString() is { } source)
@@ -25,7 +21,6 @@ internal sealed class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEn
return [];
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, IEnumerable<int> value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(Comma, value));

View File

@@ -6,10 +6,6 @@ using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 枚举转换器
/// </summary>
/// <typeparam name="TEnum">枚举的类型</typeparam>
[HighQuality]
internal sealed class UnsafeEnumConverter<TEnum> : JsonConverter<TEnum>
where TEnum : struct, Enum
@@ -19,18 +15,12 @@ internal sealed class UnsafeEnumConverter<TEnum> : JsonConverter<TEnum>
private readonly JsonSerializeType readAs;
private readonly JsonSerializeType writeAs;
/// <summary>
/// 构造一个新的枚举转换器
/// </summary>
/// <param name="readAs">读取</param>
/// <param name="writeAs">写入</param>
public UnsafeEnumConverter(JsonSerializeType readAs, JsonSerializeType writeAs)
{
this.readAs = readAs;
this.writeAs = writeAs;
}
/// <inheritdoc/>
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConverTEnum, JsonSerializerOptions options)
{
if (readAs == JsonSerializeType.Number)
@@ -46,13 +36,12 @@ internal sealed class UnsafeEnumConverter<TEnum> : JsonConverter<TEnum>
throw new JsonException();
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
switch (writeAs)
{
case JsonSerializeType.Number:
WriteEnumValue(writer, value, enumTypeCode);
WriteNumberValue(writer, value, enumTypeCode);
break;
case JsonSerializeType.NumberString:
writer.WriteStringValue(value.ToString("D"));
@@ -123,7 +112,7 @@ internal sealed class UnsafeEnumConverter<TEnum> : JsonConverter<TEnum>
throw new JsonException();
}
private static void WriteEnumValue(Utf8JsonWriter writer, TEnum value, TypeCode typeCode)
private static void WriteNumberValue(Utf8JsonWriter writer, TEnum value, TypeCode typeCode)
{
switch (typeCode)
{

View File

@@ -6,14 +6,8 @@ using System.Text.Json.Serialization.Metadata;
namespace Snap.Hutao.Core.Json;
/// <summary>
/// Json 选项
/// </summary>
internal static class JsonOptions
{
/// <summary>
/// 默认的Json序列化选项
/// </summary>
public static readonly JsonSerializerOptions Default = new()
{
AllowTrailingCommas = true,

View File

@@ -6,18 +6,10 @@ using System.Text.Json.Serialization.Metadata;
namespace Snap.Hutao.Core.Json;
/// <summary>
/// Json 类型信息解析器
/// </summary>
[HighQuality]
internal static class JsonTypeInfoResolvers
{
private static readonly Type JsonEnumAttributeType = typeof(JsonEnumAttribute);
/// <summary>
/// 解析枚举类型
/// </summary>
/// <param name="typeInfo">Json 类型信息</param>
public static void ResolveEnumType(JsonTypeInfo typeInfo)
{
if (typeInfo.Kind != JsonTypeInfoKind.Object)

View File

@@ -1,30 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Notifications;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppNotifications;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Shell;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Core.Windowing.HotKey;
using Snap.Hutao.Core.Windowing.NotifyIcon;
using Snap.Hutao.Service;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Discord;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Job;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.UI.Input.HotKey;
using Snap.Hutao.UI.Shell;
using Snap.Hutao.UI.Xaml;
using Snap.Hutao.UI.Xaml.View.Page;
using Snap.Hutao.UI.Xaml.View.Window;
using Snap.Hutao.ViewModel.Guide;
using System.Diagnostics;
namespace Snap.Hutao.Core.LifeCycle;
/// <summary>
/// 激活
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IAppActivation))]
@@ -37,56 +35,109 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
public const string ImportUIAFFromClipboard = nameof(ImportUIAFFromClipboard);
private const string CategoryAchievement = "ACHIEVEMENT";
private const string CategoryDailyNote = "DAILYNOTE";
private const string UrlActionImport = "/IMPORT";
private const string UrlActionRefresh = "/REFRESH";
private readonly IServiceProvider serviceProvider;
private readonly ICurrentXamlWindowReference currentWindowReference;
private readonly IServiceProvider serviceProvider;
private readonly ILogger<AppActivation> logger;
private readonly ITaskContext taskContext;
private readonly SemaphoreSlim activateSemaphore = new(1);
/// <inheritdoc/>
public void Activate(HutaoActivationArguments args)
{
// Before activate, we try to redirect to the opened process in App,
// And we check if it's a toast activation.
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
return;
}
HandleActivationExclusiveAsync(args).SafeForget(logger);
HandleActivationAsync(args).SafeForget();
async ValueTask HandleActivationExclusiveAsync(HutaoActivationArguments args)
{
await taskContext.SwitchToBackgroundAsync();
if (activateSemaphore.CurrentCount > 0)
{
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
{
switch (args.Kind)
{
case HutaoActivationKind.Protocol:
{
ArgumentNullException.ThrowIfNull(args.ProtocolActivatedUri);
await HandleProtocolActivationAsync(args.ProtocolActivatedUri, args.IsRedirectTo).ConfigureAwait(false);
break;
}
case HutaoActivationKind.Launch:
{
ArgumentNullException.ThrowIfNull(args.LaunchActivatedArguments);
await HandleLaunchActivationAsync(args.IsRedirectTo).ConfigureAwait(false);
break;
}
case HutaoActivationKind.AppNotification:
{
ArgumentNullException.ThrowIfNull(args.AppNotificationActivatedArguments);
await HandleAppNotificationActivationAsync(args.AppNotificationActivatedArguments, args.IsRedirectTo).ConfigureAwait(false);
break;
}
}
}
}
}
}
public void NotificationInvoked(AppNotificationManager manager, AppNotificationActivatedEventArgs args)
{
HandleAppNotificationActivationAsync(args.Arguments, false).SafeForget(logger);
}
/// <inheritdoc/>
public void PostInitialization()
{
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget();
ToastNotificationManagerCompat.OnActivated += NotificationActivate;
RunPostInitializationAsync().SafeForget(logger);
using (activateSemaphore.Enter())
async ValueTask RunPostInitializationAsync()
{
// TODO: Introduced in 1.10.2, remove in later version
serviceProvider.GetRequiredService<IJumpListInterop>().ClearAsync().SafeForget();
serviceProvider.GetRequiredService<IScheduleTaskInterop>().UnregisterAllTasks();
await taskContext.SwitchToBackgroundAsync();
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
{
return;
// TODO: Introduced in 1.10.2, remove in later version
{
serviceProvider.GetRequiredService<IJumpListInterop>().ClearAsync().SafeForget(logger);
serviceProvider.GetRequiredService<IScheduleTaskInterop>().UnregisterAllTasks();
}
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
{
return;
}
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget(logger);
// RegisterHotKey should be called from main thread
await taskContext.SwitchToMainThreadAsync();
serviceProvider.GetRequiredService<HotKeyOptions>().RegisterAll();
if (serviceProvider.GetRequiredService<AppOptions>().IsNotifyIconEnabled)
{
XamlApplicationLifetime.LaunchedWithNotifyIcon = true;
await taskContext.SwitchToMainThreadAsync();
serviceProvider.GetRequiredService<App>().DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown;
_ = serviceProvider.GetRequiredService<NotifyIconController>();
}
serviceProvider.GetRequiredService<IDiscordService>().SetNormalActivityAsync().SafeForget(logger);
serviceProvider.GetRequiredService<IQuartzService>().StartAsync().SafeForget(logger);
if (serviceProvider.GetRequiredService<IMetadataService>() is IMetadataServiceInitialization metadataServiceInitialization)
{
metadataServiceInitialization.InitializeInternalAsync().SafeForget(logger);
}
if (serviceProvider.GetRequiredService<IHutaoUserService>() is IHutaoUserServiceInitialization hutaoUserServiceInitialization)
{
hutaoUserServiceInitialization.InitializeInternalAsync().SafeForget(logger);
}
}
serviceProvider.GetRequiredService<HotKeyOptions>().RegisterAll();
if (serviceProvider.GetRequiredService<AppOptions>().IsNotifyIconEnabled)
{
XamlWindowLifetime.ApplicationLaunchedWithNotifyIcon = true;
serviceProvider.GetRequiredService<App>().DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown;
_ = serviceProvider.GetRequiredService<NotifyIconController>();
}
serviceProvider.GetRequiredService<IQuartzService>().StartAsync(default).SafeForget();
}
}
@@ -103,203 +154,145 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
await taskContext.SwitchToMainThreadAsync();
if (currentWindowReference.Window is null)
switch (currentWindowReference.Window)
{
currentWindowReference.Window = serviceProvider.GetRequiredService<LaunchGameWindow>();
return;
}
case null:
LaunchGameWindow launchGameWindow = serviceProvider.GetRequiredService<LaunchGameWindow>();
currentWindowReference.Window = launchGameWindow;
if (currentWindowReference.Window is MainWindow)
{
await serviceProvider
.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.LaunchGamePage>(INavigationAwaiter.Default, true)
.ConfigureAwait(false);
launchGameWindow.SwitchTo();
launchGameWindow.BringToForeground();
return;
return;
}
else
{
// We have a non-Main Window, just exit current process anyway
Process.GetCurrentProcess().Kill();
case MainWindow:
await serviceProvider
.GetRequiredService<INavigationService>()
.NavigateAsync<LaunchGamePage>(INavigationAwaiter.Default, true)
.ConfigureAwait(false);
return;
case LaunchGameWindow currentLaunchGameWindow:
currentLaunchGameWindow.SwitchTo();
currentLaunchGameWindow.BringToForeground();
return;
default:
Process.GetCurrentProcess().Kill();
return;
}
}
private void NotificationActivate(ToastNotificationActivatedEventArgsCompat args)
{
ToastArguments toastArgs = ToastArguments.Parse(args.Argument);
if (toastArgs.TryGetValue(Action, out string? action))
{
if (action == LaunchGame)
{
_ = toastArgs.TryGetValue(Uid, out string? uid);
HandleLaunchGameActionAsync(uid).SafeForget();
}
}
}
private async ValueTask HandleActivationAsync(HutaoActivationArguments args)
{
if (activateSemaphore.CurrentCount > 0)
{
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
{
await HandleActivationCoreAsync(args).ConfigureAwait(false);
}
}
}
private async ValueTask HandleActivationCoreAsync(HutaoActivationArguments args)
{
if (args.Kind is HutaoActivationKind.Protocol)
{
ArgumentNullException.ThrowIfNull(args.ProtocolActivatedUri);
await HandleUrlActivationAsync(args.ProtocolActivatedUri, args.IsRedirectTo).ConfigureAwait(false);
}
else if (args.Kind is HutaoActivationKind.Launch)
{
ArgumentNullException.ThrowIfNull(args.LaunchActivatedArguments);
switch (args.LaunchActivatedArguments)
{
default:
{
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
break;
}
}
}
}
private async ValueTask HandleNormalLaunchActionAsync()
{
// Increase launch times
LocalSetting.Update(SettingKeys.LaunchTimes, 0, x => unchecked(x + 1));
// If the guide is completed, we check if there's any unfulfilled resource category present.
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
{
if (StaticResource.IsAnyUnfulfilledCategoryPresent())
{
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
}
}
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
{
await taskContext.SwitchToMainThreadAsync();
currentWindowReference.Window = serviceProvider.GetRequiredService<GuideWindow>();
}
else
{
await WaitMainWindowAsync().ConfigureAwait(false);
}
}
private async ValueTask WaitMainWindowAsync()
{
if (currentWindowReference.Window is not null)
{
return;
}
await taskContext.SwitchToMainThreadAsync();
currentWindowReference.Window = serviceProvider.GetRequiredService<MainWindow>();
await taskContext.SwitchToBackgroundAsync();
if (serviceProvider.GetRequiredService<IMetadataService>() is IMetadataServiceInitialization metadataServiceInitialization)
{
metadataServiceInitialization.InitializeInternalAsync().SafeForget();
}
if (serviceProvider.GetRequiredService<IHutaoUserService>() is IHutaoUserServiceInitialization hutaoUserServiceInitialization)
{
hutaoUserServiceInitialization.InitializeInternalAsync().SafeForget();
}
serviceProvider.GetRequiredService<IDiscordService>().SetNormalActivityAsync().SafeForget();
}
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirectTo)
private async ValueTask HandleProtocolActivationAsync(Uri uri, bool isRedirectTo)
{
UriBuilder builder = new(uri);
string category = builder.Host.ToUpperInvariant();
string action = builder.Path.ToUpperInvariant();
string parameter = builder.Query.ToUpperInvariant();
// string parameter = builder.Query.ToUpperInvariant();
switch (category)
{
case CategoryAchievement:
{
await WaitMainWindowAsync().ConfigureAwait(false);
await HandleAchievementActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
break;
}
await WaitMainWindowOrCurrentAsync().ConfigureAwait(false);
if (currentWindowReference.Window is not MainWindow)
{
// TODO: Send notification to hint?
return;
}
switch (action)
{
case UrlActionImport:
{
await taskContext.SwitchToMainThreadAsync();
INavigationAwaiter navigationAwaiter = new NavigationExtra(ImportUIAFFromClipboard);
#pragma warning disable CA1849
// We can't await here to navigate to Achievment Page, the Achievement
// ViewModel requires the Metadata Service to be initialized.
serviceProvider
.GetRequiredService<INavigationService>()
.Navigate<AchievementPage>(navigationAwaiter, true);
#pragma warning restore CA1849
break;
}
}
case CategoryDailyNote:
{
await HandleDailyNoteActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
break;
}
default:
{
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
await HandleLaunchActivationAsync(isRedirectTo).ConfigureAwait(false);
break;
}
}
}
private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirectTo)
private async ValueTask HandleLaunchActivationAsync(bool isRedirectTo)
{
_ = parameter;
_ = isRedirectTo;
switch (action)
if (!isRedirectTo)
{
case UrlActionImport:
{
await taskContext.SwitchToMainThreadAsync();
// Increase launch times
LocalSetting.Update(SettingKeys.LaunchTimes, 0, x => unchecked(x + 1));
INavigationAwaiter navigationAwaiter = new NavigationExtra(ImportUIAFFromClipboard);
await serviceProvider
.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.AchievementPage>(navigationAwaiter, true)
.ConfigureAwait(false);
break;
// If the guide is completed, we check if there's any unfulfilled resource category present.
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
{
if (StaticResource.IsAnyUnfulfilledCategoryPresent())
{
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
}
}
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
{
await taskContext.SwitchToMainThreadAsync();
GuideWindow guideWindow = serviceProvider.GetRequiredService<GuideWindow>();
currentWindowReference.Window = guideWindow;
guideWindow.SwitchTo();
guideWindow.BringToForeground();
return;
}
}
await WaitMainWindowOrCurrentAsync().ConfigureAwait(false);
}
private async ValueTask HandleAppNotificationActivationAsync(IDictionary<string, string> arguments, bool isRedirectTo)
{
if (arguments.TryGetValue(Action, out string? action))
{
if (action == LaunchGame)
{
_ = arguments.TryGetValue(Uid, out string? uid);
await HandleLaunchGameActionAsync(uid).ConfigureAwait(false);
}
}
else
{
await HandleLaunchActivationAsync(isRedirectTo).ConfigureAwait(false);
}
}
private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirectTo)
private async ValueTask WaitMainWindowOrCurrentAsync()
{
_ = parameter;
switch (action)
if (currentWindowReference.Window is { } window)
{
case UrlActionRefresh:
{
try
{
await serviceProvider
.GetRequiredService<IDailyNoteService>()
.RefreshDailyNotesAsync()
.ConfigureAwait(false);
}
catch
{
}
// Check if it's redirected.
if (!isRedirectTo)
{
// It's a direct open process, should exit immediately.
Process.GetCurrentProcess().Kill();
}
break;
}
window.SwitchTo();
window.BringToForeground();
return;
}
await taskContext.SwitchToMainThreadAsync();
MainWindow mainWindow = serviceProvider.GetRequiredService<MainWindow>();
currentWindowReference.Window = mainWindow;
mainWindow.SwitchTo();
mainWindow.BringToForeground();
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.Windows.AppLifecycle;
using Microsoft.Windows.AppNotifications;
using Windows.ApplicationModel.Activation;
namespace Snap.Hutao.Core.LifeCycle;
@@ -12,12 +13,6 @@ namespace Snap.Hutao.Core.LifeCycle;
[HighQuality]
internal static class AppActivationArgumentsExtensions
{
/// <summary>
/// 尝试获取协议启动的Uri
/// </summary>
/// <param name="activatedEventArgs">应用程序激活参数</param>
/// <param name="uri">协议Uri</param>
/// <returns>是否存在协议Uri</returns>
public static bool TryGetProtocolActivatedUri(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out Uri? uri)
{
uri = null;
@@ -30,15 +25,10 @@ internal static class AppActivationArgumentsExtensions
return true;
}
/// <summary>
/// 尝试获取启动的参数
/// </summary>
/// <param name="activatedEventArgs">应用程序激活参数</param>
/// <param name="arguments">参数</param>
/// <returns>是否存在参数</returns>
public static bool TryGetLaunchActivatedArguments(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
{
arguments = null;
if (activatedEventArgs.Data is not ILaunchActivatedEventArgs launchArgs)
{
return false;
@@ -47,4 +37,21 @@ internal static class AppActivationArgumentsExtensions
arguments = launchArgs.Arguments.Trim();
return true;
}
public static bool TryGetAppNotificationActivatedArguments(this AppActivationArguments activatedEventArgs, out string? argument, [NotNullWhen(true)] out IDictionary<string, string>? arguments, [NotNullWhen(true)] out IDictionary<string, string>? userInput)
{
argument = null;
arguments = null;
userInput = null;
if (activatedEventArgs.Data is not AppNotificationActivatedEventArgs appNotificationArgs)
{
return false;
}
argument = appNotificationArgs.Argument;
arguments = appNotificationArgs.Arguments;
userInput = appNotificationArgs.UserInput;
return true;
}
}

View File

@@ -21,11 +21,6 @@ internal static class AppInstanceExtension
// Hold the reference here to prevent memory corruption.
private static HANDLE redirectEventHandle;
/// <summary>
/// 同步非阻塞重定向
/// </summary>
/// <param name="appInstance">app实例</param>
/// <param name="args">参数</param>
public static unsafe void RedirectActivationTo(this AppInstance appInstance, AppActivationArguments args)
{
try

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