Compare commits

...

246 Commits

Author SHA1 Message Date
DismissedLight
1c261b7866 completing 2024-07-25 10:44:01 +08:00
DismissedLight
fc2d590c42 Advanced Island prototype 2024-07-24 23:12:45 +08:00
DismissedLight
45724801ee island rework 2024-07-24 16:43:17 +08:00
DismissedLight
4ef65a2811 [skip ci] infobar text resources 2024-07-23 10:50:40 +08:00
DismissedLight
e7bcc6e3ae #1838 #1832 2024-07-23 10:49:29 +08:00
DismissedLight
618f55acbc handle mirror null edge case 2024-07-22 22:45:35 +08:00
DismissedLight
c761d8b7ad update source selectable 2024-07-22 22:36:31 +08:00
DismissedLight
b6a0592102 Merge pull request #1847 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-1f9aa84b91 2024-07-22 17:22:14 +08:00
DismissedLight
57e042ec1c update dialog rework 2024-07-22 17:20:29 +08:00
dependabot[bot]
36e5885ed6 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.3 to 3.5.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.4.3...v3.5.0)

Updates `MSTest.TestFramework` from 3.4.3 to 3.5.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.4.3...v3.5.0)

---
updated-dependencies:
- 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-07-22 07:06:19 +00:00
DismissedLight
599ddd147c single threaded update package download 2 2024-07-22 14:39:28 +08:00
DismissedLight
04cd4e7137 single threaded update package download 2024-07-22 14:28:21 +08:00
DismissedLight
f3934ce2cd refactor GetApplicationFileFromUriAsync calls 2024-07-22 14:00:20 +08:00
DismissedLight
8dd74c6c89 refine http info bar 2024-07-22 13:42:31 +08:00
DismissedLight
ebbaf0e36a fix #1845 2024-07-22 13:38:25 +08:00
DismissedLight
78726cd2ea Stored Attendance 2024-07-19 09:57:26 +08:00
DismissedLight
707fc67e51 catch all exception in http req/rsp 2024-07-17 16:57:23 +08:00
qhy040404
fbe6abc63a try to fix qr login 2024-07-17 14:10:30 +08:00
DismissedLight
4e7f8e2a97 Update AdvancedCollectionView.cs 2024-07-17 13:46:57 +08:00
Masterain
1ea92413f9 New translations sh.resx (English) (#1822) 2024-07-16 22:22:05 -07:00
Masterain
46c117edff New Crowdin updates (#1812) 2024-07-17 11:28:31 +08:00
DismissedLight
9c4a9fc09a bump version 2024-07-17 11:27:17 +08:00
DismissedLight
605fe5a3af Merge pull request #1816 from DGP-Studio/feat/unlockfps-loadexe
fix #1814
2024-07-17 11:18:26 +08:00
DismissedLight
7581cf8c8f fix pattern matching offset loop 2024-07-17 11:09:36 +08:00
DismissedLight
6b67811bae fix finding module 2024-07-17 10:49:57 +08:00
DismissedLight
6863cbb113 fix matching pattern 2024-07-17 10:44:19 +08:00
DismissedLight
6b03ccdacc try fix unlocking fps 2024-07-17 10:40:33 +08:00
DismissedLight
ad90c6b792 cultivation dialog spacing 2024-07-16 23:41:37 +08:00
DismissedLight
3ea7d59985 prevent showing update webview multiple times 2024-07-16 22:36:23 +08:00
DismissedLight
555043dfaa fix build 2024-07-16 17:30:50 +08:00
DismissedLight
84e05017ba impl #1720 2024-07-16 17:30:25 +08:00
DismissedLight
2c139a1ff6 cultivation save mode 2024-07-16 14:48:21 +08:00
DismissedLight
04114fb170 AdvancedCollectionView background creation 2024-07-15 23:38:08 +08:00
DismissedLight
a8065bf6e6 refine AdvancedCollectionView 2024-07-15 23:07:23 +08:00
DismissedLight
bfdb4b0060 refine AdvancedCollectionView 2024-07-15 17:31:14 +08:00
Masterain
6489f66d13 New Crowdin updates (#1800) 2024-07-15 13:38:19 +08:00
DismissedLight
eb57ac5952 limit ex/import uid selections height 2024-07-15 00:04:15 +08:00
DismissedLight
d57865fed9 append resource strings 2024-07-14 23:56:23 +08:00
DismissedLight
110af48385 uigf 4 support 2024-07-14 23:51:09 +08:00
DismissedLight
d30ef6daa0 [skip ci] uigf 4 support part 4 2024-07-14 21:41:07 +08:00
DismissedLight
e98bee8a9b [skip ci] uigf 4 support part 3 2024-07-13 22:56:55 +08:00
DismissedLight
71e0452c6e [skip ci] [skip ci] uigf 4 support part 2 2024-07-13 19:19:18 +08:00
qhy040404
d866c46646 fix #1810 2024-07-13 09:20:33 +08:00
DismissedLight
05d0faf131 [skip ci] uigf 4 support part 1 2024-07-12 17:06:59 +08:00
DismissedLight
83f5f25324 [skip ci] code style 2024-07-11 17:03:56 +08:00
DismissedLight
0c0290c446 impl #1497 2024-07-11 16:57:28 +08:00
DismissedLight
c3efd8d806 infobar view optimization 2024-07-10 16:52:50 +08:00
DismissedLight
e8d613d81f bump packages version 2024-07-10 13:54:12 +08:00
qhy040404
b7d03bee77 fix weapon wiki unclickable 2024-07-09 19:07:21 +08:00
DismissedLight
55799d0731 Update RoleCombatData.cs 2024-07-09 17:04:05 +08:00
DismissedLight
0a24e19625 try fix uid profilepicture duplicated 2024-07-09 14:05:47 +08:00
DismissedLight
a32b787352 Update InfoBarView.xaml 2024-07-08 23:45:14 +08:00
DismissedLight
f73b3af180 fix infobar view duplicate items 2024-07-08 17:19:00 +08:00
DismissedLight
bad60f1d65 refactor infobar view 2024-07-08 16:39:48 +08:00
DismissedLight
823ffdb5ad open default browser for update log content link 2024-07-08 13:22:57 +08:00
DismissedLight
544469f078 fix webview title text clipping 2024-07-08 11:13:38 +08:00
DismissedLight
04d4fa0c29 show updatelog after update 2024-07-08 10:48:10 +08:00
DismissedLight
f5dbabc586 optimization 2024-07-07 22:22:26 +08:00
DismissedLight
bfefbc58fa disable interaction when page is initializing 2024-07-06 19:12:50 +08:00
DismissedLight
5b98ba3fc4 dailynote auto refresh defaults to false 2024-07-06 18:20:44 +08:00
DismissedLight
32a22695e3 fix gachalog deletion not refresh view 2024-07-06 17:45:30 +08:00
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
DismissedLight
fd59b471cb Merge pull request #1701 from DGP-Studio/develop 2024-06-05 22:12:35 +08:00
1085 changed files with 21620 additions and 16715 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

@@ -64,12 +64,10 @@ 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
@@ -111,12 +109,10 @@ 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

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

@@ -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");
}
@@ -80,6 +89,19 @@ public sealed class JsonSerializeTest
Assert.AreEqual(result, """{"A":1,"B":2}""");
}
[TestMethod]
public void LowercaseStringCanDeserializeAsEnum()
{
string source = """
{
"Value": "a"
}
""";
SampleClassHoldEnum sample = JsonSerializer.Deserialize<SampleClassHoldEnum>(source)!;
Assert.AreEqual(sample.Value, SampleEnum.A);
}
private sealed class SampleDelegatePropertyClass
{
public int A { get => B; set => B = value; }
@@ -92,6 +114,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; }
@@ -104,6 +131,18 @@ public sealed class JsonSerializeTest
public int B { get; set; }
}
[JsonConverter(typeof(JsonStringEnumConverter))]
private enum SampleEnum
{
A,
B,
}
private sealed class SampleClassHoldEnum
{
public SampleEnum Value { get; set; }
}
[JsonDerivedType(typeof(SampleClassImplementedInterface))]
private interface ISampleInterface
{

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

@@ -0,0 +1,51 @@
using System;
using System.Text.Json;
namespace Snap.Hutao.Test.IncomingFeature;
[TestClass]
public class UnlockerIslandFunctionOffsetTest
{
private static readonly JsonSerializerOptions Options = new()
{
WriteIndented = true,
};
[TestMethod]
public void GenerateJson()
{
UnlockerIslandConfigurationWrapper wrapper = new()
{
Oversea = new()
{
FunctionOffsetFieldOfView = 0x00000000_01688E60,
FunctionOffsetTargetFrameRate = 0x00000000_018834D0,
FunctionOffsetFog = 0x00000000_00FB2AD0,
},
Chinese = new()
{
FunctionOffsetFieldOfView = 0x00000000_01684560,
FunctionOffsetTargetFrameRate = 0x00000000_0187EBD0,
FunctionOffsetFog = 0x00000000_00FAE1D0,
},
};
Console.WriteLine(JsonSerializer.Serialize(wrapper, Options));
}
private sealed class UnlockerIslandConfigurationWrapper
{
public required UnlockerIslandConfiguration Oversea { get; set; }
public required UnlockerIslandConfiguration Chinese { get; set; }
}
private sealed class UnlockerIslandConfiguration
{
public required uint FunctionOffsetFieldOfView { get; set; }
public required uint FunctionOffsetTargetFrameRate { get; set; }
public required uint FunctionOffsetFog { get; set; }
}
}

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

@@ -14,8 +14,8 @@
<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.10.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.4.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.4.3" />
<PackageReference Include="MSTest.TestAdapter" Version="3.5.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.5.0" />
<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,12 @@
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 System.Diagnostics;
namespace Snap.Hutao;
@@ -59,7 +59,7 @@ public sealed partial class App : Application
public new void Exit()
{
XamlLifetime.ApplicationExiting = true;
XamlApplicationLifetime.Exiting = true;
base.Exit();
}
@@ -68,6 +68,10 @@ 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))
@@ -81,14 +85,7 @@ public sealed partial class App : Application
LogDiagnosticInformation();
// Manually invoke
HutaoActivationArguments hutaoArgs = HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs);
if (hutaoArgs.Kind is HutaoActivationKind.Toast)
{
Exit();
return;
}
activation.Activate(hutaoArgs);
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
activation.PostInitialization();
}
catch (Exception ex)

View File

@@ -1,28 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao;
/// <summary>
/// 应用程序资源提供器
/// </summary>
[Injection(InjectAs.Transient, typeof(IAppResourceProvider))]
internal sealed class AppResourceProvider : IAppResourceProvider
{
private readonly App app;
/// <summary>
/// 构造一个新的应用程序资源提供器
/// </summary>
/// <param name="app">应用</param>
public AppResourceProvider(App app)
{
this.app = app;
}
/// <inheritdoc/>
public T GetResource<T>(string name)
{
return (T)app.Resources[name];
}
}

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

@@ -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,19 +2,27 @@
// 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;
@@ -34,7 +42,8 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
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;
@@ -48,23 +57,18 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
{
get => LazyInitializer.EnsureInitialized(ref cacheFolder, () =>
{
return serviceProvider.GetRequiredService<RuntimeOptions>().GetLocalCacheImageCacheFolder();
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)
@@ -88,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));
}
@@ -145,6 +191,50 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
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)
{
foreach (string filePath in filePaths)
@@ -236,4 +326,21 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
HashSet<string>? set = memoryCache.GetOrCreate(CacheFailedDownloadTasksName, entry => new HashSet<string>());
set?.Add(uri.ToString());
}
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)
{
this.serviceProvider = serviceProvider;
}
public IDisposable SuppressChangeCurrentItem()
{
return new CurrentItemSuppression(this);
}
protected override void OnCurrentChangedOverride()
{
if (!savingToDatabase)
{
return;
}
TEntity? currentItem = CurrentItem;
foreach (TEntity item in SourceCollection)
{
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)
{
this.serviceProvider = serviceProvider;
}
public IDisposable SuppressChangeCurrentItem()
{
return new CurrentItemSuppression(this);
}
protected override void OnCurrentChangedOverride()
{
if (!savingToDatabase)
{
return;
}
TEntityAccess? currentItem = CurrentItem;
foreach (TEntityAccess item in SourceCollection)
{
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, IEntityAccess<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, IEntityAccess<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,17 +12,17 @@ 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
{
return source.SingleOrDefault(i => i.IsSelected);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TSource? SelectedOrFirstOrDefault<TSource>(this IEnumerable<TSource> source)
where TSource : ISelectable
{
return source.SingleOrDefault(i => i.IsSelected) ?? source.FirstOrDefault();
}
}

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

@@ -30,10 +30,7 @@ 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);
static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
{

View File

@@ -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)
@@ -80,6 +73,15 @@ internal sealed class HutaoException : Exception
throw new NotSupportedException(message, innerException);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void NotSupportedIf(bool condition, string? message = default, Exception? innerException = default)
{
if (condition)
{
throw new NotSupportedException(message, innerException);
}
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static OperationCanceledException OperationCanceled(string message, Exception? innerException = default)

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

@@ -3,7 +3,7 @@
using Windows.Graphics;
namespace Snap.Hutao.Core.Windowing;
namespace Snap.Hutao.Core.Graphics;
internal readonly struct RectInt16
{

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

@@ -13,15 +13,22 @@ 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)

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

View File

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

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

@@ -3,7 +3,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Win32.Registry;
using System.Linq.Expressions;
using System.Net;
using System.Reflection;
@@ -27,7 +26,7 @@ internal sealed partial class HttpProxyUsingSystemProxy : ObservableObject, IWeb
UpdateInnerProxy();
watcher = new(ProxySettingPath, OnSystemProxySettingsChanged);
watcher.Start();
watcher.Start(serviceProvider.GetRequiredService<ILogger<HttpProxyUsingSystemProxy>>());
}
public string CurrentProxyUri
@@ -76,8 +75,8 @@ internal sealed partial class HttpProxyUsingSystemProxy : ObservableObject, IWeb
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,43 @@
// 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)
{
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

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using Windows.ApplicationModel;
namespace Snap.Hutao.Core;
internal static class InstalledLocation
{
public static string GetAbsolutePath(string relativePath)
{
return Path.Combine(Package.Current.InstalledLocation.Path, relativePath);
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Globalization;
namespace Snap.Hutao.Core.Json.Converter;
internal sealed class DateTimeConverter : JsonConverter<DateTime>
{
private const string Format = "yyyy-MM-dd HH:mm:ss";
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.GetString() is { } dataTimeString)
{
return DateTime.ParseExact(dataTimeString, Format, CultureInfo.InvariantCulture);
}
return default;
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(Format, CultureInfo.InvariantCulture));
}
}

View File

@@ -5,17 +5,11 @@ using System.Globalization;
namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 实现日期的转换
/// 此转换器无法实现无损往返
/// 必须在反序列化后调整 Offset
/// </summary>
[HighQuality]
internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
// 此转换器无法实现无损往返 必须在反序列化后调整 Offset
internal sealed 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)
@@ -40,19 +30,18 @@ internal sealed class UnsafeEnumConverter<TEnum> : JsonConverter<TEnum>
if (reader.GetString() is { } str)
{
return Enum.Parse<TEnum>(str);
return Enum.Parse<TEnum>(str, ignoreCase: true);
}
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,57 +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 RuntimeOptions runtimeOptions;
private readonly ILogger<AppActivation> logger;
private readonly ITaskContext taskContext;
private readonly SemaphoreSlim activateSemaphore = new(1);
/// <inheritdoc/>
public void Activate(HutaoActivationArguments args)
{
HandleActivationAsync(args).SafeForget();
HandleActivationExclusiveAsync(args).SafeForget(logger);
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()
{
RunPostInitializationAsync().SafeForget();
RunPostInitializationAsync().SafeForget(logger);
async ValueTask RunPostInitializationAsync()
{
await taskContext.SwitchToBackgroundAsync();
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget();
ToastNotificationManagerCompat.OnActivated += NotificationActivate;
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
{
// TODO: Introduced in 1.10.2, remove in later version
serviceProvider.GetRequiredService<IJumpListInterop>().ClearAsync().SafeForget();
serviceProvider.GetRequiredService<IScheduleTaskInterop>().UnregisterAllTasks();
{
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)
{
XamlLifetime.ApplicationLaunchedWithNotifyIcon = true;
XamlApplicationLifetime.LaunchedWithNotifyIcon = true;
await taskContext.SwitchToMainThreadAsync();
serviceProvider.GetRequiredService<App>().DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown;
_ = serviceProvider.GetRequiredService<NotifyIconController>();
}
serviceProvider.GetRequiredService<IQuartzService>().StartAsync(default).SafeForget();
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);
}
}
}
}
@@ -118,7 +168,7 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
case MainWindow:
await serviceProvider
.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.LaunchGamePage>(INavigationAwaiter.Default, true)
.NavigateAsync<LaunchGamePage>(INavigationAwaiter.Default, true)
.ConfigureAwait(false);
return;
@@ -133,55 +183,55 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
}
}
private void NotificationActivate(ToastNotificationActivatedEventArgsCompat args)
private async ValueTask HandleProtocolActivationAsync(Uri uri, bool isRedirectTo)
{
ToastArguments toastArgs = ToastArguments.Parse(args.Argument);
UriBuilder builder = new(uri);
if (toastArgs.TryGetValue(Action, out string? action))
{
if (action == LaunchGame)
{
_ = toastArgs.TryGetValue(Uid, out string? uid);
HandleLaunchGameActionAsync(uid).SafeForget();
}
}
}
string category = builder.Host.ToUpperInvariant();
string action = builder.Path.ToUpperInvariant();
private async ValueTask HandleActivationAsync(HutaoActivationArguments args)
{
await taskContext.SwitchToBackgroundAsync();
if (activateSemaphore.CurrentCount > 0)
// string parameter = builder.Query.ToUpperInvariant();
switch (category)
{
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:
case CategoryAchievement:
{
await WaitMainWindowOrCurrentAsync().ConfigureAwait(false);
if (currentWindowReference.Window is not MainWindow)
{
await HandleNormalLaunchActionAsync(args.IsRedirectTo).ConfigureAwait(false);
break;
// 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;
}
}
break;
}
default:
{
await HandleLaunchActivationAsync(isRedirectTo).ConfigureAwait(false);
break;
}
}
}
private async ValueTask HandleNormalLaunchActionAsync(bool isRedirectTo)
private async ValueTask HandleLaunchActivationAsync(bool isRedirectTo)
{
if (!isRedirectTo)
{
@@ -208,11 +258,33 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
guideWindow.BringToForeground();
return;
}
if (Version.Parse(LocalSetting.Get(SettingKeys.LastVersion, "0.0.0.0")) < runtimeOptions.Version)
{
XamlApplicationLifetime.IsFirstRunAfterUpdate = true;
LocalSetting.Set(SettingKeys.LastVersion, $"{runtimeOptions.Version}");
}
}
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 WaitMainWindowOrCurrentAsync()
{
if (currentWindowReference.Window is { } window)
@@ -229,100 +301,5 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
mainWindow.SwitchTo();
mainWindow.BringToForeground();
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)
{
UriBuilder builder = new(uri);
string category = builder.Host.ToUpperInvariant();
string action = builder.Path.ToUpperInvariant();
string parameter = builder.Query.ToUpperInvariant();
switch (category)
{
case CategoryAchievement:
{
await WaitMainWindowOrCurrentAsync().ConfigureAwait(false);
await HandleAchievementActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
break;
}
case CategoryDailyNote:
{
await HandleDailyNoteActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
break;
}
default:
{
await HandleNormalLaunchActionAsync(isRedirectTo).ConfigureAwait(false);
break;
}
}
}
private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirectTo)
{
_ = parameter;
_ = isRedirectTo;
switch (action)
{
case UrlActionImport:
{
await taskContext.SwitchToMainThreadAsync();
INavigationAwaiter navigationAwaiter = new NavigationExtra(ImportUIAFFromClipboard);
await serviceProvider
.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.AchievementPage>(navigationAwaiter, true)
.ConfigureAwait(false);
break;
}
}
}
private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirectTo)
{
_ = parameter;
switch (action)
{
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;
}
}
}
}

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

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.UI.Xaml;
using Snap.Hutao.Win32.Foundation;
namespace Snap.Hutao.Core.LifeCycle;

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Primitives;
using Microsoft.Windows.AppLifecycle;
namespace Snap.Hutao.Core.LifeCycle;
@@ -10,14 +9,16 @@ internal sealed class HutaoActivationArguments
{
public bool IsRedirectTo { get; set; }
public bool IsToastActivated { get; set; }
public HutaoActivationKind Kind { get; set; }
public Uri? ProtocolActivatedUri { get; set; }
public string? LaunchActivatedArguments { get; set; }
public IDictionary<string, string>? AppNotificationActivatedArguments { get; set; }
public IDictionary<string, string>? AppNotificationActivatedUserInput { get; set; }
public static HutaoActivationArguments FromAppActivationArguments(AppActivationArguments args, bool isRedirected = false)
{
HutaoActivationArguments result = new()
@@ -33,15 +34,6 @@ internal sealed class HutaoActivationArguments
if (args.TryGetLaunchActivatedArguments(out string? arguments))
{
result.LaunchActivatedArguments = arguments;
foreach (StringSegment segment in new StringTokenizer(arguments, [' ']))
{
if (segment.AsSpan().SequenceEqual("-ToastActivated"))
{
result.Kind = HutaoActivationKind.Toast;
break;
}
}
}
break;
@@ -55,6 +47,19 @@ internal sealed class HutaoActivationArguments
result.ProtocolActivatedUri = uri;
}
break;
}
case ExtendedActivationKind.AppNotification:
{
result.Kind = HutaoActivationKind.AppNotification;
if (args.TryGetAppNotificationActivatedArguments(out string? argument, out IDictionary<string, string>? arguments, out IDictionary<string, string>? userInput))
{
result.LaunchActivatedArguments = argument;
result.AppNotificationActivatedArguments = arguments;
result.AppNotificationActivatedUserInput = userInput;
}
break;
}
}

View File

@@ -7,6 +7,6 @@ internal enum HutaoActivationKind
{
None,
Launch,
Toast,
AppNotification,
Protocol,
}

View File

@@ -1,16 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Windows.AppNotifications;
namespace Snap.Hutao.Core.LifeCycle;
internal interface IAppActivation
{
void Activate(HutaoActivationArguments args);
void PostInitialization();
}
void NotificationInvoked(AppNotificationManager manager, AppNotificationActivatedEventArgs args);
internal interface IAppActivationActionHandlersAccess
{
ValueTask HandleLaunchGameActionAsync(string? uid = null);
void PostInitialization();
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle;
internal interface IAppActivationActionHandlersAccess
{
ValueTask HandleLaunchGameActionAsync(string? uid = null);
}

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