Compare commits

...

219 Commits

Author SHA1 Message Date
Lightczx
26748e5885 generic AdvancedCollectionView 2024-02-06 11:16:59 +08:00
DismissedLight
5f180846f6 refactor 2024-02-05 23:08:00 +08:00
qhy040404
95024e4107 refine #1334 2024-02-05 21:47:52 +08:00
DismissedLight
678ec191a6 Merge pull request #1364 from DGP-Studio/feat/notification_permission_disabled 2024-02-05 17:26:11 +08:00
Lightczx
40b26d9d3c code style 2024-02-05 17:25:59 +08:00
qhy040404
e5677e7de4 detect notification permission
And prevent the crash caused by lack of permission when sending toast
2024-02-05 16:53:50 +08:00
Lightczx
9ae8b182e7 fix empty ui alignment 2024-02-05 16:43:29 +08:00
DismissedLight
02fe498cce Merge pull request #1363 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-dd9cc865bd 2024-02-05 15:52:16 +08:00
Lightczx
f924f78ee2 code style 2024-02-05 15:52:00 +08:00
Lightczx
977e749939 rename style 2024-02-05 15:35:02 +08:00
dependabot[bot]
f8ad3b98fc Bump the packages group in /src/Snap.Hutao with 1 update
Bumps the packages group in /src/Snap.Hutao with 1 update: [MSTest.TestAdapter](https://github.com/microsoft/testfx).


Updates `MSTest.TestAdapter` from 3.1.1 to 3.2.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.1.1...v.3.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-05 07:13:34 +00:00
Lightczx
ee39f7fec5 refine all page ui 2024-02-05 13:55:16 +08:00
DismissedLight
c8c1d21c9b refine wiki avatar page 2024-02-04 23:29:01 +08:00
Lightczx
f06825f246 refine cultivation page 2024-02-04 11:23:52 +08:00
Lightczx
8954f7e325 support Asia length 10 uid 2024-02-04 09:30:07 +08:00
DismissedLight
976441de18 refine spiralabyss page 2024-02-03 23:13:25 +08:00
DismissedLight
60a49971f6 Merge pull request #1356 from DGP-Studio/feat/reorder 2024-02-03 21:50:20 +08:00
DismissedLight
fb5de92283 code style 2024-02-03 21:49:51 +08:00
qhy040404
046f3ace94 reorderable user 2024-02-03 17:28:50 +08:00
qhy040404
4c369ad0ad impl #1334 2024-02-03 17:28:50 +08:00
DismissedLight
7efaaae3e1 fix syntax tree parsing 2024-02-03 17:26:17 +08:00
Lightczx
446bdb2b49 syntax tree incompleted 2024-02-02 17:12:41 +08:00
Lightczx
20277b8b79 Add callconv on win32 apis 2024-02-02 10:38:40 +08:00
Lightczx
7baf125f88 Impl IReorderable 2024-02-02 10:03:46 +08:00
Lightczx
a4e782da78 refine #1316 2024-02-01 16:17:49 +08:00
Lightczx
d5551e5cdf fix #1316 2024-02-01 14:30:50 +08:00
Lightczx
f016a4a27f refine #1347 2024-02-01 09:42:55 +08:00
DismissedLight
8b931b6d89 Merge pull request #1349 from DGP-Studio/fix/1347
fix #1347
2024-02-01 09:23:40 +08:00
Lightczx
b942ceb914 codestyle 2024-02-01 09:22:52 +08:00
qhy040404
f7a49e52e0 fix #1347 2024-02-01 00:10:23 +08:00
qhy040404
d4bd610fe2 temporary fix qr login
let's play zzz :(
2024-01-31 23:00:07 +08:00
Lightczx
a3dcfd3804 metadata special name handling 2024-01-31 17:25:43 +08:00
Lightczx
592525d149 typo fix for #1344 2024-01-31 14:20:57 +08:00
Lightczx
83c4598df5 fix #1333 again 2024-01-31 11:37:48 +08:00
DismissedLight
aa680388ad refine ui 2024-01-30 21:59:42 +08:00
Lightczx
ba4f59de30 update dynamic proxy injection 2024-01-30 17:31:23 +08:00
Lightczx
8780cf385e move class 2024-01-30 15:58:18 +08:00
Lightczx
431cdd1253 refine achievement page 2024-01-30 14:11:54 +08:00
Lightczx
9a8827fb40 fix typo 2024-01-30 11:54:09 +08:00
DismissedLight
d88a6ca301 Merge pull request #1327 from DGP-Studio/l10n_develop 2024-01-30 11:29:56 +08:00
DismissedLight
5d401794e5 Merge branch 'develop' into l10n_develop 2024-01-30 11:29:48 +08:00
Lightczx
26396443dc add 4.4 tower wave description 2024-01-30 11:18:37 +08:00
Lightczx
6b755d934d setting width trigger performance 2024-01-30 09:26:43 +08:00
DismissedLight
7d5b057269 Merge pull request #1337 from Scighost/develop 2024-01-30 09:21:17 +08:00
Lightczx
917c173eb2 code style 2024-01-30 09:18:11 +08:00
Scighost
28d702422e Responsive setting page 2024-01-29 20:21:30 +08:00
Lightczx
7612ab5da3 fix #1331 2024-01-29 16:37:49 +08:00
Lightczx
ab436ecb2f add package convert directory permission override 2024-01-29 16:26:50 +08:00
Lightczx
457e3ff4d5 fix #1333 2024-01-29 16:05:53 +08:00
Lightczx
224c4e52ea Activation using NamedPipe 2024-01-29 15:51:09 +08:00
DismissedLight
2a5c7b21fd Merge pull request #1335 from DGP-Studio/dependabot/nuget/src/Snap.Hutao/develop/packages-5aeabc7cca 2024-01-29 15:50:29 +08:00
dependabot[bot]
53f8291a66 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.1.1 to 3.2.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.1.1...v.3.2.0)

Updates `MSTest.TestFramework` from 3.1.1 to 3.2.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.1.1...v.3.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 07:25:34 +00:00
DismissedLight
b621d5406a ui update 2024-01-28 21:13:50 +08:00
Masterain
12f4847aea New translations sh.resx (Portuguese) 2024-01-28 02:29:48 -08:00
Masterain
39a3d31f38 New translations sh.resx (Indonesian) 2024-01-28 02:29:47 -08:00
Masterain
70ac0b13a5 New translations sh.resx (English) 2024-01-28 02:29:46 -08:00
Masterain
a3520a4991 New translations sh.resx (Chinese Traditional) 2024-01-28 02:29:45 -08:00
Masterain
aed0284e4b New translations sh.resx (Russian) 2024-01-28 02:29:44 -08:00
Masterain
96ef07cbe5 New translations sh.resx (Korean) 2024-01-28 02:29:43 -08:00
Masterain
d1f37f37ac New translations sh.resx (Japanese) 2024-01-28 02:29:42 -08:00
DismissedLight
1fe09f3069 impl #1279 2024-01-28 17:44:15 +08:00
DismissedLight
32d9355c3a refine http related code 2024-01-27 23:11:41 +08:00
Masterain
71f170d51e New translations sh.resx (Portuguese) 2024-01-27 01:57:10 -08:00
Masterain
60015b6354 New translations sh.resx (Indonesian) 2024-01-27 01:57:08 -08:00
Masterain
eecae3ea4f New translations sh.resx (English) 2024-01-27 01:57:07 -08:00
Masterain
1831166f1e New translations sh.resx (Chinese Traditional) 2024-01-27 01:57:06 -08:00
Masterain
a98915ea24 New translations sh.resx (Russian) 2024-01-27 01:57:05 -08:00
Masterain
0d46656f57 New translations sh.resx (Korean) 2024-01-27 01:57:04 -08:00
Masterain
c814a5c28f New translations sh.resx (Japanese) 2024-01-27 01:57:03 -08:00
DismissedLight
9c3d59cc6f fix loopback 2024-01-27 13:52:48 +08:00
DismissedLight
890cf3f3ea Merge pull request #1286 from DGP-Studio/feat/feedback_network 2024-01-27 13:02:28 +08:00
DismissedLight
196bbb54c3 code style 2024-01-27 13:01:56 +08:00
DismissedLight
0481b9e474 pt resx as analyzer files 2024-01-27 09:56:35 +08:00
qhy040404
c4f3eb68e8 code style 2024-01-27 09:19:18 +08:00
qhy040404
c2e9f3a926 verify set 2024-01-27 00:47:09 +08:00
qhy040404
fb1fe3e40f add loopback status 2024-01-27 00:38:41 +08:00
qhy040404
75ed512e4a add current proxy to feedback page 2024-01-26 22:08:38 +08:00
Masterain
dd4dd33d93 New translations sh.resx (Japanese) 2024-01-26 01:47:29 -08:00
Lightczx
1e216e9823 refine com import 2024-01-26 17:18:57 +08:00
Lightczx
f823cb5f1a ui rework 2024-01-26 15:38:00 +08:00
Lightczx
2c6d25f0a3 limit update thread count 2024-01-25 17:22:32 +08:00
Masterain
817f768263 New translations sh.resx (Portuguese) 2024-01-25 00:36:14 -08:00
Masterain
2998fbb167 New translations sh.resx (English) 2024-01-25 00:36:12 -08:00
Masterain
8f0f94054d New translations sh.resx (Russian) 2024-01-25 00:36:10 -08:00
Lightczx
17a5d4d3a2 impl #1320 2024-01-25 15:59:52 +08:00
Lightczx
a1c604e68a add required win32 apis 2024-01-25 15:50:46 +08:00
Lightczx
948ec9a822 code style 2024-01-25 12:55:26 +08:00
Lightczx
f83174d690 com interface impl IUnknown 2024-01-25 11:24:17 +08:00
Lightczx
d686debbfb manually pinvoke from win32metadata 2024-01-25 11:01:45 +08:00
Lightczx
45248d75e1 pt-BR -> pt 2024-01-19 16:52:04 +08:00
Lightczx
22646cfab2 windows app sdk self contained 2024-01-19 16:25:57 +08:00
Lightczx
d0237a3c89 Add pt-BR locale 2024-01-19 15:41:11 +08:00
Tony
73c80fad10 Finish PT-BR translation (#1314) 2024-01-19 08:17:53 +08:00
Masterain
320bed9fcb New Crowdin updates (#1312)
* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Portuguese)
2024-01-17 15:10:26 -08:00
qhy040404
bb12aca3b4 Add PT translation (unfinished) (#1311)
Co-authored-by: t0piy <t.tony.br01@gmail.com>
Co-authored-by: Tony <66571593+t0piy@users.noreply.github.com>
2024-01-17 15:06:53 -08:00
DismissedLight
c7b5d98fb1 Merge pull request #1295 from DGP-Studio/fix/daily_note_task 2024-01-15 14:24:31 +08:00
qhy040404
7cc96f94f2 code style 2024-01-12 09:30:11 +08:00
qhy040404
b35355f9a3 improve daily note information 2024-01-11 20:39:02 +08:00
Masterain
745815657d New Crowdin updates (#1293) 2024-01-11 19:19:43 +08:00
DismissedLight
d93a9f41f3 bump version 2024-01-11 19:07:12 +08:00
qhy040404
910f099c6d reset debug console state (#1290)
Co-authored-by: DismissedLight <1686188646@qq.com>
2024-01-11 19:04:23 +08:00
Lightczx
e68449ec0c Update NativeMethods.txt 2024-01-10 17:06:23 +08:00
Lightczx
e484fbed21 update dependency 2024-01-10 10:42:35 +08:00
Masterain
88af6d28a9 New Crowdin updates (#1282)
* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)
2024-01-09 00:23:28 -08:00
Lightczx
3ab34f0992 Update HtmlDescriptionTextBlock.cs 2024-01-09 10:53:07 +08:00
Lightczx
5e875a7f18 add strings to resources 2 2024-01-09 10:38:33 +08:00
Lightczx
89d98748e8 add strings to resources 2024-01-09 10:10:31 +08:00
Masterain
d33cd894b9 New Crowdin updates (#1256)
* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)

* New translations sh.resx (Japanese)

* New translations sh.resx (Korean)

* New translations sh.resx (Russian)

* New translations sh.resx (Chinese Traditional)

* New translations sh.resx (English)

* New translations sh.resx (Indonesian)
2024-01-08 14:26:30 -08:00
DismissedLight
f0c19b419e Merge pull request #1276 from DGP-Studio/feat/dynamic_proxy 2024-01-08 22:20:30 +08:00
DismissedLight
f1d9787e45 fix method call 2024-01-08 22:18:07 +08:00
DismissedLight
5f9b4a7cb2 refactor RegistryWatcher 2024-01-08 21:55:59 +08:00
qhy040404
8710150897 use reflect to reduce code size 2024-01-08 20:19:05 +08:00
qhy040404
92c1b12764 dynamic proxy 2024-01-08 18:24:02 +08:00
Lightczx
d73bd557f3 remove settings appearance backdrop transparent 2024-01-08 16:52:37 +08:00
Lightczx
777d7d1056 remove winrt marshaller 2024-01-08 16:30:53 +08:00
Lightczx
1a944dae9c add transparent backdrop 2024-01-08 15:16:41 +08:00
Lightczx
a26c52ba97 typo 2024-01-08 11:43:45 +08:00
Lightczx
5fab03d57e Update FeedbackPage.xaml 2024-01-08 11:41:23 +08:00
Lightczx
e8a459cb41 refine #1039 2024-01-08 11:36:16 +08:00
DismissedLight
04df5a7bf1 impl #1039 2024-01-07 23:23:59 +08:00
DismissedLight
1ebcc2fc89 add documentation client 2024-01-07 14:49:02 +08:00
DismissedLight
e9917e788d Merge pull request #1273 from DGP-Studio/feat/identify_monitor 2024-01-06 23:39:41 +08:00
DismissedLight
9665876d52 code style 2 2024-01-06 23:38:26 +08:00
DismissedLight
8921816873 code style 2024-01-06 22:57:25 +08:00
DismissedLight
2698761594 fix convert game path 2024-01-06 20:03:14 +08:00
qhy040404
3ae4210ca0 add i18n 2024-01-06 18:32:39 +08:00
qhy040404
2f5e0cbe39 impl #1261 2024-01-06 18:25:10 +08:00
DismissedLight
d3444a9435 typo 2024-01-06 15:22:40 +08:00
DismissedLight
8b6f95c3d9 add package convert check 2024-01-06 15:21:51 +08:00
DismissedLight
88b8335e5b Merge pull request #1271 from DGP-Studio/feat/refresh_data_size 2024-01-05 23:52:55 +08:00
qhy040404
061aba715b refresh data folder size after deleting server cache 2024-01-05 23:50:28 +08:00
DismissedLight
da80631b72 code style 2024-01-05 23:28:35 +08:00
DismissedLight
97acf872bc remove status when game exited 2024-01-05 23:28:05 +08:00
DismissedLight
addaf1a9e3 Merge pull request #1270 from DGP-Studio/feat/launch-pipeline 2024-01-05 22:46:00 +08:00
DismissedLight
76183901da clean up 2024-01-05 22:33:10 +08:00
Lightczx
87ee81e7fa add handlers 2024-01-05 17:29:30 +08:00
DismissedLight
f2f858de15 create infrastructure 2024-01-04 22:51:58 +08:00
DismissedLight
c434521004 Merge pull request #1265 from DGP-Studio/fix/schedule 2024-01-04 16:03:54 +08:00
Lightczx
27ed2cefc1 fix #1242 2024-01-04 16:01:52 +08:00
qhy040404
6dc1e664b0 add task register check and delete script if register is failed 2024-01-04 13:32:43 +08:00
DismissedLight
51c3dde24b Merge pull request #1263 from DSakura207/main 2024-01-04 09:18:23 +08:00
DSakura207
2d497faaa5 Update Contributing.md 2024-01-03 18:35:47 -06:00
DSakura207
4783934b92 Add .vsconfig for installing workloads and extensions 2024-01-03 18:17:09 -06:00
DismissedLight
03d235876a Merge pull request #1260 from DGP-Studio/develop 2024-01-03 22:18:36 +08:00
DismissedLight
f49e9669af update version 2024-01-03 22:18:08 +08:00
DismissedLight
533c70caaa allow null package convert state 2024-01-03 21:53:13 +08:00
DismissedLight
dd59302bb3 fix bilibili server crash 2024-01-03 20:40:37 +08:00
DismissedLight
96e42f51f0 Merge pull request #1254 from DGP-Studio/develop 2024-01-03 20:02:54 +08:00
DismissedLight
5a19c19759 update version 2024-01-03 20:01:47 +08:00
DismissedLight
8fb831ef7c fix startup launch game card crash 2024-01-03 19:58:54 +08:00
Masterain
a30c8d8678 Update automation 2024-01-03 03:56:53 -08:00
DismissedLight
2655e835f8 1.9.2 package 2024-01-03 19:34:56 +08:00
qhy040404
ffd74703cd Update MGMT-publish.yml 2024-01-03 19:26:23 +08:00
DismissedLight
584465dc45 Merge pull request #1253 from DGP-Studio/develop 2024-01-03 19:14:57 +08:00
DismissedLight
a1e751160d Merge pull request #1235 from DGP-Studio/l10n_develop 2024-01-03 19:04:09 +08:00
DismissedLight
d78d2cf51a typo 2024-01-03 18:21:11 +08:00
Masterain
24709bfbf9 Update .gitlab-ci.yml 2024-01-03 01:48:36 -08:00
Lightczx
9be396b175 impl #1228 2024-01-03 17:09:51 +08:00
Lightczx
bb83e76d33 impl #1241 2024-01-03 16:17:27 +08:00
Lightczx
1ca24c8a78 remove unused using 2024-01-03 14:37:17 +08:00
DismissedLight
3d56aef221 Merge pull request #1250 from DGP-Studio/feat/1244 2024-01-03 14:26:35 +08:00
Lightczx
d43f2e76c4 code style 2024-01-03 14:26:21 +08:00
qhy040404
104fb9a3b0 finish up 2024-01-03 10:43:36 +08:00
qhy040404
d6b79584b6 streams need rework and resx 2024-01-02 23:21:11 +08:00
qhy040404
fcd0b65257 impl #1244 2024-01-02 21:15:24 +08:00
DismissedLight
802951edd7 impl #1055 2024-01-02 21:12:29 +08:00
DismissedLight
79fc42aa3b fix spinwait 2024-01-02 18:45:35 +08:00
Lightczx
fb0491dc57 get object form registry 2024-01-02 17:10:45 +08:00
Lightczx
b81d088379 rename script 2024-01-02 16:36:52 +08:00
qhy040404
553d267625 impl #1239 (#1246)
Co-authored-by: DismissedLight <1686188646@qq.com>
2024-01-02 14:26:21 +08:00
Lightczx
199e753103 use discord game sdk raw abi 2024-01-02 14:08:38 +08:00
qhy040404
48774960a7 Update GameRegistryContentTest.cs 2024-01-02 10:20:57 +08:00
DismissedLight
7bfea0e090 Create GameRegistryContentTest.cs 2024-01-01 23:21:38 +08:00
qhy040404
f0f9e387a8 direct to right doc 2024-01-01 19:35:01 +08:00
DismissedLight
f71a34a6be Merge pull request #1243 from DGP-Studio/fix/1208
fix #1208
2024-01-01 00:13:59 +08:00
DismissedLight
e6fd0b833b fix 1203 status deserialize 2023-12-31 23:59:55 +08:00
DismissedLight
d2c33cf19c optimize cache image placeholder presentation 2023-12-31 23:50:01 +08:00
qhy040404
59a7d6746f fix #1208 2023-12-31 23:36:29 +08:00
Masterain
1d074f5313 New translations sh.resx (Chinese Traditional) 2023-12-31 03:24:56 -08:00
Masterain
769a1c3812 New translations sh.resx (Russian) 2023-12-31 03:24:55 -08:00
Masterain
b54717fa9b New translations sh.resx (Japanese) 2023-12-31 03:24:54 -08:00
Masterain
ffa0b05a12 New translations sh.resx (Indonesian) 2023-12-30 02:47:33 -08:00
Masterain
d07a33f3e4 New translations sh.resx (English) 2023-12-29 02:30:33 -08:00
Lightczx
b49cd924d0 add source link 2023-12-29 13:51:27 +08:00
Lightczx
49db3003c9 fix launch game window 2023-12-29 11:56:44 +08:00
Lightczx
314c771020 clear selected game account after scheme changed 2023-12-29 11:39:01 +08:00
Lightczx
967f6f76f0 refuse convert for game in Program Files folder 2023-12-29 11:11:53 +08:00
Lightczx
5d05c31af5 fix startup crash 2023-12-29 10:05:03 +08:00
Masterain
bbd274c391 Update README.md (#1234)
* Update README.md

* Update README.md
2023-12-28 01:34:20 -08:00
Masterain
f8a8a929ac New translations sh.resx (Indonesian) 2023-12-28 01:21:13 -08:00
Masterain
cf3298dbd0 New translations sh.resx (English) 2023-12-28 01:21:12 -08:00
Masterain
a8b887def2 New translations sh.resx (Chinese Traditional) 2023-12-28 01:21:11 -08:00
Masterain
5a937b0838 New translations sh.resx (Russian) 2023-12-28 01:21:10 -08:00
Masterain
c016ae1cb8 New translations sh.resx (Korean) 2023-12-28 01:21:08 -08:00
Masterain
c7fdf8001d New translations sh.resx (Japanese) 2023-12-28 01:21:07 -08:00
Lightczx
64998453a1 Update LaunchGameViewModel.cs 2023-12-28 17:07:15 +08:00
Lightczx
9fdedd78d0 refactor Launch Game Pipeline 2023-12-28 17:06:45 +08:00
Lightczx
58e4d1b90e fix ci 2023-12-28 10:30:10 +08:00
Lightczx
e0d11bf9a0 impl #1199 2023-12-28 10:13:41 +08:00
Lightczx
51be2c76aa remove unused strings 2023-12-27 13:44:20 +08:00
DismissedLight
686d2378de Merge pull request #1232 from DGP-Studio/feat/elevate_restart 2023-12-27 13:34:48 +08:00
Lightczx
e2d5baffe0 remove INotifyPropertyChanged on TitleView 2023-12-27 13:33:01 +08:00
Lightczx
4001cc7051 code style 2023-12-27 13:31:21 +08:00
qhy040404
b106fe4729 add restart as admin 2023-12-27 10:44:10 +08:00
DismissedLight
d138d856e4 prepare 1203 types 2023-12-26 22:46:50 +08:00
DismissedLight
91f16c1701 impl #1230 2023-12-26 22:10:57 +08:00
DismissedLight
54d21b24f7 use package manager to update 2023-12-26 21:34:42 +08:00
Lightczx
268c2d0543 Update Snap.Hutao.csproj 2023-12-26 11:47:02 +08:00
Lightczx
acdcee7558 fix ci 2023-12-26 10:42:30 +08:00
Lightczx
371e469db7 optimize progress invocation 2023-12-26 10:36:59 +08:00
DismissedLight
22a974408d Merge pull request #1227 from DGP-Studio/feat/hotkey_flyout 2023-12-25 19:43:23 +08:00
DismissedLight
055b343571 fixup 2023-12-25 19:40:43 +08:00
qhy040404
84e56792b0 use flyout to show special keyboard keys 2023-12-25 19:26:59 +08:00
DismissedLight
da95b7837a Merge pull request #1218 from DGP-Studio/feat/goodbye_pwsh 2023-12-24 21:51:11 +08:00
DismissedLight
48ddb4c091 code style 2023-12-24 21:50:47 +08:00
qhy040404
ea95f2e2b1 say goodbye to powershell 2023-12-24 17:09:49 +08:00
DismissedLight
93077104b8 direct set registry value 2023-12-24 13:52:06 +08:00
DismissedLight
3ffdc901c7 fix server convert set game path null 2023-12-24 12:52:06 +08:00
DismissedLight
0d66c85744 remove redundant element 2023-12-23 20:42:35 +08:00
445 changed files with 22528 additions and 5937 deletions

View File

@@ -51,7 +51,7 @@ body:
description: |
在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
如果你的程序已经无法启动,请下载并运行[工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将显示你的设备 ID
如果你的程序已经无法启动,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将显示你的设备 ID
validations:
required: false
@@ -87,7 +87,7 @@ body:
label: 发生了什么?
description: |
详细的描述问题发生前后的行为,以便我们解决问题。**如果你的问题涉及程序崩溃,你应当检查 Windows 事件查看器,并将相关的 `.Net 错误`详情附上**
如果你无法找到该日志,请下载并运行[工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将转储问题日志至工具运行目录中的 `Snap.Hutao Error Log.txt`
如果你无法找到该日志,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将转储问题日志至工具运行目录中的 `Snap.Hutao Error Log.txt`
validations:
required: true

View File

@@ -51,7 +51,7 @@ body:
description: |
In Snap Hutao's settings page, you can find and copy your device ID
If your issue is about program crash, please fill this so we can dump the log and locate the source easier
If your program cannot startup, please download and run [this tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will shows your device ID.
If your program cannot startup, please download and run [Diagnosis Tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will shows your device ID.
validations:
required: false
@@ -87,7 +87,7 @@ body:
label: What Happened?
description: |
Describe your issue in detail to help us identify the issue. **If your issue is about program crash, you should check Windows Event Viewer, and attach associated `.Net Error` details here**If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID.
If you cannot find it, please download and run [this tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will dump the error log to `Snap.Hutao Error Log.txt` in the working directory of the tool.
If you cannot find it, please download and run [Diagnosis Tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will dump the error log to `Snap.Hutao Error Log.txt` in the working directory of the tool.
validations:
required: true

View File

@@ -21,17 +21,11 @@ body:
- [ ] 更新日志
- [ ] 功能文档更新
## 发布版本 [半自动]
- [ ] 在 GitHub 个人设置中更新 [Publish-Automate Beta PAT](https://github.com/settings/tokens?type=beta),有效期需小于预计发版需要天数
- [ ] 将更新的 PAT 更新至 Publish-Automate 库的 [Actions Secrets](https://github.com/DGP-Automation/Publish-Automate/settings/secrets/actions) 中
***
***
- [ ] 主分支合并入 release 分支
- [ ] 等待 Release 自动发布
- [ ] 检查极狐是否同步完成 Release
- [ ] 通知用户
- type: checkboxes
id: checklist-final

View File

@@ -12,6 +12,7 @@ on:
- '.gitmodules'
- '**.md'
- 'LICENSE'
- '**.yml'
jobs:
build:

View File

@@ -1,6 +1,7 @@
stages:
- fetch
- release
- refresh
Fetch:
stage: fetch
@@ -60,3 +61,16 @@ release:
- name: "$THIS_SHA256SUMS_NAME"
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_SHA256SUMS_NAME?inline=false"
link_type: other
- name: "artifact_archive"
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/download?file_type=archive"
link_type: other
Refresh:
stage: refresh
rules:
- if: $CI_COMMIT_TAG
needs:
- job: release
script:
- apt-get install -y curl
- curl -X PATCH "$PURGE_URL"

View File

@@ -4,13 +4,15 @@
### Setup Snap.Hutao Project
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/)
2. Open Visual Studio Installer to complete Visual Studio installation
- You need to install `.NET desktop development`, `Desktop development with C++` and `Universal Windows Platform development` components
3. Install `Single-project MSIX Packaging Tools for VS 2022` provided by Microsoft in Visual Studio marketplace
4. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device
5. Switch git branch to `develop`
6. Open project solution with your Visual Studio and then you are ready to go
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/).
- No need to select workloads; Visual Studio will handle it automatically.
- Close Visual Studio Installer to ensure a smooth installation experience for workloads.
- If using Visual Studio 2022 17.9 preview, skip step 5, as automatic extension installation is supported in this version.
2. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device.
3. Switch to the`develop` branch using git.
4. Open the project solution with your Visual Studio. Visual Studio will prompt you to install the necessary workloads, closing and reopening automatically.
5. (For Visual Studio 2022 17.8) Install the [Single-project MSIX Packaging Tools for VS 2022](https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17) provided by Microsoft in Visual Studio marketplace.
6. Open the project solution with your Visual Studio, and you are ready to go.
### Start Pull Request

View File

@@ -1,4 +1,4 @@
![Banner2-large](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/742129f4-f903-4d16-bf1e-3cbfad873ee4)
![HutaoRepoBanner2-20231222](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/2d178de1-95bc-44a1-a95e-20c5f11a8628)
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新 功能相结合,它提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
@@ -7,7 +7,29 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
## 下载使用 / Download
[<img src="https://get.microsoft.com/images/zh-cn%20light.svg" width="30%" height="30%">](https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52)
![](https://ci.appveyor.com/api/projects/status/n4s40t9llru4si9y?svg=true) [![GitHub Release](https://img.shields.io/github/release/DGP-Studio/Snap.Hutao?style=flat)](https://github.com/DGP-Studio/Snap.Hutao/releases/latest) [![Github All Releases](https://img.shields.io/github/downloads/DGP-Studio/Snap.Hutao/total.svg?style=flat)]()
---
#### 使用安装器安装 / Install with Snap.Hutao.Depolyment Installer
Snap.Hutao.Depolyment 是一个由 DGP-Studio 重新包装的 Windows 应用安装器,适用于缺少专业计算机知识的一般用户,可以在安装时同时解决缺少必要系统环境的问题。
Snap.Hutao.Depolyment is a Windows application installer repackaged by DGP-Studio for the users who lacks computer knowledge and can solve the problem of missing necessary system environment at the same time as the installation.
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao.Deployment/releases/latest)
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao.Deployment/-/releases)
#### 使用 MSIX 包安装 / Install with MSIX Package
直接使用 Snap Hutao MSIX 安装包,使用 Windows 内置的 App Installer 即可安装。如在安装中出现问题,请查阅我们的[常见问题](https://hut.ao/zh/advanced/FAQ.html)文档
Install with Snap Hutao MSIX package, can be installed with Windows built-in App Installer. If you faced any issue, please check our [FAQ](https://hut.ao/en/advanced/FAQ.html) document.
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao/releases/latest)
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao/-/releases)
## 贡献 / Contribute
@@ -22,9 +44,7 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
### 特定的原神项目 / Specific Genshin-related Projects
* [biuuu/genshin-wish-export](https://github.com/biuuu/genshin-wish-export)
* [xunkong/xunkong](https://github.com/xunkong/xunkong)
* [YuehaiTeam/cocogoat](https://github.com/YuehaiTeam/cocogoat)
* [Scighost/Starward](https://github.com/Scighost/Starward)
### 使用的技术栈 / Tech Stack
@@ -35,7 +55,6 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
* [dotnet/efcore](https://github.com/dotnet/efcore)
* [dotnet/runtime](https://github.com/dotnet/runtime)
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
* [microsoft/CsWin32](https://github.com/microsoft/CsWin32)
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)

View File

@@ -124,9 +124,6 @@ dotnet_diagnostic.SA1623.severity = none
# SA1636: File header copyright text should match
dotnet_diagnostic.SA1636.severity = none
# SA1414: Tuple types in signatures should have element names
dotnet_diagnostic.SA1414.severity = none
# SA0001: XML comment analysis disabled
dotnet_diagnostic.SA0001.severity = none
csharp_style_prefer_parameter_null_checking = true:suggestion
@@ -325,7 +322,6 @@ dotnet_diagnostic.CA2227.severity = suggestion
# CA2251: 使用 “string.Equals”
dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = true:suggestion
dotnet_diagnostic.SA1010.severity = none
[*.vb]
#### 命名样式 ####

11
src/Snap.Hutao/.vsconfig Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "1.0",
"components": [
"Microsoft.VisualStudio.Workload.ManagedDesktop",
"Microsoft.VisualStudio.Workload.NativeDesktop",
"Microsoft.VisualStudio.Workload.Universal"
],
"extensions": [
"https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17"
]
}

View File

@@ -0,0 +1,67 @@
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.Json;
namespace Snap.Hutao.Test.IncomingFeature;
[TestClass]
public class GameRegistryContentTest
{
private static readonly JsonSerializerOptions RegistryContentSerializerOptions = new()
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
[TestMethod]
[SupportedOSPlatform("windows")]
public void GetRegistryContent()
{
GetRegistryContentCore(@"Software\miHoYo\原神");
GetRegistryContentCore(@"Software\miHoYo\Genshin Impact");
}
[SupportedOSPlatform("windows")]
private static void GetRegistryContentCore(string subkey)
{
using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64))
{
RegistryKey? gameKey = key.OpenSubKey(subkey);
Assert.IsNotNull(gameKey);
Dictionary<string, object> data = [];
foreach (string valueName in gameKey.GetValueNames())
{
data[valueName] = gameKey.GetValueKind(valueName) switch
{
RegistryValueKind.DWord => (int)gameKey.GetValue(valueName)!,
RegistryValueKind.Binary => GetStringOrObject((byte[])gameKey.GetValue(valueName)!),
_ => throw new NotImplementedException()
};
}
Console.WriteLine($"Subkey: {subkey}");
Console.WriteLine(JsonSerializer.Serialize(data, RegistryContentSerializerOptions));
}
}
private static unsafe object GetStringOrObject(byte[] bytes)
{
fixed (byte* pByte = bytes)
{
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
string temp = Encoding.UTF8.GetString(span);
if (temp.AsSpan()[0] is '{' or '[')
{
return JsonSerializer.Deserialize<JsonElement>(temp);
}
return temp;
}
}
}

View File

@@ -160,9 +160,39 @@ public sealed class GeniusInvokationDecoding
ushort[] testKnownResult =
[
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
060,
019,
001,
079,
120,
120,
129,
151,
151,
153,
153,
181,
184,
184,
185,
185,
194,
194,
200,
200,
201,
201,
217,
217,
219,
241,
241,
244,
244,
245,
245,
270,
270,
];
CollectionAssert.AreEqual(resultArray, testKnownResult);

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
@@ -13,8 +13,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="MSTest.TestAdapter" Version="3.2.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.0" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,11 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
"allowMarshaling": true,
"useSafeHandles": false,
"comInterop": {
"preserveSigMethods": [
"IFileOpenDialog.Show",
"IFileSaveDialog.Show"
]
}
}

View File

@@ -1,92 +0,0 @@
// COMCTL32
DefSubclassProc
RemoveWindowSubclass
SetWindowSubclass
// DWMAPI
DwmSetWindowAttribute
// GDI32
GetDeviceCaps
// KERNEL32
AllocConsole
CloseHandle
CreateEventW
CreateRemoteThread
FreeConsole
GetConsoleMode
GetModuleHandleW
GetProcAddress
GetStdHandle
K32EnumProcessModules
K32GetModuleBaseNameW
K32GetModuleInformation
ReadProcessMemory
SetConsoleMode
SetConsoleTitle
SetEvent
VirtualAlloc
VirtualAllocEx
VirtualFree
VirtualFreeEx
WaitForSingleObject
WriteProcessMemory
// OLE32
CoCreateInstance
CoWaitForMultipleObjects
// SHELL32
SHCreateItemFromParsingName
// USER32
AttachThreadInput
FindWindowExW
GetCursorPos
GetDC
GetDpiForWindow
GetForegroundWindow
GetWindowPlacement
GetWindowThreadProcessId
ReleaseDC
RegisterHotKey
SendInput
SetForegroundWindow
UnregisterHotKey
// COM
FileOpenDialog
FileSaveDialog
IFileOpenDialog
IFileSaveDialog
IPersistFile
IShellLinkW
ShellLink
SHELL_LINK_DATA_FLAGS
// WinRT
IMemoryBufferByteAccess
// Const value
INFINITE
RPC_E_WRONG_THREAD
MAX_PATH
WM_GETMINMAXINFO
WM_HOTKEY
WM_NCRBUTTONDOWN
WM_NCRBUTTONUP
WM_NULL
// Type & Enum definition
HRESULT_FROM_WIN32
SLGP_FLAGS
// System.Threading
LPTHREAD_START_ROUTINE
// UI.WindowsAndMessaging
MINMAXINFO
// System.Com
CWMO_FLAGS

View File

@@ -1,17 +0,0 @@
using System;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
namespace Windows.Win32;
internal static partial class PInvoke
{
/// <inheritdoc cref="CoCreateInstance(Guid*, object, CLSCTX, Guid*, out object)"/>
internal static unsafe HRESULT CoCreateInstance<TClass, TInterface>(object? pUnkOuter, CLSCTX dwClsContext, out TInterface ppv)
where TInterface : class
{
HRESULT hr = CoCreateInstance(typeof(TClass).GUID, pUnkOuter, dwClsContext, typeof(TInterface).GUID, out object o);
ppv = (TInterface)o;
return hr;
}
}

View File

@@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="NativeMethods.json" />
<None Remove="NativeMethods.txt" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="NativeMethods.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using Windows.Win32.UI.WindowsAndMessaging;
[assembly: InternalsVisibleTo("Snap.Hutao")]
namespace Snap.Hutao.Win32;
/// <summary>
/// 结构体封送
/// </summary>
internal static class StructMarshal
{
/// <summary>
/// 构造一个新的 <see cref="Windows.Win32.UI.WindowsAndMessaging.WINDOWPLACEMENT"/>
/// </summary>
/// <returns>新的实例</returns>
public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
{
return new() { length = unchecked((uint)sizeof(WINDOWPLACEMENT)) };
}
}

View File

@@ -1,72 +0,0 @@
using System;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Windows.Win32.CsWin32.InteropServices;
internal class WinRTCustomMarshaler : ICustomMarshaler
{
private static readonly string? AssemblyFullName = typeof(Windows.Foundation.IMemoryBuffer).Assembly.FullName;
private readonly string className;
private bool lookedForFromAbi;
private MethodInfo? fromAbiMethod;
private WinRTCustomMarshaler(string className)
{
this.className = className;
}
public static ICustomMarshaler GetInstance(string cookie)
{
return new WinRTCustomMarshaler(cookie);
}
public void CleanUpManagedData(object ManagedObj)
{
}
public void CleanUpNativeData(nint pNativeData)
{
Marshal.Release(pNativeData);
}
public int GetNativeDataSize()
{
throw new NotSupportedException();
}
public nint MarshalManagedToNative(object ManagedObj)
{
throw new NotSupportedException();
}
public object MarshalNativeToManaged(nint thisPtr)
{
return className switch
{
"Windows.System.DispatcherQueueController" => Windows.System.DispatcherQueueController.FromAbi(thisPtr),
_ => MarshalNativeToManagedSlow(thisPtr),
};
}
private object MarshalNativeToManagedSlow(nint pNativeData)
{
if (!lookedForFromAbi)
{
Type? type = Type.GetType($"{className}, {AssemblyFullName}");
fromAbiMethod = type?.GetMethod("FromAbi");
lookedForFromAbi = true;
}
if (fromAbiMethod is not null)
{
return fromAbiMethod.Invoke(default, new object[] { pNativeData })!;
}
else
{
return Marshal.GetObjectForIUnknown(pNativeData);
}
}
}

View File

@@ -8,12 +8,11 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A95A964-04B1-477A-BDE7-505525B3CAD8}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.vsconfig = .vsconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Win32", "Snap.Hutao.Win32\Snap.Hutao.Win32.csproj", "{0F7ABEB2-5107-4037-B9DC-84D288FB0801}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -66,22 +65,6 @@ Global
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.Build.0 = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.ActiveCfg = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.Build.0 = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.ActiveCfg = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.Build.0 = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.ActiveCfg = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.Build.0 = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.ActiveCfg = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.Build.0 = Debug|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.Build.0 = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.ActiveCfg = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.Build.0 = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.ActiveCfg = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.Build.0 = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.ActiveCfg = Release|Any CPU
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -23,6 +23,7 @@
<ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/SettingsStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Thickness.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
@@ -32,6 +33,7 @@
x:Key="LargeGridViewItemStyle"
BasedOn="{StaticResource DefaultGridViewItemStyle}"
TargetType="GridViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
<Style

View File

@@ -6,6 +6,7 @@ using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Shell;
using System.Diagnostics;
@@ -36,8 +37,6 @@ public sealed partial class App : Application
----------------------------------------------------------------
""";
private const string AppInstanceKey = "main";
private readonly IServiceProvider serviceProvider;
private readonly IActivation activation;
private readonly ILogger<App> logger;
@@ -50,7 +49,6 @@ public sealed partial class App : Application
{
// Load app resource
InitializeComponent();
activation = serviceProvider.GetRequiredService<IActivation>();
logger = serviceProvider.GetRequiredService<ILogger<App>>();
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
@@ -64,25 +62,21 @@ public sealed partial class App : Application
try
{
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
AppInstance firstInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey);
if (firstInstance.IsCurrent)
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
{
logger.LogInformation(ConsoleBanner);
// manually invoke
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
activation.InitializeWith(firstInstance);
LogDiagnosticInformation();
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
}
else
{
// Redirect the activation (and args) to the "main" instance, and exit.
firstInstance.RedirectActivationTo(activatedEventArgs);
Process.GetCurrentProcess().Kill();
Exit();
return;
}
logger.LogInformation(ConsoleBanner);
LogDiagnosticInformation();
// manually invoke
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
activation.Initialize();
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
}
catch
{

View File

@@ -0,0 +1,768 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections;
using CommunityToolkit.WinUI.Helpers;
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Core.ExceptionService;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Reflection;
using System.Runtime.CompilerServices;
using Windows.Foundation;
using Windows.Foundation.Collections;
using NotifyCollectionChangedAction = System.Collections.Specialized.NotifyCollectionChangedAction;
namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPropertyChanged, ISupportIncrementalLoading, IComparer<T>
where T : class
{
private readonly List<T> view;
private readonly ObservableCollection<SortDescription> sortDescriptions;
private readonly Dictionary<string, PropertyInfo?> sortProperties;
private readonly bool liveShapingEnabled;
private readonly HashSet<string?> observedFilterProperties = [];
private IList<T> source;
private Predicate<T>? filter;
private int deferCounter;
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
public AdvancedCollectionView()
: this(new List<T>(0))
{
}
public AdvancedCollectionView(IList<T> source, bool isLiveShaping = false)
{
liveShapingEnabled = isLiveShaping;
view = [];
sortDescriptions = [];
sortDescriptions.CollectionChanged += SortDescriptionsCollectionChanged;
sortProperties = [];
Source = source;
}
public event EventHandler<object>? CurrentChanged;
public event CurrentChangingEventHandler? CurrentChanging;
public event PropertyChangedEventHandler? PropertyChanged;
public event VectorChangedEventHandler<object>? VectorChanged;
public IList<T> Source
{
get => source;
[MemberNotNull(nameof(source))]
set
{
if (ReferenceEquals(source, value))
{
return;
}
if (source is not null)
{
DetachPropertyChangedHandler(source);
}
source = value;
AttachPropertyChangedHandler(source);
sourceWeakEventListener?.Detach();
if (source is INotifyCollectionChanged sourceNotifyCollectionChanged)
{
sourceWeakEventListener = new WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>(this)
{
// Call the actual collection changed event
OnEventAction = (source, changed, arg3) => SourceNotifyCollectionChangedCollectionChanged(source, arg3),
// The source doesn't exist anymore
OnDetachAction = (listener) =>
{
ArgumentNullException.ThrowIfNull(sourceWeakEventListener);
sourceNotifyCollectionChanged.CollectionChanged -= sourceWeakEventListener.OnEvent;
},
};
sourceNotifyCollectionChanged.CollectionChanged += sourceWeakEventListener.OnEvent;
}
HandleSourceChanged();
OnPropertyChanged();
}
}
public int Count
{
get => view.Count;
}
public bool IsReadOnly
{
get => source is null || source.IsReadOnly;
}
public IObservableVector<object> CollectionGroups
{
get => default!;
}
public T? CurrentItem
{
get => CurrentPosition > -1 && CurrentPosition < view.Count ? view[CurrentPosition] : default;
set => MoveCurrentTo(value);
}
public int CurrentPosition { get; private set; }
public bool HasMoreItems
{
get => source is ISupportIncrementalLoading { HasMoreItems: true };
}
public bool IsCurrentAfterLast
{
get => CurrentPosition >= view.Count;
}
public bool IsCurrentBeforeFirst
{
get => CurrentPosition < 0;
}
public bool CanFilter
{
get => true;
}
public Predicate<T>? Filter
{
get => filter;
set
{
if (filter == value)
{
return;
}
filter = value;
HandleFilterChanged();
}
}
public bool CanSort
{
get => true;
}
public IList<SortDescription> SortDescriptions
{
get => sortDescriptions;
}
public IEnumerable<T> SourceCollection
{
get => source;
}
public IReadOnlyList<T> View
{
get => view;
}
public T this[int index]
{
get => view[index];
set => view[index] = value;
}
public void Refresh()
{
HandleSourceChanged();
}
public void RefreshFilter()
{
HandleFilterChanged();
}
public void RefreshSorting()
{
HandleSortChanged();
}
public IEnumerator<T> GetEnumerator()
{
return view.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return view.GetEnumerator();
}
public void Add(T item)
{
ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only.");
source.Add(item);
}
public void Clear()
{
ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only.");
source.Clear();
}
public bool Contains(T item)
{
return view.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
view.CopyTo(array, arrayIndex);
}
public bool Remove(T item)
{
ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only.");
source.Remove(item);
return true;
}
[SuppressMessage("", "SH007")]
public int IndexOf(T? item)
{
return view.IndexOf(item!);
}
public void Insert(int index, T item)
{
ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only.");
source.Insert(index, item);
}
public void RemoveAt(int index)
{
Remove(view[index]);
}
public bool MoveCurrentTo(T? item)
{
return (item is not null && item.Equals(CurrentItem)) || MoveCurrentToIndex(IndexOf(item));
}
public bool MoveCurrentToPosition(int index)
{
return MoveCurrentToIndex(index);
}
public bool MoveCurrentToFirst()
{
return MoveCurrentToIndex(0);
}
public bool MoveCurrentToLast()
{
return MoveCurrentToIndex(view.Count - 1);
}
public bool MoveCurrentToNext()
{
return MoveCurrentToIndex(CurrentPosition + 1);
}
public bool MoveCurrentToPrevious()
{
return MoveCurrentToIndex(CurrentPosition - 1);
}
public IAsyncOperation<LoadMoreItemsResult>? LoadMoreItemsAsync(uint count)
{
return (source as ISupportIncrementalLoading)?.LoadMoreItemsAsync(count);
}
public void ObserveFilterProperty(string propertyName)
{
observedFilterProperties.Add(propertyName);
}
public void ClearObservedFilterProperties()
{
observedFilterProperties.Clear();
}
public IDisposable DeferRefresh()
{
return new NotificationDeferrer(this);
}
int IComparer<T>.Compare(T? x, T? y)
{
if (sortProperties.Count <= 0)
{
Type listType = source.GetType();
Type? type;
if (listType.IsGenericType)
{
type = listType.GetGenericArguments()[0];
}
else
{
type = x?.GetType();
}
foreach (SortDescription sd in sortDescriptions)
{
if (!string.IsNullOrEmpty(sd.PropertyName))
{
sortProperties[sd.PropertyName] = type?.GetProperty(sd.PropertyName);
}
}
}
foreach (SortDescription sd in sortDescriptions)
{
T? cx, cy;
if (string.IsNullOrEmpty(sd.PropertyName))
{
cx = x;
cy = y;
}
else
{
PropertyInfo? pi = sortProperties[sd.PropertyName];
cx = (T?)pi?.GetValue(x);
cy = (T?)pi?.GetValue(y);
}
int cmp = sd.Comparer.Compare(cx, cy);
if (cmp is not 0)
{
return sd.Direction is SortDirection.Ascending ? +cmp : -cmp;
}
}
return 0;
}
internal void OnPropertyChanged([CallerMemberName] string propertyName = default!)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void ItemOnPropertyChanged(object? item, PropertyChangedEventArgs e)
{
if (!liveShapingEnabled)
{
return;
}
ArgumentNullException.ThrowIfNull(item);
T typedItem = (T)item;
bool? filterResult = filter?.Invoke(typedItem);
if (filterResult.HasValue && observedFilterProperties.Contains(e.PropertyName))
{
int viewIndex = view.IndexOf(typedItem);
if (viewIndex != -1 && !filterResult.Value)
{
RemoveFromView(viewIndex, typedItem);
}
else if (viewIndex == -1 && filterResult.Value)
{
int index = source.IndexOf(typedItem);
HandleItemAdded(index, typedItem);
}
}
if ((filterResult ?? true) && SortDescriptions.Any(sd => sd.PropertyName == e.PropertyName))
{
int oldIndex = view.IndexOf(typedItem);
// Check if item is in view:
if (oldIndex < 0)
{
return;
}
view.RemoveAt(oldIndex);
int targetIndex = view.BinarySearch(typedItem, this);
if (targetIndex < 0)
{
targetIndex = ~targetIndex;
}
// Only trigger expensive UI updates if the index really changed:
if (targetIndex != oldIndex)
{
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, oldIndex, typedItem));
view.Insert(targetIndex, typedItem);
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, targetIndex, typedItem));
}
else
{
view.Insert(targetIndex, typedItem);
}
}
else if (string.IsNullOrEmpty(e.PropertyName))
{
HandleSourceChanged();
}
}
private void AttachPropertyChangedHandler(IEnumerable items)
{
if (!liveShapingEnabled || items is null)
{
return;
}
foreach (object item in items)
{
if (item is INotifyPropertyChanged notifyPropertyChanged)
{
notifyPropertyChanged.PropertyChanged += ItemOnPropertyChanged;
}
}
}
private void DetachPropertyChangedHandler(IEnumerable items)
{
if (!liveShapingEnabled || items is null)
{
return;
}
foreach (object item in items)
{
if (item is INotifyPropertyChanged notifyPropertyChanged)
{
notifyPropertyChanged.PropertyChanged -= ItemOnPropertyChanged;
}
}
}
private void HandleSortChanged()
{
sortProperties.Clear();
view.Sort(this);
sortProperties.Clear();
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
}
private void HandleFilterChanged()
{
if (filter is not null)
{
for (int index = 0; index < view.Count; index++)
{
T item = view[index];
if (filter(item))
{
continue;
}
RemoveFromView(index, item);
index--;
}
}
HashSet<T> viewHash = new(view);
int viewIndex = 0;
for (int index = 0; index < source.Count; index++)
{
T item = source[index];
if (viewHash.Contains(item))
{
viewIndex++;
continue;
}
if (HandleItemAdded(index, item, viewIndex))
{
viewIndex++;
}
}
}
private void HandleSourceChanged()
{
sortProperties.Clear();
T? currentItem = CurrentItem;
view.Clear();
foreach (T item in Source)
{
if (filter is not null && !filter(item))
{
continue;
}
if (sortDescriptions.Count > 0)
{
int targetIndex = view.BinarySearch(item, this);
if (targetIndex < 0)
{
targetIndex = ~targetIndex;
}
view.Insert(targetIndex, item);
}
else
{
view.Add(item);
}
}
sortProperties.Clear();
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
MoveCurrentTo(currentItem);
}
private void SourceNotifyCollectionChangedCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
ArgumentNullException.ThrowIfNull(e.NewItems);
AttachPropertyChangedHandler(e.NewItems);
if (deferCounter <= 0)
{
if (e.NewItems?.Count == 1)
{
object? newItem = e.NewItems[0];
ArgumentNullException.ThrowIfNull(newItem);
HandleItemAdded(e.NewStartingIndex, (T)newItem);
}
else
{
HandleSourceChanged();
}
}
break;
case NotifyCollectionChangedAction.Remove:
ArgumentNullException.ThrowIfNull(e.OldItems);
DetachPropertyChangedHandler(e.OldItems);
if (deferCounter <= 0)
{
if (e.OldItems?.Count == 1)
{
object? oldItem = e.OldItems[0];
ArgumentNullException.ThrowIfNull(oldItem);
HandleItemRemoved(e.OldStartingIndex, (T)oldItem);
}
else
{
HandleSourceChanged();
}
}
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Reset:
if (deferCounter <= 0)
{
HandleSourceChanged();
}
break;
}
}
private bool HandleItemAdded(int newStartingIndex, T newItem, int? viewIndex = null)
{
if (filter is not null && !filter(newItem))
{
return false;
}
int newViewIndex = view.Count;
if (sortDescriptions.Count > 0)
{
sortProperties.Clear();
newViewIndex = view.BinarySearch(newItem, this);
if (newViewIndex < 0)
{
newViewIndex = ~newViewIndex;
}
}
else if (filter is not null)
{
if (source is null)
{
HandleSourceChanged();
return false;
}
if (newStartingIndex == 0 || view.Count == 0)
{
newViewIndex = 0;
}
else if (newStartingIndex == source.Count - 1)
{
newViewIndex = view.Count;
}
else if (viewIndex.HasValue)
{
newViewIndex = viewIndex.Value;
}
else
{
for (int i = 0, j = 0; i < source.Count; i++)
{
if (i == newStartingIndex)
{
newViewIndex = j;
break;
}
if (Equals(view[j], source[i]))
{
j++;
}
}
}
}
view.Insert(newViewIndex, newItem);
if (newViewIndex <= CurrentPosition)
{
CurrentPosition++;
}
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, newViewIndex, newItem));
return true;
}
private void HandleItemRemoved(int oldStartingIndex, T oldItem)
{
if (filter is not null && !filter(oldItem))
{
return;
}
if (oldStartingIndex < 0 || oldStartingIndex >= view.Count || !Equals(view[oldStartingIndex], oldItem))
{
oldStartingIndex = view.IndexOf(oldItem);
}
if (oldStartingIndex < 0)
{
return;
}
RemoveFromView(oldStartingIndex, oldItem);
}
private void RemoveFromView(int itemIndex, T item)
{
view.RemoveAt(itemIndex);
if (itemIndex <= CurrentPosition)
{
CurrentPosition--;
}
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, itemIndex, item));
}
private void SortDescriptionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (deferCounter > 0)
{
return;
}
HandleSortChanged();
}
private bool MoveCurrentToIndex(int i)
{
if (i < -1 || i >= view.Count)
{
return false;
}
if (i == CurrentPosition)
{
return false;
}
CurrentChangingEventArgs e = new();
OnCurrentChanging(e);
if (e.Cancel)
{
return false;
}
CurrentPosition = i;
OnCurrentChanged(default!);
return true;
}
private void OnCurrentChanging(CurrentChangingEventArgs e)
{
if (deferCounter > 0)
{
return;
}
CurrentChanging?.Invoke(this, e);
}
private void OnCurrentChanged(object e)
{
if (deferCounter > 0)
{
return;
}
CurrentChanged?.Invoke(this, e);
OnPropertyChanged(nameof(CurrentItem));
}
private void OnVectorChanged(IVectorChangedEventArgs e)
{
if (deferCounter > 0)
{
return;
}
VectorChanged?.Invoke(this, e);
OnPropertyChanged(nameof(Count));
}
internal sealed class NotificationDeferrer : IDisposable
{
private readonly AdvancedCollectionView<T> advancedCollectionView;
private readonly T? currentItem;
public NotificationDeferrer(AdvancedCollectionView<T> acvs)
{
advancedCollectionView = acvs;
currentItem = advancedCollectionView.CurrentItem;
advancedCollectionView.deferCounter++;
}
public void Dispose()
{
advancedCollectionView.MoveCurrentTo(currentItem);
advancedCollectionView.deferCounter--;
advancedCollectionView.Refresh();
}
}
}

View File

@@ -0,0 +1,105 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections;
using Microsoft.UI.Xaml.Data;
using System.Collections;
namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
where T : class
{
bool CanFilter { get; }
bool CanSort { get; }
object? ICollectionView.CurrentItem
{
get => CurrentItem;
}
new T? CurrentItem { get; }
Predicate<T>? Filter { get; set; }
IList<SortDescription> SortDescriptions { get; }
IEnumerable<T> SourceCollection { get; }
object IList<object>.this[int index]
{
get => this[index];
set => this[index] = (T)value;
}
new T this[int index] { get; set; }
void ICollection<object>.Add(object item)
{
Add((T)item);
}
void Add(T item);
void ClearObservedFilterProperties();
bool ICollection<object>.Contains(object item)
{
return Contains((T)item);
}
bool Contains(T item);
void ICollection<object>.CopyTo(object[] array, int arrayIndex)
{
CopyTo((T[])array, arrayIndex);
}
void CopyTo(T[] array, int arrayIndex);
IDisposable DeferRefresh();
IEnumerator<object> IEnumerable<object>.GetEnumerator()
{
return GetEnumerator();
}
new IEnumerator<T> GetEnumerator();
int IList<object>.IndexOf(object item)
{
return IndexOf((T)item);
}
int IndexOf(T item);
void IList<object>.Insert(int index, object item)
{
Insert(index, (T)item);
}
void Insert(int index, T item);
bool ICollectionView.MoveCurrentTo(object item)
{
return MoveCurrentTo((T)item);
}
bool MoveCurrentTo(T item);
void ObserveFilterProperty(string propertyName);
void Refresh();
void RefreshFilter();
void RefreshSorting();
bool ICollection<object>.Remove(object item)
{
return Remove((T)item);
}
bool Remove(T item);
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Foundation.Collections;
namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
internal sealed class VectorChangedEventArgs : IVectorChangedEventArgs
{
public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = null!)
{
CollectionChange = cc;
Index = (uint)index;
}
public CollectionChange CollectionChange { get; }
public uint Index { get; }
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("PaneCornerRadius", typeof(CornerRadius), default, nameof(OnPaneCornerRadiusChanged), IsAttached = true, AttachedType = typeof(NavigationView))]
public sealed partial class NavigationViewHelper
{
private static void OnPaneCornerRadiusChanged(DependencyObject dp, DependencyPropertyChangedEventArgs args)
{
NavigationView navigationView = (NavigationView)dp;
CornerRadius newValue = (CornerRadius)args.NewValue;
if (navigationView.IsLoaded)
{
SetNavigationViewPaneCornerRadius(navigationView, newValue);
return;
}
navigationView.Loaded += (s, e) =>
{
NavigationView loadedNavigationView = (NavigationView)s;
SetNavigationViewPaneCornerRadius(loadedNavigationView, newValue);
};
}
private static void SetNavigationViewPaneCornerRadius(NavigationView navigationView, CornerRadius value)
{
if (navigationView.FindDescendant("RootSplitView") is SplitView splitView)
{
splitView.CornerRadius = value;
}
}
}

View File

@@ -7,7 +7,6 @@ using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("LeftPanelMaxWidth", typeof(double), IsAttached = true, AttachedType = typeof(ScrollViewer))]
[DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))]
public sealed partial class ScrollViewerHelper
{

View File

@@ -20,4 +20,4 @@ public sealed partial class SettingsExpanderHelper
}
}
}
}
}

View File

@@ -40,12 +40,7 @@ internal sealed class CachedImage : Implementation.ImageEx
{
// The image is corrupted, remove it.
imageCache.Remove(imageUri);
return null;
}
catch (OperationCanceledException)
{
// task was explicitly canceled
return null;
return default;
}
}
}

View File

@@ -17,6 +17,7 @@
CornerRadius="{TemplateBinding CornerRadius}">
<Image
Name="PlaceholderImage"
Margin="{TemplateBinding PlaceholderMargin}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Source="{TemplateBinding PlaceholderSource}"

View File

@@ -11,10 +11,6 @@ using Windows.Foundation;
namespace Snap.Hutao.Control.Image.Implementation;
internal delegate void ImageExFailedEventHandler(object sender, ImageExFailedEventArgs e);
internal delegate void ImageExOpenedEventHandler(object sender, ImageExOpenedEventArgs e);
[SuppressMessage("", "CA1001")]
[SuppressMessage("", "SH003")]
[TemplateVisualState(Name = LoadingState, GroupName = CommonGroup)]
@@ -22,98 +18,34 @@ internal delegate void ImageExOpenedEventHandler(object sender, ImageExOpenedEve
[TemplateVisualState(Name = UnloadedState, GroupName = CommonGroup)]
[TemplateVisualState(Name = FailedState, GroupName = CommonGroup)]
[TemplatePart(Name = PartImage, Type = typeof(object))]
internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
[TemplatePart(Name = PartPlaceholderImage, Type = typeof(object))]
[DependencyProperty("Stretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("DecodePixelHeight", typeof(int), 0)]
[DependencyProperty("DecodePixelWidth", typeof(int), 0)]
[DependencyProperty("DecodePixelType", typeof(DecodePixelType), DecodePixelType.Physical)]
[DependencyProperty("IsCacheEnabled", typeof(bool), false)]
[DependencyProperty("EnableLazyLoading", typeof(bool), false, nameof(EnableLazyLoadingChanged))]
[DependencyProperty("LazyLoadingThreshold", typeof(double), default(double), nameof(LazyLoadingThresholdChanged))]
[DependencyProperty("PlaceholderSource", typeof(object), default(object))]
[DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("PlaceholderMargin", typeof(Thickness))]
[DependencyProperty("Source", typeof(object), default(object), nameof(SourceChanged))]
internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
{
protected const string PartImage = "Image";
protected const string PartPlaceholderImage = "PlaceholderImage";
protected const string CommonGroup = "CommonStates";
protected const string LoadingState = "Loading";
protected const string LoadedState = "Loaded";
protected const string UnloadedState = "Unloaded";
protected const string FailedState = "Failed";
private static readonly DependencyProperty StretchProperty = DependencyProperty.Register(nameof(Stretch), typeof(Stretch), typeof(ImageExBase), new PropertyMetadata(Stretch.Uniform));
private static readonly DependencyProperty DecodePixelHeightProperty = DependencyProperty.Register(nameof(DecodePixelHeight), typeof(int), typeof(ImageExBase), new PropertyMetadata(0));
private static readonly DependencyProperty DecodePixelTypeProperty = DependencyProperty.Register(nameof(DecodePixelType), typeof(int), typeof(ImageExBase), new PropertyMetadata(DecodePixelType.Physical));
private static readonly DependencyProperty DecodePixelWidthProperty = DependencyProperty.Register(nameof(DecodePixelWidth), typeof(int), typeof(ImageExBase), new PropertyMetadata(0));
private static readonly DependencyProperty IsCacheEnabledProperty = DependencyProperty.Register(nameof(IsCacheEnabled), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false));
private static readonly DependencyProperty EnableLazyLoadingProperty = DependencyProperty.Register(nameof(EnableLazyLoading), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false, EnableLazyLoadingChanged));
private static readonly DependencyProperty LazyLoadingThresholdProperty = DependencyProperty.Register(nameof(LazyLoadingThreshold), typeof(double), typeof(ImageExBase), new PropertyMetadata(default(double), LazyLoadingThresholdChanged));
private static readonly DependencyProperty PlaceholderSourceProperty = DependencyProperty.Register(nameof(PlaceholderSource), typeof(ImageSource), typeof(ImageExBase), new PropertyMetadata(default(ImageSource), PlaceholderSourceChanged));
private static readonly DependencyProperty PlaceholderStretchProperty = DependencyProperty.Register(nameof(PlaceholderStretch), typeof(Stretch), typeof(ImageExBase), new PropertyMetadata(default(Stretch)));
private static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(ImageExBase), new PropertyMetadata(null, SourceChanged));
private CancellationTokenSource? tokenSource;
private object? lazyLoadingSource;
private bool isInViewport;
public event ImageExFailedEventHandler? ImageExFailed;
public event ImageExOpenedEventHandler? ImageExOpened;
public event EventHandler? ImageExInitialized;
public bool IsInitialized { get; private set; }
public int DecodePixelHeight
{
get => (int)GetValue(DecodePixelHeightProperty);
set => SetValue(DecodePixelHeightProperty, value);
}
public DecodePixelType DecodePixelType
{
get => (DecodePixelType)GetValue(DecodePixelTypeProperty);
set => SetValue(DecodePixelTypeProperty, value);
}
public int DecodePixelWidth
{
get => (int)GetValue(DecodePixelWidthProperty);
set => SetValue(DecodePixelWidthProperty, value);
}
public Stretch Stretch
{
get => (Stretch)GetValue(StretchProperty);
set => SetValue(StretchProperty, value);
}
public bool IsCacheEnabled
{
get => (bool)GetValue(IsCacheEnabledProperty);
set => SetValue(IsCacheEnabledProperty, value);
}
public bool EnableLazyLoading
{
get => (bool)GetValue(EnableLazyLoadingProperty);
set => SetValue(EnableLazyLoadingProperty, value);
}
public double LazyLoadingThreshold
{
get => (double)GetValue(LazyLoadingThresholdProperty);
set => SetValue(LazyLoadingThresholdProperty, value);
}
public ImageSource PlaceholderSource
{
get => (ImageSource)GetValue(PlaceholderSourceProperty);
set => SetValue(PlaceholderSourceProperty, value);
}
public Stretch PlaceholderStretch
{
get => (Stretch)GetValue(PlaceholderStretchProperty);
set => SetValue(PlaceholderStretchProperty, value);
}
public object Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
public bool WaitUntilLoaded
{
get => true;
@@ -121,11 +53,9 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
protected object? Image { get; private set; }
public abstract CompositionBrush GetAlphaMask();
protected object? PlaceholderImage { get; private set; }
protected virtual void OnPlaceholderSourceChanged(DependencyPropertyChangedEventArgs e)
{
}
public abstract CompositionBrush GetAlphaMask();
protected virtual Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
@@ -136,61 +66,11 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(this, LoadedState, true);
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
}
protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e)
{
VisualStateManager.GoToState(this, FailedState, true);
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new FileNotFoundException(e.ErrorMessage)));
}
protected void AttachImageOpened(RoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageOpened += handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageOpened += handler;
}
}
protected void RemoveImageOpened(RoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageOpened -= handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageOpened -= handler;
}
}
protected void AttachImageFailed(ExceptionRoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageFailed += handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageFailed += handler;
}
}
protected void RemoveImageFailed(ExceptionRoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageFailed -= handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageFailed -= handler;
}
}
protected override void OnApplyTemplate()
@@ -199,11 +79,10 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
RemoveImageFailed(OnImageFailed);
Image = GetTemplateChild(PartImage);
PlaceholderImage = GetTemplateChild(PartPlaceholderImage);
IsInitialized = true;
ImageExInitialized?.Invoke(this, EventArgs.Empty);
if (Source is null || !EnableLazyLoading || isInViewport)
{
lazyLoadingSource = null;
@@ -218,23 +97,73 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
AttachImageFailed(OnImageFailed);
base.OnApplyTemplate();
void AttachImageOpened(RoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageOpened += handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageOpened += handler;
}
}
void AttachImageFailed(ExceptionRoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageFailed += handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageFailed += handler;
}
}
void RemoveImageOpened(RoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageOpened -= handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageOpened -= handler;
}
}
void RemoveImageFailed(ExceptionRoutedEventHandler handler)
{
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.ImageFailed -= handler;
}
else if (Image is ImageBrush brush)
{
brush.ImageFailed -= handler;
}
}
}
private static void EnableLazyLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageExBase control)
if (d is not ImageExBase control)
{
bool value = (bool)e.NewValue;
if (value)
{
control.LayoutUpdated += control.ImageExBase_LayoutUpdated;
return;
}
control.InvalidateLazyLoading();
}
else
{
control.LayoutUpdated -= control.ImageExBase_LayoutUpdated;
}
bool value = (bool)e.NewValue;
if (value)
{
control.LayoutUpdated += control.OnImageExBaseLayoutUpdated;
control.InvalidateLazyLoading();
}
else
{
control.LayoutUpdated -= control.OnImageExBaseLayoutUpdated;
}
}
@@ -246,14 +175,6 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
}
}
private static void PlaceholderSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageExBase control)
{
control.OnPlaceholderSourceChanged(e);
}
}
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ImageExBase control)
@@ -261,17 +182,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
return;
}
if (e.OldValue is null || e.NewValue is null || !e.OldValue.Equals(e.NewValue))
if (e.OldValue is not null && e.NewValue is not null && e.OldValue.Equals(e.NewValue))
{
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
{
control.lazyLoadingSource = null;
control.SetSource(e.NewValue);
}
else
{
control.lazyLoadingSource = e.NewValue;
}
return;
}
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
{
control.lazyLoadingSource = null;
control.SetSource(e.NewValue);
}
else
{
control.lazyLoadingSource = e.NewValue;
}
}
@@ -301,11 +224,24 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
{
VisualStateManager.GoToState(this, LoadedState, true);
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
}
}
[SuppressMessage("", "IDE0019")]
private void AttachPlaceholderSource(ImageSource? source)
{
// 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;
}
else if (PlaceholderImage is ImageBrush brush)
{
brush.ImageSource = source;
}
}
private async void SetSource(object? source)
{
if (!IsInitialized)
@@ -326,22 +262,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
VisualStateManager.GoToState(this, LoadingState, true);
ImageSource? imageSource = source as ImageSource;
if (imageSource is not null)
if (source as ImageSource is { } imageSource)
{
AttachSource(imageSource);
return;
}
Uri? uri = source as Uri;
if (uri is null)
if (source as Uri is not { } uri)
{
string? url = source as string ?? source.ToString();
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
{
VisualStateManager.GoToState(this, FailedState, true);
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new UriFormatException("Invalid uri specified.")));
return;
}
}
@@ -355,61 +288,131 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
{
await LoadImageAsync(uri, tokenSource.Token).ConfigureAwait(true);
}
catch (Exception ex)
{
SetPlaceholderSource(PlaceholderSource);
if (ex is OperationCanceledException)
{
// nothing to do as cancellation has been requested.
}
else
{
VisualStateManager.GoToState(this, FailedState, true);
}
}
}
private async void SetPlaceholderSource(object? source)
{
if (!IsInitialized)
{
return;
}
tokenSource?.Cancel();
tokenSource = new CancellationTokenSource();
AttachPlaceholderSource(null);
if (source is null)
{
return;
}
if (source as ImageSource is { } imageSource)
{
AttachPlaceholderSource(imageSource);
return;
}
if (source as Uri is not { } uri)
{
string? url = source as string ?? source.ToString();
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
{
return;
}
}
if (!IsHttpUri(uri) && !uri.IsAbsoluteUri)
{
uri = new Uri("ms-appx:///" + uri.OriginalString.TrimStart('/'));
}
try
{
if (uri is null)
{
return;
}
ImageSource? img = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
// Only attach our image if we still have a valid request.
AttachPlaceholderSource(img);
}
}
catch (OperationCanceledException)
{
// nothing to do as cancellation has been requested.
}
catch (Exception e)
catch
{
VisualStateManager.GoToState(this, FailedState, true);
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e));
}
}
private async Task LoadImageAsync(Uri imageUri, CancellationToken token)
{
if (imageUri is not null)
if (imageUri is null)
{
if (IsCacheEnabled)
return;
}
if (IsCacheEnabled)
{
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
// Only attach our image if we still have a valid request.
AttachSource(img);
}
}
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
{
string source = imageUri.OriginalString;
const string base64Head = "base64,";
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
if (index >= 0)
{
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
BitmapImage bitmap = new();
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
// Only attach our image if we still have a valid request.
AttachSource(img);
AttachSource(bitmap);
}
}
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
}
else
{
AttachSource(new BitmapImage(imageUri)
{
string source = imageUri.OriginalString;
const string base64Head = "base64,";
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
if (index >= 0)
{
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
BitmapImage bitmap = new();
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
AttachSource(bitmap);
}
}
}
else
{
AttachSource(new BitmapImage(imageUri)
{
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
});
}
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
});
}
}
private void ImageExBase_LayoutUpdated(object? sender, object e)
private void OnImageExBaseLayoutUpdated(object? sender, object e)
{
InvalidateLazyLoading();
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Image.Implementation;
internal sealed class ImageExFailedEventArgs : EventArgs
{
public ImageExFailedEventArgs(Exception errorException)
{
ErrorMessage = ErrorException?.Message;
ErrorException = errorException;
}
public Exception? ErrorException { get; private set; }
public string? ErrorMessage { get; private set; }
}

View File

@@ -1,8 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Image.Implementation;
internal sealed class ImageExOpenedEventArgs : EventArgs
{
}

View File

@@ -7,6 +7,7 @@ namespace Snap.Hutao.Control;
[TemplateVisualState(Name = "LoadingIn", GroupName = "CommonStates")]
[TemplateVisualState(Name = "LoadingOut", GroupName = "CommonStates")]
[TemplatePart(Name = "ContentGrid", Type = typeof(FrameworkElement))]
internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
{
public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(Loading), new PropertyMetadata(default(bool), IsLoadingPropertyChanged));

View File

@@ -3,7 +3,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shc="using:Snap.Hutao.Control">
<Style TargetType="shc:Loading">
<Style BasedOn="{StaticResource DefaultLoadingStyle}" TargetType="shc:Loading"/>
<Style x:Key="DefaultLoadingStyle" TargetType="shc:Loading">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>

View File

@@ -18,12 +18,15 @@ internal sealed class FontIconExtension : MarkupExtension
/// </summary>
public string Glyph { get; set; } = default!;
public double FontSize { get; set; } = 12;
/// <inheritdoc/>
protected override object ProvideValue()
{
return new FontIcon()
{
Glyph = Glyph,
FontSize = FontSize,
};
}
}

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Control.Media;
/// <summary>
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
/// </summary>
internal struct Hsl32
internal struct Hsla32
{
/// <summary>
/// The Hue in 0..360 range.

View File

@@ -46,13 +46,14 @@ internal struct Rgba32
/// <summary>
/// 使用 RGBA 代码初始化新的结构
/// </summary>
/// <param name="code">RGBA 代码</param>
public unsafe Rgba32(uint code)
/// <param name="xrgbaCode">RGBA 代码</param>
public unsafe Rgba32(uint xrgbaCode)
{
// RRGGBBAA -> AABBGGRR
// uint layout: 0xRRGGBBAA is AABBGGRR
// AABBGGRR -> RRGGBBAA
fixed (Rgba32* pSelf = &this)
{
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(code);
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(xrgbaCode);
}
}
@@ -66,14 +67,14 @@ internal struct Rgba32
public static unsafe implicit operator Color(Rgba32 hexColor)
{
// AABBGGRR -> BBGGRRAA
// AABBGGRR -> 000000AA
uint a = (*(uint*)&hexColor) >> 24;
// Goal : Rgba32:RRGGBBAA(0xAABBGGRR) -> Color: AARRGGBB(0xBBGGRRAA)
// Step1: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:AA000000(0x000000AA)
uint a = ((*(uint*)&hexColor) >> 24) & 0x000000FF;
// AABBGGRR -> BBGGRR00
uint rgb = (*(uint*)&hexColor) << 8;
// Step2: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:00RRGGBB(0xRRGGBB00)
uint rgb = ((*(uint*)&hexColor) << 8) & 0xFFFFFF00;
// BBGGRR00 + 000000AA
// Step2: UInt32:00RRGGBB(0xRRGGBB00) + UInt32:AA000000(0x000000AA) -> UInt32:AARRGGBB(0xRRGGBBAA)
uint rgba = rgb + a;
return *(Color*)&rgba;
@@ -84,7 +85,7 @@ internal struct Rgba32
/// </summary>
/// <param name="hsl">HSL 颜色</param>
/// <returns>RGBA8颜色</returns>
public static Rgba32 FromHsl(Hsl32 hsl)
public static Rgba32 FromHsl(Hsla32 hsl)
{
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
double h1 = hsl.H / 60;
@@ -141,7 +142,7 @@ internal struct Rgba32
/// 转换到 HSL 颜色
/// </summary>
/// <returns>HSL 颜色</returns>
public readonly Hsl32 ToHsl()
public readonly Hsla32 ToHsl()
{
const double toDouble = 1.0 / 255;
double r = toDouble * R;
@@ -174,7 +175,7 @@ internal struct Rgba32
double lightness = 0.5 * (max + min);
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
Hsl32 ret;
Hsla32 ret;
ret.H = 60 * h1;
ret.S = saturation;
ret.L = lightness;

View File

@@ -1,10 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.System.WinRT;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Win32;
using Windows.Win32.System.WinRT;
using WinRT;
namespace Snap.Hutao.Control.Media;
@@ -26,8 +25,7 @@ internal static class SoftwareBitmapExtension
{
using (IMemoryBufferReference reference = buffer.CreateReference())
{
reference.As<IMemoryBufferByteAccess>().GetBuffer(out byte* data, out uint length);
Span<Bgra32> bytes = new(data, unchecked((int)length / sizeof(Bgra32)));
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
foreach (ref Bgra32 pixel in bytes)
{
byte baseAlpha = pixel.A;

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Controls;
namespace Snap.Hutao.Control.Panel;
[DependencyProperty("MinItemWidth", typeof(double))]
internal sealed partial class UniformPanel : UniformGrid
{
public UniformPanel()
{
Columns = 1;
SizeChanged += OnSizeChanged;
}
private void OnSizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e)
{
Columns = (int)((e.NewSize.Width + ColumnSpacing) / (MinItemWidth + ColumnSpacing));
}
}

View File

@@ -7,7 +7,9 @@ using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Control.Media;
using Snap.Hutao.Control.Text.Syntax.MiHoYo;
using Snap.Hutao.Control.Theme;
using Snap.Hutao.Metadata;
using Windows.Foundation;
using Windows.UI;
@@ -15,20 +17,12 @@ namespace Snap.Hutao.Control.Text;
/// <summary>
/// 专用于呈现描述文本的文本块
/// Some part of this file came from:
/// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
/// </summary>
[HighQuality]
[DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))]
[DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))]
internal sealed partial class DescriptionTextBlock : ContentControl
{
private static readonly int ColorTagFullLength = "<color=#FFFFFFFF></color>".Length;
private static readonly int ColorTagLeftLength = "<color=#FFFFFFFF>".Length;
private static readonly int ItalicTagFullLength = "<i></i>".Length;
private static readonly int ItalicTagLeftLength = "<i>".Length;
private readonly TypedEventHandler<FrameworkElement, object> actualThemeChangedEventHandler;
/// <summary>
@@ -51,9 +45,15 @@ internal sealed partial class DescriptionTextBlock : ContentControl
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
ReadOnlySpan<char> description = (string)e.NewValue;
UpdateDescription(textBlock, description);
try
{
UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
}
catch (Exception ex)
{
_ = ex;
}
}
private static void OnTextStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -62,101 +62,106 @@ internal sealed partial class DescriptionTextBlock : ContentControl
textBlock.Style = (Style)e.NewValue;
}
private static void UpdateDescription(TextBlock textBlock, in ReadOnlySpan<char> description)
private static void UpdateDescription(TextBlock textBlock, MiHoYoSyntaxTree syntaxTree)
{
textBlock.Inlines.Clear();
AppendNode(textBlock, textBlock.Inlines, syntaxTree.Root);
}
int last = 0;
for (int i = 0; i < description.Length;)
private static void AppendNode(TextBlock textBlock, InlineCollection inlines, MiHoYoSyntaxNode node)
{
switch (node.Kind)
{
// newline
if (description[i..].StartsWith(@"\n"))
{
AppendText(textBlock, description[last..i]);
AppendLineBreak(textBlock);
i += 2;
last = i;
}
case MiHoYoSyntaxKind.Root:
foreach (MiHoYoSyntaxNode child in ((MiHoYoRootSyntax)node).Children)
{
AppendNode(textBlock, inlines, child);
}
// color tag
else if (description[i..].StartsWith("<c"))
{
AppendText(textBlock, description[last..i]);
Rgba32 color = new(description.Slice(i + 8, 8).ToString());
int length = description[(i + ColorTagLeftLength)..].IndexOf('<');
AppendColorText(textBlock, description.Slice(i + ColorTagLeftLength, length), color);
i += length + ColorTagFullLength;
last = i;
}
// italic
else if (description[i..].StartsWith("<i"))
{
AppendText(textBlock, description[last..i]);
int length = description[(i + ItalicTagLeftLength)..].IndexOf('<');
AppendItalicText(textBlock, description.Slice(i + ItalicTagLeftLength, length));
i += length + ItalicTagFullLength;
last = i;
}
else
{
i += 1;
}
if (i == description.Length - 1)
{
AppendText(textBlock, description[last..(i + 1)]);
}
break;
case MiHoYoSyntaxKind.PlainText:
AppendPlainText(textBlock, inlines, (MiHoYoPlainTextSyntax)node);
break;
case MiHoYoSyntaxKind.ColorText:
AppendColorText(textBlock, inlines, (MiHoYoColorTextSyntax)node);
break;
case MiHoYoSyntaxKind.ItalicText:
AppendItalicText(textBlock, inlines, (MiHoYoItalicTextSyntax)node);
break;
}
}
private static void AppendText(TextBlock text, in ReadOnlySpan<char> slice)
private static void AppendPlainText(TextBlock textBlock, InlineCollection inlines, MiHoYoPlainTextSyntax plainText)
{
text.Inlines.Add(new Run { Text = slice.ToString() });
// PlainText doesn't have children
inlines.Add(new Run { Text = plainText.Span.ToString() });
}
private static void AppendColorText(TextBlock text, in ReadOnlySpan<char> slice, Rgba32 color)
private static void AppendColorText(TextBlock textBlock, InlineCollection inlines, MiHoYoColorTextSyntax colorText)
{
Rgba32 color = new(colorText.ColorSpan.ToString());
Color targetColor;
if (ThemeHelper.IsDarkMode(text.ActualTheme))
if (ThemeHelper.IsDarkMode(textBlock.ActualTheme))
{
targetColor = color;
}
else
{
// Make lighter in light mode
Hsl32 hsl = color.ToHsl();
Hsla32 hsl = color.ToHsl();
hsl.L *= 0.3;
targetColor = Rgba32.FromHsl(hsl);
}
text.Inlines.Add(new Run
if (colorText.Children.Count > 1)
{
Text = slice.ToString(),
Foreground = new SolidColorBrush(targetColor),
});
Span span = new()
{
Foreground = new SolidColorBrush(targetColor),
};
foreach (MiHoYoSyntaxNode child in colorText.Children)
{
AppendNode(textBlock, span.Inlines, child);
}
}
else
{
inlines.Add(new Run
{
Text = colorText.ContentSpan.ToString(),
Foreground = new SolidColorBrush(targetColor),
});
}
}
private static void AppendItalicText(TextBlock text, in ReadOnlySpan<char> slice)
private static void AppendItalicText(TextBlock textBlock, InlineCollection inlines, MiHoYoItalicTextSyntax italicText)
{
text.Inlines.Add(new Run
if (italicText.Children.Count > 1)
{
Text = slice.ToString(),
FontStyle = Windows.UI.Text.FontStyle.Italic,
});
}
Span span = new()
{
FontStyle = Windows.UI.Text.FontStyle.Italic,
};
private static void AppendLineBreak(TextBlock text)
{
text.Inlines.Add(new LineBreak());
foreach (MiHoYoSyntaxNode child in italicText.Children)
{
AppendNode(textBlock, span.Inlines, child);
}
}
else
{
inlines.Add(new Run
{
Text = italicText.ContentSpan.ToString(),
FontStyle = Windows.UI.Text.FontStyle.Italic,
});
}
}
private void OnActualThemeChanged(FrameworkElement sender, object args)
{
// Simply re-apply texts
UpdateDescription((TextBlock)Content, Description);
UpdateDescription((TextBlock)Content, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle(Description)));
}
}

View File

@@ -139,7 +139,7 @@ internal sealed partial class HtmlDescriptionTextBlock : ContentControl
else
{
// Make lighter in light mode
Hsl32 hsl = color.ToHsl();
Hsla32 hsl = color.ToHsl();
hsl.L *= 0.3;
targetColor = Rgba32.FromHsl(hsl);
}
@@ -156,7 +156,7 @@ internal sealed partial class HtmlDescriptionTextBlock : ContentControl
text.Inlines.Add(new Run
{
Text = slice.ToString(),
FontWeight = FontWeights.Bold,
FontWeight = FontWeights.SemiBold,
});
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
internal enum MiHoYoColorKind
{
None,
Rgba,
Rgb,
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
{
public MiHoYoColorTextSyntax(MiHoYoColorKind colorKind, string text, int start, int end)
: base(MiHoYoSyntaxKind.ColorText, text, start, end)
{
ColorKind = colorKind;
}
public MiHoYoColorTextSyntax(MiHoYoColorKind colorKind, string text, in TextPosition position)
: base(MiHoYoSyntaxKind.ColorText, text, position)
{
ColorKind = colorKind;
}
public MiHoYoColorKind ColorKind { get; }
public override TextPosition ContentPosition
{
get
{
return ColorKind switch
{
MiHoYoColorKind.Rgba => new(Position.Start + 17, Position.End - 8),
MiHoYoColorKind.Rgb => new(Position.Start + 15, Position.End - 8),
_ => throw Must.NeverHappen(),
};
}
}
public TextPosition ColorPosition
{
get
{
return ColorKind switch
{
MiHoYoColorKind.Rgba => new(Position.Start + 8, Position.Start + 16),
MiHoYoColorKind.Rgb => new(Position.Start + 8, Position.Start + 14),
_ => throw Must.NeverHappen(),
};
}
}
public ReadOnlySpan<char> ColorSpan { get => Text.AsSpan()[ColorPosition.Start..ColorPosition.End]; }
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
internal sealed class MiHoYoItalicTextSyntax : MiHoYoXmlElementSyntax
{
public MiHoYoItalicTextSyntax(string text, int start, int end)
: base(MiHoYoSyntaxKind.ItalicText, text, start, end)
{
}
public MiHoYoItalicTextSyntax(string text, in TextPosition position)
: base(MiHoYoSyntaxKind.ItalicText, text, position)
{
}
public override TextPosition ContentPosition { get => new(Position.Start + 3, Position.End - 4); }
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
internal sealed class MiHoYoPlainTextSyntax : MiHoYoSyntaxNode
{
public MiHoYoPlainTextSyntax(string text, int start, int end)
: base(MiHoYoSyntaxKind.PlainText, text, start, end)
{
}
public MiHoYoPlainTextSyntax(string text, in TextPosition position)
: base(MiHoYoSyntaxKind.PlainText, text, position)
{
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
internal sealed class MiHoYoRootSyntax : MiHoYoSyntaxNode
{
public MiHoYoRootSyntax(string text, int start, int end)
: base(MiHoYoSyntaxKind.Root, text, start, end)
{
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
internal enum MiHoYoSyntaxKind
{
None,
Root,
PlainText,
ColorText,
ItalicText,
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
internal abstract class MiHoYoSyntaxNode : SyntaxNode<MiHoYoSyntaxNode, MiHoYoSyntaxKind>
{
public MiHoYoSyntaxNode(MiHoYoSyntaxKind kind, string text, int start, int end)
: base(kind, text, start, end)
{
}
public MiHoYoSyntaxNode(MiHoYoSyntaxKind kind, string text, in TextPosition position)
: base(kind, text, position)
{
}
}

View File

@@ -0,0 +1,146 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
internal sealed class MiHoYoSyntaxTree
{
public MiHoYoSyntaxNode Root { get; set; } = default!;
public string Text { get; set; } = default!;
public static MiHoYoSyntaxTree Parse(string text)
{
MiHoYoRootSyntax root = new(text, 0, text.Length);
ParseComponents(text, root);
MiHoYoSyntaxTree tree = new()
{
Text = text,
Root = root,
};
return tree;
}
private static void ParseComponents(string text, MiHoYoSyntaxNode syntax)
{
TextPosition contentPosition = syntax switch
{
MiHoYoXmlElementSyntax xmlSyntax => xmlSyntax.ContentPosition,
_ => syntax.Position,
};
ReadOnlySpan<char> contentSpan = text.AsSpan().Slice(contentPosition.Start, contentPosition.Length);
int endOfProcessedAtContent = 0;
while (true)
{
if (endOfProcessedAtContent >= contentSpan.Length)
{
break;
}
int indexOfXmlLeftOpeningAtUnprocessedContent = contentSpan[endOfProcessedAtContent..].IndexOf('<');
// End of content
if (indexOfXmlLeftOpeningAtUnprocessedContent < 0)
{
TextPosition position = new(contentPosition.Start + endOfProcessedAtContent, contentPosition.End);
MiHoYoPlainTextSyntax plainText = new(text, position);
syntax.Children.Add(plainText);
break;
}
// We have plain text between xml elements
if (indexOfXmlLeftOpeningAtUnprocessedContent > 0)
{
TextPosition position = new(0, indexOfXmlLeftOpeningAtUnprocessedContent);
TextPosition positionAtContent = position.RightShift(endOfProcessedAtContent);
TextPosition positionAtText = positionAtContent.RightShift(contentPosition.Start);
MiHoYoPlainTextSyntax plainText = new(text, positionAtText);
syntax.Children.Add(plainText);
endOfProcessedAtContent = positionAtContent.End;
continue;
}
// Peek the next character after '<'
int indexOfXmlLeftOpeningAtContent = endOfProcessedAtContent + indexOfXmlLeftOpeningAtUnprocessedContent;
switch (contentSpan[indexOfXmlLeftOpeningAtContent + 1])
{
case 'c':
{
int endOfXmlColorRightClosingAtUnprocessedContent = EndOfXmlClosing(contentSpan[indexOfXmlLeftOpeningAtContent..], out int endOfXmlColorLeftClosingAtUnprocessedContent);
MiHoYoColorKind colorKind = endOfXmlColorLeftClosingAtUnprocessedContent switch
{
17 => MiHoYoColorKind.Rgba,
15 => MiHoYoColorKind.Rgb,
_ => throw Must.NeverHappen(),
};
TextPosition position = new(0, endOfXmlColorRightClosingAtUnprocessedContent);
TextPosition positionAtContent = position.RightShift(endOfProcessedAtContent);
TextPosition positionAtText = positionAtContent.RightShift(contentPosition.Start);
MiHoYoColorTextSyntax colorText = new(colorKind, text, positionAtText);
ParseComponents(text, colorText);
syntax.Children.Add(colorText);
endOfProcessedAtContent = positionAtContent.End;
break;
}
case 'i':
{
int endOfXmlItalicRightClosingAtUnprocessedContent = EndOfXmlClosing(contentSpan[indexOfXmlLeftOpeningAtContent..], out _);
TextPosition position = new(0, endOfXmlItalicRightClosingAtUnprocessedContent);
TextPosition positionAtContent = position.RightShift(endOfProcessedAtContent);
TextPosition positionAtText = positionAtContent.RightShift(contentPosition.Start);
MiHoYoItalicTextSyntax italicText = new(text, positionAtText);
ParseComponents(text, italicText);
syntax.Children.Add(italicText);
endOfProcessedAtContent = positionAtContent.End;
break;
}
}
}
}
private static int EndOfXmlClosing(in ReadOnlySpan<char> span, out int endOfLeftClosing)
{
endOfLeftClosing = 0;
int openingCount = 0;
int closingCount = 0;
int current = 0;
// Considering <i>text1</i>text2<i>text3</i>
// Considering <i>text1<span>text2</span>text3</i>
while (true)
{
int leftMarkIndex = span[current..].IndexOf('<');
if (span[current..][leftMarkIndex + 1] is '/')
{
closingCount++;
}
else
{
openingCount++;
}
current += span[current..].IndexOf('>') + 1;
if (openingCount is 1 && closingCount is 0)
{
endOfLeftClosing = current;
}
if (openingCount == closingCount)
{
return current;
}
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
internal abstract class MiHoYoXmlElementSyntax : MiHoYoSyntaxNode
{
public MiHoYoXmlElementSyntax(MiHoYoSyntaxKind kind, string text, int start, int end)
: base(kind, text, start, end)
{
}
public MiHoYoXmlElementSyntax(MiHoYoSyntaxKind kind, string text, in TextPosition position)
: base(kind, text, position)
{
}
public abstract TextPosition ContentPosition { get; }
public ReadOnlySpan<char> ContentSpan { get => Text.AsSpan(ContentPosition.Start, ContentPosition.Length); }
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Text.Syntax;
internal abstract class SyntaxNode<TSelf, TKind>
where TSelf : SyntaxNode<TSelf, TKind>
where TKind : struct, Enum
{
public SyntaxNode(TKind kind, string text, int start, int end)
{
Kind = kind;
Text = text;
Position = new(start, end);
}
public SyntaxNode(TKind kind, string text, in TextPosition position)
{
Kind = kind;
Text = text;
Position = position;
}
public TKind Kind { get; protected set; }
public List<TSelf> Children { get; } = [];
public TextPosition Position { get; protected set; }
public ReadOnlySpan<char> Span { get => Text.AsSpan().Slice(Position.Start, Position.Length); }
protected string Text { get; set; } = default!;
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Control.Text.Syntax;
[DebuggerDisplay("[{Start}..{End}]")]
internal readonly struct TextPosition
{
public readonly int Start;
public readonly int End;
public TextPosition(int start, int end)
{
Start = start;
End = end;
}
public readonly int Length
{
get => End - Start;
}
public TextPosition LeftShift(int offset)
{
return new(Start - offset, End - offset);
}
public TextPosition RightShift(int offset)
{
return new(Start + offset, End + offset);
}
}

View File

@@ -18,16 +18,50 @@
Offset="0,4,0"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<Style x:Key="BorderCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
</Style>
<Style
x:Key="AcrylicSecondaryBorderCardStyle"
BasedOn="{StaticResource BorderCardStyle}"
TargetType="Border">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="{ThemeResource SystemControlChromeMediumAcrylicElementMediumBrush}"/>
</Style>
<Style
x:Key="AcrylicBorderCardStyle"
BasedOn="{StaticResource BorderCardStyle}"
TargetType="Border">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="{ThemeResource SystemControlAcrylicElementBrush}"/>
</Style>
<Style x:Key="GridCardStyle" TargetType="Grid">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
</Style>
</ResourceDictionary>
<Style
x:Key="AcrylicGridCardStyle"
BasedOn="{StaticResource GridCardStyle}"
TargetType="Grid">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="{ThemeResource SystemControlAcrylicElementBrush}"/>
</Style>
<Style
x:Key="AcrylicSecondaryGridCardStyle"
BasedOn="{StaticResource GridCardStyle}"
TargetType="Grid">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="{ThemeResource SystemControlChromeMediumAcrylicElementMediumBrush}"/>
</Style>
</ResourceDictionary>

View File

@@ -18,4 +18,10 @@
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="6"/>
</Style>
<Style
x:Key="FlyoutPresenterPadding16And10Style"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="16,10"/>
</Style>
</ResourceDictionary>

View File

@@ -2,6 +2,53 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shch="using:Snap.Hutao.Control.Helper">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<AcrylicBrush
x:Key="InfoBarErrorSeverityBackgroundBrush"
FallbackColor="#FDE7E9"
TintColor="#FDE7E9"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarWarningSeverityBackgroundBrush"
FallbackColor="#FFF4CE"
TintColor="#FFF4CE"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarSuccessSeverityBackgroundBrush"
FallbackColor="#DFF6DD"
TintColor="#DFF6DD"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarInformationalSeverityBackgroundBrush"
FallbackColor="#80F6F6F6"
TintColor="#80F6F6F6"
TintOpacity="0.6"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<AcrylicBrush
x:Key="InfoBarErrorSeverityBackgroundBrush"
FallbackColor="#442726"
TintColor="#442726"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarWarningSeverityBackgroundBrush"
FallbackColor="#433519"
TintColor="#433519"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarSuccessSeverityBackgroundBrush"
FallbackColor="#393D1B"
TintColor="#393D1B"
TintOpacity="0.6"/>
<AcrylicBrush
x:Key="InfoBarInformationalSeverityBackgroundBrush"
FallbackColor="#34424d"
TintColor="#34424d"
TintOpacity="0.6"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<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>

View File

@@ -20,6 +20,9 @@
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
<StackPanel Spacing="4"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="StackPanelSpacing8Template">
<StackPanel Spacing="8"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
<cwcont:UniformGrid
ColumnSpacing="2"

View File

@@ -9,6 +9,8 @@
<x:Double x:Key="ContentDialogMinHeight">64</x:Double>
<x:Double x:Key="LargeAppBarButtonWidth">100</x:Double>
<x:Double x:Key="AppBarThemeCompactActualHeight">50</x:Double>
<!-- ProgressBar -->
<x:Double x:Key="LargeBackgroundProgressBarOpacity">0.2</x:Double>
</ResourceDictionary>

View File

@@ -1,5 +1,758 @@
<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"
xmlns:cw="using:CommunityToolkit.WinUI">
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
<Thickness x:Key="PivotHeaderItemMargin">16,0,0,0</Thickness>
<Thickness x:Key="PivotItemMargin">0</Thickness>
<Style x:Key="CardPivotStyle" TargetType="Pivot">
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Background" Value="{ThemeResource PivotBackground}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Grid/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Pivot">
<Grid
x:Name="RootElement"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}">
<Grid.Resources>
<Style x:Key="BaseContentControlStyle" TargetType="ContentControl">
<Setter Property="FontFamily" Value="XamlAutoFontFamily"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<ContentPresenter
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
OpticalMarginAlignment="TrimSideBearings"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="TitleContentControlStyle"
BasedOn="{StaticResource BaseContentControlStyle}"
TargetType="ContentControl">
<Setter Property="FontFamily" Value="{ThemeResource PivotTitleFontFamily}"/>
<Setter Property="FontWeight" Value="{ThemeResource PivotTitleThemeFontWeight}"/>
<Setter Property="FontSize" Value="{ThemeResource PivotTitleFontSize}"/>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ContentControl
x:Name="TitleContentControl"
Grid.Row="0"
Margin="{StaticResource PivotPortraitThemePadding}"
Content="{TemplateBinding Title}"
ContentTemplate="{TemplateBinding TitleTemplate}"
IsTabStop="False"
Style="{StaticResource TitleContentControlStyle}"
Visibility="Collapsed"/>
<Grid Grid.Row="1">
<Grid.Resources>
<ControlTemplate x:Key="NextTemplate" TargetType="Button">
<Border
x:Name="Root"
Background="{ThemeResource PivotNextButtonBackground}"
BorderBrush="{ThemeResource PivotNextButtonBorderBrush}"
BorderThickness="{ThemeResource PivotNavButtonBorderThemeThickness}">
<FontIcon
x:Name="Arrow"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Foreground="{ThemeResource PivotNextButtonForeground}"
Glyph="&#xE76C;"
MirroredWhenRightToLeft="True"
UseLayoutRounding="False"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBackgroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBorderBrushPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonForegroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBackgroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBorderBrushPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonForegroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
<ControlTemplate x:Key="PreviousTemplate" TargetType="Button">
<Border
x:Name="Root"
Background="{ThemeResource PivotPreviousButtonBackground}"
BorderBrush="{ThemeResource PivotPreviousButtonBorderBrush}"
BorderThickness="{ThemeResource PivotNavButtonBorderThemeThickness}">
<FontIcon
x:Name="Arrow"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Foreground="{ThemeResource PivotPreviousButtonForeground}"
Glyph="&#xE76B;"
MirroredWhenRightToLeft="True"
UseLayoutRounding="False"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBackgroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBorderBrushPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonForegroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBackgroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBorderBrushPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonForegroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Grid.Resources>
<ScrollViewer
x:Name="ScrollViewer"
Margin="{TemplateBinding Padding}"
VerticalContentAlignment="Stretch"
BringIntoViewOnFocusChange="False"
HorizontalScrollBarVisibility="Hidden"
HorizontalSnapPointsAlignment="Center"
HorizontalSnapPointsType="MandatorySingle"
Template="{StaticResource ScrollViewerScrollBarlessTemplate}"
VerticalScrollBarVisibility="Disabled"
VerticalScrollMode="Disabled"
VerticalSnapPointsType="None"
ZoomMode="Disabled">
<PivotPanel x:Name="Panel" VerticalAlignment="Stretch">
<Grid x:Name="PivotLayoutElement">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.RenderTransform>
<CompositeTransform x:Name="PivotLayoutElementTranslateTransform"/>
</Grid.RenderTransform>
<ItemsPresenter x:Name="PivotItemPresenter" Grid.Row="1">
<ItemsPresenter.RenderTransform>
<TransformGroup>
<TranslateTransform x:Name="ItemsPresenterTranslateTransform"/>
<CompositeTransform x:Name="ItemsPresenterCompositeTransform"/>
</TransformGroup>
</ItemsPresenter.RenderTransform>
</ItemsPresenter>
<Border
Grid.Row="0"
Margin="16,16,16,0"
cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Grid Style="{ThemeResource AcrylicSecondaryGridCardStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="LeftHeaderPresenter"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{TemplateBinding LeftHeader}"
ContentTemplate="{TemplateBinding LeftHeaderTemplate}"/>
<ContentControl
x:Name="HeaderClipper"
Grid.Column="1"
HorizontalContentAlignment="Stretch"
UseSystemFocusVisuals="{StaticResource UseSystemFocusVisuals}">
<ContentControl.Clip>
<RectangleGeometry x:Name="HeaderClipperGeometry"/>
</ContentControl.Clip>
<Grid>
<Grid.RenderTransform>
<CompositeTransform x:Name="HeaderOffsetTranslateTransform"/>
</Grid.RenderTransform>
<PivotHeaderPanel x:Name="StaticHeader" Visibility="Collapsed">
<PivotHeaderPanel.RenderTransform>
<CompositeTransform x:Name="StaticHeaderTranslateTransform"/>
</PivotHeaderPanel.RenderTransform>
</PivotHeaderPanel>
<PivotHeaderPanel x:Name="Header">
<PivotHeaderPanel.RenderTransform>
<CompositeTransform x:Name="HeaderTranslateTransform"/>
</PivotHeaderPanel.RenderTransform>
</PivotHeaderPanel>
<Rectangle
x:Name="FocusFollower"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Control.IsTemplateFocusTarget="True"
Fill="Transparent"
IsHitTestVisible="False"/>
</Grid>
</ContentControl>
<Button
x:Name="PreviousButton"
Grid.Column="1"
Width="20"
Height="36"
Margin="{ThemeResource PivotNavButtonMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="Transparent"
IsEnabled="False"
IsTabStop="False"
Opacity="0"
Template="{StaticResource PreviousTemplate}"
UseSystemFocusVisuals="False"/>
<Button
x:Name="NextButton"
Grid.Column="1"
Width="20"
Height="36"
Margin="{ThemeResource PivotNavButtonMargin}"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="Transparent"
IsEnabled="False"
IsTabStop="False"
Opacity="0"
Template="{StaticResource NextTemplate}"
UseSystemFocusVisuals="False"/>
<ContentPresenter
x:Name="RightHeaderPresenter"
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{TemplateBinding RightHeader}"
ContentTemplate="{TemplateBinding RightHeaderTemplate}"/>
</Grid>
</Border>
</Grid>
</PivotPanel>
</ScrollViewer>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="Orientation">
<VisualState x:Name="Portrait">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl" Storyboard.TargetProperty="Margin">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPortraitThemePadding}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Landscape">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl" Storyboard.TargetProperty="Margin">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotLandscapeThemePadding}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="NavigationButtonsVisibility">
<VisualState x:Name="NavigationButtonsHidden"/>
<VisualState x:Name="NavigationButtonsVisible">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="PreviousButtonVisible">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="NextButtonVisible">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="HeaderStates">
<VisualState x:Name="HeaderDynamic"/>
<VisualState x:Name="HeaderStatic">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Header" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="StaticHeader" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="CardPivotStyle2" TargetType="Pivot">
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Background" Value="{ThemeResource PivotBackground}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Grid/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Pivot">
<Grid
x:Name="RootElement"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}">
<Grid.Resources>
<Style x:Key="BaseContentControlStyle" TargetType="ContentControl">
<Setter Property="FontFamily" Value="XamlAutoFontFamily"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<ContentPresenter
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
OpticalMarginAlignment="TrimSideBearings"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="TitleContentControlStyle"
BasedOn="{StaticResource BaseContentControlStyle}"
TargetType="ContentControl">
<Setter Property="FontFamily" Value="{ThemeResource PivotTitleFontFamily}"/>
<Setter Property="FontWeight" Value="{ThemeResource PivotTitleThemeFontWeight}"/>
<Setter Property="FontSize" Value="{ThemeResource PivotTitleFontSize}"/>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ContentControl
x:Name="TitleContentControl"
Grid.Row="0"
Margin="{StaticResource PivotPortraitThemePadding}"
Content="{TemplateBinding Title}"
ContentTemplate="{TemplateBinding TitleTemplate}"
IsTabStop="False"
Style="{StaticResource TitleContentControlStyle}"
Visibility="Collapsed"/>
<Grid Grid.Row="1">
<Grid.Resources>
<ControlTemplate x:Key="NextTemplate" TargetType="Button">
<Border
x:Name="Root"
Background="{ThemeResource PivotNextButtonBackground}"
BorderBrush="{ThemeResource PivotNextButtonBorderBrush}"
BorderThickness="{ThemeResource PivotNavButtonBorderThemeThickness}">
<FontIcon
x:Name="Arrow"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Foreground="{ThemeResource PivotNextButtonForeground}"
Glyph="&#xE76C;"
MirroredWhenRightToLeft="True"
UseLayoutRounding="False"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBackgroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBorderBrushPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonForegroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBackgroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBorderBrushPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonForegroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
<ControlTemplate x:Key="PreviousTemplate" TargetType="Button">
<Border
x:Name="Root"
Background="{ThemeResource PivotPreviousButtonBackground}"
BorderBrush="{ThemeResource PivotPreviousButtonBorderBrush}"
BorderThickness="{ThemeResource PivotNavButtonBorderThemeThickness}">
<FontIcon
x:Name="Arrow"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Foreground="{ThemeResource PivotPreviousButtonForeground}"
Glyph="&#xE76B;"
MirroredWhenRightToLeft="True"
UseLayoutRounding="False"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBackgroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBorderBrushPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonForegroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBackgroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBorderBrushPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonForegroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Grid.Resources>
<ScrollViewer
x:Name="ScrollViewer"
Margin="{TemplateBinding Padding}"
VerticalContentAlignment="Stretch"
BringIntoViewOnFocusChange="False"
HorizontalScrollBarVisibility="Hidden"
HorizontalSnapPointsAlignment="Center"
HorizontalSnapPointsType="MandatorySingle"
Template="{StaticResource ScrollViewerScrollBarlessTemplate}"
VerticalScrollBarVisibility="Disabled"
VerticalScrollMode="Disabled"
VerticalSnapPointsType="None"
ZoomMode="Disabled">
<PivotPanel x:Name="Panel" VerticalAlignment="Stretch">
<Grid x:Name="PivotLayoutElement">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.RenderTransform>
<CompositeTransform x:Name="PivotLayoutElementTranslateTransform"/>
</Grid.RenderTransform>
<ItemsPresenter x:Name="PivotItemPresenter" Grid.Row="1">
<ItemsPresenter.RenderTransform>
<TransformGroup>
<TranslateTransform x:Name="ItemsPresenterTranslateTransform"/>
<CompositeTransform x:Name="ItemsPresenterCompositeTransform"/>
</TransformGroup>
</ItemsPresenter.RenderTransform>
</ItemsPresenter>
<Border
Grid.Row="0"
Margin="16,16,16,0"
cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Grid Style="{ThemeResource AcrylicSecondaryGridCardStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="LeftHeaderPresenter"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{TemplateBinding LeftHeader}"
ContentTemplate="{TemplateBinding LeftHeaderTemplate}"/>
<ContentControl
x:Name="HeaderClipper"
Grid.Column="1"
HorizontalContentAlignment="Stretch"
UseSystemFocusVisuals="{StaticResource UseSystemFocusVisuals}">
<ContentControl.Clip>
<RectangleGeometry x:Name="HeaderClipperGeometry"/>
</ContentControl.Clip>
<Grid>
<Grid.RenderTransform>
<CompositeTransform x:Name="HeaderOffsetTranslateTransform"/>
</Grid.RenderTransform>
<PivotHeaderPanel x:Name="StaticHeader" Visibility="Collapsed">
<PivotHeaderPanel.RenderTransform>
<CompositeTransform x:Name="StaticHeaderTranslateTransform"/>
</PivotHeaderPanel.RenderTransform>
</PivotHeaderPanel>
<PivotHeaderPanel x:Name="Header">
<PivotHeaderPanel.RenderTransform>
<CompositeTransform x:Name="HeaderTranslateTransform"/>
</PivotHeaderPanel.RenderTransform>
</PivotHeaderPanel>
<Rectangle
x:Name="FocusFollower"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Control.IsTemplateFocusTarget="True"
Fill="Transparent"
IsHitTestVisible="False"/>
</Grid>
</ContentControl>
<Button
x:Name="PreviousButton"
Grid.Column="1"
Width="20"
Height="36"
Margin="{ThemeResource PivotNavButtonMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="Transparent"
IsEnabled="False"
IsTabStop="False"
Opacity="0"
Template="{StaticResource PreviousTemplate}"
UseSystemFocusVisuals="False"/>
<Button
x:Name="NextButton"
Grid.Column="1"
Width="20"
Height="36"
Margin="{ThemeResource PivotNavButtonMargin}"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="Transparent"
IsEnabled="False"
IsTabStop="False"
Opacity="0"
Template="{StaticResource NextTemplate}"
UseSystemFocusVisuals="False"/>
<ContentPresenter
x:Name="RightHeaderPresenter"
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{TemplateBinding RightHeader}"
ContentTemplate="{TemplateBinding RightHeaderTemplate}"/>
</Grid>
</Border>
</Grid>
</PivotPanel>
</ScrollViewer>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="Orientation">
<VisualState x:Name="Portrait">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl" Storyboard.TargetProperty="Margin">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPortraitThemePadding}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Landscape">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl" Storyboard.TargetProperty="Margin">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotLandscapeThemePadding}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="NavigationButtonsVisibility">
<VisualState x:Name="NavigationButtonsHidden"/>
<VisualState x:Name="NavigationButtonsVisible">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="PreviousButtonVisible">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="NextButtonVisible">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="Opacity">
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="IsEnabled">
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="HeaderStates">
<VisualState x:Name="HeaderDynamic"/>
<VisualState x:Name="HeaderStatic">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Header" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="StaticHeader" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -42,7 +42,7 @@
Grid.ColumnSpan="2"
Margin="{TemplateBinding Padding}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MaxWidth="{Binding Path=(shch:ScrollViewerHelper.LeftPanelMaxWidth), RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ScrollContentPresenter x:Name="ScrollContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}"/>

View File

@@ -6,6 +6,12 @@
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
<x:Double x:Key="SettingsCardContentControlMinWidth">120</x:Double>
<x:Double x:Key="SettingsCardContentControlMinWidth2">160</x:Double>
<x:Double x:Key="SettingsCardContentControlSpacing">10</x:Double>
<Thickness x:Key="SettingsCardAlignSettingsExpanderPadding">16,16,44,16</Thickness>
<Thickness x:Key="SettingsExpanderItemHasIconPadding">16,8,16,8</Thickness>
<Style
x:Key="SettingsSectionHeaderTextBlockStyle"
@@ -15,6 +21,14 @@
<Setter Property="Margin" Value="1,29,0,5"/>
</Style.Setters>
</Style>
<Style
x:Key="SettingsCardHeaderTextBlockStyle"
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
TargetType="TextBlock">
<Style.Setters>
<Setter Property="Margin" Value="1,0,0,5"/>
</Style.Setters>
</Style>
<Style
x:Key="SettingsContentComboBoxStyle"
BasedOn="{StaticResource DefaultComboBoxStyle}"

View File

@@ -0,0 +1,5 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Thickness x:Key="ListViewInSplitPanePadding">0,2</Thickness>
<!-- https://github.com/microsoft/microsoft-ui-xaml/issues/4811 -->
<x:Int32 x:Key="__DiscardPageOverride">0</x:Int32>
</ResourceDictionary>

View File

@@ -4,6 +4,7 @@
<x:String x:Key="DocumentLink_Home">https://hut.ao</x:String>
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String>
<x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String>
<x:String x:Key="DocumentLink_Loopback">https://hut.ao/zh/advanced/FAQ.html#%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87%E7%BD%91%E7%BB%9C%E4%BB%A3%E7%90%86%E4%BD%BF%E7%94%A8%E8%83%A1%E6%A1%83%E5%B7%A5%E5%85%B7%E7%AE%B1</x:String>
<!-- Other -->
<x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String>
@@ -27,9 +28,12 @@
<!-- EmotionIcon -->
<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_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>
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
<x:String x:Key="UI_EmotionIcon433">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon433.png</x:String>
<x:String x:Key="UI_EmotionIcon445">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
</ResourceDictionary>

View File

@@ -18,11 +18,11 @@ internal sealed class CommandLineBuilder
/// <summary>
/// 当符合条件时添加参数
/// </summary>
/// <param name="name">参数名称</param>
/// <param name="condition">条件</param>
/// <param name="name">参数名称</param>
/// <param name="value">值</param>
/// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIf(string name, bool condition, object? value = null)
public CommandLineBuilder AppendIf(bool condition, string name, object? value = null)
{
return condition ? Append(name, value) : this;
}
@@ -35,7 +35,7 @@ internal sealed class CommandLineBuilder
/// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIfNotNull(string name, object? value = null)
{
return AppendIf(name, value is not null, value);
return AppendIf(value is not null, name, value);
}
/// <summary>

View File

@@ -1,25 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core.Database;
internal sealed class ObservableReorderableDbCollection<T> : ObservableCollection<T>
where T : class, IReorderable
internal sealed class ObservableReorderableDbCollection<TEntity> : ObservableCollection<TEntity>
where TEntity : class, IReorderable
{
private readonly DbContext dbContext;
private bool previousChangeIsRemoved;
private readonly IServiceProvider serviceProvider;
public ObservableReorderableDbCollection(List<T> items, DbContext dbContext)
: base(AdjustIndex(items))
public ObservableReorderableDbCollection(List<TEntity> items, IServiceProvider serviceProvider)
: base(AdjustIndex(items.SortBy(x => x.Index)))
{
this.dbContext = dbContext;
this.serviceProvider = serviceProvider;
}
public IAdvancedCollectionView? View { get; set; }
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
@@ -27,26 +31,18 @@ internal sealed class ObservableReorderableDbCollection<T> : ObservableCollectio
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
previousChangeIsRemoved = true;
break;
case NotifyCollectionChangedAction.Add:
if (!previousChangeIsRemoved)
{
return;
}
OnReorder();
previousChangeIsRemoved = false;
break;
}
}
private static List<T> AdjustIndex(List<T> list)
private static List<TEntity> AdjustIndex(List<TEntity> list)
{
Span<T> span = CollectionsMarshal.AsSpan(list);
Span<TEntity> span = CollectionsMarshal.AsSpan(list);
for (int i = 0; i < list.Count; i++)
{
ref readonly T item = ref span[i];
ref readonly TEntity item = ref span[i];
item.Index = i;
}
@@ -55,12 +51,79 @@ internal sealed class ObservableReorderableDbCollection<T> : ObservableCollectio
private void OnReorder()
{
AdjustIndex((List<T>)Items);
DbSet<T> dbSet = dbContext.Set<T>();
foreach (ref readonly T item in CollectionsMarshal.AsSpan((List<T>)Items))
using (View?.DeferRefresh())
{
dbSet.UpdateAndSave(item);
AdjustIndex((List<TEntity>)Items);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
foreach (ref readonly TEntity item in CollectionsMarshal.AsSpan((List<TEntity>)Items))
{
dbSet.UpdateAndSave(item);
}
}
}
}
}
[SuppressMessage("", "SA1402")]
internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> : ObservableCollection<TEntityOnly>
where TEntityOnly : class, IEntityOnly<TEntity>
where TEntity : class, IReorderable
{
private readonly IServiceProvider serviceProvider;
public ObservableReorderableDbCollection(List<TEntityOnly> items, IServiceProvider serviceProvider)
: base(AdjustIndex(items.SortBy(x => x.Entity.Index)))
{
this.serviceProvider = serviceProvider;
}
public IAdvancedCollectionView? View { get; set; }
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Add:
OnReorder();
break;
}
}
private static List<TEntityOnly> AdjustIndex(List<TEntityOnly> list)
{
Span<TEntityOnly> span = CollectionsMarshal.AsSpan(list);
for (int i = 0; i < list.Count; i++)
{
ref readonly TEntityOnly item = ref span[i];
item.Entity.Index = i;
}
return list;
}
private void OnReorder()
{
using (View?.DeferRefresh())
{
AdjustIndex((List<TEntityOnly>)Items);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
foreach (ref readonly TEntityOnly item in CollectionsMarshal.AsSpan((List<TEntityOnly>)Items))
{
dbSet.UpdateAndSave(item.Entity);
}
}
}
}
}

View File

@@ -31,7 +31,7 @@ internal static class DependencyInjection
.AddJsonOptions()
.AddDatabase()
.AddInjections()
.AddHttpClients()
.AddAllHttpClients()
// Discrete services
.AddSingleton<IMessenger, WeakReferenceMessenger>()
@@ -48,10 +48,10 @@ internal static class DependencyInjection
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InitializeCulture(this IServiceProvider serviceProvider)
{
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
appOptions.PreviousCulture = CultureInfo.CurrentCulture;
CultureOptions cultureOptions = serviceProvider.GetRequiredService<CultureOptions>();
cultureOptions.SystemCulture = CultureInfo.CurrentCulture;
CultureInfo cultureInfo = appOptions.CurrentCulture;
CultureInfo cultureInfo = cultureOptions.CurrentCulture;
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;

View File

@@ -38,8 +38,8 @@ internal static class IocConfiguration
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
{
RuntimeOptions hutaoOptions = provider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(hutaoOptions.DataFolder, "Userdata.db");
RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
// Temporarily create a context

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.IO.Http.Proxy;
using Snap.Hutao.Web.Hoyolab;
using System.Net.Http;
@@ -14,12 +15,26 @@ internal static partial class IocHttpClientConfiguration
{
private const string ApplicationJson = "application/json";
/// <summary>
/// 添加 <see cref="HttpClient"/>
/// 此方法将会自动生成
/// </summary>
/// <param name="services">集合</param>
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddAllHttpClients(this IServiceCollection services)
{
services
.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler())
.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
{
HttpClientHandler clientHandler = (HttpClientHandler)handler;
clientHandler.UseProxy = true;
clientHandler.Proxy = provider.GetRequiredService<DynamicHttpProxy>();
});
})
.AddHttpClients();
return services;
}
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial IServiceCollection AddHttpClients(this IServiceCollection services);
/// <summary>
@@ -29,10 +44,10 @@ internal static partial class IocHttpClientConfiguration
/// <param name="client">配置后的客户端</param>
private static void DefaultConfiguration(IServiceProvider serviceProvider, HttpClient client)
{
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent);
client.DefaultRequestHeaders.UserAgent.ParseAdd(runtimeOptions.UserAgent);
}
/// <summary>
@@ -89,6 +104,7 @@ internal static partial class IocHttpClientConfiguration
/// HoYoLAB web
/// </summary>
/// <param name="client">配置后的客户端</param>
[SuppressMessage("", "IDE0051")]
private static void XRpc4Configuration(HttpClient client)
{
client.Timeout = Timeout.InfiniteTimeSpan;

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
internal sealed class HutaoException : Exception
{
public HutaoException(HutaoExceptionKind kind, string message, Exception? innerException)
: this(message, innerException)
{
Kind = kind;
}
private HutaoException(string message, Exception? innerException)
: base($"{message}\n{innerException?.Message}", innerException)
{
}
public HutaoExceptionKind Kind { get; private set; }
public static void ThrowIf(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
{
if (condition)
{
throw new HutaoException(kind, message, innerException);
}
}
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
internal enum HutaoExceptionKind
{
None,
}

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Package;
using System.IO;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.ExceptionService;
@@ -35,6 +36,22 @@ internal static class ThrowHelper
throw new GameFileOperationException(message, inner);
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static InvalidDataException InvalidData(string message, Exception? inner = default)
{
throw new InvalidDataException(message, inner);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void InvalidDataIf([DoesNotReturnIf(true)] bool condition, string message, Exception? inner = default)
{
if (condition)
{
throw new InvalidDataException(message, inner);
}
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static InvalidOperationException InvalidOperation(string message, Exception? inner = default)
@@ -49,6 +66,22 @@ internal static class ThrowHelper
throw new NotSupportedException();
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static NotSupportedException NotSupported(string message)
{
throw new NotSupportedException(message);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void NotSupportedIf(bool condition, string message)
{
if (condition)
{
throw new NotSupportedException(message);
}
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static OperationCanceledException OperationCanceled(string message, Exception? inner = default)

View File

@@ -0,0 +1,83 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.NetworkManagement.WindowsFirewall;
using Snap.Hutao.Win32.Security;
using System.Runtime.InteropServices;
using static Snap.Hutao.Win32.AdvApi32;
using static Snap.Hutao.Win32.ApiMsWinNetIsolation;
using static Snap.Hutao.Win32.Macros;
namespace Snap.Hutao.Core.IO.Http.Loopback;
[Injection(InjectAs.Singleton)]
internal sealed unsafe class LoopbackManager : ObservableObject
{
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
private readonly string hutaoContainerStringSID = default!;
private bool isLoopbackEnabled;
public LoopbackManager(IServiceProvider serviceProvider)
{
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
INET_FIREWALL_APP_CONTAINER* pContainers = default;
try
{
WIN32_ERROR error = NetworkIsolationEnumAppContainers(NETISO_FLAG.NETISO_FLAG_MAX, out uint acCount, out pContainers);
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
for (uint i = 0; i < acCount; i++)
{
INET_FIREWALL_APP_CONTAINER* pContainer = pContainers + i;
ReadOnlySpan<char> appContainerName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pContainer->appContainerName);
if (appContainerName.Equals(runtimeOptions.FamilyName, StringComparison.Ordinal))
{
ConvertSidToStringSidW(pContainer->appContainerSid, out PWSTR stringSid);
hutaoContainerStringSID = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid).ToString();
break;
}
}
}
finally
{
// This function returns 1 rather than 0 specfied in the document.
_ = NetworkIsolationFreeAppContainers(pContainers);
}
WIN32_ERROR error2 = NetworkIsolationGetAppContainerConfig(out uint accCount, out SID_AND_ATTRIBUTES* pSids);
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error2));
for (uint i = 0; i < accCount; i++)
{
ConvertSidToStringSidW((pSids + i)->Sid, out PWSTR stringSid);
ReadOnlySpan<char> stringSidSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid);
if (stringSidSpan.Equals(hutaoContainerStringSID, StringComparison.Ordinal))
{
IsLoopbackEnabled = true;
break;
}
}
}
public bool IsLoopbackEnabled { get => isLoopbackEnabled; private set => SetProperty(ref isLoopbackEnabled, value); }
public void EnableLoopback()
{
NetworkIsolationGetAppContainerConfig(out uint accCount, out SID_AND_ATTRIBUTES* pSids);
List<SID_AND_ATTRIBUTES> sids = new((int)(accCount + 1));
for (uint i = 0; i < accCount; i++)
{
sids.Add(*(pSids + i));
}
ConvertStringSidToSidW(hutaoContainerStringSID, out PSID pSid);
SID_AND_ATTRIBUTES sidAndAttributes = default;
sidAndAttributes.Sid = pSid;
sids.Add(sidAndAttributes);
IsLoopbackEnabled = NetworkIsolationSetAppContainerConfig(CollectionsMarshal.AsSpan(sids)) is WIN32_ERROR.ERROR_SUCCESS;
}
}

View File

@@ -0,0 +1,110 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Win32.Registry;
using System.Net;
using System.Reflection;
namespace Snap.Hutao.Core.IO.Http.Proxy;
[Injection(InjectAs.Singleton)]
internal sealed partial class DynamicHttpProxy : ObservableObject, IWebProxy, IDisposable
{
private const string ProxySettingPath = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections";
private static readonly Lazy<MethodInfo> LazyConstructSystemProxyMethod = new(GetConstructSystemProxyMethod);
private readonly IServiceProvider serviceProvider;
private readonly RegistryWatcher watcher;
private IWebProxy innerProxy = default!;
public DynamicHttpProxy(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
UpdateInnerProxy();
watcher = new(ProxySettingPath, OnSystemProxySettingsChanged);
watcher.Start();
}
public string CurrentProxyUri
{
get
{
Uri? proxyUri = GetProxy("https://hut.ao".ToUri());
return proxyUri is null
? SH.ViewPageFeedbackCurrentProxyNoProxyDescription
: proxyUri.AbsoluteUri;
}
}
public IWebProxy InnerProxy
{
get => innerProxy;
[MemberNotNull(nameof(innerProxy))]
set
{
if (ReferenceEquals(innerProxy, value))
{
return;
}
(innerProxy as IDisposable)?.Dispose();
innerProxy = value;
}
}
public ICredentials? Credentials
{
get => InnerProxy.Credentials;
set => InnerProxy.Credentials = value;
}
public Uri? GetProxy(Uri destination)
{
return InnerProxy.GetProxy(destination);
}
public bool IsBypassed(Uri host)
{
return InnerProxy.IsBypassed(host);
}
public void Dispose()
{
(innerProxy as IDisposable)?.Dispose();
watcher.Dispose();
}
public void OnSystemProxySettingsChanged()
{
UpdateInnerProxy();
// TaskContext can't be injected directly since there are some recursive dependencies.
ITaskContext taskContext = serviceProvider.GetRequiredService<ITaskContext>();
taskContext.BeginInvokeOnMainThread(() => OnPropertyChanged(nameof(CurrentProxyUri)));
}
private static MethodInfo GetConstructSystemProxyMethod()
{
Type? systemProxyInfoType = typeof(System.Net.Http.SocketsHttpHandler).Assembly.GetType("System.Net.Http.SystemProxyInfo");
ArgumentNullException.ThrowIfNull(systemProxyInfoType);
MethodInfo? constructSystemProxyMethod = systemProxyInfoType.GetMethod("ConstructSystemProxy", BindingFlags.Static | BindingFlags.Public);
ArgumentNullException.ThrowIfNull(constructSystemProxyMethod);
return constructSystemProxyMethod;
}
[MemberNotNull(nameof(innerProxy))]
private void UpdateInnerProxy()
{
IWebProxy? proxy = LazyConstructSystemProxyMethod.Value.Invoke(default, default) as IWebProxy;
ArgumentNullException.ThrowIfNull(proxy);
InnerProxy = proxy;
}
}

View File

@@ -18,6 +18,7 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
private readonly long contentLength;
private readonly int bufferSize;
private readonly SafeFileHandle destFileHandle;
private readonly int maxDegreeOfParallelism;
private readonly List<Shard> shards;
private HttpShardCopyWorker(HttpShardCopyWorkerOptions<TStatus> options)
@@ -28,6 +29,7 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
contentLength = options.ContentLength;
bufferSize = options.BufferSize;
destFileHandle = options.GetFileHandle();
maxDegreeOfParallelism = options.MaxDegreeOfParallelism;
shards = CalculateShards(contentLength);
static List<Shard> CalculateShards(long contentLength)
@@ -56,7 +58,11 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
public Task CopyAsync(IProgress<TStatus> progress, CancellationToken token = default)
{
ShardProgress shardProgress = new(progress, statusFactory, contentLength);
return Parallel.ForEachAsync(shards, token, (shard, token) => CopyShardAsync(shard, shardProgress, token));
ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = maxDegreeOfParallelism,
};
return Parallel.ForEachAsync(shards, parallelOptions, (shard, token) => CopyShardAsync(shard, shardProgress, token));
async ValueTask CopyShardAsync(Shard shard, IProgress<ShardStatus> progress, CancellationToken token)
{

View File

@@ -22,6 +22,8 @@ internal sealed class HttpShardCopyWorkerOptions<TStatus>
public int BufferSize { get; set; } = 80 * 1024;
public int MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount;
public SafeFileHandle GetFileHandle()
{
return File.OpenHandle(DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.RandomAccess | FileOptions.Asynchronous, ContentLength);

View File

@@ -11,11 +11,14 @@ namespace Snap.Hutao.Core.IO.Ini;
[HighQuality]
internal static class IniSerializer
{
/// <summary>
/// 反序列化
/// </summary>
/// <param name="fileStream">文件流</param>
/// <returns>Ini 元素集合</returns>
public static List<IniElement> DeserializeFromFile(string filePath)
{
using (FileStream readStream = File.OpenRead(filePath))
{
return Deserialize(readStream);
}
}
public static List<IniElement> Deserialize(FileStream fileStream)
{
List<IniElement> results = [];
@@ -50,11 +53,14 @@ internal static class IniSerializer
return results;
}
/// <summary>
/// 序列化
/// </summary>
/// <param name="fileStream">写入的流</param>
/// <param name="elements">元素</param>
public static void SerializeToFile(string filePath, IEnumerable<IniElement> elements)
{
using (FileStream writeStream = File.Create(filePath))
{
Serialize(writeStream, elements);
}
}
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
{
using (StreamWriter writer = new(fileStream))

View File

@@ -3,7 +3,7 @@
using CommunityToolkit.WinUI.Notifications;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Discord;
@@ -24,24 +24,9 @@ namespace Snap.Hutao.Core.LifeCycle;
[SuppressMessage("", "CA1001")]
internal sealed partial class Activation : IActivation
{
/// <summary>
/// 操作
/// </summary>
public const string Action = nameof(Action);
/// <summary>
/// Uid
/// </summary>
public const string Uid = nameof(Uid);
/// <summary>
/// 启动游戏启动参数
/// </summary>
public const string LaunchGame = nameof(LaunchGame);
/// <summary>
/// 从剪贴板导入成就
/// </summary>
public const string ImportUIAFFromClipboard = nameof(ImportUIAFFromClipboard);
private const string CategoryAchievement = "ACHIEVEMENT";
@@ -55,29 +40,20 @@ internal sealed partial class Activation : IActivation
private readonly SemaphoreSlim activateSemaphore = new(1);
/// <inheritdoc/>
public void Activate(object? sender, AppActivationArguments args)
public void Activate(HutaoActivationArguments args)
{
_ = sender;
if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
HandleActivationAsync(args, true).SafeForget();
return;
}
HandleActivationAsync(args).SafeForget();
}
/// <inheritdoc/>
public void NonRedirectToActivate(object? sender, AppActivationArguments args)
public void Initialize()
{
_ = sender;
if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
HandleActivationAsync(args, false).SafeForget();
}
}
/// <inheritdoc/>
public void InitializeWith(AppInstance appInstance)
{
appInstance.Activated += Activate;
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget();
ToastNotificationManagerCompat.OnActivated += NotificationActivate;
}
@@ -95,44 +71,40 @@ internal sealed partial class Activation : IActivation
}
}
private async ValueTask HandleActivationAsync(AppActivationArguments args, bool isRedirected)
private async ValueTask HandleActivationAsync(HutaoActivationArguments args)
{
if (activateSemaphore.CurrentCount > 0)
{
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
{
await HandleActivationCoreAsync(args, isRedirected).ConfigureAwait(false);
await HandleActivationCoreAsync(args).ConfigureAwait(false);
}
}
}
private async ValueTask HandleActivationCoreAsync(AppActivationArguments args, bool isRedirected)
private async ValueTask HandleActivationCoreAsync(HutaoActivationArguments args)
{
if (args.Kind == ExtendedActivationKind.Protocol)
if (args.Kind is HutaoActivationKind.Protocol)
{
if (args.TryGetProtocolActivatedUri(out Uri? uri))
{
await HandleUrlActivationAsync(uri, isRedirected).ConfigureAwait(false);
}
ArgumentNullException.ThrowIfNull(args.ProtocolActivatedUri);
await HandleUrlActivationAsync(args.ProtocolActivatedUri, args.IsRedirectTo).ConfigureAwait(false);
}
else if (args.Kind == ExtendedActivationKind.Launch)
else if (args.Kind is HutaoActivationKind.Launch)
{
if (args.TryGetLaunchActivatedArgument(out string? arguments))
ArgumentNullException.ThrowIfNull(args.LaunchActivatedArguments);
switch (args.LaunchActivatedArguments)
{
switch (arguments)
{
case LaunchGame:
{
await HandleLaunchGameActionAsync().ConfigureAwait(false);
break;
}
case LaunchGame:
{
await HandleLaunchGameActionAsync().ConfigureAwait(false);
break;
}
default:
{
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
break;
}
}
default:
{
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
break;
}
}
}
}
@@ -190,11 +162,11 @@ internal sealed partial class Activation : IActivation
serviceProvider
.GetRequiredService<IDiscordService>()
.SetNormalActivity()
.SetNormalActivityAsync()
.SafeForget();
}
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirected)
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirectTo)
{
UriBuilder builder = new(uri);
@@ -207,13 +179,13 @@ internal sealed partial class Activation : IActivation
case CategoryAchievement:
{
await WaitMainWindowAsync().ConfigureAwait(false);
await HandleAchievementActionAsync(action, parameter, isRedirected).ConfigureAwait(false);
await HandleAchievementActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
break;
}
case CategoryDailyNote:
{
await HandleDailyNoteActionAsync(action, parameter, isRedirected).ConfigureAwait(false);
await HandleDailyNoteActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
break;
}
@@ -225,10 +197,10 @@ internal sealed partial class Activation : IActivation
}
}
private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirected)
private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirectTo)
{
_ = parameter;
_ = isRedirected;
_ = isRedirectTo;
switch (action)
{
case UrlActionImport:
@@ -245,7 +217,7 @@ internal sealed partial class Activation : IActivation
}
}
private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirected)
private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirectTo)
{
_ = parameter;
switch (action)
@@ -264,7 +236,7 @@ internal sealed partial class Activation : IActivation
}
// Check if it's redirected.
if (!isRedirected)
if (!isRedirectTo)
{
// It's a direct open process, should exit immediately.
Process.GetCurrentProcess().Kill();

View File

@@ -36,7 +36,7 @@ internal static class AppActivationArgumentsExtensions
/// <param name="activatedEventArgs">应用程序激活参数</param>
/// <param name="arguments">参数</param>
/// <returns>是否存在参数</returns>
public static bool TryGetLaunchActivatedArgument(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
public static bool TryGetLaunchActivatedArguments(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
{
arguments = null;
if (activatedEventArgs.Data is not ILaunchActivatedEventArgs launchArgs)

View File

@@ -2,10 +2,11 @@
// Licensed under the MIT license.
using Microsoft.Windows.AppLifecycle;
using Windows.Win32.Foundation;
using Windows.Win32.Security;
using Windows.Win32.System.Com;
using static Windows.Win32.PInvoke;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.System.Com;
using static Snap.Hutao.Win32.ConstValues;
using static Snap.Hutao.Win32.Kernel32;
using static Snap.Hutao.Win32.Ole32;
namespace Snap.Hutao.Core.LifeCycle;
@@ -18,7 +19,7 @@ internal static class AppInstanceExtension
private static readonly WaitCallback RunActionWaitCallback = RunAction;
// Hold the reference here to prevent memory corruption.
private static HANDLE redirectEventHandle = HANDLE.Null;
private static HANDLE redirectEventHandle;
/// <summary>
/// 同步非阻塞重定向
@@ -27,19 +28,24 @@ internal static class AppInstanceExtension
/// <param name="args">参数</param>
public static unsafe void RedirectActivationTo(this AppInstance appInstance, AppActivationArguments args)
{
redirectEventHandle = CreateEvent(default(SECURITY_ATTRIBUTES*), true, false, null);
// use ThreadPool.UnsafeQueueUserWorkItem to cancel stacktrace
// like ExecutionContext.SuppressFlow
ThreadPool.UnsafeQueueUserWorkItem(RunActionWaitCallback, () =>
try
{
appInstance.RedirectActivationToAsync(args).AsTask().Wait();
SetEvent(redirectEventHandle);
});
redirectEventHandle = CreateEventW(default, true, false, default);
ReadOnlySpan<HANDLE> handles = new(ref redirectEventHandle);
CoWaitForMultipleObjects((uint)CWMO_FLAGS.CWMO_DEFAULT, INFINITE, handles, out uint _);
CloseHandle(redirectEventHandle);
// use ThreadPool.UnsafeQueueUserWorkItem to cancel stacktrace
// like ExecutionContext.SuppressFlow
ThreadPool.UnsafeQueueUserWorkItem(RunActionWaitCallback, () =>
{
appInstance.RedirectActivationToAsync(args).AsTask().Wait();
SetEvent(redirectEventHandle);
});
CoWaitForMultipleObjects(CWMO_FLAGS.CWMO_DEFAULT, INFINITE, [redirectEventHandle], out uint _);
}
finally
{
CloseHandle(redirectEventHandle);
}
}
[SuppressMessage("", "SH007")]

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Windows.Win32.Foundation;
using Snap.Hutao.Win32.Foundation;
using WinRT.Interop;
namespace Snap.Hutao.Core.LifeCycle;
@@ -19,6 +19,6 @@ internal static class CurrentWindowReferenceExtension
{
return reference.Window is IWindowOptionsSource optionsSource
? optionsSource.WindowOptions.Hwnd
: (HWND)WindowNative.GetWindowHandle(reference.Window);
: WindowNative.GetWindowHandle(reference.Window);
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Windows.AppLifecycle;
namespace Snap.Hutao.Core.LifeCycle;
internal sealed class HutaoActivationArguments
{
public bool IsRedirectTo { get; set; }
public HutaoActivationKind Kind { get; set; }
public Uri? ProtocolActivatedUri { get; set; }
public string? LaunchActivatedArguments { get; set; }
public static HutaoActivationArguments FromAppActivationArguments(AppActivationArguments args, bool isRedirected = false)
{
HutaoActivationArguments result = new()
{
IsRedirectTo = isRedirected,
};
switch (args.Kind)
{
case ExtendedActivationKind.Launch:
{
result.Kind = HutaoActivationKind.Launch;
if (args.TryGetLaunchActivatedArguments(out string? arguments))
{
result.LaunchActivatedArguments = arguments;
}
break;
}
case ExtendedActivationKind.Protocol:
{
result.Kind = HutaoActivationKind.Protocol;
if (args.TryGetProtocolActivatedUri(out Uri? uri))
{
result.ProtocolActivatedUri = uri;
}
break;
}
}
return result;
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle;
internal enum HutaoActivationKind
{
None,
Launch,
Protocol,
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Windows.AppLifecycle;
namespace Snap.Hutao.Core.LifeCycle;
/// <summary>
@@ -10,24 +8,7 @@ namespace Snap.Hutao.Core.LifeCycle;
/// </summary>
internal interface IActivation
{
/// <summary>
/// 响应激活事件
/// 激活事件一般不会在UI线程上触发
/// </summary>
/// <param name="sender">发送方</param>
/// <param name="args">激活参数</param>
void Activate(object? sender, AppActivationArguments args);
void Activate(HutaoActivationArguments args);
/// <summary>
/// 使用当前 App 实例初始化激活
/// </summary>
/// <param name="appInstance">App 实例</param>
void InitializeWith(AppInstance appInstance);
/// <summary>
/// 无转发触发激活事件
/// </summary>
/// <param name="sender">发送方</param>
/// <param name="args">激活参数</param>
void NonRedirectToActivate(object? sender, AppActivationArguments args);
void Initialize();
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO.Pipes;
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
internal static class NamedPipeClientStreamExtension
{
public static bool TryConnectOnce(this NamedPipeClientStream clientStream)
{
try
{
clientStream.Connect(TimeSpan.Zero);
return true;
}
catch (TimeoutException)
{
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
internal enum PipePacketCommand : byte
{
None = 0,
RedirectActivation = 10,
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
internal enum PipePacketContentType : byte
{
None = 0,
Json = 1,
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct PipePacketHeader
{
public byte Version;
public PipePacketType Type;
public PipePacketCommand Command;
public PipePacketContentType ContentType;
public int ContentLength;
public ulong Checksum;
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
internal enum PipePacketType : byte
{
None = 0,
Request = 1,
Response = 2,
Termination = 3,
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Windows.AppLifecycle;
using System.IO.Hashing;
using System.IO.Pipes;
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
[Injection(InjectAs.Singleton)]
internal sealed class PrivateNamedPipeClient : IDisposable
{
private readonly NamedPipeClientStream clientStream = new(".", "Snap.Hutao.PrivateNamedPipe", PipeDirection.InOut, PipeOptions.Asynchronous | PipeOptions.WriteThrough);
public unsafe bool TryRedirectActivationTo(AppActivationArguments args)
{
if (clientStream.TryConnectOnce())
{
{
PipePacketHeader redirectActivationPacket = default;
redirectActivationPacket.Version = 1;
redirectActivationPacket.Type = PipePacketType.Request;
redirectActivationPacket.Command = PipePacketCommand.RedirectActivation;
redirectActivationPacket.ContentType = PipePacketContentType.Json;
HutaoActivationArguments hutaoArgs = HutaoActivationArguments.FromAppActivationArguments(args, isRedirected: true);
byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(hutaoArgs);
redirectActivationPacket.ContentLength = jsonBytes.Length;
redirectActivationPacket.Checksum = XxHash64.HashToUInt64(jsonBytes);
clientStream.Write(new(&redirectActivationPacket, sizeof(PipePacketHeader)));
clientStream.Write(jsonBytes);
}
{
PipePacketHeader terminationPacket = default;
terminationPacket.Version = 1;
terminationPacket.Type = PipePacketType.Termination;
clientStream.Write(new(&terminationPacket, sizeof(PipePacketHeader)));
}
clientStream.Flush();
return true;
}
return false;
}
public void Dispose()
{
clientStream.Dispose();
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
[Injection(InjectAs.Singleton)]
[ConstructorGenerated]
internal sealed partial class PrivateNamedPipeMessageDispatcher
{
private readonly IServiceProvider serviceProvider;
public void RedirectActivation(HutaoActivationArguments? args)
{
if (args is null)
{
return;
}
serviceProvider.GetRequiredService<IActivation>().Activate(args);
}
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
using System.IO.Hashing;
using System.IO.Pipes;
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
[Injection(InjectAs.Singleton)]
[ConstructorGenerated]
internal sealed partial class PrivateNamedPipeServer : IDisposable
{
private readonly PrivateNamedPipeMessageDispatcher messageDispatcher;
private readonly NamedPipeServerStream serverStream = new("Snap.Hutao.PrivateNamedPipe", PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.WriteThrough);
private readonly CancellationTokenSource serverTokenSource = new();
private readonly SemaphoreSlim serverSemaphore = new(1);
public void Dispose()
{
serverTokenSource.Cancel();
serverSemaphore.Wait();
serverSemaphore.Dispose();
serverTokenSource.Dispose();
serverStream.Dispose();
}
public async ValueTask RunAsync()
{
using (await serverSemaphore.EnterAsync(serverTokenSource.Token).ConfigureAwait(false))
{
while (!serverTokenSource.IsCancellationRequested)
{
try
{
await serverStream.WaitForConnectionAsync(serverTokenSource.Token).ConfigureAwait(false);
RunPacketSession(serverStream, serverTokenSource.Token);
}
catch (OperationCanceledException)
{
}
}
}
}
private static unsafe byte[] GetValidatedContent(NamedPipeServerStream serverStream, PipePacketHeader* header)
{
byte[] content = new byte[header->ContentLength];
serverStream.ReadAtLeast(content, header->ContentLength, false);
ThrowHelper.InvalidDataIf(XxHash64.HashToUInt64(content) != header->Checksum, "PipePacket Content Hash incorrect");
return content;
}
private unsafe void RunPacketSession(NamedPipeServerStream serverStream, CancellationToken token)
{
Span<byte> headerSpan = stackalloc byte[sizeof(PipePacketHeader)];
bool sessionTerminated = false;
while (serverStream.IsConnected && !sessionTerminated && !token.IsCancellationRequested)
{
serverStream.ReadExactly(headerSpan);
fixed (byte* pHeader = headerSpan)
{
PipePacketHeader* header = (PipePacketHeader*)pHeader;
switch ((header->Type, header->Command, header->ContentType))
{
case (PipePacketType.Request, PipePacketCommand.RedirectActivation, PipePacketContentType.Json):
ReadOnlySpan<byte> content = GetValidatedContent(serverStream, header);
messageDispatcher.RedirectActivation(JsonSerializer.Deserialize<HutaoActivationArguments>(content));
break;
case (PipePacketType.Termination, _, _):
serverStream.Disconnect();
sessionTerminated = true;
return;
}
}
}
}
}

View File

@@ -2,9 +2,9 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Setting;
using Windows.Win32.Foundation;
using Windows.Win32.System.Console;
using static Windows.Win32.PInvoke;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.System.Console;
using static Snap.Hutao.Win32.Kernel32;
namespace Snap.Hutao.Core.Logging;
@@ -26,7 +26,7 @@ internal sealed class ConsoleWindowLifeTime : IDisposable
SetConsoleMode(inputHandle, mode);
}
SetConsoleTitle("Snap Hutao Debug Console");
SetConsoleTitleW("Snap Hutao Debug Console");
}
}
}

View File

@@ -8,116 +8,125 @@ using System.IO;
using System.Security.Principal;
using Windows.ApplicationModel;
using Windows.Storage;
using Windows.UI.Notifications;
namespace Snap.Hutao.Core;
[Injection(InjectAs.Singleton)]
internal sealed class RuntimeOptions
{
private readonly bool isWebView2Supported;
private readonly string webView2Version = SH.CoreWebView2HelperVersionUndetected;
private readonly IServiceProvider serviceProvider;
private bool? isElevated;
public RuntimeOptions(ILogger<RuntimeOptions> logger)
private readonly Lazy<(Version Version, string UserAgent)> lazyVersionAndUserAgent = new(() =>
{
AppLaunchTime = DateTimeOffset.UtcNow;
Version version = Package.Current.Id.Version.ToVersion();
return (version, $"Snap Hutao/{version}");
});
DataFolder = GetDataFolderPath();
LocalCache = ApplicationData.Current.LocalCacheFolder.Path;
InstalledLocation = Package.Current.InstalledLocation.Path;
FamilyName = Package.Current.Id.FamilyName;
private readonly Lazy<string> lazyDataFolder = new(() =>
{
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
Version = Package.Current.Id.Version.ToVersion();
UserAgent = $"Snap Hutao/{Version}";
DeviceId = GetUniqueUserId();
DetectWebView2Environment(logger, out webView2Version, out isWebView2Supported);
static string GetDataFolderPath()
if (!string.IsNullOrEmpty(preferredPath))
{
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
Directory.CreateDirectory(preferredPath);
return preferredPath;
}
if (!string.IsNullOrEmpty(preferredPath))
{
Directory.CreateDirectory(preferredPath);
return preferredPath;
}
// Fallback to MyDocuments
string myDocuments = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
// Fallback to MyDocuments
string myDocuments = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
string path = Path.GetFullPath(Path.Combine(myDocuments, folderName));
Directory.CreateDirectory(path);
return path;
string path = Path.GetFullPath(Path.Combine(myDocuments, folderName));
Directory.CreateDirectory(path);
return path;
});
private readonly Lazy<string> lazyDeviceId = new(() =>
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
return Convert.ToMd5HexString($"{userName}{machineGuid}");
});
private readonly Lazy<(string Version, bool Supported)> lazyWebViewEnvironment = new(() =>
{
try
{
string version = CoreWebView2Environment.GetAvailableBrowserVersionString();
return (version, true);
}
catch (FileNotFoundException)
{
return (SH.CoreWebView2HelperVersionUndetected, false);
}
});
private readonly Lazy<bool> lazyElevated = new(() =>
{
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
{
return true;
}
static string GetUniqueUserId()
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
return Convert.ToMd5HexString($"{userName}{machineGuid}");
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
});
static void DetectWebView2Environment(ILogger<RuntimeOptions> logger, out string webView2Version, out bool isWebView2Supported)
{
try
{
webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString();
isWebView2Supported = true;
}
catch (FileNotFoundException ex)
{
webView2Version = SH.CoreWebView2HelperVersionUndetected;
isWebView2Supported = false;
logger.LogError(ex, "WebView2 Runtime not installed.");
}
}
private readonly Lazy<string> lazyLocalCache = new(() => ApplicationData.Current.LocalCacheFolder.Path);
private readonly Lazy<string> lazyInstalledLocation = new(() => Package.Current.InstalledLocation.Path);
private readonly Lazy<string> lazyFamilyName = new(() => Package.Current.Id.FamilyName);
private bool isToastAvailable;
private bool isToastAvailableInitialized;
private object isToastAvailableLock = new();
public RuntimeOptions(IServiceProvider serviceProvider, ILogger<RuntimeOptions> logger)
{
this.serviceProvider = serviceProvider;
AppLaunchTime = DateTimeOffset.UtcNow;
}
public Version Version { get; }
public Version Version { get => lazyVersionAndUserAgent.Value.Version; }
public string UserAgent { get; }
public string UserAgent { get => lazyVersionAndUserAgent.Value.UserAgent; }
public string InstalledLocation { get; }
public string InstalledLocation { get => lazyInstalledLocation.Value; }
public string DataFolder { get; }
public string DataFolder { get => lazyDataFolder.Value; }
public string LocalCache { get; }
public string LocalCache { get => lazyLocalCache.Value; }
public string FamilyName { get; }
public string FamilyName { get => lazyFamilyName.Value; }
public string DeviceId { get; }
public string DeviceId { get => lazyDeviceId.Value; }
public string WebView2Version { get => webView2Version; }
public string WebView2Version { get => lazyWebViewEnvironment.Value.Version; }
public bool IsWebView2Supported { get => isWebView2Supported; }
public bool IsWebView2Supported { get => lazyWebViewEnvironment.Value.Supported; }
public bool IsElevated
public bool IsElevated { get => lazyElevated.Value; }
public bool IsToastAvailable
{
get
{
return isElevated ??= GetElevated();
return LazyInitializer.EnsureInitialized(ref isToastAvailable, ref isToastAvailableInitialized, ref isToastAvailableLock, GetIsToastAvailable);
static bool GetElevated()
bool GetIsToastAvailable()
{
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
{
return true;
}
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
ITaskContext taskContext = serviceProvider.GetRequiredService<ITaskContext>();
return taskContext.InvokeOnMainThread(() => ToastNotificationManager.CreateToastNotifier().Setting is NotificationSetting.Enabled);
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core;
internal static class RuntimeOptionsExtension
{
public static string GetDataFolderUpdateCacheFolderFile(this RuntimeOptions options, string fileName)
{
string directory = Path.Combine(options.DataFolder, "UpdateCache");
Directory.CreateDirectory(directory);
return Path.Combine(directory, fileName);
}
public static string GetDataFolderServerCacheFolder(this RuntimeOptions options)
{
return Path.Combine(options.DataFolder, "ServerCache");
}
}

View File

@@ -7,30 +7,30 @@ namespace Snap.Hutao.Core.Setting;
/// 设置键
/// </summary>
[HighQuality]
[SuppressMessage("", "SA1124")]
internal static class SettingKeys
{
#region MainWindow
public const string WindowRect = "WindowRect";
public const string IsNavPaneOpen = "IsNavPaneOpen";
public const string LaunchTimes = "LaunchTimes";
public const string DataFolderPath = "DataFolderPath";
public const string PassportUserName = "PassportUserName";
public const string PassportPassword = "PassportPassword";
public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked";
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds";
#endregion
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
#region Application
public const string LaunchTimes = "LaunchTimes";
public const string DataFolderPath = "DataFolderPath";
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled2";
#endregion
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
#region Passport
public const string PassportUserName = "PassportUserName";
public const string PassportPassword = "PassportPassword";
#endregion
#region Cultivation
public const string CultivationAvatarLevelCurrent = "CultivationAvatarLevelCurrent";
public const string CultivationAvatarLevelTarget = "CultivationAvatarLevelTarget";
public const string CultivationAvatarSkillACurrent = "CultivationAvatarSkillACurrent";
@@ -43,13 +43,24 @@ internal static class SettingKeys
public const string CultivationWeapon90LevelTarget = "CultivationWeapon90LevelTarget";
public const string CultivationWeapon70LevelCurrent = "CultivationWeapon70LevelCurrent";
public const string CultivationWeapon70LevelTarget = "CultivationWeapon70LevelTarget";
#endregion
#region HomeCard Dashboard
public const string IsHomeCardLaunchGamePresented = "IsHomeCardLaunchGamePresented";
public const string IsHomeCardGachaStatisticsPresented = "IsHomeCardGachaStatisticsPresented";
public const string IsHomeCardAchievementPresented = "IsHomeCardAchievementPresented";
public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented";
#endregion
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
#region DevTool
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
public const string OverrideUpdateVersionComparison = "OverrideUpdateVersionComparison";
public const string OverridePackageConvertDirectoryPermissionsRequirement = "OverridePackageConvertDirectoryPermissionsRequirement";
#endregion
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
#region Obsolete
[Obsolete("重置调试控制台开关")]
public const string IsAllocConsoleDebugModeEnabledLegacy1 = "IsAllocConsoleDebugModeEnabled";
#endregion
}

View File

@@ -39,6 +39,11 @@ internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
}
catch (Exception)
{
if (WScriptExists(DailyNoteRefreshScriptName, out string fullPath))
{
File.Delete(fullPath);
}
return false;
}
}
@@ -63,7 +68,7 @@ internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
public bool IsDailyNoteRefreshEnabled()
{
return WScriptExists(DailyNoteRefreshScriptName, out _);
return TaskService.Instance.RootFolder.Tasks.Any(task => task.Name is DailyNoteRefreshTaskName);
}
/// <summary>

View File

@@ -1,16 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.System.Com;
using Snap.Hutao.Win32.UI.Shell;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Storage;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
using static Windows.Win32.PInvoke;
using static Snap.Hutao.Win32.Macros;
using static Snap.Hutao.Win32.Ole32;
namespace Snap.Hutao.Core.Shell;
@@ -19,51 +17,60 @@ namespace Snap.Hutao.Core.Shell;
internal sealed partial class ShellLinkInterop : IShellLinkInterop
{
private readonly RuntimeOptions runtimeOptions;
private readonly AppOptions appOptions;
public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
{
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
try
{
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri);
using (Stream inputStream = (await iconFile.OpenReadAsync()).AsStream())
{
using (FileStream outputStream = File.Create(targetLogoPath))
{
await inputStream.CopyToAsync(outputStream).ConfigureAwait(false);
}
}
await iconFile.OverwriteCopyAsync(targetLogoPath).ConfigureAwait(false);
}
catch
{
return false;
}
HRESULT result = CoCreateInstance<ShellLink, IShellLinkW>(null, CLSCTX.CLSCTX_INPROC_SERVER, out IShellLinkW shellLink);
Marshal.ThrowExceptionForHR(result);
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath);
}
shellLink.SetPath(appOptions.PowerShellPath);
shellLink.SetArguments($"""
-Command "Start-Process shell:AppsFolder\{runtimeOptions.FamilyName}!App -verb runas"
""");
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_SHOWMINNOACTIVE);
shellLink.SetIconLocation(targetLogoPath, 0);
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath)
{
bool result = false;
IPersistFile persistFile = (IPersistFile)shellLink;
try
// DO NOT revert if condition, COM interfaces need to be released properly
HRESULT hr = CoCreateInstance(in ShellLink.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IShellLinkW.IID, out IShellLinkW* pShellLink);
if (SUCCEEDED(hr))
{
persistFile.Save(target, false);
}
catch (UnauthorizedAccessException)
{
return false;
pShellLink->SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
pShellLink->SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
pShellLink->SetIconLocation(targetLogoPath, 0);
if (SUCCEEDED(pShellLink->QueryInterface(in IShellLinkDataList.IID, out IShellLinkDataList* pShellLinkDataList)))
{
pShellLinkDataList->GetFlags(out uint flags);
pShellLinkDataList->SetFlags(flags | (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER);
pShellLinkDataList->Release();
}
if (SUCCEEDED(pShellLink->QueryInterface(in IPersistFile.IID, out IPersistFile* pPersistFile)))
{
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");
if (SUCCEEDED(pPersistFile->Save(target, false)))
{
result = true;
}
pPersistFile->Release();
}
pShellLink->Release();
}
return true;
return result;
}
}

View File

@@ -8,12 +8,12 @@ internal readonly struct Delay
/// <summary>
/// 随机延迟
/// </summary>
/// <param name="minMilliSeconds">最小,闭</param>
/// <param name="maxMilliSeconds">最小,开</param>
/// <param name="min">最小,闭</param>
/// <param name="max">最小,开</param>
/// <returns>任务</returns>
public static ValueTask Random(int minMilliSeconds, int maxMilliSeconds)
public static ValueTask RandomMilliSeconds(int min, int max)
{
return Task.Delay((int)(System.Random.Shared.NextDouble() * (maxMilliSeconds - minMilliSeconds)) + minMilliSeconds).AsValueTask();
return Task.Delay((int)(System.Random.Shared.NextDouble() * (max - min)) + min).AsValueTask();
}
public static ValueTask FromSeconds(int seconds)

View File

@@ -48,4 +48,45 @@ internal static class DispatcherQueueExtension
exceptionDispatchInfo?.Throw();
}
}
/// <summary>
/// 在调度器队列同步调用,直到执行结束,会持续阻塞当前线程
/// </summary>
/// <param name="dispatcherQueue">调度器队列</param>
/// <param name="action">执行的回调</param>
/// <typeparam name="T">返回类型</typeparam>
/// <returns>回调返回值</returns>
public static T Invoke<T>(this DispatcherQueue dispatcherQueue, Func<T> action)
{
T result = default!;
if (dispatcherQueue.HasThreadAccess)
{
return action();
}
ExceptionDispatchInfo? exceptionDispatchInfo = null;
using (ManualResetEventSlim blockEvent = new(false))
{
dispatcherQueue.TryEnqueue(() =>
{
try
{
result = action();
}
catch (Exception ex)
{
exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex);
}
finally
{
blockEvent.Set();
}
});
blockEvent.Wait();
exceptionDispatchInfo?.Throw();
return result;
}
}
}

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