Compare commits

..

445 Commits

Author SHA1 Message Date
qhy040404
702a3d0e62 refactor launchgame resources 2024-06-17 15:01:36 +08:00
qhy040404
176535ca98 minor change 2024-06-17 13:14:03 +08:00
qhy040404
36eab42692 Add HoyoPlay API 2024-06-17 12:55:17 +08:00
Lightczx
75ea2b807f Update AsyncBarrier.cs 2024-06-17 09:57:21 +08:00
DismissedLight
e8eed46d82 Merge pull request #1725 from DGP-Studio/feat/gamerole_profilepicture 2024-06-16 22:46:47 +08:00
DismissedLight
ff9b553a19 code style 2024-06-16 22:46:24 +08:00
qhy040404
95d64c2895 make UserGameRole observable 2024-06-16 20:41:19 +08:00
qhy040404
558551c8ad rename 2024-06-16 19:14:33 +08:00
qhy040404
d05c196b7c apply changes 2024-06-16 19:04:51 +08:00
DismissedLight
502fb6dbed fix notification activation 2024-06-16 17:27:20 +08:00
qhy040404
4fa5270070 fix failed notification activate 2024-06-16 01:43:43 +08:00
DismissedLight
94fda223fc drop notification 2024-06-16 01:25:52 +08:00
Mikachu2333
18103b4deb modify alpha's tip (#1730)
Co-authored-by: LinkChou <linkchou@yandex.com>
2024-06-16 01:21:36 +08:00
qhy040404
16ac52e71d use list instead of mappppps 2024-06-16 01:19:56 +08:00
qhy040404
73825d391e fix build 2024-06-16 00:47:02 +08:00
qhy040404
3b2eeb84a7 add profile picture for each game role 2024-06-16 00:46:55 +08:00
DismissedLight
3e8655fd55 Merge pull request #1722 from DGP-Studio/feat/window
make windows transient
2024-06-15 16:04:58 +08:00
DismissedLight
fe38e14ae8 code style 2024-06-15 15:54:04 +08:00
DismissedLight
a174493819 Merge branch 'feat/window' of https://github.com/DGP-Studio/Snap.Hutao into feat/window 2024-06-15 14:32:03 +08:00
qhy040404
3a57d55c62 make windows transient 2024-06-15 14:31:29 +08:00
DismissedLight
99f35ca6db avatar property grid view rework 2024-06-15 00:43:59 +08:00
DismissedLight
c423e8b72d ProfilePicture add unlock type 2024-06-14 23:18:11 +08:00
DismissedLight
7ff78def46 fix hotkey can't register 2024-06-14 11:05:56 +08:00
qhy040404
bc9018f4bf make windows transient 2024-06-13 19:48:06 +08:00
qhy040404
107963b7ac Update issue template 2024-06-13 18:39:03 +08:00
DismissedLight
4e89406f2f Merge pull request #1721 from DGP-Studio/feat/1715 2024-06-13 16:15:21 +08:00
Lightczx
8119de3fa9 code style 2024-06-13 16:15:08 +08:00
qhy040404
7a8c233b10 review requests 2024-06-13 15:36:50 +08:00
qhy040404
cc71aa9c82 impl #1715 2024-06-13 12:51:22 +08:00
DismissedLight
4276481284 Add CachedImage Debug Layer 2024-06-11 21:05:24 +08:00
Lightczx
6f3159ae0c [skip ci] QA announcement name 2024-06-11 17:01:14 +08:00
Lightczx
c1b3412ba1 fix QA ComboBox width issue 2024-06-11 16:56:33 +08:00
Lightczx
99b3613319 fix #1688 2024-06-11 15:42:23 +08:00
Lightczx
069407abbc use weapon sort 2024-06-11 15:06:15 +08:00
DismissedLight
98c8df5c8e Merge pull request #1712 from DGP-Studio/feat/v3_cultivation 2024-06-11 14:04:49 +08:00
Lightczx
7cfcc17763 refactor 2024-06-11 14:00:48 +08:00
qhy040404
23741c4e48 exclude unavailable avatars 2024-06-11 13:12:37 +08:00
qhy040404
5f4b68d538 add cache to minimal deltas 2024-06-11 12:55:54 +08:00
Lightczx
9ef0d8c57d add SCIP solver 2024-06-11 12:31:51 +08:00
qhy040404
f0bfea51cf move to inventory service 2024-06-11 00:06:01 +08:00
DismissedLight
905454eb02 refactor 2024-06-10 23:31:38 +08:00
DismissedLight
05c3a575bc adjust db service parameter 2024-06-10 23:03:23 +08:00
DismissedLight
3e26e247cd refactor metadata abstraction 2024-06-10 22:43:50 +08:00
qhy040404
293b1e214d migrate all v2 api to v3 api 2024-06-10 22:37:56 +08:00
qhy040404
063665e77e refresh inventory 2024-06-10 22:37:55 +08:00
DismissedLight
50389ac06c Merge pull request #1713 from DGP-Studio/fix/dailynote 2024-06-10 22:12:57 +08:00
qhy040404
b99b34945e fix #1711 2024-06-10 11:05:58 +08:00
qhy040404
94a96c76bc fix #1710 2024-06-10 10:57:29 +08:00
DismissedLight
5cf3046257 Merge pull request #1694 from Mikachu2333/develop 2024-06-06 15:16:22 +08:00
Lightczx
89f8dedb57 fix url protocol launch lock 2024-06-06 13:11:24 +08:00
LinkChou
3c1e9237aa replace Uid to UID 2024-06-06 11:54:11 +08:00
LinkChou
e7cb01b302 Merge branch 'develop' of https://github.com/Mikachu2333/Snap.Hutao into develop 2024-06-06 11:48:03 +08:00
LinkChou
4cd971e166 Add some 2024-06-06 11:47:37 +08:00
Mikachu2333
7a9657f0cb Merge branch 'DGP-Studio:develop' into develop 2024-06-06 11:44:31 +08:00
Lightczx
82e6b62231 correctly free library 2024-06-06 09:29:50 +08:00
LinkChou
374c4d796d reformat 2024-06-06 07:25:11 +08:00
Mikachu2333
6e149a5be3 Update SH.resx 2024-06-06 01:44:32 +08:00
Mikachu2333
00ad0ef346 correct format 2024-06-06 01:39:10 +08:00
Mikachu2333
f22f165592 Merge branch 'develop' into develop 2024-06-06 01:33:09 +08:00
DismissedLight
5d8a39fe43 bump version 2024-06-05 21:29:28 +08:00
DismissedLight
521534be05 Merge pull request #1667 from DGP-Studio/l10n_develop 2024-06-05 21:22:17 +08:00
DismissedLight
b1364db3ac Merge pull request #1697 from DGP-Studio/opt/launch_game_activation 2024-06-05 20:35:18 +08:00
qhy040404
031cf77c27 refine LaunchGameAction 2024-06-05 19:09:57 +08:00
LinkChou
49c75dde2a Chinese text improve 2024-06-05 18:43:45 +08:00
Lightczx
3200c5e60b fix NTHeader offset 2024-06-05 17:22:45 +08:00
Lightczx
b392a6f8e5 Align HMODULE ptr 2024-06-05 17:17:52 +08:00
Lightczx
3e8e109123 use image header to fetch image size 2024-06-05 16:51:59 +08:00
Lightczx
91c886befb code style 2024-06-05 16:15:46 +08:00
Lightczx
32bdfe12af Fix Unlock Fps Attempt 2 2024-06-05 16:05:51 +08:00
Lightczx
eac67b6f44 Fix Unlock Fps Attempt 1 2024-06-05 15:28:50 +08:00
Lightczx
0dcba220c5 fix Launch Game ViewModel scope 2024-06-05 13:42:31 +08:00
Masterain
a204eaa95c New translations sh.resx (Vietnamese) 2024-06-04 18:31:38 -07:00
Masterain
35491c4eb1 New translations sh.resx (French) 2024-06-04 18:31:36 -07:00
Masterain
706401350c New translations sh.resx (Indonesian) 2024-06-04 18:31:36 -07:00
Masterain
c8ba04ee11 New translations sh.resx (English) 2024-06-04 18:31:34 -07:00
Masterain
b080a553c3 New translations sh.resx (Chinese Traditional) 2024-06-04 18:31:33 -07:00
Masterain
baf5612333 New translations sh.resx (Russian) 2024-06-04 18:31:32 -07:00
Masterain
eacd697cfe New translations sh.resx (Portuguese) 2024-06-04 18:31:31 -07:00
Masterain
11dc8e60bb New translations sh.resx (Korean) 2024-06-04 18:31:29 -07:00
Masterain
bba62996a0 New translations sh.resx (Japanese) 2024-06-04 18:31:28 -07:00
DismissedLight
db15b6a30c Merge pull request #1673 from DGP-Studio/ref/disable_web_login 2024-06-05 09:26:14 +08:00
Lightczx
1b0356b5ef fix #1669 2024-06-05 09:24:29 +08:00
qhy040404
6e498f5ede disable web login temporarily
maybe temporarily...
2024-06-04 22:30:32 +08:00
qhy040404
3117aefd54 try to fix permission 2024-06-04 21:00:27 +08:00
qhy040404
34ea240272 inline sign 2024-06-04 20:51:03 +08:00
DismissedLight
6b23ae5332 fix viewmode locker scope 2024-06-04 20:40:52 +08:00
qhy040404
c197d8a35a fix unresolved makeappx.exe 2024-06-04 20:22:47 +08:00
Lightczx
b0fa05283a code style 2024-06-04 17:19:45 +08:00
Masterain
c85a74dfc3 New translations sh.resx (Chinese Traditional) 2024-06-03 07:31:29 -07:00
DismissedLight
f7e53399b4 4.7 compatible 2024-06-03 21:49:06 +08:00
DismissedLight
52ac588a3a Merge pull request #1670 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-7c1ae0f6e4 2024-06-03 16:21:21 +08:00
Lightczx
cd6c1f6b59 batch compute api endpoint 2024-06-03 16:18:12 +08:00
dependabot[bot]
7c734ce4aa Bump the packages group in /src/Snap.Hutao with 2 updates
Bumps the packages group in /src/Snap.Hutao with 2 updates: [MSTest.TestAdapter](https://github.com/microsoft/testfx) and [MSTest.TestFramework](https://github.com/microsoft/testfx).


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

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

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

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


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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 07:57:43 +00:00
qhy040404
cd91af8ae9 use ref readonly 2024-05-27 09:29:58 +08:00
qhy040404
8680960931 clear jumplist 2024-05-26 22:10:15 +08:00
qhy040404
67f6fda900 another impl of hint when icon promoted for win10 2024-05-26 21:53:22 +08:00
DismissedLight
db4b0d3dcb Merge pull request #1641 from DGP-Studio/develop 2024-05-26 18:16:20 +08:00
DismissedLight
ec0abb4b16 bump version 2024-05-26 18:15:30 +08:00
DismissedLight
8cdf2b01f2 minor fix 2024-05-26 15:06:55 +08:00
qhy040404
c1bf3dad52 fix daily note setting 2024-05-26 11:16:46 +08:00
Lightczx
492e867391 exception refine 2024-05-24 17:16:33 +08:00
Lightczx
9cbb36f3e6 disable runtime marshaling 2024-05-24 16:54:05 +08:00
Lightczx
e15c06a194 fix build 2024-05-24 14:58:36 +08:00
Lightczx
f71a2a3d40 code style 2024-05-24 14:27:26 +08:00
DismissedLight
fb89e5f9f0 code style 2024-05-24 00:32:44 +08:00
DismissedLight
3f97f4207c Merge pull request #1636 from DGP-Studio/fix/1632
fix #1632
2024-05-23 20:14:47 +08:00
DismissedLight
a4ea798ad0 Merge pull request #1637 from DGP-Studio/feat/dailynote_prompt 2024-05-23 20:14:11 +08:00
qhy040404
80a0daf02d refine ui 2024-05-23 19:36:48 +08:00
qhy040404
c511d27b2f disable settings when not elevated 2024-05-23 19:36:48 +08:00
qhy040404
c5dcbc9c79 fix #1632 2024-05-23 19:36:48 +08:00
qhy040404
91949214ba apply suggestion 2024-05-23 19:21:29 +08:00
qhy040404
a126a330ff prompt user dailynote auto-refresh is not effective while notify icon is disabled 2024-05-23 19:21:29 +08:00
Lightczx
055846dfd6 fix notify icon context menu theme 2024-05-23 15:00:20 +08:00
DismissedLight
b68462b56e Update README.md 2024-05-23 11:46:48 +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
Masterain
8252e43bac Update alpha.yml
#1623
2024-05-15 21:18:35 -07: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
qhy040404
ce58e35a8f Merge pull request #1617 from DGP-Studio/typo/ci 2024-05-12 16:54:24 +08:00
目棃
a10e9e40a2 ✏️ 修正CI内容 2024-05-12 16:46:32 +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
DismissedLight
1633f2c668 Merge pull request #1511 from DGP-Studio/readme2024 2024-03-29 15:48:37 +08:00
DismissedLight
241122eadc Merge pull request #1517 from DGP-Studio/develop 2024-03-29 15:47:39 +08:00
DismissedLight
4e27d40b76 Merge pull request #1509 from DGP-Studio/l10n_develop 2024-03-29 15:29:48 +08:00
Masterain
4f42a299d1 New translations sh.resx (English) 2024-03-29 00:28:14 -07:00
Masterain
80aad5f09e New translations sh.resx (Indonesian) 2024-03-29 00:24:57 -07:00
Masterain
35370c392b New translations sh.resx (English) 2024-03-29 00:24:56 -07:00
Masterain
115dae8966 New translations sh.resx (Chinese Traditional) 2024-03-29 00:24:55 -07:00
Masterain
795e9730b9 New translations sh.resx (Russian) 2024-03-29 00:24:54 -07:00
Masterain
7dac0d6f73 New translations sh.resx (Portuguese) 2024-03-29 00:24:53 -07:00
Masterain
7c42b7e8ed New translations sh.resx (Korean) 2024-03-29 00:24:52 -07:00
Masterain
3c766f55b5 New translations sh.resx (Japanese) 2024-03-29 00:24:51 -07:00
Lightczx
38a07caeab update version 2024-03-29 15:22:49 +08:00
Lightczx
5fb55ea645 update desc 2024-03-29 15:19:43 +08:00
Masterain
aba568d8c6 New translations sh.resx (Indonesian) 2024-03-28 23:27:17 -07:00
Masterain
116bd58a81 New translations sh.resx (English) 2024-03-28 23:27:16 -07:00
Masterain
46cc5f7f1e New translations sh.resx (Chinese Traditional) 2024-03-28 23:27:15 -07:00
Masterain
3e5ca2440b New translations sh.resx (Russian) 2024-03-28 23:27:14 -07:00
Masterain
081a60f3f8 New translations sh.resx (Portuguese) 2024-03-28 23:27:13 -07:00
Masterain
4a5ddd872d New translations sh.resx (Korean) 2024-03-28 23:27:12 -07:00
Masterain
81e5159615 New translations sh.resx (Japanese) 2024-03-28 23:27:11 -07:00
Lightczx
d5eec58157 add hint for mihoyo passport login 2024-03-29 14:16:37 +08:00
Lightczx
0242d3d0ca fix #1504 2024-03-28 15:31:05 +08:00
Lightczx
3fcb4caaf1 wiki avatar title no wrap 2024-03-28 14:17:28 +08:00
DismissedLight
ae2ddee6c4 Merge pull request #1510 from GoddessLuBoYan/issue1427 2024-03-28 13:32:58 +08:00
Lightczx
089938c1c0 code style 2024-03-28 13:16:40 +08:00
Lightczx
c515f70e95 code style 2024-03-28 11:19:56 +08:00
Lightczx
317b68b1ce improve image cache 2024-03-28 11:18:09 +08:00
DismissedLight
7b5a6ccae0 Merge pull request #1513 from DGP-Studio/feat/cultivation_stat_finished 2024-03-28 11:00:33 +08:00
qhy040404
13108b6694 show finished icon if we have enough items 2024-03-27 16:23:30 +08:00
goddessluboyan
c4ff359995 修复多语言问题 2024-03-26 13:37:27 +08:00
goddessluboyan
70399f7a7d 删除调试用的信息 2024-03-26 13:37:27 +08:00
goddessluboyan
3d1a365079 实现对材料统计页面中的材料做初步排序。
更细致的排序、分类功能,需要集成观测枢功能才能进行。
2024-03-26 13:37:27 +08:00
Masterain
28b09d56f3 New translations sh.resx (Indonesian) 2024-03-25 07:59:18 -07:00
Masterain
995860463f New translations sh.resx (English) 2024-03-25 07:59:17 -07:00
Masterain
474d06457c New translations sh.resx (Chinese Traditional) 2024-03-25 07:59:16 -07:00
Masterain
7534729794 New translations sh.resx (Russian) 2024-03-25 07:59:15 -07:00
Masterain
167d66aa67 New translations sh.resx (Portuguese) 2024-03-25 07:59:14 -07:00
Masterain
e20a9f3ff4 New translations sh.resx (Korean) 2024-03-25 07:59:12 -07:00
Masterain
868232d98f New translations sh.resx (Japanese) 2024-03-25 07:59:11 -07:00
DismissedLight
ab1b3c02c8 Merge pull request #1500 from DGP-Studio/feat/suggestbox_not_found 2024-03-25 22:43:47 +08:00
DismissedLight
8ef5dc9302 code style 2024-03-25 22:41:58 +08:00
Masterain
4c7e02cd5c New translations sh.resx (Chinese Traditional) 2024-03-24 11:14:18 -07:00
Masterain
d8e0d74be6 New translations sh.resx (Chinese Traditional) 2024-03-24 10:09:39 -07:00
Masterain
655f97353c New translations sh.resx (Chinese Traditional) 2024-03-24 09:12:43 -07:00
qhy040404
a387e0dbf5 fix achievement search reset 2024-03-23 12:59:38 +08:00
qhy040404
8111c1e662 Disable unknown token 2024-03-23 12:58:49 +08:00
qhy040404
9cd9193425 show not found in view 2024-03-23 12:58:49 +08:00
qhy040404
d531c81fa2 show not found in search box 2024-03-23 12:58:49 +08:00
DismissedLight
1d0a72493e achievement city typedef 2024-03-21 22:27:16 +08:00
Lightczx
f2b361819b fix userview 2024-03-20 10:42:14 +08:00
qhy040404
41f7245a1a fix #1503 2024-03-19 22:46:58 +08:00
DismissedLight
889e914f7f colorized log message 2024-03-19 22:23:47 +08:00
Lightczx
9f90ec221c colorized 2024-03-19 17:30:23 +08:00
DismissedLight
8fc874fd09 Merge pull request #1498 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-f9ad2d8712 2024-03-19 15:13:47 +08:00
Lightczx
f42ec1ea12 change banner color 2024-03-18 17:25:45 +08:00
Lightczx
5cc3cf264c refactor 2024-03-18 17:15:42 +08:00
Lightczx
e38517a2ad optimize uniformpanel 2024-03-18 16:54:17 +08:00
dependabot[bot]
cdc0fb8a82 Bump the packages group in /src/Snap.Hutao with 1 update
Bumps the packages group in /src/Snap.Hutao with 1 update: [coverlet.collector](https://github.com/coverlet-coverage/coverlet).


Updates `coverlet.collector` from 6.0.1 to 6.0.2
- [Release notes](https://github.com/coverlet-coverage/coverlet/releases)
- [Commits](https://github.com/coverlet-coverage/coverlet/compare/v6.0.1...v6.0.2)

---
updated-dependencies:
- dependency-name: coverlet.collector
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: packages
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 07:31:21 +00:00
ema
d50a6df14c upgrade SH.ja.resx translation (#1491) 2024-03-16 17:06:34 +08:00
DismissedLight
4886904530 refactor controls 2024-03-16 16:59:19 +08:00
DismissedLight
ac34376c13 Merge pull request #1484 from DGP-Studio/feat/spinwait_game_window 2024-03-16 16:57:17 +08:00
qhy040404
3bcee3d149 Apply suggestion 2024-03-16 16:05:18 +08:00
qhy040404
3d3b03851e Use SpinWait to wait game window instead of WaitForInputIdle 2024-03-16 15:59:38 +08:00
DismissedLight
dc64302424 Merge pull request #1483 from DGP-Studio/fix/theme 2024-03-16 14:57:54 +08:00
qhy040404
a2586b0ef2 fix different theme between window and dialog 2024-03-15 20:32:16 +08:00
DismissedLight
eee84a338e significantly reduce uniformstaggeredlayout resize lag 2024-03-15 20:24:32 +08:00
Lightczx
be30362b52 uniformstaggeredlayout optimization 2024-03-15 17:18:25 +08:00
Lightczx
38f36bbb82 fix gachalog page closing crash 2024-03-15 14:21:38 +08:00
Lightczx
704866b16a update dependency 2024-03-15 13:43:55 +08:00
Lightczx
ca9783bc1b corner radius on auto suggest tokenbox 2024-03-15 10:54:06 +08:00
DismissedLight
6e8e151fff fix unlocking fps 2024-03-14 20:32:06 +08:00
DismissedLight
b98dc9f5d3 fix unlocking fps 2024-03-14 19:58:01 +08:00
Masterain
206100d8ef New Crowdin updates (#1477) 2024-03-14 14:41:52 +08:00
Lightczx
1a74c7ca96 fix #1476 2024-03-14 11:24:55 +08:00
DismissedLight
88528fa28d hide zero count section 2024-03-13 21:18:11 +08:00
DismissedLight
263cea9225 Merge pull request #1475 from DGP-Studio/fix/token_ui 2024-03-13 21:09:13 +08:00
Lightczx
2879bd653a add chronicled wish cloud statistics 2024-03-13 17:25:42 +08:00
qhy040404
3d061e3bdb adjust ui 2024-03-13 16:52:46 +08:00
qhy040404
022527829e refine wiki token ui 2 2024-03-13 16:39:16 +08:00
qhy040404
0fcf10dfa7 refine wiki token ui 2024-03-13 16:16:44 +08:00
Lightczx
7fc6ecc3c3 fix gachalog overview scrollbar margin 2024-03-13 15:17:40 +08:00
927 changed files with 28257 additions and 8706 deletions

View File

@@ -19,7 +19,7 @@ body:
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
required: true
- label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
- label: 我知道[文档站](https://hut.ao/zh/menu.html)的导航栏中有**搜索功能**,且已经搜索过相关关键词
required: true
- label: 我的问题不是[已完成](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90)的问题也不是一个别人已发布的**重复的**问题
@@ -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

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

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:

View File

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

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

View File

@@ -10,7 +10,7 @@ jobs:
- uses: actions/stale@v9
with:
any-of-labels: 'needs-more-info,需要更多信息'
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 3 days.'
stale-issue-message: 'This issue is stale because it has been open 7 days with no activity. Remove stale label or comment or this will be closed in 3 days.'
days-before-stale: 7
days-before-close: 3
close-issue-reason: not_planned

View File

@@ -17,7 +17,15 @@ 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)
[![fr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27fr%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)
[![vi translation](https://img.shields.io/badge/dynamic/json?color=blue&label=vi&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27vi%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 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。
@@ -46,13 +54,13 @@ Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translatio
* [CommunityToolkit/dotnet](https://github.com/CommunityToolkit/dotnet)
* [CommunityToolkit/Labs-Windows](https://github.com/CommunityToolkit/Labs-Windows)
* [CommunityToolkit/Windows](https://github.com/CommunityToolkit/Windows)
* [dahall/taskscheduler](https://github.com/dahall/taskscheduler)
* [dotnet/efcore](https://github.com/dotnet/efcore)
* [dotnet/runtime](https://github.com/dotnet/runtime)
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
* [quartznet/quartznet](https://github.com/quartznet/quartznet)
### 支撑项目 / Supporter Project
@@ -64,9 +72,9 @@ Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translatio
Snap Hutao is currently using sponsored software from the following service providers.
| [![](https://www.netlify.com/v3/img/components/netlify-light.svg)](https://www.netlify.com/) | [![](https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg)](https://crowdin.com/) | [![](https://gitlab.cn/images/icons/logos/logo-121-75.svg)](https://gitlab.cn/) |
|:----------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|
| [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/73ae8b90-f3c7-4033-b2b7-f4126331ce66)](https://about.signpath.io) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/49aed8ee-9f19-4a8a-998c-7b93ee286d65)](https://1password.com/) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/ad121220-d2d3-4f49-b215-b6d063dc229d)](https://about.signpath.io) |
|:-:|:-:|:-:|
|[![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/73ae8b90-f3c7-4033-b2b7-f4126331ce66)](https://about.signpath.io)|[![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/49aed8ee-9f19-4a8a-998c-7b93ee286d65)](https://1password.com/)|[![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/ad121220-d2d3-4f49-b215-b6d063dc229d)](https://www.digitalocean.com)|
|[![jetbrains](https://github.com/DGP-Studio/Snap.Hutao/assets/36357191/4105772a-728a-4a84-9c6e-d713a5698a20)](https://www.jetbrains.com/opensource/)|||
- Netlify provides document and home page hosting service for Snap Hutao
@@ -80,6 +88,8 @@ Snap Hutao is currently using sponsored software from the following service prov
- DigitalOcean provides reliable cloud database for Snap Hutao database backup
- Jetbrains provides powerful IDE for Snap Hutao infrastructure services coding
## 开发 / Development
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)

View File

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

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

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

View File

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

View File

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

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

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -46,7 +47,14 @@ public sealed class UnsafeRuntimeBehaviorTest
Assert.AreEqual(1212, testStruct.Value4);
}
[TestMethod]
public unsafe void UnsafeUtf8StringReference()
{
void* ptr = Unsafe.AsPointer(ref MemoryMarshal.GetReference("test"u8));
GC.Collect(GC.MaxGeneration);
ReadOnlySpan<byte> bytes = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr);
Console.WriteLine(System.Text.Encoding.UTF8.GetString(bytes));
}
private readonly struct TestStruct
{

View File

@@ -12,10 +12,11 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" 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="coverlet.collector" Version="6.0.1">
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.4.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.4.3" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

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"/>
@@ -29,6 +30,7 @@
<ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///View/Card/Primitive/CardProgressBar.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style

View File

@@ -3,11 +3,13 @@
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Microsoft.Windows.AppNotifications;
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Shell;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Windowing;
using System.Diagnostics;
namespace Snap.Hutao;
@@ -21,24 +23,24 @@ namespace Snap.Hutao;
[SuppressMessage("", "SH001")]
public sealed partial class App : Application
{
private const string ConsoleBanner = """
private const string ConsoleBanner = $"""
----------------------------------------------------------------
_____ _ _ _
/ ____| | | | | | |
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
_____ _ _ _
/ ____| | | | | | |
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
____) || | | || (_| || |_) |_ | | | || |_| || |_| (_| || (_) |
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
| |
|_|
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
| |
|_|
Snap.Hutao is a open source software developed by DGP Studio.
Copyright (C) 2022 - 2024 DGP Studio, All Rights Reserved.
----------------------------------------------------------------
""";
private readonly IServiceProvider serviceProvider;
private readonly IActivation activation;
private readonly IAppActivation activation;
private readonly ILogger<App> logger;
/// <summary>
@@ -49,38 +51,47 @@ 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()
{
XamlLifetime.ApplicationExiting = true;
base.Exit();
}
/// <inheritdoc/>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
try
{
// Important: You must call AppNotificationManager::Default().Register
// before calling AppInstance.GetCurrent.GetActivatedEventArgs.
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
AppNotificationManager.Default.Register();
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
{
logger.LogDebug("Application exiting on RedirectActivationTo");
Exit();
return;
}
logger.LogInformation(ConsoleBanner);
logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow));
LogDiagnosticInformation();
// manually invoke
// Manually invoke
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
activation.Initialize();
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
activation.PostInitialization();
}
catch
catch (Exception ex)
{
// AppInstance.GetCurrent() calls failed
logger.LogError(ex, "Application failed in App.OnLaunched");
Process.GetCurrentProcess().Kill();
}
}
@@ -89,8 +100,8 @@ public sealed partial class App : Application
{
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
logger.LogInformation("FamilyName: {name}", runtimeOptions.FamilyName);
logger.LogInformation("Version: {version}", runtimeOptions.Version);
logger.LogInformation("LocalCache: {folder}", runtimeOptions.LocalCache);
logger.LogColorizedInformation(("FamilyName: {Name}", ConsoleColor.Blue), (runtimeOptions.FamilyName, ConsoleColor.Cyan));
logger.LogColorizedInformation(("Version: {Version}", ConsoleColor.Blue), (runtimeOptions.Version, ConsoleColor.Cyan));
logger.LogColorizedInformation(("LocalCache: {Path}", ConsoleColor.Blue), (runtimeOptions.LocalCache, ConsoleColor.Cyan));
}
}

View File

@@ -1,9 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Snap.Hutao.Control.Extension;
using System.Collections;
namespace Snap.Hutao.Control.AutoSuggestBox;
@@ -18,23 +22,44 @@ internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox
TextChanged += OnFilterSuggestionRequested;
QuerySubmitted += OnQuerySubmitted;
TokenItemAdding += OnTokenItemAdding;
TokenItemAdded += OnTokenItemModified;
TokenItemRemoved += OnTokenItemModified;
TokenItemAdded += OnTokenItemCollectionChanged;
TokenItemRemoved += OnTokenItemCollectionChanged;
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (this.FindDescendant("SuggestionsPopup") is Popup { Child: Border { Child: ListView listView } border })
{
IAppResourceProvider appResourceProvider = this.ServiceProvider().GetRequiredService<IAppResourceProvider>();
listView.Background = null;
listView.Margin = appResourceProvider.GetResource<Thickness>("AutoSuggestListPadding");
border.Background = appResourceProvider.GetResource<Microsoft.UI.Xaml.Media.Brush>("AutoSuggestBoxSuggestionsListBackground");
CornerRadius overlayCornerRadius = appResourceProvider.GetResource<CornerRadius>("OverlayCornerRadius");
CornerRadiusFilterConverter cornerRadiusFilterConverter = new() { Filter = CornerRadiusFilterKind.Bottom };
border.CornerRadius = (CornerRadius)cornerRadiusFilterConverter.Convert(overlayCornerRadius, typeof(CornerRadius), default, default);
}
}
private void OnFilterSuggestionRequested(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if (string.IsNullOrWhiteSpace(Text))
{
return;
sender.ItemsSource = AvailableTokens
.OrderBy(kvp => kvp.Value.Kind)
.Select(kvp => kvp.Value);
}
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
{
sender.ItemsSource = AvailableTokens.Values.Where(q => q.Value.Contains(Text, StringComparison.OrdinalIgnoreCase));
// TODO: CornerRadius
// Popup? popup = this.FindDescendant("SuggestionsPopup") as Popup;
sender.ItemsSource = AvailableTokens
.Where(kvp => kvp.Value.Value.Contains(Text, StringComparison.OrdinalIgnoreCase))
.OrderBy(kvp => kvp.Value.Kind)
.ThenBy(kvp => kvp.Value.Order)
.Select(kvp => kvp.Value)
.DefaultIfEmpty(SearchToken.NotFound);
}
}
@@ -45,7 +70,7 @@ internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox
return;
}
CommandExtension.TryExecute(FilterCommand, FilterCommandParameter);
CommandInvocation.TryExecute(FilterCommand, FilterCommandParameter);
}
private void OnTokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
@@ -55,11 +80,23 @@ internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox
return;
}
args.Item = AvailableTokens.GetValueOrDefault(args.TokenText) ?? new SearchToken(SearchTokenKind.None, args.TokenText);
if (AvailableTokens.GetValueOrDefault(args.TokenText) is { } token)
{
args.Item = token;
}
else
{
args.Cancel = true;
}
}
private void OnTokenItemModified(TokenizingTextBox sender, object args)
private void OnTokenItemCollectionChanged(TokenizingTextBox sender, object args)
{
CommandExtension.TryExecute(FilterCommand, FilterCommandParameter);
if (args is SearchToken { Kind: SearchTokenKind.None } token)
{
((IList)sender.ItemsSource).Remove(token);
}
FilterCommand.TryExecute(FilterCommandParameter);
}
}

View File

@@ -7,13 +7,16 @@ namespace Snap.Hutao.Control.AutoSuggestBox;
internal sealed class SearchToken
{
public SearchToken(SearchTokenKind kind, string value, Uri? iconUri = null, Uri? sideIconUri = null, Color? quality = null)
public static readonly SearchToken NotFound = new(SearchTokenKind.None, SH.ControlAutoSuggestBoxNotFoundValue, 0);
public SearchToken(SearchTokenKind kind, string value, int order, Uri? iconUri = null, Uri? sideIconUri = null, Color? quality = null)
{
Value = value;
Kind = kind;
IconUri = iconUri;
SideIconUri = sideIconUri;
Quality = quality;
Order = order;
}
public SearchTokenKind Kind { get; }
@@ -26,6 +29,8 @@ internal sealed class SearchToken
public Color? Quality { get; }
public int Order { get; }
public override string ToString()
{
return Value;

View File

@@ -6,12 +6,12 @@ namespace Snap.Hutao.Control.AutoSuggestBox;
internal enum SearchTokenKind
{
None,
AssociationType,
Avatar,
BodyType,
ElementName,
FightProperty,
ItemQuality,
Weapon,
WeaponType,
FightProperty,
ElementName,
AssociationType,
BodyType,
Avatar,
Weapon,
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Behavior;
[SuppressMessage("", "CA1001")]
[DependencyProperty("MilliSecondsDelay", typeof(int))]
internal sealed partial class InfoBarDelayCloseBehavior : BehaviorBase<InfoBar>
{
private readonly CancellationTokenSource closeTokenSource = new();
protected override void OnAssociatedObjectLoaded()
{
AssociatedObject.Closed += OnInfoBarClosed;
if (MilliSecondsDelay > 0)
{
DelayCoreAsync().SafeForget();
}
}
private async ValueTask DelayCoreAsync()
{
try
{
await Task.Delay(MilliSecondsDelay, closeTokenSource.Token).ConfigureAwait(true);
}
catch
{
return;
}
if (AssociatedObject is not null)
{
AssociatedObject.IsOpen = false;
}
}
private void OnInfoBarClosed(InfoBar infoBar, InfoBarClosedEventArgs args)
{
if (args.Reason is InfoBarCloseReason.CloseButton)
{
closeTokenSource.Cancel();
}
AssociatedObject.Closed -= OnInfoBarClosed;
}
}

View File

@@ -33,6 +33,7 @@ internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavio
protected override bool Uninitialize()
{
periodicTimerCancellationTokenSource.Cancel();
AssociatedObject.ActualThemeChanged -= OnActualThemeChanged;
return true;
}

View File

@@ -13,6 +13,4 @@ namespace Snap.Hutao.Control;
/// </summary>
[HighQuality]
[DependencyProperty("DataContext", typeof(object))]
internal sealed partial class BindingProxy : DependencyObject
{
}
internal sealed partial class BindingProxy : DependencyObject;

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

@@ -30,7 +30,7 @@ internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, IN
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
public AdvancedCollectionView()
: this(new List<T>(0))
: this([])
{
}

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
internal sealed class VectorChangedEventArgs : IVectorChangedEventArgs
{
public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = null!)
public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = default!)
{
CollectionChange = cc;
Index = (uint)index;

View File

@@ -1,38 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation.Collections;
namespace Snap.Hutao.Control.Collection.Alternating;
[DependencyProperty("ItemAlternateBackground", typeof(Microsoft.UI.Xaml.Media.Brush))]
internal sealed partial class AlternatingItemsControl : ItemsControl
{
private readonly VectorChangedEventHandler<object> itemsVectorChangedEventHandler;
public AlternatingItemsControl()
{
itemsVectorChangedEventHandler = OnItemsVectorChanged;
Items.VectorChanged += itemsVectorChangedEventHandler;
}
private void OnItemsVectorChanged(IObservableVector<object> items, IVectorChangedEventArgs args)
{
if (args.CollectionChange is CollectionChange.Reset)
{
int index = (int)args.Index;
for (int i = index; i < items.Count; i++)
{
if (items[i] is IAlternatingItem item)
{
item.Background = i % 2 is 0 ? default : ItemAlternateBackground;
}
else
{
break;
}
}
}
}
}

View File

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

View File

@@ -3,6 +3,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Control;
@@ -40,6 +41,6 @@ internal abstract class DependencyValueConverter<TFrom, TTo> : DependencyObject,
/// <returns>源</returns>
public virtual TFrom ConvertBack(TTo to)
{
throw Must.NeverHappen();
throw HutaoException.NotSupported();
}
}

View File

@@ -3,7 +3,7 @@
namespace Snap.Hutao.Control.Extension;
internal static class CommandExtension
internal static class CommandInvocation
{
public static bool TryExecute(this ICommand? command, object? parameter = null)
{

View File

@@ -2,11 +2,13 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Control.Extension;
internal static class DependencyObjectExtension
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IServiceProvider ServiceProvider(this DependencyObject obj)
{
return Ioc.Default;

View File

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

View File

@@ -7,6 +7,8 @@ namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("SquareLength", typeof(double), 0D, nameof(OnSquareLengthChanged), IsAttached = true, AttachedType = typeof(FrameworkElement))]
[DependencyProperty("IsActualThemeBindingEnabled", typeof(bool), false, nameof(OnIsActualThemeBindingEnabled), IsAttached = true, AttachedType = typeof(FrameworkElement))]
[DependencyProperty("ActualTheme", typeof(ElementTheme), ElementTheme.Default, IsAttached = true, AttachedType = typeof(FrameworkElement))]
public sealed partial class FrameworkElementHelper
{
private static void OnSquareLengthChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
@@ -15,4 +17,22 @@ public sealed partial class FrameworkElementHelper
element.Width = (double)e.NewValue;
element.Height = (double)e.NewValue;
}
private static void OnIsActualThemeBindingEnabled(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = (FrameworkElement)dp;
if ((bool)e.NewValue)
{
element.ActualThemeChanged += OnActualThemeChanged;
}
else
{
element.ActualThemeChanged -= OnActualThemeChanged;
}
static void OnActualThemeChanged(FrameworkElement sender, object args)
{
SetActualTheme(sender, sender.ActualTheme);
}
}
}

View File

@@ -1,10 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO.DataTransfer;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
namespace Snap.Hutao.Control.Image;
@@ -12,29 +17,32 @@ namespace Snap.Hutao.Control.Image;
/// 缓存图像
/// </summary>
[HighQuality]
internal sealed class CachedImage : Implementation.ImageEx
[DependencyProperty("SourceName", typeof(string), "Unknown")]
[DependencyProperty("CachedName", typeof(string), "Unknown")]
internal sealed partial class CachedImage : Implementation.ImageEx
{
/// <summary>
/// 构造一个新的缓存图像
/// </summary>
public CachedImage()
{
IsCacheEnabled = true;
EnableLazyLoading = false;
DefaultStyleKey = typeof(CachedImage);
DefaultStyleResourceUri = "ms-appx:///Control/Image/CachedImage.xaml".ToUri();
}
/// <inheritdoc/>
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
protected override async Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
// We can only use Ioc to retrieve IImageCache, no IServiceProvider is available.
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
SourceName = Path.GetFileName(imageUri.ToString());
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
try
{
Verify.Operation(!string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
CachedName = Path.GetFileName(file);
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)
{
@@ -43,4 +51,27 @@ internal sealed class CachedImage : Implementation.ImageEx
return default;
}
}
}
[Command("CopyToClipboardCommand")]
private async Task CopyToClipboard()
{
if (Image is Microsoft.UI.Xaml.Controls.Image { Source: BitmapImage bitmap })
{
using (FileStream netStream = File.OpenRead(bitmap.UriSource.LocalPath))
{
using (IRandomAccessStream fxStream = netStream.AsRandomAccessStream())
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(fxStream);
SoftwareBitmap softwareBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
using (InMemoryRandomAccessStream memory = new())
{
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, memory);
encoder.SetSoftwareBitmap(softwareBitmap);
await encoder.FlushAsync();
Ioc.Default.GetRequiredService<IClipboardProvider>().SetBitmap(memory);
}
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
<ResourceDictionary
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shci="using:Snap.Hutao.Control.Image">
@@ -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">
@@ -15,6 +14,13 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem IsEnabled="False" Text="{TemplateBinding SourceName}"/>
<MenuFlyoutItem IsEnabled="False" Text="{TemplateBinding CachedName}"/>
<MenuFlyoutItem Command="{Binding CopyToClipboardCommand, RelativeSource={RelativeSource TemplatedParent}}" Text="复制图像"/>
</MenuFlyout>
</Grid.ContextFlyout>
<Image
Name="PlaceholderImage"
Margin="{TemplateBinding PlaceholderMargin}"

View File

@@ -7,21 +7,14 @@ using Windows.Media.Casting;
namespace Snap.Hutao.Control.Image.Implementation;
internal class ImageEx : ImageExBase
[DependencyProperty("NineGrid", typeof(Thickness))]
internal partial class ImageEx : ImageExBase
{
private static readonly DependencyProperty NineGridProperty = DependencyProperty.Register(nameof(NineGrid), typeof(Thickness), typeof(ImageEx), new PropertyMetadata(default(Thickness)));
public ImageEx()
: base()
{
}
public Thickness NineGrid
{
get => (Thickness)GetValue(NineGridProperty);
set => SetValue(NineGridProperty, value);
}
public override CompositionBrush GetAlphaMask()
{
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)

View File

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

View File

@@ -1,151 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using System.Numerics;
using Windows.Foundation;
namespace Snap.Hutao.Control.Layout;
internal sealed class DefaultItemCollectionTransitionProvider : ItemCollectionTransitionProvider
{
private const double DefaultAnimationDurationInMs = 300.0;
static DefaultItemCollectionTransitionProvider()
{
AnimationSlowdownFactor = 1.0;
}
public static double AnimationSlowdownFactor { get; set; }
protected override bool ShouldAnimateCore(ItemCollectionTransition transition)
{
return true;
}
protected override void StartTransitions(IList<ItemCollectionTransition> transitions)
{
List<ItemCollectionTransition> addTransitions = [];
List<ItemCollectionTransition> removeTransitions = [];
List<ItemCollectionTransition> moveTransitions = [];
foreach (ItemCollectionTransition transition in addTransitions)
{
switch (transition.Operation)
{
case ItemCollectionTransitionOperation.Add:
addTransitions.Add(transition);
break;
case ItemCollectionTransitionOperation.Remove:
removeTransitions.Add(transition);
break;
case ItemCollectionTransitionOperation.Move:
moveTransitions.Add(transition);
break;
}
}
StartAddTransitions(addTransitions, removeTransitions.Count > 0, moveTransitions.Count > 0);
StartRemoveTransitions(removeTransitions);
StartMoveTransitions(moveTransitions, removeTransitions.Count > 0);
}
private static void StartAddTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveTransitions, bool hasMoveTransitions)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation fadeInAnimation = compositor.CreateScalarKeyFrameAnimation();
fadeInAnimation.InsertKeyFrame(0.0f, 0.0f);
if (hasMoveTransitions && hasRemoveTransitions)
{
fadeInAnimation.InsertKeyFrame(0.66f, 0.0f);
}
else if (hasMoveTransitions || hasRemoveTransitions)
{
fadeInAnimation.InsertKeyFrame(0.5f, 0.0f);
}
fadeInAnimation.InsertKeyFrame(1.0f, 1.0f);
fadeInAnimation.Duration = TimeSpan.FromMilliseconds(
DefaultAnimationDurationInMs * ((hasRemoveTransitions ? 1 : 0) + (hasMoveTransitions ? 1 : 0) + 1) * AnimationSlowdownFactor);
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
visual.StartAnimation("Opacity", fadeInAnimation);
batch.End();
batch.Completed += (_, _) => progress.Complete();
}
}
private static void StartRemoveTransitions(IList<ItemCollectionTransition> transitions)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation fadeOutAnimation = compositor.CreateScalarKeyFrameAnimation();
fadeOutAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue");
fadeOutAnimation.InsertKeyFrame(1.0f, 0.0f);
fadeOutAnimation.Duration = TimeSpan.FromMilliseconds(DefaultAnimationDurationInMs * AnimationSlowdownFactor);
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
visual.StartAnimation(nameof(Visual.Opacity), fadeOutAnimation);
batch.End();
batch.Completed += (_, _) =>
{
visual.Opacity = 1.0f;
progress.Complete();
};
}
}
private static void StartMoveTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveAnimations)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
// Animate offset.
if (transition.OldBounds.X != transition.NewBounds.X ||
transition.OldBounds.Y != transition.NewBounds.Y)
{
AnimateOffset(visual, compositor, transition.OldBounds, transition.NewBounds, hasRemoveAnimations);
}
batch.End();
batch.Completed += (_, _) => progress.Complete();
}
}
private static void AnimateOffset(Visual visual, Compositor compositor, Rect oldBounds, Rect newBounds, bool hasRemoveAnimations)
{
Vector2KeyFrameAnimation offsetAnimation = compositor.CreateVector2KeyFrameAnimation();
offsetAnimation.SetVector2Parameter("delta", new Vector2(
(float)(oldBounds.X - newBounds.X),
(float)(oldBounds.Y - newBounds.Y)));
offsetAnimation.SetVector2Parameter("final", default);
offsetAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue + delta");
if (hasRemoveAnimations)
{
offsetAnimation.InsertExpressionKeyFrame(0.5f, "delta");
}
offsetAnimation.InsertExpressionKeyFrame(1.0f, "final");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(
DefaultAnimationDurationInMs * ((hasRemoveAnimations ? 1 : 0) + 1) * AnimationSlowdownFactor);
visual.StartAnimation("TransformMatrix._41_42", offsetAnimation);
}
}

View File

@@ -18,14 +18,12 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = new UniformStaggeredLayoutState(context);
base.InitializeForContextCore(context);
}
/// <inheritdoc/>
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = null;
base.UninitializeForContextCore(context);
}
/// <inheritdoc/>
@@ -65,12 +63,12 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
/// <inheritdoc/>
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
if (context.ItemCount == 0)
if (context.ItemCount is 0)
{
return new Size(availableSize.Width, 0);
}
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
if ((context.RealizationRect.Width is 0) && (context.RealizationRect.Height is 0))
{
return new Size(availableSize.Width, 0.0f);
}
@@ -82,16 +80,10 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
(int numberOfColumns, double columnWidth) = GetNumberOfColumnsAndWidth(availableWidth, MinItemWidth, MinColumnSpacing);
if (columnWidth != state.ColumnWidth)
{
// The items will need to be remeasured
state.Clear();
}
state.ColumnWidth = columnWidth;
// adjust for column spacing on all columns expect the first
double totalWidth = state.ColumnWidth + ((numberOfColumns - 1) * (state.ColumnWidth + MinColumnSpacing));
double totalWidth = ((state.ColumnWidth + MinColumnSpacing) * numberOfColumns) - MinColumnSpacing;
if (totalWidth > availableWidth)
{
numberOfColumns--;
@@ -103,7 +95,6 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
if (numberOfColumns != state.NumberOfColumns)
{
// The items will not need to be remeasured, but they will need to go into new columns
state.ClearColumns();
}
@@ -170,7 +161,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
item.Element.Measure(new Size(state.ColumnWidth, availableHeight));
if (item.Height != item.Element.DesiredSize.Height)
{
// this item changed size; we need to recalculate layout for everything after this
// this item changed size; we need to recalculate layout for everything after this item
state.RemoveFromIndex(i + 1);
item.Height = item.Element.DesiredSize.Height;
columnHeights[columnIndex] = item.Top + item.Height;
@@ -201,16 +192,16 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
// Cycle through each column and arrange the items that are within the realization bounds
for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
{
UniformStaggeredColumnLayout layout = state.GetColumnLayout(columnIndex);
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(layout))
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(state.GetColumnLayout(columnIndex)))
{
double bottom = item.Top + item.Height;
if (bottom < context.RealizationRect.Top)
{
// element is above the realization bounds
// Element is above the realization bounds
continue;
}
// Partial or fully in the view
if (item.Top <= context.RealizationRect.Bottom)
{
double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex);
@@ -229,21 +220,22 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
return finalSize;
}
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double minColumnSpacing)
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double columnSpacing)
{
// test if the width can fit in 2 items
if ((2 * minItemWidth) + minColumnSpacing > availableWidth)
if ((2 * minItemWidth) + columnSpacing > availableWidth)
{
return (1, availableWidth);
}
int columnCount = Math.Max(1, (int)((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing)));
double columnWidthAddSpacing = (availableWidth + minColumnSpacing) / columnCount;
return (columnCount, columnWidthAddSpacing - minColumnSpacing);
int columnCount = Math.Max(1, (int)((availableWidth + columnSpacing) / (minItemWidth + columnSpacing)));
double columnWidthWithSpacing = (availableWidth + columnSpacing) / columnCount;
return (columnCount, columnWidthWithSpacing - columnSpacing);
}
private static int GetLowestColumnIndex(in ReadOnlySpan<double> columnHeights)
{
// We want to find the leftest column with the lowest height
int columnIndex = 0;
double height = columnHeights[0];
for (int j = 1; j < columnHeights.Length; j++)
@@ -260,13 +252,11 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
panel.InvalidateMeasure();
((UniformStaggeredLayout)d).InvalidateMeasure();
}
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
panel.InvalidateMeasure();
((UniformStaggeredLayout)d).InvalidateMeasure();
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Runtime.InteropServices;
@@ -67,46 +66,6 @@ internal sealed class UniformStaggeredLayoutState
return columnLayout[columnIndex];
}
/// <summary>
/// Clear everything that has been calculated.
/// </summary>
internal void Clear()
{
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
// The first element must be force refreshed otherwise
// it will use the old one realized
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Now we need to refresh the first element of each column
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Finally we need to refresh the whole layout when we reset
if (context.ItemCount > 0)
{
for (int i = 0; i < context.ItemCount; i++)
{
RecycleElementAt(i);
}
}
columnLayout.Clear();
items.Clear();
}
/// <summary>
/// Clear the layout columns so they will be recalculated.
/// </summary>
internal void ClearColumns()
{
columnLayout.Clear();
}
/// <summary>
/// Gets the estimated height of the layout.
/// </summary>
/// <returns>The estimated height of the layout.</returns>
/// <remarks>
/// If all of the items have been calculated then the actual height will be returned.
/// If all of the items have not been calculated then an estimated height will be calculated based on the average height of the items.
/// </remarks>
internal double GetHeight()
{
double desiredHeight = columnLayout.Values.Max(c => c.Height);
@@ -139,10 +98,37 @@ internal sealed class UniformStaggeredLayoutState
return desiredHeight;
}
internal void Clear()
{
RecycleElements();
ClearColumns();
ClearItems();
}
internal void ClearColumns()
{
columnLayout.Clear();
}
internal void ClearItems()
{
items.Clear();
}
internal void RecycleElements()
{
if (context.ItemCount > 0)
{
for (int i = 0; i < items.Count; i++)
{
RecycleElementAt(i);
}
}
}
internal void RecycleElementAt(int index)
{
UIElement element = context.GetOrCreateElementAt(index);
context.RecycleElement(element);
context.RecycleElement(context.GetOrCreateElementAt(index));
}
internal void RemoveFromIndex(int index)
@@ -175,7 +161,7 @@ internal sealed class UniformStaggeredLayoutState
{
for (int i = startIndex; i <= endIndex; i++)
{
if (i > items.Count)
if (i >= items.Count)
{
break;
}
@@ -184,7 +170,7 @@ internal sealed class UniformStaggeredLayoutState
item.Height = 0;
item.Top = 0;
// We must recycle all elements to ensure that it gets the correct context
// We must recycle all removed elements to ensure that it gets the correct context
RecycleElementAt(i);
}

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 WrapItem(int index)
{
Index = index;
}
public static Point EmptyPosition { get; } = new(float.NegativeInfinity, float.NegativeInfinity);
public int Index { get; }
public Size Size { get; set; } = Size.Empty;
public Point Position { get; set; } = EmptyPosition;
public UIElement? Element { get; set; }
}

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

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.Control;
@@ -17,7 +18,7 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
public Loading()
{
DefaultStyleKey = typeof(Loading);
DefaultStyleResourceUri = new("ms-appx:///Control/Loading.xaml");
DefaultStyleResourceUri = "ms-appx:///Control/Loading.xaml".ToUri();
}
public bool IsLoading
@@ -36,9 +37,18 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
private static void IsLoadingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Loading control = (Loading)d;
control.presenter ??= control.GetTemplateChild("ContentGrid") as FrameworkElement;
control?.Update();
if ((bool)e.NewValue)
{
control.presenter ??= control.GetTemplateChild("ContentGrid") as FrameworkElement;
}
else if (control.presenter is not null)
{
XamlMarkupHelper.UnloadObject(control.presenter);
control.presenter = null;
}
control.Update();
}
private void Update()

View File

@@ -23,7 +23,8 @@
<ContentPresenter
x:Name="ContentGrid"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
x:Load="True">
<ContentPresenter.RenderTransform>
<CompositeTransform/>
</ContentPresenter.RenderTransform>
@@ -84,4 +85,4 @@
</Setter>
</Style>
</ResourceDictionary>
</ResourceDictionary>

View File

@@ -12,7 +12,6 @@ internal sealed class UInt32Extension : MarkupExtension
protected override object ProvideValue()
{
_ = uint.TryParse(Value, out uint result);
return result;
return XamlBindingHelper.ConvertValue(typeof(uint), Value);
}
}

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

@@ -39,24 +39,6 @@ internal static class SoftwareBitmapExtension
}
}
public static unsafe double Luminance(this SoftwareBitmap softwareBitmap)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
{
using (IMemoryBufferReference reference = buffer.CreateReference())
{
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
double sum = 0;
foreach (ref readonly Bgra32 pixel in bytes)
{
sum += pixel.Luminance;
}
return sum / bytes.Length;
}
}
}
public static unsafe Bgra32 GetAccentColor(this SoftwareBitmap softwareBitmap)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))

View File

@@ -82,8 +82,8 @@ internal partial class EqualPanel : Microsoft.UI.Xaml.Controls.Panel
(d as EqualPanel)?.InvalidateMeasure();
}
private void OnHorizontalAlignmentChanged(DependencyObject sender, DependencyProperty dp)
private static void OnHorizontalAlignmentChanged(DependencyObject d, DependencyProperty dp)
{
InvalidateMeasure();
(d as EqualPanel)?.InvalidateMeasure();
}
}

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,27 +34,29 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
protected override Size ArrangeOverride(Size finalSize)
{
int itemCount = Children.Count;
double availableWidthPerItem = (finalSize.Width - (Spacing * (itemCount - 1))) / itemCount;
double actualItemWidth = Math.Max(MinItemWidth, availableWidthPerItem);
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;
}
return finalSize;
}
private void OnLoaded(object sender, RoutedEventArgs e)
private static void OnLoaded(object sender, RoutedEventArgs e)
{
MinWidth = (MinItemWidth * Children.Count) + (Spacing * (Children.Count - 1));
HorizontalEqualPanel panel = (HorizontalEqualPanel)sender;
int vivibleChildrenCount = panel.Children.Count(child => child.Visibility is Visibility.Visible);
panel.MinWidth = (panel.MinItemWidth * vivibleChildrenCount) + (panel.Spacing * (vivibleChildrenCount - 1));
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
private static void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
InvalidateMeasure();
((HorizontalEqualPanel)sender).InvalidateMeasure();
}
}

View File

@@ -1,21 +1,53 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;
[DependencyProperty("MinItemWidth", typeof(double))]
internal sealed partial class UniformPanel : UniformGrid
[DependencyProperty("ColumnSpacing", typeof(double))]
[DependencyProperty("RowSpacing", typeof(double))]
internal sealed partial class UniformPanel : Microsoft.UI.Xaml.Controls.Panel
{
public UniformPanel()
private int columns;
protected override Size MeasureOverride(Size availableSize)
{
Columns = 1;
SizeChanged += OnSizeChanged;
columns = (int)((availableSize.Width + ColumnSpacing) / (MinItemWidth + ColumnSpacing));
double availableItemWidth = ((availableSize.Width + ColumnSpacing) / columns) - ColumnSpacing;
double maxDesiredHeight = 0;
foreach (UIElement child in Children)
{
child.Measure(new Size(availableItemWidth, availableSize.Height));
maxDesiredHeight = Math.Max(maxDesiredHeight, child.DesiredSize.Height);
}
int desiredRows = (int)Math.Ceiling(Children.Count / (double)columns);
double desiredHeight = ((maxDesiredHeight + RowSpacing) * desiredRows) - RowSpacing;
return new Size(availableSize.Width, desiredHeight);
}
private void OnSizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e)
protected override Size ArrangeOverride(Size finalSize)
{
Columns = (int)((e.NewSize.Width + ColumnSpacing) / (MinItemWidth + ColumnSpacing));
double itemWidth = ((finalSize.Width + ColumnSpacing) / columns) - ColumnSpacing;
for (int index = 0; index < Children.Count; index++)
{
UIElement child = Children[index];
int row = index / columns;
int column = index % columns;
double x = column * (itemWidth + ColumnSpacing);
double y = row * (child.DesiredSize.Height + RowSpacing);
child.Arrange(new Rect(x, y, itemWidth, child.DesiredSize.Height));
}
return finalSize;
}
}

View File

@@ -3,8 +3,10 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.View.Helper;
using Snap.Hutao.ViewModel.Abstraction;
namespace Snap.Hutao.Control;
@@ -15,7 +17,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 +25,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)
@@ -36,6 +38,11 @@ internal class ScopedPage : Page
extra.NotifyNavigationCompleted();
}
public virtual void UnloadObjectOverride(DependencyObject unloadableObject)
{
XamlMarkupHelper.UnloadObject(unloadableObject);
}
/// <summary>
/// 初始化
/// 应当在 InitializeComponent() 前调用
@@ -44,9 +51,23 @@ 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
{
TViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>();
using (viewModel.DisposeLock.Enter())
{
viewModel.IsViewDisposed = false;
viewModel.CancellationToken = viewCancellationTokenSource.Token;
viewModel.DeferContentLoader = new DeferContentLoader(this);
}
DataContext = viewModel;
}
catch (Exception ex)
{
pageScope.ServiceProvider.GetRequiredService<ILogger<ScopedPage>>().LogError(ex, "Failed to initialize view model.");
throw;
}
}
/// <inheritdoc/>
@@ -72,7 +93,11 @@ internal class ScopedPage : Page
DisposeViewModel();
}
DataContext = null;
if (this.IsDisposed())
{
return;
}
Unloaded -= unloadEventHandler;
}
@@ -84,14 +109,14 @@ internal class ScopedPage : Page
viewCancellationTokenSource.Cancel();
IViewModel viewModel = (IViewModel)DataContext;
using (SemaphoreSlim locker = viewModel.DisposeLock)
using (viewModel.DisposeLock.Enter())
{
// Wait to ensure viewmodel operation is completed
locker.Wait();
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

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Service.Notification;
namespace Snap.Hutao.Control.Selector;
internal sealed class InfoBarTemplateSelector : DataTemplateSelector
{
public DataTemplate ActionButtonEnabled { get; set; } = default!;
public DataTemplate ActionButtonDisabled { get; set; } = default!;
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
if (item is InfoBarOptions { ActionButtonContent: { }, ActionButtonCommand: { } })
{
return ActionButtonEnabled;
}
return ActionButtonDisabled;
}
}

View File

@@ -21,8 +21,8 @@ internal sealed partial class SizeRestrictedContentControl : ContentControl
element.Measure(availableSize);
Size contentDesiredSize = element.DesiredSize;
Size contentActualOrDesiredSize = new(
Math.Max(element.ActualWidth, contentDesiredSize.Width),
Math.Max(element.ActualHeight, contentDesiredSize.Height));
Math.Min(Math.Max(element.ActualWidth, contentDesiredSize.Width), availableSize.Width),
Math.Min(Math.Max(element.ActualHeight, contentDesiredSize.Height), availableSize.Height));
if (IsWidthRestricted)
{

View File

@@ -45,15 +45,7 @@ internal sealed partial class DescriptionTextBlock : ContentControl
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
try
{
UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
}
catch (Exception ex)
{
_ = ex;
}
UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
}
private static void OnTextStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

View File

@@ -14,6 +14,7 @@ using Windows.UI;
namespace Snap.Hutao.Control.Text;
// TODO: change the parsing to syntax tree
[DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))]
[DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))]
internal sealed partial class HtmlDescriptionTextBlock : ContentControl

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
@@ -27,7 +29,7 @@ internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
{
MiHoYoColorKind.Rgba => new(Position.Start + 17, Position.End - 8),
MiHoYoColorKind.Rgb => new(Position.Start + 15, Position.End - 8),
_ => throw Must.NeverHappen(),
_ => throw HutaoException.NotSupported(),
};
}
}
@@ -40,7 +42,7 @@ internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
{
MiHoYoColorKind.Rgba => new(Position.Start + 8, Position.Start + 16),
MiHoYoColorKind.Rgb => new(Position.Start + 8, Position.Start + 14),
_ => throw Must.NeverHappen(),
_ => throw HutaoException.NotSupported(),
};
}
}

View File

@@ -1,8 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
// TODO: Pooling syntax nodes to reduce memory allocation
internal sealed class MiHoYoSyntaxTree
{
public MiHoYoSyntaxNode Root { get; set; } = default!;
@@ -75,7 +78,7 @@ internal sealed class MiHoYoSyntaxTree
{
17 => MiHoYoColorKind.Rgba,
15 => MiHoYoColorKind.Rgb,
_ => throw Must.NeverHappen(),
_ => throw HutaoException.NotSupported(),
};
TextPosition position = new(0, endOfXmlColorRightClosingAtUnprocessedContent);

View File

@@ -10,6 +10,7 @@
<shmmc:AchievementIconConverter x:Key="AchievementIconConverter"/>
<shmmc:AvatarCardConverter x:Key="AvatarCardConverter"/>
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
<shmmc:AvatarIconCircleConverter x:Key="AvatarIconCircleConverter"/>
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
<shmmc:DescriptionsParametersDescriptor x:Key="DescParamDescriptor"/>

View File

@@ -2,4 +2,5 @@
<CornerRadius x:Key="ControlCornerRadiusTop">4,4,0,0</CornerRadius>
<CornerRadius x:Key="ControlCornerRadiusBottom">0,0,4,4</CornerRadius>
<CornerRadius x:Key="ControlCornerRadiusTopRightAndBottomLeft">0,4,0,4</CornerRadius>
<CornerRadius x:Key="CornerRadiusAll16">16</CornerRadius>
</ResourceDictionary>

View File

@@ -52,6 +52,12 @@
<Thickness x:Key="InfoBarIconMargin">19,16,19,16</Thickness>
<Thickness x:Key="InfoBarContentRootPadding">0,0,0,0</Thickness>
<x:Double x:Key="InfoBarIconFontSize">20</x:Double>
<Thickness x:Key="InfoBarTitleHorizontalOrientationMargin">0,0,0,0</Thickness>
<Thickness x:Key="InfoBarMessageHorizontalOrientationMargin">12,0,0,0</Thickness>
<Thickness x:Key="InfoBarTitleVerticalOrientationMargin">0,16,0,0</Thickness>
<Thickness x:Key="InfoBarMessageVerticalOrientationMargin">0,6,0,0</Thickness>
<!-- TODO: When will DefaultInfoBarStyle added -->
<Style TargetType="InfoBar">
<Setter Property="shch:InfoBarHelper.IsTextSelectionEnabled" Value="False"/>
@@ -128,6 +134,7 @@
<InfoBarPanel
Grid.Column="1"
Margin="{StaticResource InfoBarPanelMargin}"
VerticalAlignment="Center"
HorizontalOrientationPadding="{StaticResource InfoBarPanelHorizontalOrientationPadding}"
VerticalOrientationPadding="{StaticResource InfoBarPanelVerticalOrientationPadding}">
<TextBlock
@@ -173,6 +180,7 @@
<Button
Name="CloseButton"
Grid.Column="2"
VerticalAlignment="Top"
Command="{TemplateBinding CloseButtonCommand}"
CommandParameter="{TemplateBinding CloseButtonCommandParameter}"
Style="{TemplateBinding CloseButtonStyle}">
@@ -236,7 +244,14 @@
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="SeverityLevels">
<VisualState x:Name="Informational"/>
<VisualState x:Name="Informational">
<VisualState.Setters>
<Setter Target="ContentRoot.Background" Value="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}"/>
<Setter Target="IconBackground.Foreground" Value="{ThemeResource InfoBarInformationalSeverityIconBackground}"/>
<Setter Target="StandardIcon.Text" Value="{StaticResource InfoBarInformationalIconGlyph}"/>
<Setter Target="StandardIcon.Foreground" Value="{ThemeResource InfoBarInformationalSeverityIconForeground}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Error">
<VisualState.Setters>
<Setter Target="ContentRoot.Background" Value="{ThemeResource InfoBarErrorSeverityBackgroundBrush}"/>

View File

@@ -23,6 +23,9 @@
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing4Template">
<StackPanel Orientation="Horizontal" Spacing="4"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing6Template">
<StackPanel Orientation="Horizontal" Spacing="6"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
<StackPanel Spacing="4"/>
</ItemsPanelTemplate>

View File

@@ -1,4 +1,4 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TransitionCollection x:Key="ContentThemeTransitions">
<ContentThemeTransition/>
</TransitionCollection>
@@ -20,4 +20,4 @@
<TransitionCollection x:Key="NavigationThemeTransitions">
<NavigationThemeTransition/>
</TransitionCollection>
</ResourceDictionary>
</ResourceDictionary>

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>
@@ -30,6 +35,7 @@
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
<x:String x:Key="UI_EmotionIcon52">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon89">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon89.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Control;
@@ -39,6 +40,6 @@ internal abstract class ValueConverter<TFrom, TTo> : IValueConverter
/// <returns>源</returns>
public virtual TFrom ConvertBack(TTo to)
{
throw Must.NeverHappen();
throw HutaoException.NotSupported();
}
}

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,18 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
/// <summary>
/// 可克隆
/// </summary>
/// <typeparam name="TSelf">自身类型</typeparam>
[HighQuality]
internal interface ICloneable<out TSelf>
{
/// <summary>
/// 克隆
/// </summary>
/// <returns>新的克隆</returns>
TSelf Clone();
}

View File

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

View File

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

View File

@@ -1,22 +1,23 @@
// 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.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Core.Caching;
/// <summary>
/// Provides methods and tools to cache files in a folder
/// The class's name will become the cache folder's name
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IImageCache))]
@@ -24,9 +25,9 @@ namespace Snap.Hutao.Core.Caching;
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string CacheFolderName = nameof(ImageCache);
private const string CacheFailedDownloadTasksName = $"{nameof(ImageCache)}.FailedDownloadTasks";
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = FrozenDictionary.ToFrozenDictionary(
private static readonly FrozenDictionary<int, TimeSpan> DelayFromRetryCount = FrozenDictionary.ToFrozenDictionary(
[
KeyValuePair.Create(0, TimeSpan.FromSeconds(4)),
KeyValuePair.Create(1, TimeSpan.FromSeconds(16)),
@@ -35,20 +36,19 @@ 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;
private string CacheFolder
{
get => LazyInitializer.EnsureInitialized(ref cacheFolder, () =>
{
baseFolder ??= serviceProvider.GetRequiredService<RuntimeOptions>().LocalCache;
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
return info.FullName;
return serviceProvider.GetRequiredService<RuntimeOptions>().GetLocalCacheImageCacheFolder();
});
}
@@ -104,12 +104,12 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
{
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
{
logger.LogDebug("Begin downloading image file from '{Uri}' to '{File}'", uri, filePath);
logger.LogColorizedInformation("Begin to download file from '{Uri}' to '{File}'", (uri, ConsoleColor.Cyan), (filePath, ConsoleColor.Cyan));
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
}
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
{
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", uri);
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", (uri, ConsoleColor.Cyan));
await task.ConfigureAwait(false);
}
@@ -132,10 +132,7 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
private static string GetCacheFileName(Uri uri)
{
string url = uri.ToString();
byte[] chars = Encoding.UTF8.GetBytes(url);
byte[] hash = SHA1.HashData(chars);
return System.Convert.ToHexString(hash);
return Hash.SHA1HexString(uri.ToString());
}
private static bool IsFileInvalid(string file, bool treatNullFileAsInvalid = true)
@@ -145,8 +142,7 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
return treatNullFileAsInvalid;
}
FileInfo fileInfo = new(file);
return fileInfo.Length == 0;
return new FileInfo(file).Length == 0;
}
private void RemoveCore(IEnumerable<string> filePaths)
@@ -168,44 +164,76 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
[SuppressMessage("", "SH003")]
private async Task DownloadFileAsync(Uri uri, string baseFile)
{
int retryCount = 0;
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
while (retryCount < 3)
using (HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache)))
{
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);
}
int retryCount = 0;
if (message.IsSuccessStatusCode)
HttpRequestMessageBuilder requestMessageBuilder = httpRequestMessageBuilderFactory
.Create()
.SetRequestUri(uri)
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase)) // These headers are only available for our own api
.Get();
while (retryCount < 3)
{
requestMessageBuilder.Resurrect();
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
{
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
using (FileStream fileStream = File.Create(baseFile))
// Redirect detection
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
}
if (responseMessage.IsSuccessStatusCode)
{
if (responseMessage.Content.Headers.ContentType?.MediaType is "application/json")
{
DebugTrackFailedUri(uri);
string raw = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
logger.LogColorizedCritical("Failed to download '{Uri}' with unexpected body '{Raw}'", (uri, ConsoleColor.Red), (raw, ConsoleColor.DarkYellow));
return;
}
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
using (FileStream fileStream = File.Create(baseFile))
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
}
}
switch (responseMessage.StatusCode)
{
case HttpStatusCode.TooManyRequests:
{
retryCount++;
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? DelayFromRetryCount[retryCount];
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;
}
default:
DebugTrackFailedUri(uri);
logger.LogColorizedCritical("Failed to download '{Uri}' with status code '{StatusCode}'", (uri, ConsoleColor.Red), (responseMessage.StatusCode, ConsoleColor.DarkYellow));
return;
}
}
}
switch (message.StatusCode)
{
case HttpStatusCode.TooManyRequests:
{
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;
}
default:
return;
}
}
}
}
[Conditional("DEBUG")]
private void DebugTrackFailedUri(Uri uri)
{
HashSet<string>? set = memoryCache.GetOrCreate(CacheFailedDownloadTasksName, entry => new HashSet<string>());
set?.Add(uri.ToString());
}
}

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

@@ -70,7 +70,7 @@ internal sealed class ObservableReorderableDbCollection<TEntity> : ObservableCol
[SuppressMessage("", "SA1402")]
internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> : ObservableCollection<TEntityOnly>
where TEntityOnly : class, IEntityOnly<TEntity>
where TEntityOnly : class, IEntityAccess<TEntity>
where TEntity : class, IReorderable
{
private readonly IServiceProvider serviceProvider;

View File

@@ -38,7 +38,7 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
return;
}
if (serviceProvider.IsDisposedSlow())
if (serviceProvider.IsDisposed())
{
return;
}
@@ -73,7 +73,7 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
[ConstructorGenerated]
internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
where TEntityOnly : class, IEntityOnly<TEntity>
where TEntityOnly : class, IEntityAccess<TEntity>
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntityOnly>, new()
{
@@ -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,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Quartz;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Service;
using System.Globalization;
@@ -24,14 +25,23 @@ 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()
.AddInjections()
.AddAllHttpClients()
.AddConfiguredHttpClients()
// Discrete services
.AddSingleton<IMessenger, WeakReferenceMessenger>()

View File

@@ -33,28 +33,28 @@ 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)
{
RuntimeOptions runtimeOptions = provider.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))
static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
{
if (context.Database.GetPendingMigrations().Any())
{
System.Diagnostics.Debug.WriteLine("[Database] Performing AppDbContext Migrations");
context.Database.Migrate();
}
}
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
builder
.EnableSensitiveDataLogging()
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
.UseSqlite(sqlConnectionString);
// Temporarily create a context
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString))
{
if (context.Database.GetPendingMigrations().Any())
{
System.Diagnostics.Debug.WriteLine("[Database] Performing AppDbContext Migrations");
context.Database.Migrate();
}
}
builder
.EnableSensitiveDataLogging()
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
.UseSqlite(sqlConnectionString);
}
}
}

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