Compare commits

...

208 Commits

Author SHA1 Message Date
qhy040404
df7f685a3c prompt user dailynote auto-refresh is not effective while notify icon is disabled 2024-05-23 10:55:43 +08:00
qhy040404
9d47082f47 fix #1631 2024-05-22 22:59:53 +08:00
DismissedLight
cd4516d9a7 Merge pull request #1630 from DGP-Studio/feat/1595 2024-05-22 17:02:13 +08:00
Lightczx
f7e94fe2f2 refine view 2024-05-22 16:46:06 +08:00
qhy040404
e93802d5a5 partial impl #1595 2024-05-22 15:37:04 +08:00
Lightczx
370f2fe1f7 impl #1542 2024-05-22 12:44:20 +08:00
qhy040404
c6f747a89b fix wrap layout 2024-05-21 23:53:23 +08:00
DismissedLight
cf431719df temp fix wrap layout 2024-05-21 23:14:51 +08:00
DismissedLight
c36c15f9be Merge pull request #1628 from DGP-Studio/fix/1609
fix #1609
2024-05-21 22:07:27 +08:00
DismissedLight
f80a63f557 Merge pull request #1627 from DGP-Studio/fix/1608
fix #1608
2024-05-21 22:03:48 +08:00
DismissedLight
36376f5af6 Update SH.resx 2024-05-21 22:03:10 +08:00
DismissedLight
2c057458a3 Merge pull request #1626 from DGP-Studio/fix/1613
fix #1613
2024-05-21 21:59:38 +08:00
DismissedLight
f0f50e0e30 Merge pull request #1625 from DGP-Studio/fix/1588
fix #1588
2024-05-21 21:48:25 +08:00
DismissedLight
b834daef93 Merge pull request #1619 from DGP-Studio/fix/new_dailynote_archon 2024-05-21 21:34:04 +08:00
DismissedLight
ff10543c21 Merge pull request #1615 from DGP-Studio/build/alpha 2024-05-21 21:12:58 +08:00
qhy040404
a55b25ae53 add alpha build constant 2024-05-21 17:34:45 +08:00
qhy040404
c0fbb823d4 fix empty archon quest in new added dailynote 2024-05-21 17:34:29 +08:00
qhy040404
4306af94be fix #1588 2024-05-21 17:34:03 +08:00
qhy040404
dfc83d4a34 fix #1613 2024-05-21 17:33:44 +08:00
qhy040404
c6a47eb7be fix #1608 2024-05-21 17:33:20 +08:00
qhy040404
7413a81ff4 fix #1609 2024-05-21 17:32:59 +08:00
Lightczx
24d143ea9f add France 2024-05-21 17:15:58 +08:00
DismissedLight
9f6611cd20 Merge pull request #1621 from DGP-Studio/feat/ShellNotifyIcon 2024-05-21 17:07:46 +08:00
Lightczx
784c727a38 hint when promoted 2024-05-21 17:05:55 +08:00
Lightczx
1bf517f95d add notifyicon setting 2024-05-21 15:27:10 +08:00
DismissedLight
8b9190d941 use icon path to determine guid 2024-05-19 17:04:08 +08:00
DismissedLight
16e0ab56f6 revert window scope 2024-05-19 14:48:52 +08:00
DismissedLight
b10df0bed1 revert isenabled 2024-05-19 13:25:56 +08:00
DismissedLight
c4d1f371f1 remove dailynote runtime elevation check 2024-05-19 13:09:10 +08:00
DismissedLight
92a151441b dailynote refresh refactor 2024-05-18 21:37:27 +08:00
Lightczx
faefc9c093 Update GameScreenCaptureSession.cs 2024-05-17 14:47:35 +08:00
Lightczx
c6e6d08707 capture HDR support 2024-05-17 11:59:06 +08:00
DismissedLight
4323ced7dc Update SpinWaitPolyfill.cs 2024-05-16 22:18:36 +08:00
DismissedLight
8a1781b449 fix registry watcher 2024-05-16 21:32:52 +08:00
Lightczx
72aff568b3 fixing capture 2024-05-16 17:24:36 +08:00
Lightczx
f15a692f03 fix window state 2024-05-16 11:11:49 +08:00
DismissedLight
5868d53cca fix rasterization scale 2024-05-15 22:51:54 +08:00
Lightczx
7d7c8d485e [skip ci] remove using 2024-05-15 17:14:29 +08:00
Lightczx
6edcf97ec9 refactor xaml window controller 2024-05-15 17:13:59 +08:00
Lightczx
f4593cd325 refactor cached image 2024-05-15 13:39:19 +08:00
Lightczx
29454b188e change alpha hint 2024-05-15 11:08:12 +08:00
Lightczx
942181561d enable feat build 2024-05-15 09:48:57 +08:00
DismissedLight
5d6e1dad01 launch game 2024-05-14 23:43:15 +08:00
DismissedLight
d52aa0d6b2 better resize policy 2024-05-14 22:19:20 +08:00
Lightczx
3ee729eacf DesktopWindowXamlSourceAccess 2024-05-14 17:31:16 +08:00
Lightczx
d3acbcde24 fix exit crashing 2024-05-13 16:33:12 +08:00
Lightczx
38e152befd window reference logic 2024-05-13 15:36:48 +08:00
DismissedLight
0f767f7e77 contextmenu 2024-05-12 22:36:22 +08:00
Lightczx
dafd3128c2 adjust lifecycle 2024-05-11 16:23:52 +08:00
DismissedLight
0556373bcf notifyicon message 2024-05-10 22:37:59 +08:00
Lightczx
e8d3a065e6 support show icon 2024-05-10 17:30:03 +08:00
Lightczx
be223909d3 shell 2024-05-09 17:32:21 +08:00
Lightczx
7da778699b code style 2024-05-09 16:25:21 +08:00
Lightczx
5bfc790ea2 pooled frame 2024-05-08 17:30:04 +08:00
DismissedLight
fc13b85739 Merge pull request #1610 from DGP-Studio/fix/dailynotevmslim/metadata 2024-05-08 09:03:25 +08:00
qhy040404
df999dbf51 ensure metadata in slim 2024-05-07 22:35:36 +08:00
DismissedLight
688562c1dd capture 2024-05-07 21:19:26 +08:00
Lightczx
051a115f84 bump dependency version 2024-05-07 16:23:30 +08:00
Lightczx
b3d75f9fa5 impl #1607 2024-05-07 16:04:46 +08:00
DismissedLight
0b90bdaa42 Merge pull request #1555 from DGP-Studio/feat/dailynote_archon 2024-05-07 15:51:43 +08:00
Lightczx
77067d27d0 code style 2024-05-07 14:23:54 +08:00
Lightczx
e04542606e code style 2024-05-07 13:36:04 +08:00
Lightczx
5be958ff64 impl #1603 2024-05-07 11:04:21 +08:00
qhy040404
a57933388d add static resource 2024-05-06 16:58:57 +08:00
qhy040404
86c6c9574b ensure metadata 2024-05-06 16:58:57 +08:00
qhy040404
47e451df2f use different icon for different state 2024-05-06 16:58:56 +08:00
qhy040404
c8592c798b add icon 2024-05-06 16:58:56 +08:00
qhy040404
5fad960b20 fix build 2024-05-06 16:58:56 +08:00
qhy040404
44ba0a90a6 remove unnecessary abstraction 2024-05-06 16:58:56 +08:00
qhy040404
883c1ca95f refactor 2024-05-06 16:58:56 +08:00
qhy040404
1a29908e5d partial impl #1203 2024-05-06 16:58:56 +08:00
DismissedLight
3e9edd2f62 capture test 2024-05-05 15:41:46 +08:00
DismissedLight
f9c18d2555 Merge pull request #1604 from DGP-Studio/windows-graphics-capture 2024-05-04 20:20:11 +08:00
DismissedLight
b5afca256a fix build 2024-05-04 20:04:45 +08:00
DismissedLight
a0e79344b1 code style 2 2024-05-04 20:01:37 +08:00
DismissedLight
3b8eba3bb1 code style 2024-05-04 18:59:29 +08:00
DismissedLight
08a3db7dc9 capture test 2024-05-04 16:15:36 +08:00
Lightczx
c02e7b0db3 static zip download retry 2024-04-30 15:02:31 +08:00
DismissedLight
a51ede5048 Merge pull request #1592 from DGP-Studio/develop 2024-04-30 13:46:53 +08:00
Lightczx
1f31c946cc bump version 2024-04-30 13:36:34 +08:00
Lightczx
7dee4a0ea5 fix launch package convert 2024-04-30 13:35:05 +08:00
DismissedLight
2fdeaa2557 Merge pull request #1589 from DGP-Studio/develop 2024-04-30 10:26:15 +08:00
DismissedLight
fbffadd546 Merge pull request #1583 from DGP-Studio/l10n_develop 2024-04-30 10:22:20 +08:00
Lightczx
00abfe6695 bump version 2024-04-30 09:58:12 +08:00
DismissedLight
f2a4d2fa53 Merge pull request #1580 from DGP-Studio/feat/896 2024-04-30 09:43:23 +08:00
DismissedLight
4246fa3d13 Merge pull request #1587 from DGP-Studio/refactor/httpclient_recycle 2024-04-30 09:41:20 +08:00
Masterain
e0a5898b3a New translations sh.resx (English) 2024-04-29 17:55:19 -07:00
qhy040404
71ac87539f Trigger GC after disposing ViewModel 2024-04-30 00:30:11 +08:00
qhy040404
3959ce1c0a code style 2024-04-30 00:29:53 +08:00
qhy040404
a56f382f8b impl #896 2024-04-30 00:29:53 +08:00
DismissedLight
04c3498b54 fix wiki avatar/weapon margin 2024-04-29 23:53:08 +08:00
Lightczx
f304e0920f log failed download tasks 2024-04-29 16:43:08 +08:00
Lightczx
6fb276af9d refine NonRudeHWND 2024-04-29 11:44:41 +08:00
Lightczx
4bd55c308a fix exception throwing 2024-04-29 10:25:40 +08:00
Lightczx
98a9f5fec9 refine cast 2024-04-29 10:16:23 +08:00
DismissedLight
0420568e73 Merge pull request #1573 from DGP-Studio/fix/1571
fix #1571
2024-04-29 10:06:13 +08:00
Lightczx
45242ff8ce refine discord controller 2024-04-28 16:46:07 +08:00
Lightczx
074cc1194b add cancellationtoken 2024-04-28 15:43:07 +08:00
Lightczx
bd4a0f0d8e dailynote optimization 2024-04-28 15:36:59 +08:00
qhy040404
bbc2d7655c add operator 2024-04-28 14:33:31 +08:00
qhy040404
588aba1395 code style 2024-04-28 12:03:42 +08:00
qhy040404
611469beb3 fix #1571 2024-04-28 11:55:31 +08:00
Masterain
1b80f79189 New translations sh.resx (English) 2024-04-27 20:36:00 -07:00
Masterain
a8cfb7fcc4 New translations sh.resx (French) 2024-04-27 19:37:12 -07:00
Masterain
10445a73b4 New translations sh.resx (Indonesian) 2024-04-27 19:37:11 -07:00
Masterain
7d00cec7c6 New translations sh.resx (English) 2024-04-27 19:37:10 -07:00
Masterain
05674fb01a New translations sh.resx (Chinese Traditional) 2024-04-27 19:37:09 -07:00
Masterain
2c6682574f New translations sh.resx (Russian) 2024-04-27 19:37:08 -07:00
Masterain
53a95ddcb9 New translations sh.resx (Portuguese) 2024-04-27 19:37:07 -07:00
Masterain
bbfd5096d7 New translations sh.resx (Korean) 2024-04-27 19:37:06 -07:00
Masterain
24407ecc05 New translations sh.resx (Japanese) 2024-04-27 19:37:05 -07:00
DismissedLight
ff2521c02c Merge pull request #1581 from DGP-Studio/feat/custom_sa_homa_not_login_dialog 2024-04-28 10:25:15 +08:00
Lightczx
5954c1a0ab fix build failed 2024-04-26 14:49:50 +08:00
Lightczx
4dc753bf5a Merge branch 'develop' of https://github.com/DGP-Studio/Snap.Hutao into develop 2024-04-26 14:47:43 +08:00
Lightczx
bd3617c15a refactor cultivation 2024-04-26 14:47:40 +08:00
qhy040404
70da292f21 refine #1575 ui 2024-04-26 12:31:27 +08:00
DismissedLight
97c5e7d37f Merge pull request #1574 from DGP-Studio/fix/1485 2024-04-26 09:27:45 +08:00
DismissedLight
388f9d5657 Merge pull request #1575 from DGP-Studio/feat/1245 2024-04-26 09:24:50 +08:00
qhy040404
74e11f3823 impl #1245 2024-04-25 22:39:05 +08:00
qhy040404
c1305cda43 set passwordbox to a constant value 2024-04-25 22:13:48 +08:00
Lightczx
b0d5051957 refactor 2024-04-25 17:29:14 +08:00
Lightczx
6a42c36a76 optimization 2024-04-25 14:44:06 +08:00
Lightczx
7cf106ec50 avatar info optimization 2024-04-24 17:12:34 +08:00
DismissedLight
f12cd63c92 Merge pull request #1563 from DGP-Studio/fix/imagecache 2024-04-23 16:55:39 +08:00
Lightczx
c441fdb6b0 code style 2024-04-23 16:55:04 +08:00
DismissedLight
09a880525b Merge pull request #1567 from Natrium0521/develop 2024-04-23 15:58:19 +08:00
Lightczx
15212d8f21 code style 2024-04-23 15:55:53 +08:00
Natrium
a60c4bff08 fix empty announcement when localized regex not match 2024-04-23 11:52:44 +08:00
qhy040404
e02985926d code style 2024-04-23 11:33:54 +08:00
Natrium
09448b7137 fix empty announcement 2024-04-23 11:15:21 +08:00
Natrium
6487df776a Fix Announcement time display 2024-04-23 00:57:34 +08:00
DismissedLight
2be11c22df Merge pull request #1560 from DGP-Studio/feat/taskbar_elevated 2024-04-22 17:01:54 +08:00
Lightczx
9ecb3d5821 adjust HttpClient scope 2024-04-22 16:51:35 +08:00
Lightczx
ca64c3e0ef impl #1550 2024-04-22 15:56:51 +08:00
qhy040404
3fe726aa63 extract logo 2024-04-22 15:23:47 +08:00
qhy040404
6b7ffe9fe9 Update Snap.Hutao.csproj 2024-04-22 15:23:46 +08:00
qhy040404
5ed5729c4e extract launcher 2024-04-22 15:23:46 +08:00
qhy040404
9ba0066f40 allow to open elevated app from taskbar 2024-04-22 15:23:38 +08:00
Lightczx
9c639fbaa4 Merge branch 'develop' of https://github.com/DGP-Studio/Snap.Hutao into develop 2024-04-22 14:17:18 +08:00
Lightczx
a592816661 Add quest type 2024-04-22 14:17:15 +08:00
DismissedLight
ec9c5ebee1 Merge pull request #1557 from DGP-Studio/fix/geetest 2024-04-22 09:30:16 +08:00
DismissedLight
9475c19b64 Merge pull request #1556 from DGP-Studio/feat/patch_statistics 2024-04-22 09:28:45 +08:00
qhy040404
9bfe7f78ef add unsafe move and delete to file operation 2024-04-20 23:48:35 +08:00
qhy040404
88d7f0bcc7 fix miss image cache 2024-04-20 22:22:53 +08:00
qhy040404
4920da4ea2 fix geetest 2024-04-20 11:27:10 +08:00
qhy040404
99b2ccb33b add patch statistics 2024-04-20 11:06:16 +08:00
Lightczx
9b94a75d6f refactor 2024-04-19 16:18:57 +08:00
Lightczx
e390ad2839 AvatarViewBuilder 2024-04-19 15:21:59 +08:00
DismissedLight
3086d59674 Merge pull request #1552 from DGP-Studio/feat/unheld_statistics_items 2024-04-19 09:40:57 +08:00
Lightczx
cb00fdbda0 code style 2024-04-19 09:41:04 +08:00
DismissedLight
1702dfcdc6 Merge branch 'develop' into feat/unheld_statistics_items 2024-04-19 09:22:57 +08:00
Lightczx
4b54b343f9 summary factory update 2024-04-18 17:13:14 +08:00
qhy040404
292b21a759 fix build 2024-04-17 15:15:00 +08:00
qhy040404
29e9413022 code style 2024-04-17 15:11:27 +08:00
qhy040404
7c923aaa5e impl #1355 2024-04-17 15:11:27 +08:00
Lightczx
c6618be0fc InitializeDataContext 2024-04-17 15:05:26 +08:00
DismissedLight
45dd276b89 Merge pull request #1546 from DGP-Studio/revert/qr_login 2024-04-17 13:52:42 +08:00
DismissedLight
8a9fb38f49 Merge pull request #1543 from DGP-Studio/fix/chronicled_hutao_statistics 2024-04-17 13:51:56 +08:00
Lightczx
1249216491 Update HorizontalEqualPanel.cs 2024-04-17 13:50:27 +08:00
DismissedLight
4bf3f4151e Merge branch 'develop' into fix/chronicled_hutao_statistics 2024-04-17 13:40:35 +08:00
Lightczx
89d909b04f httpclient scope 2024-04-17 13:37:04 +08:00
qhy040404
2cec0f5e0e refactor GachaLog statistics 2024-04-17 11:03:38 +08:00
qhy040404
bbb97cd802 fix typo 2024-04-16 17:26:53 +08:00
qhy040404
67b058f126 fix hutao statistics unavailable 2024-04-16 17:26:53 +08:00
qhy040404
b98611ccd9 Revert "temporary fix qr login"
This reverts commit d4bd610fe2.
2024-04-16 17:26:30 +08:00
Lightczx
80d6d5eb2b fix build 2024-04-16 15:53:41 +08:00
Lightczx
f682bb57e8 refactor achievement 2024-04-16 15:51:30 +08:00
qhy040404
d8310b784f Update pull_request_template.md 2024-04-16 12:51:51 +08:00
Lightczx
486c6eb308 achievement service 2024-04-16 10:13:35 +08:00
Masterain
c5d04e09da Create pull_request_template.md 2024-04-15 18:43:38 -07:00
Lightczx
0629f7c4c9 Introducing IAppInfrastructureService 2024-04-15 17:29:20 +08:00
Lightczx
b49288a98f Update DiscordController.cs 2024-04-15 13:49:30 +08:00
Masterain
df61aa3968 Update README.md 2024-04-14 18:30:39 -07:00
DismissedLight
ee99d0b665 refactor static resource 2024-04-12 21:40:00 +08:00
Lightczx
72b62aa9c6 ServerGarbageCollection 2024-04-12 17:30:10 +08:00
Lightczx
6b031e1866 update static resource setting 2024-04-12 16:52:54 +08:00
Lightczx
59c03c7f3b update WAS to 1.5.2 2024-04-12 11:57:49 +08:00
Lightczx
c03a96b44f refine guide ui 2024-04-11 16:14:04 +08:00
DismissedLight
d5a97903d3 Merge pull request #1545 from DGP-Studio/feat/version1.10guide 2024-04-11 15:57:44 +08:00
Lightczx
7f998dc87f impl #1430 2024-04-11 15:48:11 +08:00
Lightczx
2367c4759d refactor 2024-04-10 16:53:21 +08:00
DismissedLight
043e3f07d8 Merge pull request #1539 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-017b70aa21 2024-04-08 16:15:33 +08:00
dependabot[bot]
cd075c4dab 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.2.2 to 3.3.1
- [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.2.2...v3.3.1)

Updates `MSTest.TestFramework` from 3.2.2 to 3.3.1
- [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.2.2...v3.3.1)

---
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-04-08 07:37:05 +00:00
Lightczx
65252f1f69 refactor launch game view model 2024-04-08 15:33:42 +08:00
DismissedLight
f5dd5f4c1d Merge pull request #1531 from DGP-Studio/feat/fix_config 2024-04-08 14:08:21 +08:00
Lightczx
27ce55f3f7 code style 2024-04-08 14:06:04 +08:00
qhy040404
311941bb89 code style 2024-04-08 14:01:59 +08:00
qhy040404
07e2489cab use header instead of another textblock 2024-04-08 12:11:46 +08:00
qhy040404
699ac60aaf apply suggestions 2024-04-08 12:00:16 +08:00
qhy040404
dc7bc7e35d fix compile error 2024-04-08 11:59:05 +08:00
qhy040404
bf67fcf3a2 add ability to fix missing config.ini 2024-04-08 11:59:05 +08:00
DismissedLight
5093246571 Merge pull request #1538 from DGP-Studio/feat/IFileOperation 2024-04-08 11:34:09 +08:00
Lightczx
94fe192581 add IFileOperation 2024-04-08 11:25:32 +08:00
Lightczx
c679032387 button builder 2024-04-08 09:40:53 +08:00
Lightczx
d119b056c7 refactor achievement viewmodel 2024-04-07 16:50:35 +08:00
Lightczx
ab95ce8ce8 code style 2024-04-07 15:07:53 +08:00
Lightczx
0ede5b158f button builder 2024-04-07 14:49:36 +08:00
Lightczx
6c9a98c2c9 refactor infobarservice 2024-04-07 13:30:38 +08:00
Lightczx
a725fc0e9e fix clamp upper bound 2024-04-07 10:46:09 +08:00
Lightczx
5251dd9343 clamp monitor index 2024-04-07 10:44:42 +08:00
Lightczx
0b71053bf9 backup&restore game config.ini 2024-04-02 16:55:53 +08:00
Lightczx
a91499171d update flow rework 2024-04-02 15:37:20 +08:00
Masterain
b6c474cc12 Update issue templates 2024-04-01 22:39:38 -07:00
Lightczx
e041def070 remove AddUserAsync 2024-04-01 16:50:33 +08:00
Masterain
2e81ddf3f4 在设置中增加低价原石购买站点链接
### Hello April 🎁
- Update Banner
2024-03-31 22:42:48 -07:00
DismissedLight
4d988e5500 Merge pull request #1527 from Masterain98/main
April Fools
2024-04-01 13:25:55 +08:00
Masterain
36aff679c0 Update Banner 2024-03-31 22:19:33 -07:00
Lightczx
d490393108 fix setting spacing 2024-03-29 16:52:37 +08:00
661 changed files with 17525 additions and 5301 deletions

View File

@@ -40,7 +40,7 @@ body:
attributes:
label: Snap Hutao 版本
description: 在应用标题,应用程序的反馈中心界面中可以找到
placeholder: 1.4.15.0
placeholder: 1.9.9.0
validations:
required: true
@@ -62,20 +62,19 @@ body:
description: 请设置一个你认为合适的分类,这将帮助我们快速定位问题
options:
- 安装和环境
- 成就管理
- 角色信息面板
- 游戏启动器
- 祈愿记录
- 成就管理
- 我的角色
- 实时便笺
- 养成计算
- 文件缓存
- 祈愿记录
- 玩家查询
- 胡桃数据库
- 用户界面
- 胡桃云
- 胡桃帐号
- 签到
- 深境螺旋/胡桃数据库
- Wiki
- 米游社账号面板
- 每日签到奖励
- 胡桃通行证/胡桃云
- 用户界面
- 文件缓存
- 公告
- 其它
validations:

View File

@@ -40,7 +40,7 @@ body:
attributes:
label: Snap Hutao Version
description: You can find the version in application's title bar
placeholder: e.g. 1.4.15.0
placeholder: e.g. 1.9.9.0
validations:
required: true
@@ -62,20 +62,19 @@ body:
description: Please select the most associated category of your issue
options:
- Installation and Environment
- Game Launcher
- Wish Export
- Achievement
- My Character
- Game Launcher
- Realtime Note
- Develop Plan
- File Cache
- Wish Export
- Game Record
- Hutao Database
- User Interface
- Snap Hutao Cloud
- Snap Hutao Account
- Checkin
- Spiral Abyss
- Wiki
- MiHoYo Account Panel
- Daily Checkin Reward
- Hutao Passport/Hutao Cloud
- User Interface
- File Cache
- Announcement
- Other
validations:

15
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,15 @@
<!--- Hi, thanks for considering make a PR contribution to Snap Hutao, we appreciate your work. -->
<!--- Before you create this PR, please fill the following form and checklist -->
## Description
<!--- Describe your changes -->
## Related Issue
<!--- If there's an associated issue, please use [GitHub Keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests) to link it -->
<!-- e.g. fix #999, resolve #999, close #999 -->
## Checklist
- [ ] The target PR branch is `develop` branch

View File

@@ -5,6 +5,7 @@ on:
branches:
- main
- develop
- 'feat/*'
paths-ignore:
- '.gitattributes'
- '.github/**'
@@ -73,7 +74,7 @@ jobs:
> [!IMPORTANT]
> 请注意,从 Snap Hutao Alpha 2023.12.21.3 开始,我们将使用全新的 CI 证书,原有的 Snap.Hutao.CI.cer 将在几天后过期停止使用。
>
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 以安装测试版安装包
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 到 `受信任的根证书颁发机构` 以安装测试版安装包
"
echo $summary >> $Env:GITHUB_STEP_SUMMARY

View File

@@ -17,7 +17,7 @@ You can follow the instructions in the [Quick Start](https://hut.ao/en/quick-sta
## 本地化翻译 / Localization
![zh-TW translation](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-TW&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27zh-TW%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ![en translation](https://img.shields.io/badge/dynamic/json?color=blue&label=en&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27en%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ![id translation](https://img.shields.io/badge/dynamic/json?color=blue&label=id&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27id%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ![ja translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ja&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ja%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ![ko translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ko&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ko%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)![pt-PT translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pt-PT&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27pt-PT%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ru%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)
[![zh-TW translation](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-TW&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27zh-TW%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![en translation](https://img.shields.io/badge/dynamic/json?color=blue&label=en&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27en%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![id translation](https://img.shields.io/badge/dynamic/json?color=blue&label=id&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27id%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![ja translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ja&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ja%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![ko translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ko&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ko%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![pt-PT translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pt-PT&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27pt-PT%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ru%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。

View File

@@ -11,6 +11,15 @@ var version = "version";
var repoDir = "repoDir";
var outputPath = "outputPath";
// Extension
static ProcessArgumentBuilder AppendIf(this ProcessArgumentBuilder builder, string text, bool condition)
{
return condition ? builder.Append(text) : builder;
}
// Properties
string solution
{
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao.sln");
@@ -157,6 +166,7 @@ Task("Build binary package")
.Append("/p:AppxPackageSigningEnabled=false")
.Append("/p:AppxBundle=Never")
.Append("/p:AppxPackageOutput=" + outputPath)
.AppendIf("/p:AlphaConstants=IS_ALPHA_BUILD", !AppVeyor.IsRunningOnAppVeyor)
};
DotNetBuild(project, settings);

BIN
res/Banner3-large-cn.psd Normal file

Binary file not shown.

BIN
res/Banner3-large.psd Normal file

Binary file not shown.

BIN
res/Store/chs/abyss.psd Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
res/Store/chs/lancher.psd Normal file

Binary file not shown.

Binary file not shown.

BIN
res/Store/chs/wish.psd Normal file

Binary file not shown.

BIN
res/Store/en/abyss.psd Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
res/Store/en/lancher.psd Normal file

Binary file not shown.

Binary file not shown.

BIN
res/Store/en/wish.psd Normal file

Binary file not shown.

View File

@@ -110,7 +110,6 @@ dotnet_diagnostic.SA1642.severity = none
dotnet_diagnostic.IDE0005.severity = warning
dotnet_diagnostic.IDE0060.severity = none
dotnet_diagnostic.IDE0290.severity = none
# SA1208: System using directives should be placed before other using directives
dotnet_diagnostic.SA1208.severity = none
@@ -321,7 +320,8 @@ dotnet_diagnostic.CA2227.severity = suggestion
# CA2251: 使用 “string.Equals”
dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_prefer_primary_constructors = false:none
[*.vb]
#### 命名样式 ####

View File

@@ -1,4 +1,5 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace Snap.Hutao.Test.BaseClassLibrary;

View File

@@ -0,0 +1,28 @@
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public class UnsafeAccessorTest
{
[TestMethod]
public void UnsafeAccessorCanGetInterfaceProperty()
{
TestClass test = new();
int value = InternalGetInterfaceProperty(test);
Assert.AreEqual(3, value);
}
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_TestProperty")]
private static extern int InternalGetInterfaceProperty(ITestInterface instance);
interface ITestInterface
{
internal int TestProperty { get; }
}
internal sealed class TestClass : ITestInterface
{
public int TestProperty { get; } = 3;
}
}

View File

@@ -1,5 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
namespace Snap.Hutao.Test.PlatformExtensions;
@@ -10,7 +12,10 @@ public sealed class DependencyInjectionTest
.AddSingleton<IService, ServiceA>()
.AddSingleton<IService, ServiceB>()
.AddScoped<IScopedService, ServiceA>()
.AddKeyedTransient<IKeyedService, KeyedServiceA>("A")
.AddKeyedTransient<IKeyedService, KeyedServiceB>("B")
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
.AddLogging(builder => builder.AddConsole())
.BuildServiceProvider();
[TestMethod]
@@ -41,6 +46,22 @@ public sealed class DependencyInjectionTest
}
}
[TestMethod]
public void LoggerWithInterfaceTypeCanBeResolved()
{
Assert.IsNotNull(services.GetService<ILogger<IScopedService>>());
Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService)));
}
[TestMethod]
public void KeyedServicesCanBeResolvedAsEnumerable()
{
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("A"));
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("B"));
Assert.AreEqual(0, services.GetServices<IKeyedService>().Count());
}
private interface IService
{
Guid Id { get; }
@@ -86,4 +107,14 @@ public sealed class DependencyInjectionTest
{
}
}
private interface IKeyedService;
private sealed class KeyedServiceA : IKeyedService
{
}
private sealed class KeyedServiceB : IKeyedService
{
}
}

View File

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

View File

@@ -6,6 +6,7 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources/>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsCard/SettingsCard.xaml"/>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.TokenizingTextBox/TokenizingTextBox.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Loading.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Image/CachedImage.xaml"/>

View File

@@ -8,10 +8,8 @@ using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Shell;
using Snap.Hutao.Core.Windowing;
using System.Diagnostics;
using System.Text;
using static Snap.Hutao.Core.Logging.ConsoleVirtualTerminalSequences;
namespace Snap.Hutao;
@@ -41,7 +39,7 @@ public sealed partial class App : Application
""";
private readonly IServiceProvider serviceProvider;
private readonly IActivation activation;
private readonly IAppActivation activation;
private readonly ILogger<App> logger;
/// <summary>
@@ -52,13 +50,19 @@ public sealed partial class App : Application
{
// Load app resource
InitializeComponent();
activation = serviceProvider.GetRequiredService<IActivation>();
activation = serviceProvider.GetRequiredService<IAppActivation>();
logger = serviceProvider.GetRequiredService<ILogger<App>>();
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
this.serviceProvider = serviceProvider;
}
public new void Exit()
{
XamlWindowLifetime.ApplicationExiting = true;
base.Exit();
}
/// <inheritdoc/>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
@@ -75,15 +79,13 @@ public sealed partial class App : Application
logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow));
LogDiagnosticInformation();
// manually invoke
// Manually invoke
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
activation.Initialize();
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
activation.PostInitialization();
}
catch
catch (Exception ex)
{
// AppInstance.GetCurrent() calls failed
Debug.WriteLine(ex);
Process.GetCurrentProcess().Kill();
}
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal class ButtonBaseBuilder<TButton> : IButtonBaseBuilder<TButton>
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase, new()
{
public TButton Button { get; } = new();
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal static class ButtonBaseBuilderExtension
{
public static TBuilder SetContent<TBuilder, TButton>(this TBuilder builder, object? content)
where TBuilder : IButtonBaseBuilder<TButton>
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
{
builder.Configure(builder => builder.Button.Content = content);
return builder;
}
public static TBuilder SetCommand<TBuilder, TButton>(this TBuilder builder, ICommand command)
where TBuilder : IButtonBaseBuilder<TButton>
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
{
builder.Configure(builder => builder.Button.Command = command);
return builder;
}
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal sealed class ButtonBuilder : ButtonBaseBuilder<Button>;

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal static class ButtonBuilderExtension
{
public static ButtonBuilder SetContent(this ButtonBuilder builder, object? content)
{
return builder.SetContent<ButtonBuilder, Button>(content);
}
public static ButtonBuilder SetCommand(this ButtonBuilder builder, ICommand command)
{
return builder.SetCommand<ButtonBuilder, Button>(command);
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal interface IButtonBaseBuilder<TButton> : IBuilder
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
{
TButton Button { get; }
}

View File

@@ -28,4 +28,21 @@ internal static class FrameworkElementExtension
frameworkElement.IsRightTapEnabled = false;
frameworkElement.IsTabStop = false;
}
public static void InitializeDataContext<TDataContext>(this FrameworkElement frameworkElement, IServiceProvider? serviceProvider = default)
where TDataContext : class
{
IServiceProvider service = serviceProvider ?? Ioc.Default;
try
{
frameworkElement.DataContext = service.GetRequiredService<TDataContext>();
}
catch (Exception ex)
{
ILogger? logger = service.GetRequiredService(typeof(ILogger<>).MakeGenericType([frameworkElement.GetType()])) as ILogger;
logger?.LogError(ex, "Failed to initialize DataContext");
throw;
}
}
}

View File

@@ -23,22 +23,19 @@ internal sealed class CachedImage : Implementation.ImageEx
{
DefaultStyleKey = typeof(CachedImage);
DefaultStyleResourceUri = "ms-appx:///Control/Image/CachedImage.xaml".ToUri();
IsCacheEnabled = true;
EnableLazyLoading = false;
}
/// <inheritdoc/>
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
protected override async Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
try
{
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), HutaoExceptionKind.ImageCacheInvalidUri, SH.ControlImageCachedImageInvalidResourceUri);
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.
return file.ToUri();
}
catch (COMException)
{

View File

@@ -6,7 +6,6 @@
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="LazyLoadingThreshold" Value="256"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="shci:CachedImage">

View File

@@ -21,12 +21,6 @@ namespace Snap.Hutao.Control.Image.Implementation;
[TemplatePart(Name = PartImage, Type = typeof(object))]
[TemplatePart(Name = PartPlaceholderImage, Type = typeof(object))]
[DependencyProperty("Stretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("DecodePixelHeight", typeof(int), 0)]
[DependencyProperty("DecodePixelWidth", typeof(int), 0)]
[DependencyProperty("DecodePixelType", typeof(DecodePixelType), DecodePixelType.Physical)]
[DependencyProperty("IsCacheEnabled", typeof(bool), false)]
[DependencyProperty("EnableLazyLoading", typeof(bool), false, nameof(EnableLazyLoadingChanged))]
[DependencyProperty("LazyLoadingThreshold", typeof(double), default(double), nameof(LazyLoadingThresholdChanged))]
[DependencyProperty("PlaceholderSource", typeof(object), default(object))]
[DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("PlaceholderMargin", typeof(Thickness))]
@@ -42,8 +36,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
protected const string FailedState = "Failed";
private CancellationTokenSource? tokenSource;
private object? lazyLoadingSource;
private bool isInViewport;
public bool IsInitialized { get; private set; }
@@ -58,10 +50,10 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
public abstract CompositionBrush GetAlphaMask();
protected virtual Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
protected virtual Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
// By default we just use the built-in UWP image cache provided within the Image control.
return Task.FromResult<ImageSource?>(new BitmapImage(imageUri));
return Task.FromResult<Uri?>(imageUri);
}
protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
@@ -80,19 +72,10 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
RemoveImageFailed(OnImageFailed);
Image = GetTemplateChild(PartImage);
PlaceholderImage = GetTemplateChild(PartPlaceholderImage);
IsInitialized = true;
if (Source is null || !EnableLazyLoading || isInViewport)
{
lazyLoadingSource = null;
SetSource(Source);
}
else
{
lazyLoadingSource = Source;
}
SetSource(Source);
AttachImageOpened(OnImageOpened);
AttachImageFailed(OnImageFailed);
@@ -148,33 +131,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
}
}
private static void EnableLazyLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ImageExBase control)
{
return;
}
bool value = (bool)e.NewValue;
if (value)
{
control.LayoutUpdated += control.OnImageExBaseLayoutUpdated;
control.InvalidateLazyLoading();
}
else
{
control.LayoutUpdated -= control.OnImageExBaseLayoutUpdated;
}
}
private static void LazyLoadingThresholdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageExBase { EnableLazyLoading: true } control)
{
control.InvalidateLazyLoading();
}
}
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ImageExBase control)
@@ -187,15 +143,7 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return;
}
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
{
control.lazyLoadingSource = null;
control.SetSource(e.NewValue);
}
else
{
control.lazyLoadingSource = e.NewValue;
}
control.SetSource(e.NewValue);
}
private static bool IsHttpUri(Uri uri)
@@ -203,11 +151,8 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https");
}
private void AttachSource(ImageSource? source)
private void AttachSource(BitmapImage? source, Uri? uri)
{
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
// We only need to call those methods if we fail in other cases before we get here.
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.Source = source;
@@ -221,13 +166,15 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
{
VisualStateManager.GoToState(this, UnloadedState, true);
}
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
else
{
// https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-animations-and-media#optimize-image-resources
source.UriSource = uri;
VisualStateManager.GoToState(this, LoadedState, true);
}
}
private void AttachPlaceholderSource(ImageSource? source)
private void AttachPlaceholderSource(BitmapImage? source, Uri? uri)
{
if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image)
{
@@ -242,8 +189,10 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
{
VisualStateManager.GoToState(this, UnloadedState, true);
}
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
else
{
// https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-animations-and-media#optimize-image-resources
source.UriSource = uri;
VisualStateManager.GoToState(this, LoadedState, true);
}
}
@@ -256,10 +205,9 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
}
tokenSource?.Cancel();
tokenSource = new CancellationTokenSource();
AttachSource(null);
AttachSource(default, default);
if (source is null)
{
@@ -268,13 +216,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
VisualStateManager.GoToState(this, LoadingState, true);
if (source as ImageSource is { } imageSource)
{
AttachSource(imageSource);
return;
}
if (source as Uri is not { } uri)
{
string? url = source as string ?? source.ToString();
@@ -319,20 +260,13 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
tokenSource?.Cancel();
tokenSource = new();
AttachPlaceholderSource(null);
AttachPlaceholderSource(default, default);
if (source is null)
{
return;
}
if (source as ImageSource is { } imageSource)
{
AttachPlaceholderSource(imageSource);
return;
}
if (source as Uri is not { } uri)
{
string? url = source as string ?? source.ToString();
@@ -354,13 +288,13 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return;
}
ImageSource? img = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
Uri? actualUri = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
// Only attach our image if we still have a valid request.
AttachPlaceholderSource(img);
AttachPlaceholderSource(new BitmapImage(), actualUri);
}
}
catch (OperationCanceledException)
@@ -379,99 +313,13 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return;
}
if (IsCacheEnabled)
Uri? actualUri = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
// Only attach our image if we still have a valid request.
AttachSource(img);
}
}
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
{
string source = imageUri.OriginalString;
const string base64Head = "base64,";
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
if (index >= 0)
{
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
BitmapImage bitmap = new();
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
AttachSource(bitmap);
}
}
}
else
{
AttachSource(new BitmapImage(imageUri)
{
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
});
}
}
private void OnImageExBaseLayoutUpdated(object? sender, object e)
{
InvalidateLazyLoading();
}
private void InvalidateLazyLoading()
{
if (!IsLoaded)
{
isInViewport = false;
return;
}
// Find the first ascendant ScrollViewer, if not found, use the root element.
FrameworkElement? hostElement = default;
IEnumerable<FrameworkElement> ascendants = this.FindAscendants().OfType<FrameworkElement>();
foreach (FrameworkElement ascendant in ascendants)
{
hostElement = ascendant;
if (hostElement is Microsoft.UI.Xaml.Controls.ScrollViewer)
{
break;
}
}
if (hostElement is null)
{
isInViewport = false;
return;
}
Rect controlRect = TransformToVisual(hostElement).TransformBounds(StructMarshal.Rect(ActualSize));
double lazyLoadingThreshold = LazyLoadingThreshold;
// Left/Top 1 Threshold, Right/Bottom 2 Threshold
Rect hostRect = new(
0 - lazyLoadingThreshold,
0 - lazyLoadingThreshold,
hostElement.ActualWidth + (2 * lazyLoadingThreshold),
hostElement.ActualHeight + (2 * lazyLoadingThreshold));
if (controlRect.IntersectsWith(hostRect))
{
isInViewport = true;
if (lazyLoadingSource is not null)
{
object source = lazyLoadingSource;
lazyLoadingSource = null;
SetSource(source);
}
}
else
{
isInViewport = false;
// Only attach our image if we still have a valid request.
AttachSource(new BitmapImage(), actualUri);
}
}
}

View File

@@ -4,7 +4,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.Specialized;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Windows.Foundation;
@@ -64,12 +63,12 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
/// <inheritdoc/>
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
if (context.ItemCount == 0)
if (context.ItemCount is 0)
{
return new Size(availableSize.Width, 0);
}
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
if ((context.RealizationRect.Width is 0) && (context.RealizationRect.Height is 0))
{
return new Size(availableSize.Width, 0.0f);
}

View File

@@ -0,0 +1,25 @@
// Licensed to the .NET Fou// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Windows.Foundation;
namespace Snap.Hutao.Control.Layout;
internal sealed class WrapItem
{
public static Point EmptyPosition { get; } = new(float.NegativeInfinity, float.NegativeInfinity);
public WrapItem(int index)
{
Index = index;
}
public int Index { get; }
public Size Size { get; set; } = Size.Empty;
public Point Position { get; set; } = EmptyPosition;
public UIElement? Element { get; set; }
}

View File

@@ -0,0 +1,220 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.Specialized;
using Windows.Foundation;
namespace Snap.Hutao.Control.Layout;
[DependencyProperty("HorizontalSpacing", typeof(double), 0D, nameof(LayoutPropertyChanged))]
[DependencyProperty("VerticalSpacing", typeof(double), 0D, nameof(LayoutPropertyChanged))]
internal sealed partial class WrapLayout : VirtualizingLayout
{
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = new WrapLayoutState(context);
}
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = default;
}
protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
{
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
state.RemoveFromIndex(args.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Move:
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
state.RemoveFromIndex(minIndex);
state.RecycleElementAt(args.OldStartingIndex);
state.RecycleElementAt(args.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Remove:
state.RemoveFromIndex(args.OldStartingIndex);
break;
case NotifyCollectionChangedAction.Replace:
state.RemoveFromIndex(args.NewStartingIndex);
state.RecycleElementAt(args.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Reset:
state.Clear();
break;
}
base.OnItemsChangedCore(context, source, args);
}
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
if (context.ItemCount is 0)
{
return new Size(availableSize.Width, 0);
}
if ((context.RealizationRect.Width is 0) && (context.RealizationRect.Height is 0))
{
return new Size(availableSize.Width, 0.0f);
}
Size spacing = new(HorizontalSpacing, VerticalSpacing);
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
if (spacing != state.Spacing || state.AvailableWidth != availableSize.Width)
{
state.ClearPositions();
state.Spacing = spacing;
state.AvailableWidth = availableSize.Width;
}
double currentHeight = 0;
Point itemPosition = default;
for (int i = 0; i < context.ItemCount; ++i)
{
bool itemMeasured = false;
WrapItem item = state.GetItemAt(i);
if (item.Size == Size.Empty)
{
item.Element = context.GetOrCreateElementAt(i);
item.Element.Measure(availableSize);
item.Size = item.Element.DesiredSize;
itemMeasured = true;
}
Size itemSize = item.Size;
if (item.Position == WrapItem.EmptyPosition)
{
if (availableSize.Width < itemPosition.X + itemSize.Width)
{
// New Row
itemPosition.X = 0;
itemPosition.Y += currentHeight + spacing.Height;
currentHeight = 0;
}
item.Position = itemPosition;
}
itemPosition = item.Position;
double bottom = itemPosition.Y + itemSize.Height;
if (bottom < context.RealizationRect.Top)
{
// Item is "above" the bounds
if (item.Element is not null)
{
context.RecycleElement(item.Element);
item.Element = default;
}
continue;
}
else if (itemPosition.Y > context.RealizationRect.Bottom)
{
// Item is "below" the bounds.
if (item.Element is not null)
{
context.RecycleElement(item.Element);
item.Element = default;
}
// We don't need to measure anything below the bounds
break;
}
else if (!itemMeasured)
{
// Always measure elements that are within the bounds
item.Element = context.GetOrCreateElementAt(i);
item.Element.Measure(availableSize);
itemSize = item.Element.DesiredSize;
if (itemSize != item.Size)
{
// this item changed size; we need to recalculate layout for everything after this
state.RemoveFromIndex(i + 1);
item.Size = itemSize;
// did the change make it go into the new row?
if (availableSize.Width < itemPosition.X + itemSize.Width)
{
// New Row
itemPosition.X = 0;
itemPosition.Y += currentHeight + spacing.Height;
currentHeight = 0;
}
item.Position = itemPosition;
}
}
itemPosition.X += itemSize.Width + spacing.Width;
currentHeight = Math.Max(itemSize.Height, currentHeight);
}
return new Size(double.IsInfinity(availableSize.Width) ? 0 : Math.Ceiling(availableSize.Width), state.GetHeight());
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
if (context.ItemCount > 0)
{
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
for (int i = 0; i < context.ItemCount; ++i)
{
if (!ArrangeItem(context, state.GetItemAt(i)))
{
break;
}
}
}
return finalSize;
static bool ArrangeItem(VirtualizingLayoutContext context, WrapItem item)
{
if (item.Size == Size.Empty || item.Position == WrapItem.EmptyPosition)
{
return false;
}
Size size = item.Size;
Point position = item.Position;
if (context.RealizationRect.Top <= position.Y + size.Height && position.Y <= context.RealizationRect.Bottom)
{
// place the item
UIElement child = context.GetOrCreateElementAt(item.Index);
child.Arrange(new Rect(position, size));
}
else if (position.Y > context.RealizationRect.Bottom)
{
return false;
}
return true;
}
}
private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is WrapLayout layout)
{
layout.InvalidateMeasure();
layout.InvalidateArrange();
}
}
}

View File

@@ -0,0 +1,112 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.Control.Layout;
internal sealed class WrapLayoutState
{
private readonly List<WrapItem> items = [];
private readonly VirtualizingLayoutContext context;
public WrapLayoutState(VirtualizingLayoutContext context)
{
this.context = context;
}
public Orientation Orientation { get; private set; }
public Size Spacing { get; set; }
public double AvailableWidth { get; set; }
public WrapItem GetItemAt(int index)
{
if (index < 0)
{
throw new IndexOutOfRangeException();
}
if (index <= (items.Count - 1))
{
return items[index];
}
else
{
WrapItem item = new(index);
items.Add(item);
return item;
}
}
public void Clear()
{
for (int i = 0; i < items.Count; i++)
{
RecycleElementAt(i);
}
items.Clear();
}
public void RemoveFromIndex(int index)
{
if (index >= items.Count)
{
// Item was added/removed but we haven't realized that far yet
return;
}
int numToRemove = items.Count - index;
items.RemoveRange(index, numToRemove);
}
public void ClearPositions()
{
foreach (ref readonly WrapItem item in CollectionsMarshal.AsSpan(items))
{
item.Position = WrapItem.EmptyPosition;
}
}
public double GetHeight()
{
if (items.Count is 0)
{
return 0;
}
Point? lastPosition = default;
double maxHeight = 0;
Span<WrapItem> itemSpan = CollectionsMarshal.AsSpan(items);
for (int i = items.Count - 1; i >= 0; --i)
{
ref readonly WrapItem item = ref itemSpan[i];
if (item.Position == WrapItem.EmptyPosition || item.Size == Size.Empty)
{
continue;
}
if (lastPosition is not null && lastPosition.Value.Y > item.Position.Y)
{
// This is a row above the last item.
break;
}
lastPosition = item.Position;
maxHeight = Math.Max(maxHeight, item.Size.Height);
}
return lastPosition?.Y + maxHeight ?? 0;
}
public void RecycleElementAt(int index)
{
context.RecycleElement(context.GetOrCreateElementAt(index));
}
}

View File

@@ -8,45 +8,19 @@ using Windows.UI;
namespace Snap.Hutao.Control.Media;
/// <summary>
/// RGBA 颜色
/// </summary>
[HighQuality]
internal struct Rgba32
{
/// <summary>
/// R
/// </summary>
public byte R;
/// <summary>
/// G
/// </summary>
public byte G;
/// <summary>
/// B
/// </summary>
public byte B;
/// <summary>
/// A
/// </summary>
public byte A;
/// <summary>
/// 构造一个新的 RGBA8 颜色
/// </summary>
/// <param name="hex">色值字符串</param>
public Rgba32(string hex)
: this(hex.Length == 6 ? Convert.ToUInt32($"{hex}FF", 16) : Convert.ToUInt32(hex, 16))
{
}
/// <summary>
/// 使用 RGBA 代码初始化新的结构
/// </summary>
/// <param name="xrgbaCode">RGBA 代码</param>
public unsafe Rgba32(uint xrgbaCode)
{
// uint layout: 0xRRGGBBAA is AABBGGRR
@@ -80,11 +54,6 @@ internal struct Rgba32
return *(Color*)&rgba;
}
/// <summary>
/// 从 HSL 颜色转换
/// </summary>
/// <param name="hsl">HSL 颜色</param>
/// <returns>RGBA8颜色</returns>
public static Rgba32 FromHsl(Hsla32 hsl)
{
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
@@ -138,10 +107,6 @@ internal struct Rgba32
return new(r, g, b, a);
}
/// <summary>
/// 转换到 HSL 颜色
/// </summary>
/// <returns>HSL 颜色</returns>
public readonly Hsla32 ToHsl()
{
const double toDouble = 1.0 / 255;

View File

@@ -0,0 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// Some part of this file came from:
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
namespace Snap.Hutao.Control.Media;
internal struct Rgba64
{
public Half R;
public Half G;
public Half B;
public Half A;
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;
@@ -18,13 +19,14 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement child in Children)
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
{
// ScrollViewer will always return an Infinity Size, we should use ActualWidth for this situation.
double availableWidth = double.IsInfinity(availableSize.Width) ? ActualWidth : availableSize.Width;
double childAvailableWidth = (availableWidth + Spacing) / Children.Count;
double childAvailableWidth = (availableWidth + Spacing) / visibleChildren.Count;
double childMaxAvailableWidth = Math.Max(MinItemWidth, childAvailableWidth);
child.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
visibleChild.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
}
return base.MeasureOverride(availableSize);
@@ -32,14 +34,14 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
protected override Size ArrangeOverride(Size finalSize)
{
int itemCount = Children.Count;
double availableItemWidth = (finalSize.Width - (Spacing * (itemCount - 1))) / itemCount;
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
double availableItemWidth = (finalSize.Width - (Spacing * (visibleChildren.Count - 1))) / visibleChildren.Count;
double actualItemWidth = Math.Max(MinItemWidth, availableItemWidth);
double offset = 0;
foreach (UIElement child in Children)
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
{
child.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
visibleChild.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
offset += actualItemWidth + Spacing;
}
@@ -49,7 +51,8 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
private static void OnLoaded(object sender, RoutedEventArgs e)
{
HorizontalEqualPanel panel = (HorizontalEqualPanel)sender;
panel.MinWidth = (panel.MinItemWidth * panel.Children.Count) + (panel.Spacing * (panel.Children.Count - 1));
int vivibleChildrenCount = panel.Children.Count(child => child.Visibility is Visibility.Visible);
panel.MinWidth = (panel.MinItemWidth * vivibleChildrenCount) + (panel.Spacing * (vivibleChildrenCount - 1));
}
private static void OnSizeChanged(object sender, SizeChangedEventArgs e)

View File

@@ -2,8 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;

View File

@@ -15,7 +15,7 @@ internal class ScopedPage : Page
{
private readonly RoutedEventHandler unloadEventHandler;
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope currentScope;
private readonly IServiceScope pageScope;
private bool inFrame = true;
@@ -23,7 +23,7 @@ internal class ScopedPage : Page
{
unloadEventHandler = OnUnloaded;
Unloaded += unloadEventHandler;
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
pageScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
}
public async ValueTask NotifyRecipientAsync(INavigationData extra)
@@ -44,9 +44,17 @@ internal class ScopedPage : Page
protected void InitializeWith<TViewModel>()
where TViewModel : class, IViewModel
{
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
try
{
IViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
}
catch (Exception ex)
{
pageScope.ServiceProvider.GetRequiredService<ILogger<ScopedPage>>().LogError(ex, "Failed to initialize view model.");
throw;
}
}
/// <inheritdoc/>
@@ -95,7 +103,8 @@ internal class ScopedPage : Page
viewModel.IsViewDisposed = true;
// Dispose the scope
currentScope.Dispose();
pageScope.Dispose();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
}
}
}

View File

@@ -22,7 +22,6 @@ internal sealed partial class ScopedPageScopeReferenceTracker : IScopedPageScope
public IServiceScope CreateScope()
{
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
IServiceScope currentScope = serviceProvider.CreateScope();
// In case previous one is not disposed.

View File

@@ -17,8 +17,13 @@
<x:String x:Key="UI_Icon_Intee_Explore_1">https://api.snapgenshin.com/static/raw/Bg/UI_Icon_Intee_Explore_1.png</x:String>
<x:String x:Key="UI_ImgSign_ItemIcon">https://api.snapgenshin.com/static/raw/Bg/UI_ImgSign_ItemIcon.png</x:String>
<x:String x:Key="UI_ItemIcon_None">https://api.snapgenshin.com/static/raw/Bg/UI_ItemIcon_None.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Bg/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Bg/UI_MarkTower.png</x:String>
<!-- Mark -->
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Mark/UI_MarkTower.png</x:String>
<!-- ItemIcon -->
<x:String x:Key="UI_ItemIcon_201">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_201.png</x:String>

View File

@@ -1,10 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Request.Builder.Abstraction;
using System.Diagnostics;
namespace Snap.Hutao.Web.Request.Builder;
namespace Snap.Hutao.Core.Abstraction.Extension;
internal static class BuilderExtension
{

View File

@@ -1,6 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Request.Builder.Abstraction;
namespace Snap.Hutao.Core.Abstraction;
internal interface IBuilder;

View File

@@ -1,10 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.ViewModel.Guide;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.IO;
@@ -25,6 +29,7 @@ namespace Snap.Hutao.Core.Caching;
internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string CacheFolderName = nameof(ImageCache);
private const string CacheFailedDownloadTasksName = $"{nameof(ImageCache)}.FailedDownloadTasks";
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = FrozenDictionary.ToFrozenDictionary(
[
@@ -35,9 +40,11 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly IHttpClientFactory httpClientFactory;
private readonly IServiceProvider serviceProvider;
private readonly ILogger<ImageCache> logger;
private readonly IMemoryCache memoryCache;
private string? baseFolder;
private string? cacheFolder;
@@ -169,40 +176,76 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
while (retryCount < 3)
{
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
if (message.RequestMessage is { RequestUri: { } target } && target != uri)
{
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
}
HttpRequestMessageBuilder requestMessageBuilder = httpRequestMessageBuilderFactory
.Create()
.SetRequestUri(uri)
if (message.IsSuccessStatusCode)
// These headers are only available for our own api
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase))
.Get();
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
{
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
{
using (FileStream fileStream = File.Create(baseFile))
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
}
if (responseMessage.IsSuccessStatusCode)
{
if (responseMessage.Content.Headers.ContentType?.MediaType is "application/json")
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
#if DEBUG
DebugTrack(uri);
#endif
string raw = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
logger.LogColorizedCritical("Failed to download '{Uri}' with unexpected body '{Raw}'", (uri, ConsoleColor.Red), (raw, ConsoleColor.DarkYellow));
return;
}
}
}
switch (message.StatusCode)
{
case HttpStatusCode.TooManyRequests:
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
retryCount++;
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;
using (FileStream fileStream = File.Create(baseFile))
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
}
}
default:
return;
switch (responseMessage.StatusCode)
{
case HttpStatusCode.TooManyRequests:
{
retryCount++;
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;
}
default:
#if DEBUG
DebugTrack(uri);
#endif
logger.LogColorizedCritical("Failed to download '{Uri}' with status code '{StatusCode}'", (uri, ConsoleColor.Red), (responseMessage.StatusCode, ConsoleColor.DarkYellow));
return;
}
}
}
}
}
}
}
#if DEBUG
internal partial class ImageCache
{
private void DebugTrack(Uri uri)
{
HashSet<string>? set = memoryCache.GetOrCreate(CacheFailedDownloadTasksName, entry => entry.Value ??= new HashSet<string>()) as HashSet<string>;
set?.Add(uri.ToString());
}
}
#endif

View File

@@ -0,0 +1,32 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Collection;
internal sealed class TwoEnumerbleEnumerator<TFirst, TSecond> : IDisposable
{
private readonly IEnumerator<TFirst> firstEnumerator;
private readonly IEnumerator<TSecond> secondEnumerator;
public TwoEnumerbleEnumerator(IEnumerable<TFirst> firstEnumerable, IEnumerable<TSecond> secondEnumerable)
{
firstEnumerator = firstEnumerable.GetEnumerator();
secondEnumerator = secondEnumerable.GetEnumerator();
}
public (TFirst First, TSecond Second) Current { get => (firstEnumerator.Current, secondEnumerator.Current); }
public bool MoveNext(ref bool moveFirst, ref bool moveSecond)
{
moveFirst = moveFirst && firstEnumerator.MoveNext();
moveSecond = moveSecond && secondEnumerator.MoveNext();
return moveFirst || moveSecond;
}
public void Dispose()
{
firstEnumerator.Dispose();
secondEnumerator.Dispose();
}
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Core;
/// <summary>
/// 支持Md5转换
/// </summary>
[HighQuality]
internal static class Convert
{
/// <summary>
/// 获取字符串的MD5计算结果
/// </summary>
/// <param name="source">源字符串</param>
/// <returns>计算的结果</returns>
public static string ToMd5HexString(string source)
{
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
return System.Convert.ToHexString(hash);
}
}

View File

@@ -13,13 +13,6 @@ namespace Snap.Hutao.Core.Database;
[HighQuality]
internal static class DbSetExtension
{
/// <summary>
/// 添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int AddAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -27,27 +20,13 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
/// <summary>
/// 异步添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
where TEntity : class
{
dbSet.Add(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
/// <summary>
/// 添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
@@ -55,27 +34,13 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
/// <summary>
/// 异步添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities, CancellationToken token = default)
where TEntity : class
{
dbSet.AddRange(entities);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
/// <summary>
/// 移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -83,27 +48,13 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
/// <summary>
/// 异步移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
where TEntity : class
{
dbSet.Remove(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
/// <summary>
/// 更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -111,18 +62,11 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
/// <summary>
/// 异步更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
where TEntity : class
{
dbSet.Update(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -136,11 +80,11 @@ internal static class DbSetExtension
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet)
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet, CancellationToken token = default)
where TEntity : class
{
DbContext dbContext = dbSet.Context();
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
int count = await dbContext.SaveChangesAsync(token).ConfigureAwait(false);
dbContext.ChangeTracker.Clear();
return count;
}

View File

@@ -38,7 +38,7 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
return;
}
if (serviceProvider.IsDisposedSlow())
if (serviceProvider.IsDisposed())
{
return;
}
@@ -96,7 +96,7 @@ internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
return;
}
if (serviceProvider.IsDisposedSlow())
if (serviceProvider.IsDisposed())
{
return;
}

View File

@@ -1,26 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 对象扩展
/// </summary>
[HighQuality]
internal static class CastServiceExtension
{
/// <summary>
/// <see langword="as"/> 的链式调用扩展
/// </summary>
/// <typeparam name="T">目标转换类型</typeparam>
/// <param name="service">对象</param>
/// <returns>转换类型后的对象</returns>
[Obsolete("Not useful anymore")]
public static T? As<T>(this ICastService service)
where T : class
{
return service as T;
}
}

View File

@@ -2,8 +2,13 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Quartz;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Service;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Job;
using System.Collections.Specialized;
using System.Globalization;
using System.Runtime.CompilerServices;
using Windows.Globalization;
@@ -24,9 +29,18 @@ internal static class DependencyInjection
ServiceProvider serviceProvider = new ServiceCollection()
// Microsoft extension
.AddLogging(builder => builder.AddDebug().AddConsoleWindow())
.AddLogging(builder =>
{
builder
.SetMinimumLevel(LogLevel.Trace)
.AddDebug()
.AddConsoleWindow();
})
.AddMemoryCache()
// Quartz
.AddQuartz()
// Hutao extensions
.AddJsonOptions()
.AddDatabase()

View File

@@ -33,17 +33,17 @@ internal static class IocConfiguration
return services
.AddTransient(typeof(Database.ScopedDbCurrent<,>))
.AddTransient(typeof(Database.ScopedDbCurrent<,,>))
.AddDbContext<AppDbContext>(AddDbContextCore);
.AddDbContextPool<AppDbContext>(AddDbContextCore);
}
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
private static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
{
RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
// Temporarily create a context
using (AppDbContext context = AppDbContext.Create(sqlConnectionString))
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString))
{
if (context.Database.GetPendingMigrations().Any())
{

View File

@@ -18,13 +18,22 @@ internal static class ServiceProviderExtension
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDisposedSlow(this IServiceProvider? serviceProvider)
public static bool IsDisposed(this IServiceProvider? serviceProvider)
{
if (serviceProvider is null)
{
return true;
}
if (serviceProvider is ServiceProvider serviceProviderImpl)
{
return GetPrivateDisposed(serviceProviderImpl);
}
return serviceProvider.GetType().GetField("_disposed")?.GetValue(serviceProvider) is true;
}
// private bool _disposed;
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
private static extern ref bool GetPrivateDisposed(ServiceProvider serviceProvider);
}

View File

@@ -1,5 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Logging;
namespace Snap.Hutao.Core.Diagnostics;
internal readonly struct MeasureExecutionToken : IDisposable
@@ -17,6 +19,6 @@ internal readonly struct MeasureExecutionToken : IDisposable
public void Dispose()
{
logger.LogDebug("{Caller} toke {Time} ms", callerName, stopwatch.GetElapsedTime().TotalMilliseconds);
logger.LogColorizedDebug(("{Caller} toke {Time} ms", ConsoleColor.Gray), (callerName, ConsoleColor.Yellow), (stopwatch.GetElapsedTime().TotalMilliseconds, ConsoleColor.DarkGreen));
}
}

View File

@@ -5,50 +5,67 @@ namespace Snap.Hutao.Core.ExceptionService;
internal sealed class HutaoException : Exception
{
public HutaoException(HutaoExceptionKind kind, string message, Exception? innerException)
: this(message, innerException)
{
Kind = kind;
}
private HutaoException(string message, Exception? innerException)
public HutaoException(string message, Exception? innerException)
: base($"{message}\n{innerException?.Message}", innerException)
{
}
public HutaoExceptionKind Kind { get; private set; }
[DoesNotReturn]
public static HutaoException Throw(HutaoExceptionKind kind, string message, Exception? innerException = default)
public static HutaoException Throw(string message, Exception? innerException = default)
{
throw new HutaoException(kind, message, innerException);
throw new HutaoException(message, innerException);
}
public static void ThrowIf(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
public static void ThrowIf([DoesNotReturnIf(true)] bool condition, string message, Exception? innerException = default)
{
if (condition)
{
throw new HutaoException(kind, message, innerException);
throw new HutaoException(message, innerException);
}
}
public static void ThrowIfNot(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
public static void ThrowIfNot([DoesNotReturnIf(false)] bool condition, string message, Exception? innerException = default)
{
if (!condition)
{
throw new HutaoException(kind, message, innerException);
throw new HutaoException(message, innerException);
}
}
public static HutaoException ServiceTypeCastFailed<TFrom, TTo>(string name, Exception? innerException = default)
{
string message = $"This instance of '{typeof(TFrom).FullName}' '{name}' doesn't implement '{typeof(TTo).FullName}'";
throw new HutaoException(HutaoExceptionKind.ServiceTypeCastFailed, message, innerException);
}
[DoesNotReturn]
public static HutaoException GachaStatisticsInvalidItemId(uint id, Exception? innerException = default)
{
string message = SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(id);
throw new HutaoException(HutaoExceptionKind.GachaStatisticsInvalidItemId, message, innerException);
throw new HutaoException(SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(id), innerException);
}
[DoesNotReturn]
public static HutaoException UserdataCorrupted(string message, Exception? innerException = default)
{
throw new HutaoException(message, innerException);
}
[DoesNotReturn]
public static InvalidCastException InvalidCast<TFrom, TTo>(string name, Exception? innerException = default)
{
string message = $"This instance of '{typeof(TFrom).FullName}' '{name}' doesn't implement '{typeof(TTo).FullName}'";
throw new InvalidCastException(message, innerException);
}
[DoesNotReturn]
public static InvalidOperationException InvalidOperation(string message, Exception? innerException = default)
{
throw new InvalidOperationException(message, innerException);
}
[DoesNotReturn]
public static NotSupportedException NotSupported(string? message = default, Exception? innerException = default)
{
throw new NotSupportedException(message, innerException);
}
[DoesNotReturn]
public static OperationCanceledException OperationCanceled(string message, Exception? innerException = default)
{
throw new OperationCanceledException(message, innerException);
}
}

View File

@@ -1,21 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
internal enum HutaoExceptionKind
{
None,
// Foundation
ServiceTypeCastFailed,
ImageCacheInvalidUri,
// IO
FileSystemCreateFileInsufficientPermissions,
PrivateNamedPipeContentHashIncorrect,
// Service
GachaStatisticsInvalidItemId,
GameFpsUnlockingFailed,
}

View File

@@ -1,7 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.System.Com;
using Snap.Hutao.Win32.UI.Shell;
using System.IO;
using static Snap.Hutao.Win32.Macros;
using static Snap.Hutao.Win32.Ole32;
using static Snap.Hutao.Win32.Shell32;
namespace Snap.Hutao.Core.IO;
@@ -39,4 +44,57 @@ internal static class FileOperation
File.Move(sourceFileName, destFileName, false);
return true;
}
public static unsafe bool UnsafeMove(string sourceFileName, string destFileName)
{
bool result = false;
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(sourceFileName, default, in IShellItem.IID, out IShellItem* pSourceShellItem)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(destFileName, default, in IShellItem.IID, out IShellItem* pDestShellItem)))
{
pFileOperation->MoveItem(pSourceShellItem, pDestShellItem, default, default);
if (SUCCEEDED(pFileOperation->PerformOperations()))
{
result = true;
}
IUnknownMarshal.Release(pDestShellItem);
}
IUnknownMarshal.Release(pSourceShellItem);
}
IUnknownMarshal.Release(pFileOperation);
}
return result;
}
public static unsafe bool UnsafeDelete(string path)
{
bool result = false;
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(path, default, in IShellItem.IID, out IShellItem* pShellItem)))
{
pFileOperation->DeleteItem(pShellItem, default);
if (SUCCEEDED(pFileOperation->PerformOperations()))
{
result = true;
}
IUnknownMarshal.Release(pShellItem);
}
IUnknownMarshal.Release(pFileOperation);
}
return result;
}
}

View File

@@ -8,12 +8,17 @@ namespace Snap.Hutao.Core.IO.Hashing;
internal static class Hash
{
public static string SHA1HexString(string input)
public static unsafe string SHA1HexString(string input)
{
return HashCore(BitConverter.ToString, SHA1.HashData, Encoding.UTF8.GetBytes, input);
return HashCore(Convert.ToHexString, SHA1.HashData, Encoding.UTF8.GetBytes, input);
}
private static TResult HashCore<TInput, TResult>(Func<byte[], TResult> resultConverter, Func<byte[], byte[]> hashMethod, Func<TInput, byte[]> bytesConverter, TInput input)
public static unsafe string MD5HexString(string input)
{
return HashCore(Convert.ToHexString, System.Security.Cryptography.MD5.HashData, Encoding.UTF8.GetBytes, input);
}
private static unsafe TResult HashCore<TInput, TResult>(Func<byte[], TResult> resultConverter, Func<byte[], byte[]> hashMethod, Func<TInput, byte[]> bytesConverter, TInput input)
{
return resultConverter(hashMethod(bytesConverter(input)));
}

View File

@@ -34,6 +34,6 @@ internal static class MD5
public static async ValueTask<string> HashAsync(Stream stream, CancellationToken token = default)
{
byte[] bytes = await System.Security.Cryptography.MD5.HashDataAsync(stream, token).ConfigureAwait(false);
return System.Convert.ToHexString(bytes);
return Convert.ToHexString(bytes);
}
}

View File

@@ -18,6 +18,6 @@ internal static class SHA256
public static async ValueTask<string> HashAsync(Stream stream, CancellationToken token = default)
{
byte[] bytes = await System.Security.Cryptography.SHA256.HashDataAsync(stream, token).ConfigureAwait(false);
return System.Convert.ToHexString(bytes);
return Convert.ToHexString(bytes);
}
}

View File

@@ -7,7 +7,7 @@ using Snap.Hutao.Win32.NetworkManagement.WindowsFirewall;
using Snap.Hutao.Win32.Security;
using System.Runtime.InteropServices;
using static Snap.Hutao.Win32.AdvApi32;
using static Snap.Hutao.Win32.ApiMsWinNetIsolation;
using static Snap.Hutao.Win32.FirewallApi;
using static Snap.Hutao.Win32.Macros;
namespace Snap.Hutao.Core.IO.Http.Loopback;

View File

@@ -3,6 +3,7 @@
using Microsoft.Win32.SafeHandles;
using Snap.Hutao.Core.Diagnostics;
using System.Buffers;
using System.IO;
using System.Net.Http;
@@ -77,34 +78,36 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
using (HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();
Memory<byte> buffer = new byte[bufferSize];
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent())
{
int totalBytesRead = 0;
int bytesReadAfterPreviousReport = 0;
do
Memory<byte> buffer = memoryOwner.Memory;
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
{
int bytesRead = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
if (bytesRead <= 0)
int totalBytesRead = 0;
int bytesReadAfterPreviousReport = 0;
do
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
break;
}
int bytesRead = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
if (bytesRead <= 0)
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
break;
}
await RandomAccess.WriteAsync(destFileHandle, buffer[..bytesRead], shard.StartOffset + totalBytesRead, token).ConfigureAwait(false);
await RandomAccess.WriteAsync(destFileHandle, buffer[..bytesRead], shard.StartOffset + totalBytesRead, token).ConfigureAwait(false);
totalBytesRead += bytesRead;
bytesReadAfterPreviousReport += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
stopwatch = ValueStopwatch.StartNew();
totalBytesRead += bytesRead;
bytesReadAfterPreviousReport += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
stopwatch = ValueStopwatch.StartNew();
}
}
while (true);
}
while (true);
}
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Diagnostics;
using System.Buffers;
using System.IO;
namespace Snap.Hutao.Core.IO;
@@ -51,26 +52,30 @@ internal class StreamCopyWorker<TStatus>
long totalBytesRead = 0;
int bytesRead;
Memory<byte> buffer = new byte[bufferSize];
do
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(bufferSize))
{
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
if (bytesRead == 0)
{
progress.Report(statusFactory(totalBytesRead));
break;
}
Memory<byte> buffer = memoryOwner.Memory;
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
totalBytesRead += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000)
do
{
progress.Report(statusFactory(totalBytesRead));
stopwatch = ValueStopwatch.StartNew();
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
if (bytesRead is 0)
{
progress.Report(statusFactory(totalBytesRead));
break;
}
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
totalBytesRead += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000)
{
progress.Report(statusFactory(totalBytesRead));
stopwatch = ValueStopwatch.StartNew();
}
}
while (bytesRead > 0);
}
while (bytesRead > 0);
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO;
internal sealed class StreamReaderWriter : IDisposable
{
private readonly StreamReader reader;
private readonly StreamWriter writer;
public StreamReaderWriter(StreamReader reader, StreamWriter writer)
{
this.reader = reader;
this.writer = writer;
}
public StreamReader Reader { get => reader; }
public StreamWriter Writer { get => writer; }
/// <inheritdoc cref="StreamReader.ReadLineAsync(CancellationToken)"/>
public ValueTask<string?> ReadLineAsync(CancellationToken token)
{
return reader.ReadLineAsync(token);
}
/// <inheritdoc cref="StreamWriter.WriteAsync(string?)"/>
public Task WriteAsync(string value)
{
return writer.WriteAsync(value);
}
public void Dispose()
{
writer.Dispose();
reader.Dispose();
}
}

View File

@@ -29,7 +29,7 @@ internal readonly struct TempFile : IDisposable
}
catch (UnauthorizedAccessException ex)
{
HutaoException.Throw(HutaoExceptionKind.FileSystemCreateFileInsufficientPermissions, SH.CoreIOTempFileCreateFail, ex);
HutaoException.Throw(SH.CoreIOTempFileCreateFail, ex);
}
if (delete)

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core;
internal sealed class LazySlim<T>
{
private readonly Func<T> valueFactory;
[MaybeNull]
private T value;
private bool initialized;
private object? syncRoot;
public LazySlim(Func<T> valueFactory)
{
this.valueFactory = valueFactory;
}
public T Value { get => LazyInitializer.EnsureInitialized(ref value, ref initialized, ref syncRoot, valueFactory); }
}

View File

@@ -3,11 +3,18 @@
using CommunityToolkit.WinUI.Notifications;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.UI.Xaml;
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.ViewModel.Guide;
@@ -20,9 +27,9 @@ namespace Snap.Hutao.Core.LifeCycle;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IActivation))]
[Injection(InjectAs.Singleton, typeof(IAppActivation))]
[SuppressMessage("", "CA1001")]
internal sealed partial class Activation : IActivation
internal sealed partial class AppActivation : IAppActivation, IAppActivationActionHandlersAccess, IDisposable
{
public const string Action = nameof(Action);
public const string Uid = nameof(Uid);
@@ -35,13 +42,16 @@ internal sealed partial class Activation : IActivation
private const string UrlActionRefresh = "/REFRESH";
private readonly IServiceProvider serviceProvider;
private readonly ICurrentWindowReference currentWindowReference;
private readonly ICurrentXamlWindowReference currentWindowReference;
private readonly ITaskContext taskContext;
private readonly SemaphoreSlim activateSemaphore = new(1);
/// <inheritdoc/>
public void Activate(HutaoActivationArguments args)
{
// Before activate, we try to redirect to the opened process in App,
// And we check if it's a toast activation.
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
return;
@@ -51,10 +61,64 @@ internal sealed partial class Activation : IActivation
}
/// <inheritdoc/>
public void Initialize()
public void PostInitialization()
{
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget();
ToastNotificationManagerCompat.OnActivated += NotificationActivate;
using (activateSemaphore.Enter())
{
serviceProvider.GetRequiredService<HotKeyOptions>().RegisterAll();
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
{
return;
}
if (serviceProvider.GetRequiredService<AppOptions>().IsNotifyIconEnabled)
{
XamlWindowLifetime.ApplicationLaunchedWithNotifyIcon = true;
serviceProvider.GetRequiredService<App>().DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown;
_ = serviceProvider.GetRequiredService<NotifyIconController>();
}
serviceProvider.GetRequiredService<IScheduleTaskInterop>().UnregisterAllTasks();
serviceProvider.GetRequiredService<IQuartzService>().StartAsync(default).SafeForget();
}
}
public void Dispose()
{
activateSemaphore.Dispose();
}
public async ValueTask HandleLaunchGameActionAsync(string? uid = null)
{
serviceProvider
.GetRequiredService<IMemoryCache>()
.Set(ViewModel.Game.LaunchGameViewModel.DesiredUid, uid);
await taskContext.SwitchToMainThreadAsync();
if (currentWindowReference.Window is null)
{
currentWindowReference.Window = serviceProvider.GetRequiredService<LaunchGameWindow>();
return;
}
if (currentWindowReference.Window is MainWindow)
{
await serviceProvider
.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.LaunchGamePage>(INavigationAwaiter.Default, true)
.ConfigureAwait(false);
return;
}
else
{
// We have a non-Main Window, just exit current process anyway
Process.GetCurrentProcess().Kill();
}
}
private void NotificationActivate(ToastNotificationActivatedEventArgsCompat args)
@@ -94,12 +158,6 @@ internal sealed partial class Activation : IActivation
ArgumentNullException.ThrowIfNull(args.LaunchActivatedArguments);
switch (args.LaunchActivatedArguments)
{
case LaunchGame:
{
await HandleLaunchGameActionAsync().ConfigureAwait(false);
break;
}
default:
{
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
@@ -112,22 +170,21 @@ internal sealed partial class Activation : IActivation
private async ValueTask HandleNormalLaunchActionAsync()
{
// Increase launch times
LocalSetting.Update(SettingKeys.LaunchTimes, 0, x => x + 1);
LocalSetting.Update(SettingKeys.LaunchTimes, 0, x => unchecked(x + 1));
// If it's the first time launch, we show the guide window anyway.
// Otherwise, we check if there's any unfulfilled resource category present.
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
// If the guide is completed, we check if there's any unfulfilled resource category present.
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
{
if (StaticResource.IsAnyUnfulfilledCategoryPresent())
{
UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.StaticResourceBegin);
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
}
}
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language) < GuideState.Completed)
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
{
await taskContext.SwitchToMainThreadAsync();
serviceProvider.GetRequiredService<GuideWindow>();
currentWindowReference.Window = serviceProvider.GetRequiredService<GuideWindow>();
}
else
{
@@ -144,7 +201,7 @@ internal sealed partial class Activation : IActivation
await taskContext.SwitchToMainThreadAsync();
serviceProvider.GetRequiredService<MainWindow>();
currentWindowReference.Window = serviceProvider.GetRequiredService<MainWindow>();
await taskContext.SwitchToBackgroundAsync();
@@ -158,10 +215,7 @@ internal sealed partial class Activation : IActivation
hutaoUserServiceInitialization.InitializeInternalAsync().SafeForget();
}
serviceProvider
.GetRequiredService<IDiscordService>()
.SetNormalActivityAsync()
.SafeForget();
serviceProvider.GetRequiredService<IDiscordService>().SetNormalActivityAsync().SafeForget();
}
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirectTo)
@@ -244,34 +298,4 @@ internal sealed partial class Activation : IActivation
}
}
}
private async ValueTask HandleLaunchGameActionAsync(string? uid = null)
{
serviceProvider
.GetRequiredService<IMemoryCache>()
.Set(ViewModel.Game.LaunchGameViewModel.DesiredUid, uid);
await taskContext.SwitchToMainThreadAsync();
if (currentWindowReference.Window is null)
{
serviceProvider.GetRequiredService<LaunchGameWindow>();
return;
}
if (currentWindowReference.Window is MainWindow)
{
await serviceProvider
.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.LaunchGamePage>(INavigationAwaiter.Default, true)
.ConfigureAwait(false);
return;
}
else
{
// We have a non-Main Window, just exit current process anyway
Process.GetCurrentProcess().Kill();
}
}
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Win32.Foundation;
using WinRT.Interop;
namespace Snap.Hutao.Core.LifeCycle;
internal static class CurrentWindowReferenceExtension
{
public static XamlRoot GetXamlRoot(this ICurrentWindowReference reference)
{
return reference.Window.Content.XamlRoot;
}
public static HWND GetWindowHandle(this ICurrentWindowReference reference)
{
return reference.Window is IWindowOptionsSource optionsSource
? optionsSource.WindowOptions.Hwnd
: WindowNative.GetWindowHandle(reference.Window);
}
}

View File

@@ -5,19 +5,19 @@ using Microsoft.UI.Xaml;
namespace Snap.Hutao.Core.LifeCycle;
[Injection(InjectAs.Singleton, typeof(ICurrentWindowReference))]
internal sealed class CurrentWindowReference : ICurrentWindowReference
[Injection(InjectAs.Singleton, typeof(ICurrentXamlWindowReference))]
internal sealed class CurrentXamlWindowReference : ICurrentXamlWindowReference
{
private readonly WeakReference<Window> reference = new(default!);
[SuppressMessage("", "SH007")]
public Window Window
public Window? Window
{
get
{
reference.TryGetTarget(out Window? window);
return window!;
}
set => reference.SetTarget(value);
set => reference.SetTarget(value!);
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Win32.Foundation;
using WinRT.Interop;
namespace Snap.Hutao.Core.LifeCycle;
internal static class CurrentXamlWindowReferenceExtension
{
public static XamlRoot GetXamlRoot(this ICurrentXamlWindowReference reference)
{
ArgumentNullException.ThrowIfNull(reference.Window);
return reference.Window.Content.XamlRoot;
}
public static HWND GetWindowHandle(this ICurrentXamlWindowReference reference)
{
return WindowExtension.GetWindowHandle(reference.Window);
}
}

View File

@@ -1,14 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle;
/// <summary>
/// 激活
/// </summary>
internal interface IActivation
{
void Activate(HutaoActivationArguments args);
void Initialize();
}

View File

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

View File

@@ -5,10 +5,10 @@ using Microsoft.UI.Xaml;
namespace Snap.Hutao.Core.LifeCycle;
internal interface ICurrentWindowReference
internal interface ICurrentXamlWindowReference
{
/// <summary>
/// Only set in WindowController
/// </summary>
public Window Window { get; set; }
public Window? Window { get; set; }
}

View File

@@ -16,6 +16,6 @@ internal sealed partial class PrivateNamedPipeMessageDispatcher
return;
}
serviceProvider.GetRequiredService<IActivation>().Activate(args);
serviceProvider.GetRequiredService<IAppActivation>().Activate(args);
}
}

View File

@@ -49,7 +49,7 @@ internal sealed partial class PrivateNamedPipeServer : IDisposable
{
byte[] content = new byte[header->ContentLength];
serverStream.ReadAtLeast(content, header->ContentLength, false);
HutaoException.ThrowIf(XxHash64.HashToUInt64(content) != header->Checksum, HutaoExceptionKind.PrivateNamedPipeContentHashIncorrect, "PipePacket Content Hash incorrect");
HutaoException.ThrowIf(XxHash64.HashToUInt64(content) != header->Checksum, "PipePacket Content Hash incorrect");
return content;
}

View File

@@ -10,11 +10,18 @@ namespace Snap.Hutao.Core.Logging;
internal sealed class ConsoleWindowLifeTime : IDisposable
{
public const bool DebugModeEnabled =
#if IS_ALPHA_BUILD
true;
#else
false;
#endif
private readonly bool consoleWindowAllocated;
public ConsoleWindowLifeTime()
{
if (LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, false))
if (LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, DebugModeEnabled))
{
consoleWindowAllocated = AllocConsole();
if (consoleWindowAllocated)

View File

@@ -21,6 +21,11 @@ internal readonly struct LogArgument
return new(argument);
}
public static implicit operator LogArgument(double argument)
{
return new(argument);
}
public static implicit operator LogArgument((object? Argument, ConsoleColor Foreground) tuple)
{
return new(tuple.Argument, tuple.Foreground);

View File

@@ -151,7 +151,7 @@ internal static class LoggerExtension
}
else
{
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.Default);
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.ForegroundWhite);
}
}
}
@@ -164,7 +164,7 @@ internal static class LoggerExtension
// Restore default colors
if (message.ForegroundColor.HasValue || message.BackgroundColor.HasValue)
{
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.Default);
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.ForegroundWhite);
}
return resultMessageBuilder.ToString();

View File

@@ -9,7 +9,11 @@ internal static class LoggerFactoryExtension
{
builder.Services.AddSingleton<ConsoleWindowLifeTime>();
builder.AddSimpleConsole();
builder.AddSimpleConsole(options =>
{
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff ";
});
return builder;
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core;
internal readonly ref struct ReadOnlySpan2D<T>
where T : unmanaged
{
private readonly ref T reference;
private readonly int length;
private readonly int columns;
public unsafe ReadOnlySpan2D(void* pointer, int length, int columns)
{
reference = ref *(T*)pointer;
this.length = length;
this.columns = columns;
}
public ReadOnlySpan<T> this[int row]
{
get => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref reference, row * columns), columns);
}
}

View File

@@ -3,6 +3,7 @@
using Microsoft.Web.WebView2.Core;
using Microsoft.Win32;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.Setting;
using System.IO;
using System.Security.Principal;
@@ -15,15 +16,13 @@ namespace Snap.Hutao.Core;
[Injection(InjectAs.Singleton)]
internal sealed class RuntimeOptions
{
private readonly IServiceProvider serviceProvider;
private readonly Lazy<(Version Version, string UserAgent)> lazyVersionAndUserAgent = new(() =>
private readonly LazySlim<(Version Version, string UserAgent)> lazyVersionAndUserAgent = new(() =>
{
Version version = Package.Current.Id.Version.ToVersion();
return (version, $"Snap Hutao/{version}");
});
private readonly Lazy<string> lazyDataFolder = new(() =>
private readonly LazySlim<string> lazyDataFolder = new(() =>
{
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
@@ -48,14 +47,14 @@ internal sealed class RuntimeOptions
return path;
});
private readonly Lazy<string> lazyDeviceId = new(() =>
private readonly LazySlim<string> lazyDeviceId = new(() =>
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
return Convert.ToMd5HexString($"{userName}{machineGuid}");
return Hash.MD5HexString($"{userName}{machineGuid}");
});
private readonly Lazy<(string Version, bool Supported)> lazyWebViewEnvironment = new(() =>
private readonly LazySlim<(string Version, bool Supported)> lazyWebViewEnvironment = new(() =>
{
try
{
@@ -68,7 +67,7 @@ internal sealed class RuntimeOptions
}
});
private readonly Lazy<bool> lazyElevated = new(() =>
private readonly LazySlim<bool> lazyElevated = new(() =>
{
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
{
@@ -82,18 +81,18 @@ internal sealed class RuntimeOptions
}
});
private readonly Lazy<string> lazyLocalCache = new(() => ApplicationData.Current.LocalCacheFolder.Path);
private readonly Lazy<string> lazyInstalledLocation = new(() => Package.Current.InstalledLocation.Path);
private readonly Lazy<string> lazyFamilyName = new(() => Package.Current.Id.FamilyName);
private readonly LazySlim<string> lazyLocalCache = new(() => ApplicationData.Current.LocalCacheFolder.Path);
private readonly LazySlim<string> lazyInstalledLocation = new(() => Package.Current.InstalledLocation.Path);
private readonly LazySlim<string> lazyFamilyName = new(() => Package.Current.Id.FamilyName);
private bool isToastAvailable;
private bool isToastAvailableInitialized;
private object isToastAvailableLock = new();
public RuntimeOptions(IServiceProvider serviceProvider, ILogger<RuntimeOptions> logger)
private readonly LazySlim<bool> lazyToastAvailable = new(() =>
{
this.serviceProvider = serviceProvider;
ITaskContext taskContext = Ioc.Default.GetRequiredService<ITaskContext>();
return taskContext.InvokeOnMainThread(() => ToastNotificationManager.CreateToastNotifier().Setting is NotificationSetting.Enabled);
});
public RuntimeOptions()
{
AppLaunchTime = DateTimeOffset.UtcNow;
}
@@ -117,19 +116,7 @@ internal sealed class RuntimeOptions
public bool IsElevated { get => lazyElevated.Value; }
public bool IsToastAvailable
{
get
{
return LazyInitializer.EnsureInitialized(ref isToastAvailable, ref isToastAvailableInitialized, ref isToastAvailableLock, GetIsToastAvailable);
bool GetIsToastAvailable()
{
ITaskContext taskContext = serviceProvider.GetRequiredService<ITaskContext>();
return taskContext.InvokeOnMainThread(() => ToastNotificationManager.CreateToastNotifier().Setting is NotificationSetting.Enabled);
}
}
}
public bool IsToastAvailable { get => lazyToastAvailable.Value; }
public DateTimeOffset AppLaunchTime { get; }
}

View File

@@ -16,11 +16,15 @@ internal static class RuntimeOptionsExtension
public static string GetDataFolderServerCacheFolder(this RuntimeOptions options)
{
return Path.Combine(options.DataFolder, "ServerCache");
string directory = Path.Combine(options.DataFolder, "ServerCache");
Directory.CreateDirectory(directory);
return directory;
}
public static string GetDataFolderBackgroundFolder(this RuntimeOptions options)
{
return Path.Combine(options.DataFolder, "Background");
string directory = Path.Combine(options.DataFolder, "Background");
Directory.CreateDirectory(directory);
return directory;
}
}

View File

@@ -12,15 +12,22 @@ internal static class SettingKeys
{
#region MainWindow
public const string WindowRect = "WindowRect";
public const string GuideWindowRect = "GuideWindowRect";
public const string IsNavPaneOpen = "IsNavPaneOpen";
public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked";
#endregion
#region Infrastructure
public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds";
#endregion
#region Application
public const string LaunchTimes = "LaunchTimes";
public const string DataFolderPath = "DataFolderPath";
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
public const string Major1Minor10Revision0GuideState = "Major1Minor10Revision0GuideState1";
public const string StaticResourceImageQuality = "StaticResourceImageQuality";
public const string StaticResourceImageArchive = "StaticResourceImageArchive";
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled2";
#endregion
@@ -60,6 +67,10 @@ internal static class SettingKeys
#endregion
#region Obsolete
[Obsolete("重置新手引导状态")]
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
[Obsolete("重置调试控制台开关")]
public const string IsAllocConsoleDebugModeEnabledLegacy1 = "IsAllocConsoleDebugModeEnabled";
#endregion

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Shell;
/// <summary>
/// 跳转列表交互
/// </summary>
internal interface IJumpListInterop
{
/// <summary>
/// 异步配置跳转列表
/// </summary>
/// <returns>任务</returns>
ValueTask ConfigureAsync();
}

View File

@@ -8,20 +8,9 @@ namespace Snap.Hutao.Core.Shell;
/// </summary>
internal interface IScheduleTaskInterop
{
bool IsDailyNoteRefreshEnabled();
/// <summary>
/// 注册实时便笺刷新任务
/// </summary>
/// <param name="interval">间隔(秒)</param>
/// <returns>是否注册或修改成功</returns>
bool RegisterForDailyNoteRefresh(int interval);
/// <summary>
/// 卸载全部注册的任务
/// </summary>
/// <returns>是否卸载成功</returns>
bool UnregisterAllTasks();
bool UnregisterForDailyNoteRefresh();
}

View File

@@ -1,36 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.LifeCycle;
using Windows.UI.StartScreen;
namespace Snap.Hutao.Core.Shell;
/// <summary>
/// 跳转列表交互
/// </summary>
[HighQuality]
[Injection(InjectAs.Transient, typeof(IJumpListInterop))]
internal sealed class JumpListInterop : IJumpListInterop
{
/// <summary>
/// 异步配置跳转列表
/// </summary>
/// <returns>任务</returns>
public async ValueTask ConfigureAsync()
{
if (JumpList.IsSupported())
{
JumpList list = await JumpList.LoadCurrentAsync();
list.Items.Clear();
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, SH.CoreJumpListHelperLaunchGameItemDisplayName);
launchGameItem.Logo = "ms-appx:///Resource/Navigation/LaunchGame.png".ToUri();
list.Items.Add(launchGameItem);
await list.SaveAsync();
}
}
}

View File

@@ -16,60 +16,6 @@ namespace Snap.Hutao.Core.Shell;
internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
{
private const string DailyNoteRefreshTaskName = "SnapHutaoDailyNoteRefreshTask";
private const string DailyNoteRefreshScriptName = "DailyNoteRefresh";
/// <summary>
/// 注册实时便笺刷新任务
/// </summary>
/// <param name="interval">间隔(秒)</param>
/// <returns>是否注册或修改成功</returns>
public bool RegisterForDailyNoteRefresh(int interval)
{
try
{
TaskDefinition task = TaskService.Instance.NewTask();
task.RegistrationInfo.Description = SH.CoreScheduleTaskHelperDailyNoteRefreshTaskDescription;
task.Triggers.Add(new TimeTrigger() { Repetition = new(TimeSpan.FromSeconds(interval), TimeSpan.Zero), });
string scriptPath = EnsureWScriptCreated(DailyNoteRefreshScriptName, "hutao://DailyNote/Refresh");
task.Actions.Add("wscript", $@"/b ""{scriptPath}""");
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);
return true;
}
catch (Exception)
{
if (WScriptExists(DailyNoteRefreshScriptName, out string fullPath))
{
File.Delete(fullPath);
}
return false;
}
}
public bool UnregisterForDailyNoteRefresh()
{
try
{
TaskService.Instance.RootFolder.DeleteTask(DailyNoteRefreshTaskName, false);
if (WScriptExists(DailyNoteRefreshScriptName, out string fullPath))
{
File.Delete(fullPath);
}
return true;
}
catch (Exception)
{
return false;
}
}
public bool IsDailyNoteRefreshEnabled()
{
return TaskService.Instance.RootFolder.Tasks.Any(task => task.Name is DailyNoteRefreshTaskName);
}
/// <summary>
/// 卸载全部注册的任务
@@ -91,25 +37,4 @@ internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
return false;
}
}
private static string EnsureWScriptCreated(string name, string url, bool forceCreate = false)
{
if (WScriptExists(name, out string fullName) && !forceCreate)
{
return fullName;
}
string script = $"""CreateObject("WScript.Shell").Run "cmd /c start {url}", 0, False""";
File.WriteAllText(fullName, script);
return fullName;
}
private static bool WScriptExists(string name, out string fullName)
{
string tempFolder = ApplicationData.Current.TemporaryFolder.Path;
fullName = Path.Combine(tempFolder, "Script", $"{name}.vbs");
Directory.CreateDirectory(Path.Combine(tempFolder, "Script"));
return File.Exists(fullName);
}
}

View File

@@ -21,22 +21,27 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
{
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
string elevatedLauncherPath = Path.Combine(runtimeOptions.DataFolder, "Snap.Hutao.Elevated.Launcher.exe");
try
{
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri);
await iconFile.OverwriteCopyAsync(targetLogoPath).ConfigureAwait(false);
Uri elevatedLauncherUri = "ms-appx:///Snap.Hutao.Elevated.Launcher.exe".ToUri();
StorageFile launcherFile = await StorageFile.GetFileFromApplicationUriAsync(elevatedLauncherUri);
await launcherFile.OverwriteCopyAsync(elevatedLauncherPath).ConfigureAwait(false);
}
catch
{
return false;
}
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath);
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath, elevatedLauncherPath);
}
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath)
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath, string elevatedLauncherPath)
{
bool result = false;
@@ -44,18 +49,12 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
HRESULT hr = CoCreateInstance(in ShellLink.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IShellLinkW.IID, out IShellLinkW* pShellLink);
if (SUCCEEDED(hr))
{
pShellLink->SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
pShellLink->SetPath(elevatedLauncherPath);
pShellLink->SetArguments(runtimeOptions.FamilyName);
pShellLink->SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
pShellLink->SetIconLocation(targetLogoPath, 0);
if (SUCCEEDED(pShellLink->QueryInterface(in IShellLinkDataList.IID, out IShellLinkDataList* pShellLinkDataList)))
{
pShellLinkDataList->GetFlags(out uint flags);
pShellLinkDataList->SetFlags(flags | (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER);
pShellLinkDataList->Release();
}
if (SUCCEEDED(pShellLink->QueryInterface(in IPersistFile.IID, out IPersistFile* pPersistFile)))
if (SUCCEEDED(IUnknownMarshal.QueryInterface(pShellLink, in IPersistFile.IID, out IPersistFile* pPersistFile)))
{
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");
@@ -65,10 +64,10 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
result = true;
}
pPersistFile->Release();
IUnknownMarshal.Release(pPersistFile);
}
pShellLink->Release();
uint value = IUnknownMarshal.Release(pShellLink);
}
return result;

View File

@@ -15,7 +15,7 @@ internal static class SemaphoreSlimExtension
}
catch (ObjectDisposedException ex)
{
ThrowHelper.OperationCanceled(SH.CoreThreadingSemaphoreSlimDisposed, ex);
HutaoException.OperationCanceled(SH.CoreThreadingSemaphoreSlimDisposed, ex);
}
return new SemaphoreSlimToken(semaphoreSlim);
@@ -29,7 +29,7 @@ internal static class SemaphoreSlimExtension
}
catch (ObjectDisposedException ex)
{
ThrowHelper.OperationCanceled(SH.CoreThreadingSemaphoreSlimDisposed, ex);
HutaoException.OperationCanceled(SH.CoreThreadingSemaphoreSlimDisposed, ex);
}
return new SemaphoreSlimToken(semaphoreSlim);

View File

@@ -1,9 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Threading;
using System.Diagnostics;
internal delegate bool SpinWaitPredicate<T>(ref readonly T state);
namespace Snap.Hutao.Core.Threading;
internal static class SpinWaitPolyfill
{
@@ -15,4 +15,23 @@ internal static class SpinWaitPolyfill
spinner.SpinOnce();
}
}
[SuppressMessage("", "SH002")]
public static unsafe bool SpinUntil<T>(ref T state, delegate*<ref readonly T, bool> condition, TimeSpan timeout)
{
long startTime = Stopwatch.GetTimestamp();
SpinWait spinner = default;
while (!condition(ref state))
{
spinner.SpinOnce();
if (timeout < Stopwatch.GetElapsedTime(startTime))
{
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Core.Windowing.Abstraction;
internal interface IXamlWindowContentAsFrameworkElement
{
FrameworkElement ContentAccess { get; }
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Core.Windowing.Abstraction;
internal interface IXamlWindowExtendContentIntoTitleBar
{
FrameworkElement TitleBarAccess { get; }
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Graphics;
namespace Snap.Hutao.Core.Windowing.Abstraction;
internal interface IXamlWindowHasInitSize
{
SizeInt32 InitSize { get; }
}

View File

@@ -1,15 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Windowing;
namespace Snap.Hutao.Core.Windowing.Abstraction;
/// <summary>
/// 为扩展窗体提供必要的选项
/// </summary>
internal interface IWindowOptionsSource
internal interface IXamlWindowOptionsSource
{
/// <summary>
/// 窗体选项
/// </summary>
WindowOptions WindowOptions { get; }
XamlWindowOptions WindowOptions { get; }
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Windowing.Abstraction;
internal interface IXamlWindowRectPersisted : IXamlWindowHasInitSize
{
string PersistRectKey { get; }
}

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