mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
412 Commits
windows-gr
...
1.10.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a259750624 | ||
|
|
4e7f8e2a97 | ||
|
|
1ea92413f9 | ||
|
|
46c117edff | ||
|
|
9c4a9fc09a | ||
|
|
605fe5a3af | ||
|
|
7581cf8c8f | ||
|
|
6b67811bae | ||
|
|
6863cbb113 | ||
|
|
6b03ccdacc | ||
|
|
ad90c6b792 | ||
|
|
3ea7d59985 | ||
|
|
555043dfaa | ||
|
|
84e05017ba | ||
|
|
2c139a1ff6 | ||
|
|
04114fb170 | ||
|
|
a8065bf6e6 | ||
|
|
bfdb4b0060 | ||
|
|
6489f66d13 | ||
|
|
eb57ac5952 | ||
|
|
d57865fed9 | ||
|
|
110af48385 | ||
|
|
d30ef6daa0 | ||
|
|
e98bee8a9b | ||
|
|
71e0452c6e | ||
|
|
d866c46646 | ||
|
|
05d0faf131 | ||
|
|
83f5f25324 | ||
|
|
0c0290c446 | ||
|
|
c3efd8d806 | ||
|
|
e8d613d81f | ||
|
|
b7d03bee77 | ||
|
|
55799d0731 | ||
|
|
0a24e19625 | ||
|
|
a32b787352 | ||
|
|
f73b3af180 | ||
|
|
bad60f1d65 | ||
|
|
823ffdb5ad | ||
|
|
544469f078 | ||
|
|
04d4fa0c29 | ||
|
|
f5dbabc586 | ||
|
|
bfefbc58fa | ||
|
|
5b98ba3fc4 | ||
|
|
32a22695e3 | ||
|
|
ab20aa1c64 | ||
|
|
84c8d8a2e3 | ||
|
|
364b056e17 | ||
|
|
a4913e84e4 | ||
|
|
7f32437ec0 | ||
|
|
e230d7a3ef | ||
|
|
1cd6aad518 | ||
|
|
d3fbf35f34 | ||
|
|
72a1ec2122 | ||
|
|
497a5fb0f8 | ||
|
|
bfec29504b | ||
|
|
de77639f57 | ||
|
|
951ecd19d5 | ||
|
|
7f6430fe80 | ||
|
|
d133295599 | ||
|
|
b22eead953 | ||
|
|
d75c680a45 | ||
|
|
52949e3431 | ||
|
|
eeee9af09d | ||
|
|
3526e73f35 | ||
|
|
81c5acb742 | ||
|
|
48ce0c2761 | ||
|
|
c5f8d6bfd5 | ||
|
|
4ef1262d01 | ||
|
|
c05c62ac91 | ||
|
|
618299b296 | ||
|
|
0db7aa239e | ||
|
|
6a4cc56d32 | ||
|
|
5784c55d1e | ||
|
|
53167952f4 | ||
|
|
2a6b386c2c | ||
|
|
4fdb72ca30 | ||
|
|
9df46ad60e | ||
|
|
5c49818a2f | ||
|
|
4ba819ce3b | ||
|
|
96ed31c09e | ||
|
|
481753da02 | ||
|
|
ec6e1696da | ||
|
|
417b537de4 | ||
|
|
e837e425c5 | ||
|
|
a3fb0486c2 | ||
|
|
64d9d04608 | ||
|
|
fe05c8dd04 | ||
|
|
a2f9ff95a4 | ||
|
|
bd5c244eeb | ||
|
|
b619dd5b09 | ||
|
|
ee5a94b961 | ||
|
|
e30c97d0aa | ||
|
|
261377fe0a | ||
|
|
a080938ee2 | ||
|
|
deb34c2a7b | ||
|
|
c0a9e0b301 | ||
|
|
f2c9b676c9 | ||
|
|
a02ce183eb | ||
|
|
06cd462f01 | ||
|
|
17d27f9535 | ||
|
|
d97bd4fd79 | ||
|
|
0c7c25f303 | ||
|
|
2b8eed0ccc | ||
|
|
154c31e8f7 | ||
|
|
c2116af19f | ||
|
|
3144ed4ecb | ||
|
|
b6c68c69d6 | ||
|
|
12d2f2235e | ||
|
|
e89c5488d9 | ||
|
|
489bb6bab3 | ||
|
|
423b220bb3 | ||
|
|
08a082ae65 | ||
|
|
b99bcb53f2 | ||
|
|
0160e96837 | ||
|
|
ca29320139 | ||
|
|
e8a6acd2d8 | ||
|
|
3bca3a8148 | ||
|
|
e1bbaf5dc9 | ||
|
|
b1774e8365 | ||
|
|
f5a81e2f57 | ||
|
|
cfb72755a0 | ||
|
|
0a7e9afcaf | ||
|
|
743e8d8069 | ||
|
|
4c64fac354 | ||
|
|
bf50d6b9b3 | ||
|
|
471260de59 | ||
|
|
dc6dc94b45 | ||
|
|
c0f7293921 | ||
|
|
98b5436828 | ||
|
|
ff785387dc | ||
|
|
5875147bd3 | ||
|
|
cd80250fd0 | ||
|
|
f1bcef4869 | ||
|
|
2c58f34c5d | ||
|
|
b90e8d062c | ||
|
|
00c9417997 | ||
|
|
514edd97c8 | ||
|
|
7f06b0a07c | ||
|
|
3211bfbbd6 | ||
|
|
8067665026 | ||
|
|
0c1968ff49 | ||
|
|
0075d79b0c | ||
|
|
1f8a70da0d | ||
|
|
034655dc26 | ||
|
|
fb293cfc18 | ||
|
|
0433ecbce8 | ||
|
|
077243fa38 | ||
|
|
83347dfafb | ||
|
|
c9df6ac77b | ||
|
|
b626bbe443 | ||
|
|
ea8685523d | ||
|
|
b02f2b47c8 | ||
|
|
b4f7bf934e | ||
|
|
eeffa446a2 | ||
|
|
6746610ab6 | ||
|
|
f8c224048e | ||
|
|
3f110fd4d3 | ||
|
|
44ddae602d | ||
|
|
ab91f4e738 | ||
|
|
5bf1cf0530 | ||
|
|
70f4dcb2c9 | ||
|
|
f490805875 | ||
|
|
681bf08047 | ||
|
|
7b11215551 | ||
|
|
8b20f3beca | ||
|
|
18a088d83b | ||
|
|
a6971042dc | ||
|
|
87f1f2c46b | ||
|
|
c576d8f7c4 | ||
|
|
a0c1241b32 | ||
|
|
a3ab24554a | ||
|
|
9ae45a4cc4 | ||
|
|
f700faae14 | ||
|
|
57b51ed5ee | ||
|
|
5dfb7fbb63 | ||
|
|
046823245c | ||
|
|
0497d89559 | ||
|
|
9d364a291c | ||
|
|
c342147809 | ||
|
|
a86caaf229 | ||
|
|
d0b07f1308 | ||
|
|
409a223213 | ||
|
|
75ea2b807f | ||
|
|
719d934222 | ||
|
|
e8eed46d82 | ||
|
|
ff9b553a19 | ||
|
|
95d64c2895 | ||
|
|
558551c8ad | ||
|
|
d05c196b7c | ||
|
|
502fb6dbed | ||
|
|
4fa5270070 | ||
|
|
94fda223fc | ||
|
|
18103b4deb | ||
|
|
16ac52e71d | ||
|
|
73825d391e | ||
|
|
3b2eeb84a7 | ||
|
|
3e8655fd55 | ||
|
|
fe38e14ae8 | ||
|
|
a174493819 | ||
|
|
3a57d55c62 | ||
|
|
99f35ca6db | ||
|
|
c423e8b72d | ||
|
|
7ff78def46 | ||
|
|
bc9018f4bf | ||
|
|
3513268ad9 | ||
|
|
107963b7ac | ||
|
|
4e89406f2f | ||
|
|
8119de3fa9 | ||
|
|
7a8c233b10 | ||
|
|
cc71aa9c82 | ||
|
|
850ea7ed4b | ||
|
|
4276481284 | ||
|
|
6f3159ae0c | ||
|
|
c1b3412ba1 | ||
|
|
99b3613319 | ||
|
|
069407abbc | ||
|
|
98c8df5c8e | ||
|
|
7cfcc17763 | ||
|
|
23741c4e48 | ||
|
|
5f4b68d538 | ||
|
|
9ef0d8c57d | ||
|
|
f0bfea51cf | ||
|
|
905454eb02 | ||
|
|
05c3a575bc | ||
|
|
3e26e247cd | ||
|
|
293b1e214d | ||
|
|
063665e77e | ||
|
|
50389ac06c | ||
|
|
b99b34945e | ||
|
|
94a96c76bc | ||
|
|
5cf3046257 | ||
|
|
89f8dedb57 | ||
|
|
3c1e9237aa | ||
|
|
e7cb01b302 | ||
|
|
4cd971e166 | ||
|
|
7a9657f0cb | ||
|
|
82e6b62231 | ||
|
|
374c4d796d | ||
|
|
6e149a5be3 | ||
|
|
00ad0ef346 | ||
|
|
f22f165592 | ||
|
|
fd59b471cb | ||
|
|
5d8a39fe43 | ||
|
|
521534be05 | ||
|
|
b1364db3ac | ||
|
|
031cf77c27 | ||
|
|
49c75dde2a | ||
|
|
3200c5e60b | ||
|
|
b392a6f8e5 | ||
|
|
3e8e109123 | ||
|
|
91c886befb | ||
|
|
32bdfe12af | ||
|
|
eac67b6f44 | ||
|
|
0dcba220c5 | ||
|
|
a204eaa95c | ||
|
|
35491c4eb1 | ||
|
|
706401350c | ||
|
|
c8ba04ee11 | ||
|
|
b080a553c3 | ||
|
|
baf5612333 | ||
|
|
eacd697cfe | ||
|
|
11dc8e60bb | ||
|
|
bba62996a0 | ||
|
|
db15b6a30c | ||
|
|
1b0356b5ef | ||
|
|
6e498f5ede | ||
|
|
3117aefd54 | ||
|
|
34ea240272 | ||
|
|
6b23ae5332 | ||
|
|
c197d8a35a | ||
|
|
b0fa05283a | ||
|
|
c85a74dfc3 | ||
|
|
f7e53399b4 | ||
|
|
52ac588a3a | ||
|
|
cd6c1f6b59 | ||
|
|
7c734ce4aa | ||
|
|
a640374b62 | ||
|
|
ca66176d64 | ||
|
|
0f3a85e35c | ||
|
|
4bb7316ce5 | ||
|
|
7d6a9691a2 | ||
|
|
1d4409aa43 | ||
|
|
ea345f4854 | ||
|
|
72e163f613 | ||
|
|
86b04bb5a3 | ||
|
|
5859ca3c12 | ||
|
|
e34e87359f | ||
|
|
53cda02071 | ||
|
|
ff6c682e1b | ||
|
|
bae9c8a46a | ||
|
|
a8baef99d7 | ||
|
|
2c47e7d1da | ||
|
|
2cee94a529 | ||
|
|
b8b9bb2436 | ||
|
|
5511863d7f | ||
|
|
adf3f7e7b1 | ||
|
|
2232772110 | ||
|
|
cd343843b3 | ||
|
|
f5982f81c0 | ||
|
|
1e38c43727 | ||
|
|
7879f1278b | ||
|
|
f8e9b4a1b3 | ||
|
|
c9ea4b358a | ||
|
|
75287473c5 | ||
|
|
3948b81a48 | ||
|
|
e5c751771c | ||
|
|
f7723d21a3 | ||
|
|
4ce064a71a | ||
|
|
b07c569a9e | ||
|
|
c81c0c33d8 | ||
|
|
2274445303 | ||
|
|
271cac9a02 | ||
|
|
9f8f2870ae | ||
|
|
0cc4897354 | ||
|
|
7aa4696ba5 | ||
|
|
9e3ec32ae6 | ||
|
|
cd91af8ae9 | ||
|
|
8680960931 | ||
|
|
67f6fda900 | ||
|
|
db4b0d3dcb | ||
|
|
ec0abb4b16 | ||
|
|
8cdf2b01f2 | ||
|
|
c1bf3dad52 | ||
|
|
492e867391 | ||
|
|
9cbb36f3e6 | ||
|
|
e15c06a194 | ||
|
|
f71a2a3d40 | ||
|
|
fb89e5f9f0 | ||
|
|
3f97f4207c | ||
|
|
a4ea798ad0 | ||
|
|
80a0daf02d | ||
|
|
c511d27b2f | ||
|
|
c5dcbc9c79 | ||
|
|
91949214ba | ||
|
|
a126a330ff | ||
|
|
055846dfd6 | ||
|
|
b68462b56e | ||
|
|
9d47082f47 | ||
|
|
cd4516d9a7 | ||
|
|
f7e94fe2f2 | ||
|
|
e93802d5a5 | ||
|
|
370f2fe1f7 | ||
|
|
c6f747a89b | ||
|
|
cf431719df | ||
|
|
c36c15f9be | ||
|
|
f80a63f557 | ||
|
|
36376f5af6 | ||
|
|
2c057458a3 | ||
|
|
f0f50e0e30 | ||
|
|
b834daef93 | ||
|
|
ff10543c21 | ||
|
|
a55b25ae53 | ||
|
|
c0fbb823d4 | ||
|
|
4306af94be | ||
|
|
dfc83d4a34 | ||
|
|
c6a47eb7be | ||
|
|
7413a81ff4 | ||
|
|
24d143ea9f | ||
|
|
9f6611cd20 | ||
|
|
784c727a38 | ||
|
|
1bf517f95d | ||
|
|
8b9190d941 | ||
|
|
16e0ab56f6 | ||
|
|
b10df0bed1 | ||
|
|
c4d1f371f1 | ||
|
|
92a151441b | ||
|
|
faefc9c093 | ||
|
|
c6e6d08707 | ||
|
|
4323ced7dc | ||
|
|
8a1781b449 | ||
|
|
72aff568b3 | ||
|
|
8252e43bac | ||
|
|
f15a692f03 | ||
|
|
5868d53cca | ||
|
|
7d7c8d485e | ||
|
|
6edcf97ec9 | ||
|
|
f4593cd325 | ||
|
|
29454b188e | ||
|
|
942181561d | ||
|
|
5d6e1dad01 | ||
|
|
d52aa0d6b2 | ||
|
|
3ee729eacf | ||
|
|
d3acbcde24 | ||
|
|
38e152befd | ||
|
|
0f767f7e77 | ||
|
|
ce58e35a8f | ||
|
|
a10e9e40a2 | ||
|
|
dafd3128c2 | ||
|
|
0556373bcf | ||
|
|
e8d3a065e6 | ||
|
|
be223909d3 | ||
|
|
7da778699b | ||
|
|
5bfc790ea2 | ||
|
|
fc13b85739 | ||
|
|
df999dbf51 | ||
|
|
688562c1dd | ||
|
|
051a115f84 | ||
|
|
b3d75f9fa5 | ||
|
|
0b90bdaa42 | ||
|
|
77067d27d0 | ||
|
|
e04542606e | ||
|
|
5be958ff64 | ||
|
|
a57933388d | ||
|
|
86c6c9574b | ||
|
|
47e451df2f | ||
|
|
c8592c798b | ||
|
|
5fad960b20 | ||
|
|
44ba0a90a6 | ||
|
|
883c1ca95f | ||
|
|
1a29908e5d | ||
|
|
3e9edd2f62 | ||
|
|
f9c18d2555 |
2
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
@@ -19,7 +19,7 @@ body:
|
||||
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
|
||||
required: true
|
||||
|
||||
- label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
|
||||
- label: 我知道[文档站](https://hut.ao/zh/menu.html)的导航栏中有**搜索功能**,且已经搜索过相关关键词
|
||||
required: true
|
||||
|
||||
- label: 我的问题不是[已完成](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90)的问题也不是一个别人已发布的**重复的**问题
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
name: 功能请求
|
||||
name: 功能请求
|
||||
description: 通过这个议题来向开发团队分享你的想法
|
||||
title: "[Feat]: 在这里填写一个合适的标题"
|
||||
labels: ["功能", "needs-triage", "priority:none"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
labels: ["feature request", "needs-triage", "priority:none"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -24,4 +22,4 @@ body:
|
||||
label: 想要实现或优化的功能
|
||||
description: 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高
|
||||
validations:
|
||||
required: true
|
||||
required: true
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
name: Feature Request [English Form]
|
||||
description: Tell us about your thought
|
||||
title: "[Feat]: Place your title here"
|
||||
labels: ["功能", "needs-triage", "priority:none"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
labels: ["feature request", "needs-triage", "priority:none"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -22,6 +20,6 @@ body:
|
||||
id: req
|
||||
attributes:
|
||||
label: Detail of the Feature
|
||||
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted.
|
||||
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted.
|
||||
validations:
|
||||
required: true
|
||||
required: true
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -1,5 +1,5 @@
|
||||
<!--- Hi, thanks for considering make a PR contribution to Snap Hutao, we appreciate your work. -->
|
||||
<!--- Before you create this PR, please fill the following form and checklist -->
|
||||
<!--- Before you create this PR, please check our contribution guide (https://hut.ao/en/development/contribute.html) and fill out the following form and checklist -->
|
||||
|
||||
## Description
|
||||
|
||||
|
||||
61
.github/workflows/alpha.yml
vendored
61
.github/workflows/alpha.yml
vendored
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- 'feat/*'
|
||||
paths-ignore:
|
||||
- '.gitattributes'
|
||||
- '.github/**'
|
||||
@@ -44,13 +45,8 @@ jobs:
|
||||
run: dotnet tool restore && dotnet cake
|
||||
env:
|
||||
VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }}
|
||||
|
||||
- name: Sign Msix
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
shell: pwsh
|
||||
run: |
|
||||
[System.Convert]::FromBase64String("${{ secrets.CERTIFICATE }}") | Set-Content -AsByteStream temp.pfx
|
||||
signtool.exe sign /debug /v /a /fd SHA256 /f temp.pfx /p ${{ secrets.PW }} ${{ github.workspace }}\src\output\Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
|
||||
CERTIFICATE: ${{ secrets.CERTIFICATE }}
|
||||
PW: ${{ secrets.PW }}
|
||||
|
||||
- name: Upload signed msix
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
@@ -68,12 +64,55 @@ jobs:
|
||||
> 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
|
||||
|
||||
> [!TIP]
|
||||
> 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
|
||||
> 普通用户请 [点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/) 下载最新的稳定版本
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 请注意,从 Snap Hutao Alpha 2023.12.21.3 开始,我们将使用全新的 CI 证书,原有的 Snap.Hutao.CI.cer 将在几天后过期停止使用。
|
||||
>
|
||||
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 以安装测试版安装包
|
||||
> 请先安装 **[DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt)** 到 **受信任的根证书颁发机构** 以安装测试版安装包
|
||||
"
|
||||
|
||||
echo $summary >> $Env:GITHUB_STEP_SUMMARY
|
||||
fallback_build:
|
||||
runs-on: windows-latest
|
||||
needs: build
|
||||
if: failure()
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0
|
||||
|
||||
- name: Cake
|
||||
id: cake
|
||||
shell: pwsh
|
||||
run: dotnet tool restore && dotnet cake
|
||||
env:
|
||||
VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }}
|
||||
CERTIFICATE: ${{ secrets.CERTIFICATE }}
|
||||
PW: ${{ secrets.PW }}
|
||||
|
||||
- name: Upload signed msix
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}
|
||||
path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
|
||||
|
||||
- name: Add summary
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$summary = "
|
||||
> [!WARNING]
|
||||
> 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
|
||||
|
||||
> [!TIP]
|
||||
> 普通用户请 [点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/) 下载最新的稳定版本
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 请先安装 **[DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt)** 到 **受信任的根证书颁发机构** 以安装测试版安装包
|
||||
"
|
||||
|
||||
echo $summary >> $Env:GITHUB_STEP_SUMMARY
|
||||
|
||||
2
.github/workflows/close_stale.yml
vendored
2
.github/workflows/close_stale.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
any-of-labels: 'needs-more-info,需要更多信息'
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 3 days.'
|
||||
stale-issue-message: 'This issue is stale because it has been open 7 days with no activity. Remove stale label or comment or this will be closed in 3 days.'
|
||||
days-before-stale: 7
|
||||
days-before-close: 3
|
||||
close-issue-reason: not_planned
|
||||
22
README.md
22
README.md
@@ -17,7 +17,15 @@ You can follow the instructions in the [Quick Start](https://hut.ao/en/quick-sta
|
||||
|
||||
## 本地化翻译 / Localization
|
||||
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
[].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
|
||||
|
||||
Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。
|
||||
|
||||
@@ -46,13 +54,13 @@ Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translatio
|
||||
* [CommunityToolkit/dotnet](https://github.com/CommunityToolkit/dotnet)
|
||||
* [CommunityToolkit/Labs-Windows](https://github.com/CommunityToolkit/Labs-Windows)
|
||||
* [CommunityToolkit/Windows](https://github.com/CommunityToolkit/Windows)
|
||||
* [dahall/taskscheduler](https://github.com/dahall/taskscheduler)
|
||||
* [dotnet/efcore](https://github.com/dotnet/efcore)
|
||||
* [dotnet/runtime](https://github.com/dotnet/runtime)
|
||||
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
|
||||
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
|
||||
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
|
||||
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
|
||||
* [quartznet/quartznet](https://github.com/quartznet/quartznet)
|
||||
|
||||
### 支撑项目 / Supporter Project
|
||||
|
||||
@@ -64,9 +72,9 @@ Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translatio
|
||||
Snap Hutao is currently using sponsored software from the following service providers.
|
||||
|
||||
| [](https://www.netlify.com/) | [](https://crowdin.com/) | [](https://gitlab.cn/) |
|
||||
|:----------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|
|
||||
| [](https://about.signpath.io) | [](https://1password.com/) | [](https://about.signpath.io) |
|
||||
|
||||
| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
|
||||
| [](https://about.signpath.io) | [](https://1password.com/) | [](https://www.digitalocean.com) |
|
||||
| [](https://hi.ducalis.io/) | [](https://www.jetbrains.com/opensource/) | |
|
||||
|
||||
- Netlify provides document and home page hosting service for Snap Hutao
|
||||
|
||||
@@ -80,6 +88,10 @@ Snap Hutao is currently using sponsored software from the following service prov
|
||||
|
||||
- DigitalOcean provides reliable cloud database for Snap Hutao database backup
|
||||
|
||||
- [Ducalis.io](https://hi.ducalis.io/) provides Snap Hutao project with a complete decision-making toolkit for project management
|
||||
|
||||
- Jetbrains provides powerful IDE for Snap Hutao infrastructure services coding
|
||||
|
||||
## 开发 / Development
|
||||
|
||||

|
||||
|
||||
75
build.cake
75
build.cake
@@ -11,6 +11,18 @@ var version = "version";
|
||||
var repoDir = "repoDir";
|
||||
var outputPath = "outputPath";
|
||||
|
||||
var pfxPath = "pfxPath";
|
||||
var pw = "pw";
|
||||
|
||||
// Extension
|
||||
|
||||
static ProcessArgumentBuilder AppendIf(this ProcessArgumentBuilder builder, string text, bool condition)
|
||||
{
|
||||
return condition ? builder.Append(text) : builder;
|
||||
}
|
||||
|
||||
// Properties
|
||||
|
||||
string solution
|
||||
{
|
||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao.sln");
|
||||
@@ -53,6 +65,11 @@ if (GitHubActions.IsRunningOnGitHubActions)
|
||||
}
|
||||
);
|
||||
|
||||
var certificateBase64 = HasEnvironmentVariable("CERTIFICATE") ? EnvironmentVariable("CERTIFICATE") : throw new Exception("Cannot find CERTIFICATE");
|
||||
pw = HasEnvironmentVariable("PW") ? EnvironmentVariable("PW") : throw new Exception("Cannot find PW");
|
||||
pfxPath = System.IO.Path.Combine(repoDir, "temp.pfx");
|
||||
System.IO.File.WriteAllBytes(pfxPath, System.Convert.FromBase64String(certificateBase64));
|
||||
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
|
||||
@@ -79,10 +96,19 @@ else // Local
|
||||
Information($"Version: {version}");
|
||||
}
|
||||
|
||||
// Windows SDK
|
||||
var registry = new WindowsRegistry();
|
||||
var winsdkRegistry = registry.LocalMachine.OpenKey(@"SOFTWARE\Microsoft\Windows Kits\Installed Roots");
|
||||
var winsdkVersion = winsdkRegistry.GetSubKeyNames().MaxBy(key => int.Parse(key.Split(".")[2]));
|
||||
var winsdkPath = (string)winsdkRegistry.GetValue("KitsRoot10");
|
||||
var winsdkBinPath = System.IO.Path.Combine(winsdkPath, "bin", winsdkVersion, "x64");
|
||||
Information($"Windows SDK: {winsdkPath}");
|
||||
|
||||
Task("Build")
|
||||
.IsDependentOn("Build binary package")
|
||||
.IsDependentOn("Copy files")
|
||||
.IsDependentOn("Build MSIX");
|
||||
.IsDependentOn("Build MSIX")
|
||||
.IsDependentOn("Sign");
|
||||
|
||||
Task("NuGet Restore")
|
||||
.Does(() =>
|
||||
@@ -157,6 +183,7 @@ Task("Build binary package")
|
||||
.Append("/p:AppxPackageSigningEnabled=false")
|
||||
.Append("/p:AppxBundle=Never")
|
||||
.Append("/p:AppxPackageOutput=" + outputPath)
|
||||
.AppendIf("/p:AlphaConstants=IS_ALPHA_BUILD", !AppVeyor.IsRunningOnAppVeyor)
|
||||
};
|
||||
|
||||
DotNetBuild(project, settings);
|
||||
@@ -197,8 +224,11 @@ Task("Build MSIX")
|
||||
{
|
||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Local-{version}.msix");
|
||||
}
|
||||
|
||||
var makeappxPath = System.IO.Path.Combine(winsdkBinPath, "makeappx.exe");
|
||||
|
||||
var p = StartProcess(
|
||||
"makeappx.exe",
|
||||
makeappxPath,
|
||||
new ProcessSettings
|
||||
{
|
||||
Arguments = arguments
|
||||
@@ -206,7 +236,46 @@ Task("Build MSIX")
|
||||
);
|
||||
if (p != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Build failed with exit code " + p);
|
||||
throw new InvalidOperationException("Build MSIX failed with exit code " + p);
|
||||
}
|
||||
});
|
||||
|
||||
Task("Sign")
|
||||
.IsDependentOn("Build MSIX")
|
||||
.Does(() =>
|
||||
{
|
||||
if (AppVeyor.IsRunningOnAppVeyor)
|
||||
{
|
||||
Information("Move to SignPath. Skip signing.");
|
||||
return;
|
||||
}
|
||||
else if (GitHubActions.IsRunningOnGitHubActions)
|
||||
{
|
||||
if (GitHubActions.Environment.PullRequest.IsPullRequest)
|
||||
{
|
||||
Information("Is Pull Request. Skip signing.");
|
||||
return;
|
||||
}
|
||||
|
||||
var signPath = System.IO.Path.Combine(winsdkBinPath, "signtool.exe");
|
||||
var arguments = $"sign /debug /v /a /fd SHA256 /f {pfxPath} /p {pw} {System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix")}";
|
||||
|
||||
var p = StartProcess(
|
||||
signPath,
|
||||
new ProcessSettings
|
||||
{
|
||||
Arguments = arguments
|
||||
}
|
||||
);
|
||||
if (p != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Sign failed with exit code " + p);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Information("Local configuration. Skip signing.");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
files:
|
||||
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
|
||||
translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.%osx_locale%.resx
|
||||
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.resx
|
||||
translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.%osx_locale%.resx
|
||||
@@ -322,6 +322,7 @@ dotnet_diagnostic.CA2227.severity = suggestion
|
||||
dotnet_diagnostic.CA2251.severity = suggestion
|
||||
|
||||
csharp_style_prefer_primary_constructors = false:none
|
||||
dotnet_diagnostic.SA1124.severity = none
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public sealed class HttpClientTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void RedirectionHeaderTest()
|
||||
{
|
||||
HttpClientHandler handler = new()
|
||||
{
|
||||
UseCookies = false,
|
||||
AllowAutoRedirect = false,
|
||||
};
|
||||
|
||||
using (handler)
|
||||
{
|
||||
using (HttpClient httpClient = new(handler))
|
||||
{
|
||||
using (HttpRequestMessage request = new(HttpMethod.Get, "https://api.snapgenshin.com/patch/hutao/download"))
|
||||
{
|
||||
using (HttpResponseMessage response = httpClient.Send(request))
|
||||
{
|
||||
_ = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,19 +13,19 @@ public sealed class JsonSerializeTest
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
private const string SmapleObjectJson = """
|
||||
private const string SampleObjectJson = """
|
||||
{
|
||||
"A" :1
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SmapleEmptyStringObjectJson = """
|
||||
private const string SampleEmptyStringObjectJson = """
|
||||
{
|
||||
"A" : ""
|
||||
}
|
||||
""";
|
||||
|
||||
private const string SmapleNumberKeyDictionaryJson = """
|
||||
private const string SampleNumberKeyDictionaryJson = """
|
||||
{
|
||||
"111" : "12",
|
||||
"222" : "34"
|
||||
@@ -35,7 +35,7 @@ public sealed class JsonSerializeTest
|
||||
[TestMethod]
|
||||
public void DelegatePropertyCanSerialize()
|
||||
{
|
||||
SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SmapleObjectJson)!;
|
||||
SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SampleObjectJson)!;
|
||||
Assert.AreEqual(sample.B, 1);
|
||||
}
|
||||
|
||||
@@ -43,14 +43,23 @@ public sealed class JsonSerializeTest
|
||||
[ExpectedException(typeof(JsonException))]
|
||||
public void EmptyStringCannotSerializeAsNumber()
|
||||
{
|
||||
SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SmapleEmptyStringObjectJson)!;
|
||||
SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SampleEmptyStringObjectJson)!;
|
||||
Assert.AreEqual(sample.A, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EmptyStringCanSerializeAsUri()
|
||||
{
|
||||
SampleEmptyUriClass sample = JsonSerializer.Deserialize<SampleEmptyUriClass>(SampleEmptyStringObjectJson)!;
|
||||
Uri.TryCreate("", UriKind.RelativeOrAbsolute, out Uri? value);
|
||||
Console.WriteLine(value);
|
||||
Assert.AreEqual(sample.A, value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NumberStringKeyCanSerializeAsKey()
|
||||
{
|
||||
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, AlowStringNumberOptions)!;
|
||||
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SampleNumberKeyDictionaryJson, AlowStringNumberOptions)!;
|
||||
Assert.AreEqual(sample[111], "12");
|
||||
}
|
||||
|
||||
@@ -92,6 +101,11 @@ public sealed class JsonSerializeTest
|
||||
public int A { get; set; }
|
||||
}
|
||||
|
||||
private sealed class SampleEmptyUriClass
|
||||
{
|
||||
public Uri A { get; set; } = default!;
|
||||
}
|
||||
|
||||
private sealed class SampleByteArrayPropertyClass
|
||||
{
|
||||
public byte[]? Array { get; set; }
|
||||
|
||||
14
src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/ListTest.cs
Normal file
14
src/Snap.Hutao/Snap.Hutao.Test/BaseClassLibrary/ListTest.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public sealed class ListTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void IndexOfNullIsNegativeOne()
|
||||
{
|
||||
List<object> list = [new()];
|
||||
Assert.AreEqual(-1, list.IndexOf(default!));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
|
||||
[TestClass]
|
||||
public class UnsafeAccessorTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void UnsafeAccessorCanGetInterfaceProperty()
|
||||
{
|
||||
TestClass test = new();
|
||||
int value = InternalGetInterfaceProperty(test);
|
||||
Assert.AreEqual(3, value);
|
||||
}
|
||||
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_TestProperty")]
|
||||
private static extern int InternalGetInterfaceProperty(ITestInterface instance);
|
||||
|
||||
internal interface ITestInterface
|
||||
{
|
||||
internal int TestProperty { get; }
|
||||
}
|
||||
|
||||
internal sealed class TestClass : ITestInterface
|
||||
{
|
||||
public int TestProperty { get; } = 3;
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,25 @@ namespace Snap.Hutao.Test.IncomingFeature;
|
||||
public class SpiralAbyssScheduleIdTest
|
||||
{
|
||||
private static readonly TimeSpan Utc8 = new(8, 0, 0);
|
||||
private static readonly DateTimeOffset AcrobaticsBattleIntroducedTime = new(2024, 7, 1, 4, 0, 0, Utc8);
|
||||
|
||||
[TestMethod]
|
||||
public void Test()
|
||||
{
|
||||
Console.WriteLine($"当前第 {GetForDateTimeOffset(DateTimeOffset.Now)} 期");
|
||||
|
||||
DateTimeOffset dateTimeOffset = new(2020, 7, 1, 4, 0, 0, Utc8);
|
||||
Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(dateTimeOffset)} 期");
|
||||
// 2020-07-01 04:00:00 为第 1 期
|
||||
// 2024-06-16 04:00:00 为第 96 期
|
||||
// 2024-07-01 04:00:00 为第 97 期
|
||||
// 2024-07-16 04:00:00 为第 98 期
|
||||
// 2024-08-01 04:00:00 为第 99 期
|
||||
Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(new(2020, 07, 01, 4, 0, 0, Utc8))} 期");
|
||||
Console.WriteLine($"2024-06-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 06, 16, 4, 0, 0, Utc8))} 期");
|
||||
Console.WriteLine($"2024-07-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 07, 01, 4, 0, 0, Utc8))} 期");
|
||||
Console.WriteLine($"2024-07-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 07, 16, 4, 0, 0, Utc8))} 期");
|
||||
Console.WriteLine($"2024-08-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 08, 01, 4, 0, 0, Utc8))} 期");
|
||||
Console.WriteLine($"2024-08-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 08, 16, 4, 0, 0, Utc8))} 期");
|
||||
Console.WriteLine($"2024-09-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 09, 01, 4, 0, 0, Utc8))} 期");
|
||||
}
|
||||
|
||||
public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset)
|
||||
@@ -38,6 +49,12 @@ public class SpiralAbyssScheduleIdTest
|
||||
periodNum--;
|
||||
}
|
||||
|
||||
if (dateTimeOffset >= AcrobaticsBattleIntroducedTime)
|
||||
{
|
||||
// 当超过 96 期时,每一个月一期
|
||||
periodNum = (4 * 12 * 2) + ((periodNum - (4 * 12 * 2)) / 2);
|
||||
}
|
||||
|
||||
return periodNum;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.Test.PlatformExtensions;
|
||||
|
||||
@@ -11,6 +12,8 @@ public sealed class DependencyInjectionTest
|
||||
.AddSingleton<IService, ServiceA>()
|
||||
.AddSingleton<IService, ServiceB>()
|
||||
.AddScoped<IScopedService, ServiceA>()
|
||||
.AddKeyedTransient<IKeyedService, KeyedServiceA>("A")
|
||||
.AddKeyedTransient<IKeyedService, KeyedServiceB>("B")
|
||||
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
|
||||
.AddLogging(builder => builder.AddConsole())
|
||||
.BuildServiceProvider();
|
||||
@@ -50,6 +53,15 @@ public sealed class DependencyInjectionTest
|
||||
Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void KeyedServicesCanBeResolvedAsEnumerable()
|
||||
{
|
||||
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("A"));
|
||||
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("B"));
|
||||
|
||||
Assert.AreEqual(0, services.GetServices<IKeyedService>().Count());
|
||||
}
|
||||
|
||||
private interface IService
|
||||
{
|
||||
Guid Id { get; }
|
||||
@@ -95,4 +107,14 @@ public sealed class DependencyInjectionTest
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private interface IKeyedService;
|
||||
|
||||
private sealed class KeyedServiceA : IKeyedService
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class KeyedServiceB : IKeyedService
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Drawing;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Snap.Hutao.Test.RuntimeBehavior;
|
||||
|
||||
[TestClass]
|
||||
public sealed class HttpClientBehaviorTest
|
||||
{
|
||||
private const int MessageNotYetSent = 0;
|
||||
|
||||
[TestMethod]
|
||||
public async Task RetrySendHttpRequestMessage()
|
||||
{
|
||||
using (HttpClient httpClient = new())
|
||||
{
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Post, "https://jsonplaceholder.typicode.com/posts");
|
||||
JsonContent content = JsonContent.Create(new Point(12, 34));
|
||||
requestMessage.Content = content;
|
||||
using (requestMessage)
|
||||
{
|
||||
await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref GetPrivateSendStatus(requestMessage), MessageNotYetSent);
|
||||
Volatile.Write(ref GetPrivateDisposed(content), false);
|
||||
await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// private int _sendStatus
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_sendStatus")]
|
||||
private static extern ref int GetPrivateSendStatus(HttpRequestMessage message);
|
||||
|
||||
// private bool _disposed
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
|
||||
private static extern ref bool GetPrivateDisposed(HttpRequestMessage message);
|
||||
|
||||
// private bool _disposed
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
|
||||
private static extern ref bool GetPrivateDisposed(HttpContent content);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
@@ -46,7 +47,59 @@ public sealed class UnsafeRuntimeBehaviorTest
|
||||
Assert.AreEqual(1212, testStruct.Value4);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void UnsafeUtf8StringReference()
|
||||
{
|
||||
void* ptr = Unsafe.AsPointer(ref MemoryMarshal.GetReference("test"u8));
|
||||
GC.Collect(GC.MaxGeneration);
|
||||
ReadOnlySpan<byte> bytes = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr);
|
||||
Console.WriteLine(System.Text.Encoding.UTF8.GetString(bytes));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void UnsafeSizeInt32ToRectInt32Test()
|
||||
{
|
||||
RectInt32 rectInt32 = ToRectInt32(new(100, 200));
|
||||
Assert.AreEqual(rectInt32.X, 0);
|
||||
Assert.AreEqual(rectInt32.Y, 0);
|
||||
Assert.AreEqual(rectInt32.Width, 100);
|
||||
Assert.AreEqual(rectInt32.Height, 200);
|
||||
|
||||
unsafe RectInt32 ToRectInt32(SizeInt32 sizeInt32)
|
||||
{
|
||||
byte* pBytes = stackalloc byte[sizeof(RectInt32)];
|
||||
*(SizeInt32*)(pBytes + 8) = sizeInt32;
|
||||
return *(RectInt32*)pBytes;
|
||||
}
|
||||
}
|
||||
|
||||
private struct RectInt32
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
public int Width;
|
||||
public int Height;
|
||||
|
||||
public RectInt32(int x, int y, int width, int height)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
}
|
||||
|
||||
private struct SizeInt32
|
||||
{
|
||||
public int Width;
|
||||
public int Height;
|
||||
|
||||
public SizeInt32(int width, int height)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct TestStruct
|
||||
{
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.4.3" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.4.3" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -6,30 +6,39 @@
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources/>
|
||||
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.TokenizingTextBox/TokenizingTextBox.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Loading.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Image/CachedImage.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Card.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Color.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/ComboBox.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Converter.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/CornerRadius.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/FlyoutStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/FontStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Glyph.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/InfoBarOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/ItemsPanelTemplate.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/NumericValue.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/PageOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/SegmentedOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/SettingsStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Thickness.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///View/Card/Primitive/CardProgressBar.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsCard/SettingsCard.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///CommunityToolkit.Labs.WinUI.TokenView/TokenItem/TokenItem.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Elevation.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/ItemIcon.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Loading.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/StandardView.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/AutoSuggestBox/AutoSuggestTokenBox.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/CardBlock.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/CardProgressBar.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/HorizontalCard.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/VerticalCard.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Image/CachedImage.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/TextBlock/RateDeltaTextBlock.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Card.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Color.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/ComboBox.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Converter.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/CornerRadius.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/FlyoutStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/FontStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Glyph.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/InfoBarOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/ItemsPanelTemplate.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/NumericValue.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/PageOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/PivotOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/ScrollViewer.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/SegmentedOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/SettingsStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Thickness.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/TransitionCollection.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Uri.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/WindowOverride.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<Style
|
||||
@@ -43,15 +52,15 @@
|
||||
x:Name="NoneSelectionListViewItemStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
TargetType="ListViewItem">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="0,4,0,0"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Name="NoneSelectionGridViewItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
TargetType="GridViewItem">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="0,0,2,4"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Microsoft.Windows.AppNotifications;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Shell;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
@@ -39,7 +39,7 @@ public sealed partial class App : Application
|
||||
""";
|
||||
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IActivation activation;
|
||||
private readonly IAppActivation activation;
|
||||
private readonly ILogger<App> logger;
|
||||
|
||||
/// <summary>
|
||||
@@ -50,22 +50,33 @@ public sealed partial class App : Application
|
||||
{
|
||||
// Load app resource
|
||||
InitializeComponent();
|
||||
activation = serviceProvider.GetRequiredService<IActivation>();
|
||||
activation = serviceProvider.GetRequiredService<IAppActivation>();
|
||||
logger = serviceProvider.GetRequiredService<ILogger<App>>();
|
||||
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public new void Exit()
|
||||
{
|
||||
XamlApplicationLifetime.Exiting = true;
|
||||
base.Exit();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Important: You must call AppNotificationManager::Default().Register
|
||||
// before calling AppInstance.GetCurrent.GetActivatedEventArgs.
|
||||
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
|
||||
AppNotificationManager.Default.Register();
|
||||
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
|
||||
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
|
||||
{
|
||||
logger.LogDebug("Application exiting on RedirectActivationTo");
|
||||
Exit();
|
||||
return;
|
||||
}
|
||||
@@ -73,15 +84,13 @@ public sealed partial class App : Application
|
||||
logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow));
|
||||
LogDiagnosticInformation();
|
||||
|
||||
// manually invoke
|
||||
// Manually invoke
|
||||
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
|
||||
activation.Initialize();
|
||||
|
||||
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
|
||||
activation.PostInitialization();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// AppInstance.GetCurrent() calls failed
|
||||
logger.LogError(ex, "Application failed in App.OnLaunched");
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序资源提供器
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IAppResourceProvider))]
|
||||
internal sealed class AppResourceProvider : IAppResourceProvider
|
||||
{
|
||||
private readonly App app;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的应用程序资源提供器
|
||||
/// </summary>
|
||||
/// <param name="app">应用</param>
|
||||
public AppResourceProvider(App app)
|
||||
{
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T GetResource<T>(string name)
|
||||
{
|
||||
return (T)app.Resources[name];
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using System.Collections;
|
||||
|
||||
namespace Snap.Hutao.Control.AutoSuggestBox;
|
||||
|
||||
[DependencyProperty("FilterCommand", typeof(ICommand))]
|
||||
[DependencyProperty("FilterCommandParameter", typeof(object))]
|
||||
[DependencyProperty("AvailableTokens", typeof(IReadOnlyDictionary<string, SearchToken>))]
|
||||
internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox
|
||||
{
|
||||
public AutoSuggestTokenBox()
|
||||
{
|
||||
DefaultStyleKey = typeof(TokenizingTextBox);
|
||||
TextChanged += OnFilterSuggestionRequested;
|
||||
QuerySubmitted += OnQuerySubmitted;
|
||||
TokenItemAdding += OnTokenItemAdding;
|
||||
TokenItemAdded += OnTokenItemCollectionChanged;
|
||||
TokenItemRemoved += OnTokenItemCollectionChanged;
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (this.FindDescendant("SuggestionsPopup") is Popup { Child: Border { Child: ListView listView } border })
|
||||
{
|
||||
IAppResourceProvider appResourceProvider = this.ServiceProvider().GetRequiredService<IAppResourceProvider>();
|
||||
|
||||
listView.Background = null;
|
||||
listView.Margin = appResourceProvider.GetResource<Thickness>("AutoSuggestListPadding");
|
||||
|
||||
border.Background = appResourceProvider.GetResource<Microsoft.UI.Xaml.Media.Brush>("AutoSuggestBoxSuggestionsListBackground");
|
||||
CornerRadius overlayCornerRadius = appResourceProvider.GetResource<CornerRadius>("OverlayCornerRadius");
|
||||
CornerRadiusFilterConverter cornerRadiusFilterConverter = new() { Filter = CornerRadiusFilterKind.Bottom };
|
||||
border.CornerRadius = (CornerRadius)cornerRadiusFilterConverter.Convert(overlayCornerRadius, typeof(CornerRadius), default, default);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFilterSuggestionRequested(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Text))
|
||||
{
|
||||
sender.ItemsSource = AvailableTokens
|
||||
.OrderBy(kvp => kvp.Value.Kind)
|
||||
.Select(kvp => kvp.Value);
|
||||
}
|
||||
|
||||
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
|
||||
{
|
||||
sender.ItemsSource = AvailableTokens
|
||||
.Where(kvp => kvp.Value.Value.Contains(Text, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(kvp => kvp.Value.Kind)
|
||||
.ThenBy(kvp => kvp.Value.Order)
|
||||
.Select(kvp => kvp.Value)
|
||||
.DefaultIfEmpty(SearchToken.NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnQuerySubmitted(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
|
||||
{
|
||||
if (args.ChosenSuggestion is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CommandInvocation.TryExecute(FilterCommand, FilterCommandParameter);
|
||||
}
|
||||
|
||||
private void OnTokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(args.TokenText))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (AvailableTokens.GetValueOrDefault(args.TokenText) is { } token)
|
||||
{
|
||||
args.Item = token;
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTokenItemCollectionChanged(TokenizingTextBox sender, object args)
|
||||
{
|
||||
if (args is SearchToken { Kind: SearchTokenKind.None } token)
|
||||
{
|
||||
((IList)sender.ItemsSource).Remove(token);
|
||||
}
|
||||
|
||||
FilterCommand.TryExecute(FilterCommandParameter);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Labs.WinUI.MarqueeTextRns;
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
internal sealed class MarqueeTextBehavior : BehaviorBase<MarqueeText>
|
||||
{
|
||||
private readonly PointerEventHandler pointerEnteredEventHandler;
|
||||
private readonly PointerEventHandler pointerExitedEventHandler;
|
||||
|
||||
public MarqueeTextBehavior()
|
||||
{
|
||||
pointerEnteredEventHandler = OnPointerEntered;
|
||||
pointerExitedEventHandler = OnPointerExited;
|
||||
}
|
||||
|
||||
protected override bool Initialize()
|
||||
{
|
||||
AssociatedObject.PointerEntered += pointerEnteredEventHandler;
|
||||
AssociatedObject.PointerExited += pointerExitedEventHandler;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.PointerEntered -= pointerEnteredEventHandler;
|
||||
AssociatedObject.PointerExited -= pointerExitedEventHandler;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnPointerEntered(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
AssociatedObject.StartMarquee();
|
||||
}
|
||||
|
||||
private void OnPointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
AssociatedObject.StopMarquee();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Brush;
|
||||
|
||||
internal sealed class ColorSegment : IColorSegment
|
||||
{
|
||||
public ColorSegment(Color color, double value)
|
||||
{
|
||||
Color = color;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public Color Color { get; set; }
|
||||
|
||||
public double Value { get; set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Brush;
|
||||
|
||||
internal sealed class ColorSegmentCollection : List<IColorSegment>
|
||||
{
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Brush;
|
||||
|
||||
internal interface IColorSegment
|
||||
{
|
||||
Color Color { get; }
|
||||
|
||||
double Value { get; set; }
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Shapes;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Control.Brush;
|
||||
|
||||
[DependencyProperty("Source", typeof(ColorSegmentCollection), default!, nameof(OnSourceChanged))]
|
||||
internal sealed partial class SegmentedBar : ContentControl
|
||||
{
|
||||
private readonly LinearGradientBrush brush = new() { StartPoint = new(0, 0), EndPoint = new(1, 0), };
|
||||
|
||||
public SegmentedBar()
|
||||
{
|
||||
HorizontalContentAlignment = HorizontalAlignment.Stretch;
|
||||
VerticalContentAlignment = VerticalAlignment.Stretch;
|
||||
|
||||
Content = new Rectangle()
|
||||
{
|
||||
Fill = brush,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch,
|
||||
};
|
||||
}
|
||||
|
||||
private static void OnSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
{
|
||||
UpdateLinearGradientBrush((SegmentedBar)obj);
|
||||
}
|
||||
|
||||
private static void UpdateLinearGradientBrush(SegmentedBar segmentedBar)
|
||||
{
|
||||
GradientStopCollection collection = segmentedBar.brush.GradientStops;
|
||||
collection.Clear();
|
||||
|
||||
ColorSegmentCollection segmentCollection = segmentedBar.Source;
|
||||
|
||||
double total = segmentCollection.Sum(seg => seg.Value);
|
||||
if (total is 0D)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double offset = 0;
|
||||
foreach (ref readonly IColorSegment segment in CollectionsMarshal.AsSpan(segmentCollection))
|
||||
{
|
||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||
offset += segment.Value / total;
|
||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal class ButtonBaseBuilder<TButton> : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase, new()
|
||||
{
|
||||
public TButton Button { get; } = new();
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction.Extension;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal static class ButtonBaseBuilderExtension
|
||||
{
|
||||
public static TBuilder SetContent<TBuilder, TButton>(this TBuilder builder, object? content)
|
||||
where TBuilder : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
builder.Configure(builder => builder.Button.Content = content);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder SetCommand<TBuilder, TButton>(this TBuilder builder, ICommand command)
|
||||
where TBuilder : IButtonBaseBuilder<TButton>
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
builder.Configure(builder => builder.Button.Command = command);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal sealed class ButtonBuilder : ButtonBaseBuilder<Button>;
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal static class ButtonBuilderExtension
|
||||
{
|
||||
public static ButtonBuilder SetContent(this ButtonBuilder builder, object? content)
|
||||
{
|
||||
return builder.SetContent<ButtonBuilder, Button>(content);
|
||||
}
|
||||
|
||||
public static ButtonBuilder SetCommand(this ButtonBuilder builder, ICommand command)
|
||||
{
|
||||
return builder.SetCommand<ButtonBuilder, Button>(command);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Control.Builder.ButtonBase;
|
||||
|
||||
internal interface IButtonBaseBuilder<TButton> : IBuilder
|
||||
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
|
||||
{
|
||||
TButton Button { get; }
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Snap.Hutao.Control.Collection.Alternating;
|
||||
|
||||
[Obsolete("Use SettingsCard instead")]
|
||||
[DependencyProperty("ItemAlternateBackground", typeof(Microsoft.UI.Xaml.Media.Brush))]
|
||||
internal sealed partial class AlternatingItemsControl : ItemsControl
|
||||
{
|
||||
private readonly VectorChangedEventHandler<object> itemsVectorChangedEventHandler;
|
||||
|
||||
public AlternatingItemsControl()
|
||||
{
|
||||
itemsVectorChangedEventHandler = OnItemsVectorChanged;
|
||||
Items.VectorChanged += itemsVectorChangedEventHandler;
|
||||
}
|
||||
|
||||
private void OnItemsVectorChanged(IObservableVector<object> items, IVectorChangedEventArgs args)
|
||||
{
|
||||
if (args.CollectionChange is CollectionChange.Reset)
|
||||
{
|
||||
int index = (int)args.Index;
|
||||
for (int i = index; i < items.Count; i++)
|
||||
{
|
||||
if (items[i] is IAlternatingItem item)
|
||||
{
|
||||
item.Background = i % 2 is 0 ? default : ItemAlternateBackground;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Collection.Alternating;
|
||||
|
||||
[Obsolete("Use SettingsCard instead")]
|
||||
internal interface IAlternatingItem
|
||||
{
|
||||
public Microsoft.UI.Xaml.Media.Brush? Background { get; set; }
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存图像
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class CachedImage : Implementation.ImageEx
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的缓存图像
|
||||
/// </summary>
|
||||
public CachedImage()
|
||||
{
|
||||
DefaultStyleKey = typeof(CachedImage);
|
||||
DefaultStyleResourceUri = "ms-appx:///Control/Image/CachedImage.xaml".ToUri();
|
||||
|
||||
IsCacheEnabled = true;
|
||||
EnableLazyLoading = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
|
||||
|
||||
try
|
||||
{
|
||||
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
|
||||
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
|
||||
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
|
||||
return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// The image is corrupted, remove it.
|
||||
imageCache.Remove(imageUri);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Media.Casting;
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
[DependencyProperty("NineGrid", typeof(Thickness))]
|
||||
internal partial class ImageEx : ImageExBase
|
||||
{
|
||||
public ImageEx()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
|
||||
public override CompositionBrush GetAlphaMask()
|
||||
{
|
||||
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
return image.GetAlphaMask();
|
||||
}
|
||||
|
||||
return default!;
|
||||
}
|
||||
|
||||
public CastingSource GetAsCastingSource()
|
||||
{
|
||||
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
return image.GetAsCastingSource();
|
||||
}
|
||||
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Graphics.Canvas.Effects;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Control.Theme;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Image;
|
||||
|
||||
/// <summary>
|
||||
/// 支持单色的图像
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class MonoChrome : CompositionImage
|
||||
{
|
||||
private readonly TypedEventHandler<FrameworkElement, object> actualThemeChangedEventHandler;
|
||||
|
||||
private CompositionColorBrush? backgroundBrush;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的单色图像
|
||||
/// </summary>
|
||||
public MonoChrome()
|
||||
{
|
||||
actualThemeChangedEventHandler = OnActualThemeChanged;
|
||||
ActualThemeChanged += actualThemeChangedEventHandler;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override SpriteVisual CompositeSpriteVisual(Compositor compositor, LoadedImageSurface imageSurface)
|
||||
{
|
||||
CompositionColorBrush blackLayerBrush = compositor.CreateColorBrush(Colors.Black);
|
||||
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.Uniform, vRatio: 0f);
|
||||
CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBrush, imageSurfaceBrush, BlendEffectMode.Overlay);
|
||||
CompositionEffectBrush opacityBrush = compositor.CompositeLuminanceToAlphaEffectBrush(overlayBrush);
|
||||
|
||||
backgroundBrush = compositor.CreateColorBrush();
|
||||
SetBackgroundColor(backgroundBrush);
|
||||
CompositionEffectBrush alphaMaskEffectBrush = compositor.CompositeAlphaMaskEffectBrush(backgroundBrush, opacityBrush);
|
||||
|
||||
return compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
|
||||
}
|
||||
|
||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
if (backgroundBrush is not null)
|
||||
{
|
||||
SetBackgroundColor(backgroundBrush);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBackgroundColor(CompositionColorBrush backgroundBrush)
|
||||
{
|
||||
ApplicationTheme theme = ThemeHelper.ElementToApplication(ActualTheme);
|
||||
|
||||
backgroundBrush.Color = theme switch
|
||||
{
|
||||
ApplicationTheme.Light => Colors.Black,
|
||||
ApplicationTheme.Dark => Colors.White,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
|
||||
/// <summary>
|
||||
/// BGRA 结构
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal struct Bgra32
|
||||
{
|
||||
/// <summary>
|
||||
/// B
|
||||
/// </summary>
|
||||
public byte B;
|
||||
|
||||
/// <summary>
|
||||
/// G
|
||||
/// </summary>
|
||||
public byte G;
|
||||
|
||||
/// <summary>
|
||||
/// R
|
||||
/// </summary>
|
||||
public byte R;
|
||||
|
||||
/// <summary>
|
||||
/// A
|
||||
/// </summary>
|
||||
public byte A;
|
||||
|
||||
public Bgra32(byte b, byte g, byte r, byte a)
|
||||
{
|
||||
B = b;
|
||||
G = g;
|
||||
R = r;
|
||||
A = a;
|
||||
}
|
||||
|
||||
public readonly double Luminance { get => ((0.299 * R) + (0.587 * G) + (0.114 * B)) / 255; }
|
||||
|
||||
/// <summary>
|
||||
/// 从 Color 转换
|
||||
/// </summary>
|
||||
/// <param name="color">颜色</param>
|
||||
/// <returns>新的 BGRA8 结构</returns>
|
||||
public static unsafe implicit operator Bgra32(Color color)
|
||||
{
|
||||
Unsafe.SkipInit(out Bgra32 bgra8);
|
||||
*(uint*)&bgra8 = BinaryPrimitives.ReverseEndianness(*(uint*)&color);
|
||||
return bgra8;
|
||||
}
|
||||
|
||||
public static unsafe implicit operator Color(Bgra32 bgra8)
|
||||
{
|
||||
Unsafe.SkipInit(out Color color);
|
||||
*(uint*)&color = BinaryPrimitives.ReverseEndianness(*(uint*)&bgra8);
|
||||
return color;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
// Some part of this file came from:
|
||||
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
|
||||
/// </summary>
|
||||
internal struct Hsla32
|
||||
{
|
||||
/// <summary>
|
||||
/// The Hue in 0..360 range.
|
||||
/// </summary>
|
||||
public double H;
|
||||
|
||||
/// <summary>
|
||||
/// The Saturation in 0..1 range.
|
||||
/// </summary>
|
||||
public double S;
|
||||
|
||||
/// <summary>
|
||||
/// The Lightness in 0..1 range.
|
||||
/// </summary>
|
||||
public double L;
|
||||
|
||||
/// <summary>
|
||||
/// The Alpha/opacity in 0..1 range.
|
||||
/// </summary>
|
||||
public double A;
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
// Some part of this file came from:
|
||||
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
|
||||
/// <summary>
|
||||
/// RGBA 颜色
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal struct Rgba32
|
||||
{
|
||||
/// <summary>
|
||||
/// R
|
||||
/// </summary>
|
||||
public byte R;
|
||||
|
||||
/// <summary>
|
||||
/// G
|
||||
/// </summary>
|
||||
public byte G;
|
||||
|
||||
/// <summary>
|
||||
/// B
|
||||
/// </summary>
|
||||
public byte B;
|
||||
|
||||
/// <summary>
|
||||
/// A
|
||||
/// </summary>
|
||||
public byte A;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的 RGBA8 颜色
|
||||
/// </summary>
|
||||
/// <param name="hex">色值字符串</param>
|
||||
public Rgba32(string hex)
|
||||
: this(hex.Length == 6 ? Convert.ToUInt32($"{hex}FF", 16) : Convert.ToUInt32(hex, 16))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 RGBA 代码初始化新的结构
|
||||
/// </summary>
|
||||
/// <param name="xrgbaCode">RGBA 代码</param>
|
||||
public unsafe Rgba32(uint xrgbaCode)
|
||||
{
|
||||
// uint layout: 0xRRGGBBAA is AABBGGRR
|
||||
// AABBGGRR -> RRGGBBAA
|
||||
fixed (Rgba32* pSelf = &this)
|
||||
{
|
||||
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(xrgbaCode);
|
||||
}
|
||||
}
|
||||
|
||||
private Rgba32(byte r, byte g, byte b, byte a)
|
||||
{
|
||||
R = r;
|
||||
G = g;
|
||||
B = b;
|
||||
A = a;
|
||||
}
|
||||
|
||||
public static unsafe implicit operator Color(Rgba32 hexColor)
|
||||
{
|
||||
// Goal : Rgba32:RRGGBBAA(0xAABBGGRR) -> Color: AARRGGBB(0xBBGGRRAA)
|
||||
// Step1: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:AA000000(0x000000AA)
|
||||
uint a = ((*(uint*)&hexColor) >> 24) & 0x000000FF;
|
||||
|
||||
// Step2: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:00RRGGBB(0xRRGGBB00)
|
||||
uint rgb = ((*(uint*)&hexColor) << 8) & 0xFFFFFF00;
|
||||
|
||||
// Step2: UInt32:00RRGGBB(0xRRGGBB00) + UInt32:AA000000(0x000000AA) -> UInt32:AARRGGBB(0xRRGGBBAA)
|
||||
uint rgba = rgb + a;
|
||||
|
||||
return *(Color*)&rgba;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 HSL 颜色转换
|
||||
/// </summary>
|
||||
/// <param name="hsl">HSL 颜色</param>
|
||||
/// <returns>RGBA8颜色</returns>
|
||||
public static Rgba32 FromHsl(Hsla32 hsl)
|
||||
{
|
||||
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
|
||||
double h1 = hsl.H / 60;
|
||||
double x = chroma * (1 - Math.Abs((h1 % 2) - 1));
|
||||
double m = hsl.L - (0.5 * chroma);
|
||||
double r1, g1, b1;
|
||||
|
||||
if (h1 < 1)
|
||||
{
|
||||
r1 = chroma;
|
||||
g1 = x;
|
||||
b1 = 0;
|
||||
}
|
||||
else if (h1 < 2)
|
||||
{
|
||||
r1 = x;
|
||||
g1 = chroma;
|
||||
b1 = 0;
|
||||
}
|
||||
else if (h1 < 3)
|
||||
{
|
||||
r1 = 0;
|
||||
g1 = chroma;
|
||||
b1 = x;
|
||||
}
|
||||
else if (h1 < 4)
|
||||
{
|
||||
r1 = 0;
|
||||
g1 = x;
|
||||
b1 = chroma;
|
||||
}
|
||||
else if (h1 < 5)
|
||||
{
|
||||
r1 = x;
|
||||
g1 = 0;
|
||||
b1 = chroma;
|
||||
}
|
||||
else
|
||||
{
|
||||
r1 = chroma;
|
||||
g1 = 0;
|
||||
b1 = x;
|
||||
}
|
||||
|
||||
byte r = (byte)(255 * (r1 + m));
|
||||
byte g = (byte)(255 * (g1 + m));
|
||||
byte b = (byte)(255 * (b1 + m));
|
||||
byte a = (byte)(255 * hsl.A);
|
||||
|
||||
return new(r, g, b, a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到 HSL 颜色
|
||||
/// </summary>
|
||||
/// <returns>HSL 颜色</returns>
|
||||
public readonly Hsla32 ToHsl()
|
||||
{
|
||||
const double toDouble = 1.0 / 255;
|
||||
double r = toDouble * R;
|
||||
double g = toDouble * G;
|
||||
double b = toDouble * B;
|
||||
double max = Math.Max(Math.Max(r, g), b);
|
||||
double min = Math.Min(Math.Min(r, g), b);
|
||||
double chroma = max - min;
|
||||
double h1;
|
||||
|
||||
if (chroma == 0)
|
||||
{
|
||||
h1 = 0;
|
||||
}
|
||||
else if (max == r)
|
||||
{
|
||||
// The % operator doesn't do proper modulo on negative
|
||||
// numbers, so we'll add 6 before using it
|
||||
h1 = (((g - b) / chroma) + 6) % 6;
|
||||
}
|
||||
else if (max == g)
|
||||
{
|
||||
h1 = 2 + ((b - r) / chroma);
|
||||
}
|
||||
else
|
||||
{
|
||||
h1 = 4 + ((r - g) / chroma);
|
||||
}
|
||||
|
||||
double lightness = 0.5 * (max + min);
|
||||
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
|
||||
|
||||
Hsla32 ret;
|
||||
ret.H = 60 * h1;
|
||||
ret.S = saturation;
|
||||
ret.L = lightness;
|
||||
ret.A = toDouble * A;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Control.Theme;
|
||||
|
||||
internal static class KnownColors
|
||||
{
|
||||
public static readonly Color Orange = StructMarshal.Color(0xFFBC6932);
|
||||
public static readonly Color Purple = StructMarshal.Color(0xFFA156E0);
|
||||
public static readonly Color Blue = StructMarshal.Color(0xFF5180CB);
|
||||
public static readonly Color Green = StructMarshal.Color(0xFF2A8F72);
|
||||
public static readonly Color White = StructMarshal.Color(0xFF72778B);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
/// <summary>
|
||||
/// 值转换器
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">源类型</typeparam>
|
||||
/// <typeparam name="TTo">目标类型</typeparam>
|
||||
internal abstract class ValueConverter<TFrom, TTo> : IValueConverter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return Convert((TFrom)value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public object? ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return ConvertBack((TTo)value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从源类型转换到目标类型
|
||||
/// </summary>
|
||||
/// <param name="from">源</param>
|
||||
/// <returns>目标</returns>
|
||||
public abstract TTo Convert(TFrom from);
|
||||
|
||||
/// <summary>
|
||||
/// 从目标类型转换到源类型
|
||||
/// 重写时请勿调用基类方法
|
||||
/// </summary>
|
||||
/// <param name="to">目标</param>
|
||||
/// <returns>源</returns>
|
||||
public virtual TFrom ConvertBack(TTo to)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Core.Abstraction.Extension;
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
internal static class BuilderExtension
|
||||
{
|
||||
@@ -5,5 +5,5 @@ namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
internal interface IPinnable<TData>
|
||||
{
|
||||
ref readonly TData GetPinnableReference();
|
||||
ref TData GetPinnableReference();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
internal interface IResurrectable
|
||||
{
|
||||
void Resurrect();
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
@@ -11,27 +12,11 @@ namespace Snap.Hutao.Core.Caching;
|
||||
[HighQuality]
|
||||
internal interface IImageCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the file path containing cached item for given Uri
|
||||
/// </summary>
|
||||
/// <param name="uri">Uri of the item.</param>
|
||||
/// <returns>a string path</returns>
|
||||
ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri);
|
||||
|
||||
/// <summary>
|
||||
/// Removed items based on uri list passed
|
||||
/// </summary>
|
||||
/// <param name="uriForCachedItems">Enumerable uri list</param>
|
||||
ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri, ElementTheme theme);
|
||||
|
||||
void Remove(in ReadOnlySpan<Uri> uriForCachedItems);
|
||||
|
||||
/// <summary>
|
||||
/// Removed item based on uri passed
|
||||
/// </summary>
|
||||
/// <param name="uriForCachedItem">uri</param>
|
||||
void Remove(Uri uriForCachedItem);
|
||||
|
||||
/// <summary>
|
||||
/// Removes invalid cached files
|
||||
/// </summary>
|
||||
void RemoveInvalid();
|
||||
}
|
||||
@@ -2,25 +2,30 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.IO.Hashing;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.UI;
|
||||
using Snap.Hutao.ViewModel.Guide;
|
||||
using Snap.Hutao.Web;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using Snap.Hutao.Win32.System.WinRT;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics.Imaging;
|
||||
using WinRT;
|
||||
|
||||
namespace Snap.Hutao.Core.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods and tools to cache files in a folder
|
||||
/// The class's name will become the cache folder's name
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IImageCache))]
|
||||
@@ -28,17 +33,17 @@ namespace Snap.Hutao.Core.Caching;
|
||||
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
|
||||
internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
{
|
||||
private const string CacheFolderName = nameof(ImageCache);
|
||||
private const string CacheFailedDownloadTasksName = $"{nameof(ImageCache)}.FailedDownloadTasks";
|
||||
|
||||
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = FrozenDictionary.ToFrozenDictionary(
|
||||
private static readonly FrozenDictionary<int, TimeSpan> DelayFromRetryCount = FrozenDictionary.ToFrozenDictionary(
|
||||
[
|
||||
KeyValuePair.Create(0, TimeSpan.FromSeconds(4)),
|
||||
KeyValuePair.Create(1, TimeSpan.FromSeconds(16)),
|
||||
KeyValuePair.Create(2, TimeSpan.FromSeconds(64)),
|
||||
]);
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
private readonly ConcurrentDictionary<ElementThemeValueFile, Task> themefileTasks = [];
|
||||
private readonly ConcurrentDictionary<string, Task> downloadTasks = [];
|
||||
|
||||
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
@@ -46,32 +51,24 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
||||
private readonly ILogger<ImageCache> logger;
|
||||
private readonly IMemoryCache memoryCache;
|
||||
|
||||
private string? baseFolder;
|
||||
private string? cacheFolder;
|
||||
|
||||
private string CacheFolder
|
||||
{
|
||||
get => LazyInitializer.EnsureInitialized(ref cacheFolder, () =>
|
||||
{
|
||||
baseFolder ??= serviceProvider.GetRequiredService<RuntimeOptions>().LocalCache;
|
||||
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
|
||||
return info.FullName;
|
||||
string folder = serviceProvider.GetRequiredService<RuntimeOptions>().GetLocalCacheImageCacheFolder();
|
||||
Directory.CreateDirectory(Path.Combine(folder, "Light"));
|
||||
Directory.CreateDirectory(Path.Combine(folder, "Dark"));
|
||||
return folder;
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RemoveInvalid()
|
||||
{
|
||||
RemoveCore(Directory.GetFiles(CacheFolder).Where(file => IsFileInvalid(file, false)));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Remove(Uri uriForCachedItem)
|
||||
{
|
||||
Remove([uriForCachedItem]);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Remove(in ReadOnlySpan<Uri> uriForCachedItems)
|
||||
{
|
||||
if (uriForCachedItems.Length <= 0)
|
||||
@@ -95,45 +92,87 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
||||
RemoveCore(filesToDelete);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri)
|
||||
public ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri)
|
||||
{
|
||||
return GetFileFromCacheAsync(uri, ElementTheme.Default);
|
||||
}
|
||||
|
||||
public async ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri, ElementTheme theme)
|
||||
{
|
||||
string fileName = GetCacheFileName(uri);
|
||||
string filePath = Path.Combine(CacheFolder, fileName);
|
||||
string defaultFilePath = Path.Combine(CacheFolder, fileName);
|
||||
string themeOrDefaultFilePath = theme is ElementTheme.Dark or ElementTheme.Light
|
||||
? Path.Combine(CacheFolder, $"{theme}", fileName)
|
||||
: defaultFilePath;
|
||||
|
||||
if (!IsFileInvalid(filePath))
|
||||
if (!IsFileInvalid(themeOrDefaultFilePath))
|
||||
{
|
||||
return filePath;
|
||||
return themeOrDefaultFilePath;
|
||||
}
|
||||
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
try
|
||||
ElementThemeValueFile key = new(fileName, theme);
|
||||
|
||||
// To prevent re-entrancy, always try add first, and if add failed, we try to get the task
|
||||
TaskCompletionSource themeFileTcs = new();
|
||||
if (themefileTasks.TryAdd(key, themeFileTcs.Task))
|
||||
{
|
||||
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
|
||||
try
|
||||
{
|
||||
logger.LogColorizedInformation("Begin to download file from '{Uri}' to '{File}'", (uri, ConsoleColor.Cyan), (filePath, ConsoleColor.Cyan));
|
||||
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
|
||||
if (!IsFileInvalid(defaultFilePath))
|
||||
{
|
||||
await ConvertAndSaveFileToMonoChromeAsync(defaultFilePath, themeOrDefaultFilePath, theme).ConfigureAwait(false);
|
||||
return themeOrDefaultFilePath;
|
||||
}
|
||||
|
||||
TaskCompletionSource downloadTcs = new();
|
||||
if (downloadTasks.TryAdd(fileName, downloadTcs.Task))
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogColorizedInformation("Begin to download file from '{Uri}' to '{File}'", (uri, ConsoleColor.Cyan), (defaultFilePath, ConsoleColor.Cyan));
|
||||
await DownloadFileAsync(uri, defaultFilePath).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
downloadTcs.TrySetResult();
|
||||
downloadTasks.TryRemove(fileName, out _);
|
||||
}
|
||||
}
|
||||
else if (downloadTasks.TryGetValue(fileName, out Task? task))
|
||||
{
|
||||
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", (uri, ConsoleColor.Cyan));
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!IsFileInvalid(defaultFilePath))
|
||||
{
|
||||
await ConvertAndSaveFileToMonoChromeAsync(defaultFilePath, themeOrDefaultFilePath, theme).ConfigureAwait(false);
|
||||
return themeOrDefaultFilePath;
|
||||
}
|
||||
|
||||
return themeOrDefaultFilePath;
|
||||
}
|
||||
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
|
||||
finally
|
||||
{
|
||||
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", (uri, ConsoleColor.Cyan));
|
||||
await task.ConfigureAwait(false);
|
||||
themeFileTcs.TrySetResult();
|
||||
themefileTasks.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
concurrentTasks.TryRemove(fileName, out _);
|
||||
}
|
||||
finally
|
||||
else if (themefileTasks.TryGetValue(key, out Task? themeTask))
|
||||
{
|
||||
taskCompletionSource.TrySetResult();
|
||||
await themeTask.ConfigureAwait(false);
|
||||
return themeOrDefaultFilePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw HutaoException.NotSupported("The task should not be null.");
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueFile GetFileFromCategoryAndName(string category, string fileName)
|
||||
{
|
||||
Uri dummyUri = Web.HutaoEndpoints.StaticRaw(category, fileName).ToUri();
|
||||
Uri dummyUri = HutaoEndpoints.StaticRaw(category, fileName).ToUri();
|
||||
return Path.Combine(CacheFolder, GetCacheFileName(dummyUri));
|
||||
}
|
||||
|
||||
@@ -149,8 +188,51 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
||||
return treatNullFileAsInvalid;
|
||||
}
|
||||
|
||||
FileInfo fileInfo = new(file);
|
||||
return fileInfo.Length == 0;
|
||||
return new FileInfo(file).Length == 0;
|
||||
}
|
||||
|
||||
private static async ValueTask ConvertAndSaveFileToMonoChromeAsync(string sourceFile, string themeFile, ElementTheme theme)
|
||||
{
|
||||
if (string.Equals(sourceFile, themeFile, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (FileStream sourceStream = File.OpenRead(sourceFile))
|
||||
{
|
||||
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(sourceStream.AsRandomAccessStream());
|
||||
|
||||
// Always premultiplied to prevent some channels have a non-zero value when the alpha channel is zero
|
||||
using (SoftwareBitmap sourceBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied))
|
||||
{
|
||||
using (BitmapBuffer sourceBuffer = sourceBitmap.LockBuffer(BitmapBufferAccessMode.ReadWrite))
|
||||
{
|
||||
using (IMemoryBufferReference reference = sourceBuffer.CreateReference())
|
||||
{
|
||||
IMemoryBufferByteAccess byteAccess = reference.As<IMemoryBufferByteAccess>();
|
||||
byte value = theme is ElementTheme.Light ? (byte)0x00 : (byte)0xFF;
|
||||
ConvertToMonoChrome(byteAccess, value);
|
||||
}
|
||||
}
|
||||
|
||||
using (FileStream themeStream = File.Create(themeFile))
|
||||
{
|
||||
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, themeStream.AsRandomAccessStream());
|
||||
encoder.SetSoftwareBitmap(sourceBitmap);
|
||||
await encoder.FlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ConvertToMonoChrome(IMemoryBufferByteAccess byteAccess, byte background)
|
||||
{
|
||||
byteAccess.GetBuffer(out Span<Rgba32> span);
|
||||
foreach (ref Rgba32 pixel in span)
|
||||
{
|
||||
pixel.A = (byte)pixel.Luminance255;
|
||||
pixel.R = pixel.G = pixel.B = background;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveCore(IEnumerable<string> filePaths)
|
||||
@@ -172,80 +254,93 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
||||
[SuppressMessage("", "SH003")]
|
||||
private async Task DownloadFileAsync(Uri uri, string baseFile)
|
||||
{
|
||||
int retryCount = 0;
|
||||
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
|
||||
while (retryCount < 3)
|
||||
using (HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache)))
|
||||
{
|
||||
int retryCount = 0;
|
||||
|
||||
HttpRequestMessageBuilder requestMessageBuilder = httpRequestMessageBuilderFactory
|
||||
.Create()
|
||||
.SetRequestUri(uri)
|
||||
|
||||
// These headers are only available for our own api
|
||||
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase))
|
||||
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase)) // These headers are only available for our own api
|
||||
.Get();
|
||||
|
||||
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
|
||||
while (retryCount < 3)
|
||||
{
|
||||
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
{
|
||||
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
|
||||
{
|
||||
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
|
||||
}
|
||||
requestMessageBuilder.Resurrect();
|
||||
|
||||
if (responseMessage.IsSuccessStatusCode)
|
||||
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
|
||||
{
|
||||
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
{
|
||||
if (responseMessage.Content.Headers.ContentType?.MediaType is "application/json")
|
||||
// Redirect detection
|
||||
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugTrack(uri);
|
||||
#endif
|
||||
string raw = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
logger.LogColorizedCritical("Failed to download '{Uri}' with unexpected body '{Raw}'", (uri, ConsoleColor.Red), (raw, ConsoleColor.DarkYellow));
|
||||
return;
|
||||
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
|
||||
}
|
||||
|
||||
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
if (responseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
using (FileStream fileStream = File.Create(baseFile))
|
||||
if (responseMessage.Content.Headers.ContentType?.MediaType is "application/json")
|
||||
{
|
||||
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
DebugTrackFailedUri(uri);
|
||||
string raw = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
logger.LogColorizedCritical("Failed to download '{Uri}' with unexpected body '{Raw}'", (uri, ConsoleColor.Red), (raw, ConsoleColor.DarkYellow));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (responseMessage.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
|
||||
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
break;
|
||||
using (FileStream fileStream = File.Create(baseFile))
|
||||
{
|
||||
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
#if DEBUG
|
||||
DebugTrack(uri);
|
||||
#endif
|
||||
logger.LogColorizedCritical("Failed to download '{Uri}' with status code '{StatusCode}'", (uri, ConsoleColor.Red), (responseMessage.StatusCode, ConsoleColor.DarkYellow));
|
||||
return;
|
||||
switch (responseMessage.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
{
|
||||
retryCount++;
|
||||
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? DelayFromRetryCount[retryCount];
|
||||
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
DebugTrackFailedUri(uri);
|
||||
logger.LogColorizedCritical("Failed to download '{Uri}' with status code '{StatusCode}'", (uri, ConsoleColor.Red), (responseMessage.StatusCode, ConsoleColor.DarkYellow));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
internal partial class ImageCache
|
||||
{
|
||||
private void DebugTrack(Uri uri)
|
||||
[Conditional("DEBUG")]
|
||||
private void DebugTrackFailedUri(Uri uri)
|
||||
{
|
||||
HashSet<string>? set = memoryCache.GetOrCreate(CacheFailedDownloadTasksName, entry => entry.Value ??= new HashSet<string>()) as HashSet<string>;
|
||||
HashSet<string>? set = memoryCache.GetOrCreate(CacheFailedDownloadTasksName, entry => new HashSet<string>());
|
||||
set?.Add(uri.ToString());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private readonly struct ElementThemeValueFile
|
||||
{
|
||||
public readonly ValueFile File;
|
||||
public readonly ElementTheme Theme;
|
||||
|
||||
public ElementThemeValueFile(ValueFile file, ElementTheme theme)
|
||||
{
|
||||
File = file;
|
||||
Theme = theme;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(File, Theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 支持Md5转换
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class Convert
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取字符串的MD5计算结果
|
||||
/// </summary>
|
||||
/// <param name="source">源字符串</param>
|
||||
/// <returns>计算的结果</returns>
|
||||
public static string ToMd5HexString(string source)
|
||||
{
|
||||
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
|
||||
return System.Convert.ToHexString(hash);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.DataTransfer;
|
||||
namespace Snap.Hutao.Core.DataTransfer;
|
||||
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Transient, typeof(IClipboardProvider))]
|
||||
@@ -13,7 +13,6 @@ internal sealed partial class ClipboardProvider : IClipboardProvider
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<T?> DeserializeFromJsonAsync<T>()
|
||||
where T : class
|
||||
{
|
||||
@@ -31,7 +30,6 @@ internal sealed partial class ClipboardProvider : IClipboardProvider
|
||||
return JsonSerializer.Deserialize<T>(json, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetText(string text)
|
||||
{
|
||||
try
|
||||
@@ -48,7 +46,23 @@ internal sealed partial class ClipboardProvider : IClipboardProvider
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> SetTextAsync(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
DataPackage content = new() { RequestedOperation = DataPackageOperation.Copy };
|
||||
content.SetText(text);
|
||||
Clipboard.SetContent(content);
|
||||
Clipboard.Flush();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SetBitmap(IRandomAccessStream stream)
|
||||
{
|
||||
try
|
||||
@@ -65,4 +79,22 @@ internal sealed partial class ClipboardProvider : IClipboardProvider
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<bool> SetBitmapAsync(IRandomAccessStream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
RandomAccessStreamReference reference = RandomAccessStreamReference.CreateFromStream(stream);
|
||||
DataPackage content = new() { RequestedOperation = DataPackageOperation.Copy };
|
||||
content.SetBitmap(reference);
|
||||
Clipboard.SetContent(content);
|
||||
Clipboard.Flush();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Snap.Hutao.Core.DataTransfer;
|
||||
|
||||
internal interface IClipboardProvider
|
||||
{
|
||||
ValueTask<T?> DeserializeFromJsonAsync<T>()
|
||||
where T : class;
|
||||
|
||||
bool SetBitmap(IRandomAccessStream stream);
|
||||
|
||||
ValueTask<bool> SetBitmapAsync(IRandomAccessStream stream);
|
||||
|
||||
bool SetText(string text);
|
||||
|
||||
ValueTask<bool> SetTextAsync(string text);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
namespace Snap.Hutao.Core.Database.Abstraction;
|
||||
|
||||
internal interface IReorderable
|
||||
{
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Core.Database.Abstraction;
|
||||
|
||||
internal interface ISelectable : IAppDbEntity
|
||||
{
|
||||
bool IsSelected { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Database.Abstraction;
|
||||
using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.UI.Xaml.Data;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
// The scope of the view follows the scope of the service provider.
|
||||
internal sealed class AdvancedDbCollectionView<TEntity> : AdvancedCollectionView<TEntity>, IAdvancedDbCollectionView<TEntity>
|
||||
where TEntity : class, IAdvancedCollectionViewItem, ISelectable
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private bool savingToDatabase = true;
|
||||
|
||||
public AdvancedDbCollectionView(IList<TEntity> source, IServiceProvider serviceProvider)
|
||||
: base(source)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public IDisposable SuppressChangeCurrentItem()
|
||||
{
|
||||
return new CurrentItemSuppression(this);
|
||||
}
|
||||
|
||||
protected override void OnCurrentChangedOverride()
|
||||
{
|
||||
if (!savingToDatabase)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TEntity? currentItem = CurrentItem;
|
||||
|
||||
foreach (TEntity item in SourceCollection)
|
||||
{
|
||||
item.IsSelected = ReferenceEquals(item, currentItem);
|
||||
}
|
||||
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
dbContext.Set<TEntity>().ExecuteUpdate(update => update.SetProperty(entity => entity.IsSelected, false));
|
||||
|
||||
if (currentItem is not null)
|
||||
{
|
||||
dbContext.Set<TEntity>().UpdateAndSave(currentItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CurrentItemSuppression : IDisposable
|
||||
{
|
||||
private readonly AdvancedDbCollectionView<TEntity> view;
|
||||
private readonly TEntity? currentItem;
|
||||
|
||||
public CurrentItemSuppression(AdvancedDbCollectionView<TEntity> view)
|
||||
{
|
||||
this.view = view;
|
||||
currentItem = view.CurrentItem;
|
||||
view.savingToDatabase = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
view.MoveCurrentTo(currentItem);
|
||||
view.savingToDatabase = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The scope of the view follows the scope of the service provider.
|
||||
[SuppressMessage("", "SA1402")]
|
||||
internal sealed class AdvancedDbCollectionView<TEntityAccess, TEntity> : AdvancedCollectionView<TEntityAccess>, IAdvancedDbCollectionView<TEntityAccess>
|
||||
where TEntityAccess : class, IEntityAccess<TEntity>, IAdvancedCollectionViewItem
|
||||
where TEntity : class, ISelectable
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private bool savingToDatabase = true;
|
||||
|
||||
public AdvancedDbCollectionView(IList<TEntityAccess> source, IServiceProvider serviceProvider)
|
||||
: base(source)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public IDisposable SuppressChangeCurrentItem()
|
||||
{
|
||||
return new CurrentItemSuppression(this);
|
||||
}
|
||||
|
||||
protected override void OnCurrentChangedOverride()
|
||||
{
|
||||
if (!savingToDatabase)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TEntityAccess? currentItem = CurrentItem;
|
||||
|
||||
foreach (TEntityAccess item in SourceCollection)
|
||||
{
|
||||
item.Entity.IsSelected = ReferenceEquals(item, currentItem);
|
||||
}
|
||||
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
dbContext.Set<TEntity>().ExecuteUpdate(update => update.SetProperty(entity => entity.IsSelected, false));
|
||||
|
||||
if (currentItem is not null)
|
||||
{
|
||||
dbContext.Set<TEntity>().UpdateAndSave(currentItem.Entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CurrentItemSuppression : IDisposable
|
||||
{
|
||||
private readonly AdvancedDbCollectionView<TEntityAccess, TEntity> view;
|
||||
private readonly TEntityAccess? currentItem;
|
||||
|
||||
public CurrentItemSuppression(AdvancedDbCollectionView<TEntityAccess, TEntity> view)
|
||||
{
|
||||
this.view = view;
|
||||
currentItem = view.CurrentItem;
|
||||
view.savingToDatabase = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
view.MoveCurrentTo(currentItem);
|
||||
view.savingToDatabase = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,6 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Add(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -34,13 +27,6 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.AddRange(entities);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -48,13 +34,6 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Remove(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
@@ -62,13 +41,6 @@ internal static class DbSetExtension
|
||||
return dbSet.SaveChangesAndClearChangeTracker();
|
||||
}
|
||||
|
||||
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Update(entity);
|
||||
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int SaveChangesAndClearChangeTracker<TEntity>(this DbSet<TEntity> dbSet)
|
||||
where TEntity : class
|
||||
@@ -79,16 +51,6 @@ internal static class DbSetExtension
|
||||
return count;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet, CancellationToken token = default)
|
||||
where TEntity : class
|
||||
{
|
||||
DbContext dbContext = dbSet.Context();
|
||||
int count = await dbContext.SaveChangesAsync(token).ConfigureAwait(false);
|
||||
dbContext.ChangeTracker.Clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static DbContext Context<TEntity>(this DbSet<TEntity> dbSet)
|
||||
where TEntity : class
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.UI.Xaml.Data;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
internal interface IAdvancedDbCollectionView<TEntity> : IAdvancedCollectionView<TEntity>
|
||||
where TEntity : class
|
||||
{
|
||||
IDisposable SuppressChangeCurrentItem();
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 可选择的项
|
||||
/// 若要使用 <see cref="ScopedDbCurrent{TEntity, TMessage}"/>
|
||||
/// 必须实现该接口
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal interface ISelectable
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库内部Id
|
||||
/// </summary>
|
||||
Guid InnerId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前项的选中状态
|
||||
/// </summary>
|
||||
bool IsSelected { get; set; }
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Collections;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Database.Abstraction;
|
||||
using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -22,8 +22,6 @@ internal sealed class ObservableReorderableDbCollection<TEntity> : ObservableCol
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public IAdvancedCollectionView? View { get; set; }
|
||||
|
||||
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
base.OnCollectionChanged(e);
|
||||
@@ -51,38 +49,33 @@ internal sealed class ObservableReorderableDbCollection<TEntity> : ObservableCol
|
||||
|
||||
private void OnReorder()
|
||||
{
|
||||
using (View?.DeferRefresh())
|
||||
{
|
||||
AdjustIndex((List<TEntity>)Items);
|
||||
AdjustIndex((List<TEntity>)Items);
|
||||
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||
foreach (ref readonly TEntity item in CollectionsMarshal.AsSpan((List<TEntity>)Items))
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||
foreach (ref readonly TEntity item in CollectionsMarshal.AsSpan((List<TEntity>)Items))
|
||||
{
|
||||
dbSet.UpdateAndSave(item);
|
||||
}
|
||||
dbSet.UpdateAndSave(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SA1402")]
|
||||
internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> : ObservableCollection<TEntityOnly>
|
||||
where TEntityOnly : class, IEntityOnly<TEntity>
|
||||
internal sealed class ObservableReorderableDbCollection<TEntityAccess, TEntity> : ObservableCollection<TEntityAccess>
|
||||
where TEntityAccess : class, IEntityAccess<TEntity>
|
||||
where TEntity : class, IReorderable
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
public ObservableReorderableDbCollection(List<TEntityOnly> items, IServiceProvider serviceProvider)
|
||||
public ObservableReorderableDbCollection(List<TEntityAccess> items, IServiceProvider serviceProvider)
|
||||
: base(AdjustIndex(items.SortBy(x => x.Entity.Index)))
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public IAdvancedCollectionView? View { get; set; }
|
||||
|
||||
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
base.OnCollectionChanged(e);
|
||||
@@ -96,12 +89,12 @@ internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> :
|
||||
}
|
||||
}
|
||||
|
||||
private static List<TEntityOnly> AdjustIndex(List<TEntityOnly> list)
|
||||
private static List<TEntityAccess> AdjustIndex(List<TEntityAccess> list)
|
||||
{
|
||||
Span<TEntityOnly> span = CollectionsMarshal.AsSpan(list);
|
||||
Span<TEntityAccess> span = CollectionsMarshal.AsSpan(list);
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
ref readonly TEntityOnly item = ref span[i];
|
||||
ref readonly TEntityAccess item = ref span[i];
|
||||
item.Entity.Index = i;
|
||||
}
|
||||
|
||||
@@ -110,19 +103,16 @@ internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> :
|
||||
|
||||
private void OnReorder()
|
||||
{
|
||||
using (View?.DeferRefresh())
|
||||
AdjustIndex((List<TEntityAccess>)Items);
|
||||
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AdjustIndex((List<TEntityOnly>)Items);
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||
foreach (ref readonly TEntityAccess item in CollectionsMarshal.AsSpan((List<TEntityAccess>)Items))
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||
foreach (ref readonly TEntityOnly item in CollectionsMarshal.AsSpan((List<TEntityOnly>)Items))
|
||||
{
|
||||
dbSet.UpdateAndSave(item.Entity);
|
||||
}
|
||||
dbSet.UpdateAndSave(item.Entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Database.Abstraction;
|
||||
using Snap.Hutao.Model;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
internal static class ObservableReorderableDbCollectionExtension
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ObservableReorderableDbCollection<TEntity> ToObservableReorderableDbCollection<TEntity>(this IEnumerable<TEntity> source, IServiceProvider serviceProvider)
|
||||
where TEntity : class, IReorderable
|
||||
{
|
||||
return source is List<TEntity> list
|
||||
? new ObservableReorderableDbCollection<TEntity>(list, serviceProvider)
|
||||
: new ObservableReorderableDbCollection<TEntity>([.. source], serviceProvider);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ObservableReorderableDbCollection<TEntityOnly, TEntity> ToObservableReorderableDbCollection<TEntityOnly, TEntity>(this IEnumerable<TEntityOnly> source, IServiceProvider serviceProvider)
|
||||
where TEntityOnly : class, IEntityAccess<TEntity>
|
||||
where TEntity : class, IReorderable
|
||||
{
|
||||
return source is List<TEntityOnly> list
|
||||
? new ObservableReorderableDbCollection<TEntityOnly, TEntity>(list, serviceProvider)
|
||||
: new ObservableReorderableDbCollection<TEntityOnly, TEntity>([.. source], serviceProvider);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 可查询扩展
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class QueryableExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// <code>source.Where(predicate).ExecuteDelete()</code>
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">源类型</typeparam>
|
||||
/// <param name="source">源</param>
|
||||
/// <param name="predicate">条件</param>
|
||||
/// <returns>SQL返回个数</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int ExecuteDeleteWhere<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
|
||||
{
|
||||
return source.Where(predicate).ExecuteDelete();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <code>source.Where(predicate).ExecuteDeleteAsync(token)</code>
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">源类型</typeparam>
|
||||
/// <param name="source">源</param>
|
||||
/// <param name="predicate">条件</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>SQL返回个数</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ValueTask<int> ExecuteDeleteWhereAsync<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate, CancellationToken token = default)
|
||||
{
|
||||
return source.Where(predicate).ExecuteDeleteAsync(token).AsValueTask();
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 范围化的数据库当前项
|
||||
/// 简化对数据库中选中项的管理
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体的类型</typeparam>
|
||||
/// <typeparam name="TMessage">消息的类型</typeparam>
|
||||
[ConstructorGenerated]
|
||||
internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
|
||||
where TEntity : class, ISelectable
|
||||
where TMessage : Message.ValueChangedMessage<TEntity>, new()
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
private TEntity? current;
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的项
|
||||
/// </summary>
|
||||
public TEntity? Current
|
||||
{
|
||||
get => current;
|
||||
set
|
||||
{
|
||||
// prevent useless sets
|
||||
if (current == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceProvider.IsDisposed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||
|
||||
// only update when not processing a deletion
|
||||
if (value is not null && current is not null)
|
||||
{
|
||||
current.IsSelected = false;
|
||||
dbSet.UpdateAndSave(current);
|
||||
}
|
||||
|
||||
TMessage message = new() { OldValue = current, NewValue = value };
|
||||
|
||||
current = value;
|
||||
|
||||
if (current is not null)
|
||||
{
|
||||
current.IsSelected = true;
|
||||
dbSet.UpdateAndSave(current);
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ConstructorGenerated]
|
||||
internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
|
||||
where TEntityOnly : class, IEntityOnly<TEntity>
|
||||
where TEntity : class, ISelectable
|
||||
where TMessage : Message.ValueChangedMessage<TEntityOnly>, new()
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
private TEntityOnly? current;
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的项
|
||||
/// </summary>
|
||||
public TEntityOnly? Current
|
||||
{
|
||||
get => current;
|
||||
set
|
||||
{
|
||||
// prevent useless sets
|
||||
if (current == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceProvider.IsDisposed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||
|
||||
// only update when not processing a deletion
|
||||
if (value is not null)
|
||||
{
|
||||
if (current is not null)
|
||||
{
|
||||
current.Entity.IsSelected = false;
|
||||
dbSet.UpdateAndSave(current.Entity);
|
||||
}
|
||||
}
|
||||
|
||||
TMessage message = new() { OldValue = current, NewValue = value };
|
||||
|
||||
current = value;
|
||||
|
||||
if (current is not null)
|
||||
{
|
||||
current.Entity.IsSelected = true;
|
||||
dbSet.UpdateAndSave(current.Entity);
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Database.Abstraction;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
@@ -11,17 +12,17 @@ namespace Snap.Hutao.Core.Database;
|
||||
[HighQuality]
|
||||
internal static class SelectableExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取选中的值或默认值
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">源类型</typeparam>
|
||||
/// <param name="source">源</param>
|
||||
/// <returns>选中的值或默认值</returns>
|
||||
/// <exception cref="InvalidOperationException">存在多个选中的值</exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static TSource? SelectedOrDefault<TSource>(this IEnumerable<TSource> source)
|
||||
where TSource : ISelectable
|
||||
{
|
||||
return source.SingleOrDefault(i => i.IsSelected);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static TSource? SelectedOrFirstOrDefault<TSource>(this IEnumerable<TSource> source)
|
||||
where TSource : ISelectable
|
||||
{
|
||||
return source.SingleOrDefault(i => i.IsSelected) ?? source.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 可转换类型服务
|
||||
/// </summary>
|
||||
[Obsolete("Not useful anymore")]
|
||||
internal interface ICastService
|
||||
{
|
||||
}
|
||||
@@ -20,7 +20,7 @@ internal abstract class OverseaSupportFactory<TClient, TClientCN, TClientOS> : I
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public TClient Create(bool isOversea)
|
||||
public virtual TClient Create(bool isOversea)
|
||||
{
|
||||
return isOversea
|
||||
? serviceProvider.GetRequiredService<TClientOS>()
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 对象扩展
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class CastServiceExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// <see langword="as"/> 的链式调用扩展
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标转换类型</typeparam>
|
||||
/// <param name="service">对象</param>
|
||||
/// <returns>转换类型后的对象</returns>
|
||||
[Obsolete("Not useful anymore")]
|
||||
public static T? As<T>(this ICastService service)
|
||||
where T : class
|
||||
{
|
||||
return service as T;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Quartz;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Service;
|
||||
using System.Globalization;
|
||||
@@ -33,11 +34,14 @@ internal static class DependencyInjection
|
||||
})
|
||||
.AddMemoryCache()
|
||||
|
||||
// Quartz
|
||||
.AddQuartz()
|
||||
|
||||
// Hutao extensions
|
||||
.AddJsonOptions()
|
||||
.AddDatabase()
|
||||
.AddInjections()
|
||||
.AddAllHttpClients()
|
||||
.AddConfiguredHttpClients()
|
||||
|
||||
// Discrete services
|
||||
.AddSingleton<IMessenger, WeakReferenceMessenger>()
|
||||
|
||||
@@ -30,31 +30,28 @@ internal static class IocConfiguration
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
public static IServiceCollection AddDatabase(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddTransient(typeof(Database.ScopedDbCurrent<,>))
|
||||
.AddTransient(typeof(Database.ScopedDbCurrent<,,>))
|
||||
.AddDbContextPool<AppDbContext>(AddDbContextCore);
|
||||
}
|
||||
return services.AddDbContextPool<AppDbContext>(AddDbContextCore);
|
||||
|
||||
private static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
|
||||
{
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
|
||||
string sqlConnectionString = $"Data Source={dbFile}";
|
||||
|
||||
// Temporarily create a context
|
||||
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString))
|
||||
static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
|
||||
{
|
||||
if (context.Database.GetPendingMigrations().Any())
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("[Database] Performing AppDbContext Migrations");
|
||||
context.Database.Migrate();
|
||||
}
|
||||
}
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
|
||||
string sqlConnectionString = $"Data Source={dbFile}";
|
||||
|
||||
builder
|
||||
.EnableSensitiveDataLogging()
|
||||
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
|
||||
.UseSqlite(sqlConnectionString);
|
||||
// Temporarily create a context
|
||||
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString))
|
||||
{
|
||||
if (context.Database.GetPendingMigrations().Any())
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("[Database] Performing AppDbContext Migrations");
|
||||
context.Database.Migrate();
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
.EnableSensitiveDataLogging()
|
||||
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
|
||||
.UseSqlite(sqlConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
private const string ApplicationJson = "application/json";
|
||||
|
||||
public static IServiceCollection AddAllHttpClients(this IServiceCollection services)
|
||||
public static IServiceCollection AddConfiguredHttpClients(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.ConfigureHttpClientDefaults(clientBuilder =>
|
||||
@@ -27,7 +27,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
HttpClientHandler clientHandler = (HttpClientHandler)handler;
|
||||
clientHandler.AllowAutoRedirect = true;
|
||||
clientHandler.UseProxy = true;
|
||||
clientHandler.Proxy = provider.GetRequiredService<DynamicHttpProxy>();
|
||||
clientHandler.Proxy = provider.GetRequiredService<HttpProxyUsingSystemProxy>();
|
||||
});
|
||||
})
|
||||
.AddHttpClients();
|
||||
@@ -38,11 +38,6 @@ internal static partial class IocHttpClientConfiguration
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static partial IServiceCollection AddHttpClients(this IServiceCollection services);
|
||||
|
||||
/// <summary>
|
||||
/// 默认配置
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void DefaultConfiguration(IServiceProvider serviceProvider, HttpClient client)
|
||||
{
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
@@ -51,10 +46,6 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(runtimeOptions.UserAgent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥1的客户端使用此配置
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void XRpcConfiguration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
@@ -65,10 +56,6 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥2的客户端使用此配置
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void XRpc2Configuration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
@@ -84,11 +71,6 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥1的客户端使用此配置
|
||||
/// HoYoLAB app
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void XRpc3Configuration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
@@ -100,11 +82,6 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥2的客户端使用此配置
|
||||
/// HoYoLAB web
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
[SuppressMessage("", "IDE0051")]
|
||||
private static void XRpc4Configuration(HttpClient client)
|
||||
{
|
||||
|
||||
@@ -10,11 +10,6 @@ namespace Snap.Hutao.Core.DependencyInjection;
|
||||
[HighQuality]
|
||||
internal static partial class ServiceCollectionExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 向容器注册服务
|
||||
/// 此方法将会自动生成
|
||||
/// </summary>
|
||||
/// <param name="services">容器</param>
|
||||
/// <returns>可继续操作的服务集合</returns>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static partial IServiceCollection AddInjections(this IServiceCollection services);
|
||||
}
|
||||
@@ -10,30 +10,22 @@ namespace Snap.Hutao.Core.DependencyInjection;
|
||||
/// </summary>
|
||||
internal static class ServiceProviderExtension
|
||||
{
|
||||
/// <inheritdoc cref="ActivatorUtilities.CreateInstance{T}(IServiceProvider, object[])"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T CreateInstance<T>(this IServiceProvider serviceProvider, params object[] parameters)
|
||||
{
|
||||
return ActivatorUtilities.CreateInstance<T>(serviceProvider, parameters);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsDisposed(this IServiceProvider? serviceProvider)
|
||||
public static bool IsDisposed(this IServiceProvider? serviceProvider, bool treatNullAsDisposed = true)
|
||||
{
|
||||
if (serviceProvider is null)
|
||||
{
|
||||
return treatNullAsDisposed;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
||||
return false;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (serviceProvider is ServiceProvider serviceProviderImpl)
|
||||
{
|
||||
return GetPrivateDisposed(serviceProviderImpl);
|
||||
}
|
||||
|
||||
return serviceProvider.GetType().GetField("_disposed")?.GetValue(serviceProvider) is true;
|
||||
}
|
||||
|
||||
// private bool _disposed;
|
||||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
|
||||
private static extern ref bool GetPrivateDisposed(ServiceProvider serviceProvider);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此特性附加的属性会在属性改变后会异步地设置的其他属性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
|
||||
internal sealed class AlsoAsyncSetsAttribute : Attribute
|
||||
{
|
||||
public AlsoAsyncSetsAttribute(string propertyName)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoAsyncSetsAttribute(string propertyName1, string propertyName2)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoAsyncSetsAttribute(params string[] propertyNames)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此特性附加的属性会在属性改变后会设置的其他属性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
|
||||
internal sealed class AlsoSetsAttribute : Attribute
|
||||
{
|
||||
public AlsoSetsAttribute(string propertyName)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoSetsAttribute(string propertyName1, string propertyName2)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoSetsAttribute(params string[] propertyNames)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库损坏异常
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Obsolete("Use HutaoException instead")]
|
||||
internal sealed class DatabaseCorruptedException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的用户数据损坏异常
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="innerException">内部错误</param>
|
||||
public DatabaseCorruptedException(string message, Exception? innerException)
|
||||
: base(SH.FormatCoreExceptionServiceDatabaseCorruptedMessage($"{message}\n{innerException?.Message}"), innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,10 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
/// <summary>
|
||||
/// 异常记录器
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed partial class ExceptionRecorder
|
||||
@@ -16,13 +13,15 @@ internal sealed partial class ExceptionRecorder
|
||||
private readonly ILogger<ExceptionRecorder> logger;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// 记录应用程序异常
|
||||
/// </summary>
|
||||
/// <param name="app">应用程序</param>
|
||||
public void Record(Application app)
|
||||
{
|
||||
app.UnhandledException += OnAppUnhandledException;
|
||||
ConfigureDebugSettings(app);
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void ConfigureDebugSettings(Application app)
|
||||
{
|
||||
app.DebugSettings.FailFastOnErrors = false;
|
||||
|
||||
app.DebugSettings.IsBindingTracingEnabled = true;
|
||||
@@ -35,19 +34,13 @@ internal sealed partial class ExceptionRecorder
|
||||
app.DebugSettings.LayoutCycleDebugBreakLevel = LayoutCycleDebugBreakLevel.High;
|
||||
}
|
||||
|
||||
[SuppressMessage("", "CA2012")]
|
||||
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
ValueTask<string?> task = serviceProvider
|
||||
.GetRequiredService<Web.Hutao.Log.HutaoLogUploadClient>()
|
||||
.UploadLogAsync(e.Exception);
|
||||
|
||||
if (!task.IsCompleted)
|
||||
{
|
||||
task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
logger.LogError("未经处理的全局异常:\r\n{Detail}", ExceptionFormat.Format(e.Exception));
|
||||
|
||||
serviceProvider
|
||||
.GetRequiredService<Web.Hutao.Log.HutaoLogUploadClient>()
|
||||
.UploadLog(e.Exception);
|
||||
}
|
||||
|
||||
private void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
internal sealed class HutaoException : Exception
|
||||
@@ -11,11 +13,13 @@ internal sealed class HutaoException : Exception
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static HutaoException Throw(string message, Exception? innerException = default)
|
||||
{
|
||||
throw new HutaoException(message, innerException);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void ThrowIf([DoesNotReturnIf(true)] bool condition, string message, Exception? innerException = default)
|
||||
{
|
||||
if (condition)
|
||||
@@ -24,6 +28,7 @@ internal sealed class HutaoException : Exception
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void ThrowIfNot([DoesNotReturnIf(false)] bool condition, string message, Exception? innerException = default)
|
||||
{
|
||||
if (!condition)
|
||||
@@ -33,18 +38,21 @@ internal sealed class HutaoException : Exception
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static ArgumentException Argument(string message, string? paramName)
|
||||
{
|
||||
throw new ArgumentException(message, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static HutaoException GachaStatisticsInvalidItemId(uint id, Exception? innerException = default)
|
||||
{
|
||||
throw new HutaoException(SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(id), innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
public static HutaoException UserdataCorrupted(string message, Exception? innerException = default)
|
||||
{
|
||||
throw new HutaoException(message, innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static InvalidCastException InvalidCast<TFrom, TTo>(string name, Exception? innerException = default)
|
||||
{
|
||||
string message = $"This instance of '{typeof(TFrom).FullName}' '{name}' doesn't implement '{typeof(TTo).FullName}'";
|
||||
@@ -52,12 +60,30 @@ internal sealed class HutaoException : Exception
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static InvalidOperationException InvalidOperation(string message, Exception? innerException = default)
|
||||
{
|
||||
throw new InvalidOperationException(message, innerException);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static NotSupportedException NotSupported(string? message = default, Exception? innerException = default)
|
||||
{
|
||||
throw new NotSupportedException(message, innerException);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void NotSupportedIf(bool condition, string? message = default, Exception? innerException = default)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
throw new NotSupportedException(message, innerException);
|
||||
}
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static OperationCanceledException OperationCanceled(string message, Exception? innerException = default)
|
||||
{
|
||||
throw new OperationCanceledException(message, innerException);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
/// <summary>
|
||||
/// 运行环境异常
|
||||
/// 用户的计算机中的某些设置不符合要求
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Obsolete("Use HutaoException instead")]
|
||||
internal sealed class RuntimeEnvironmentException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的运行环境异常
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="innerException">内部错误</param>
|
||||
public RuntimeEnvironmentException(string message, Exception? innerException)
|
||||
: base($"{message}\n{innerException?.Message}", innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Service.Game.Package;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
/// <summary>
|
||||
/// 帮助更好的抛出异常
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[System.Diagnostics.StackTraceHidden]
|
||||
[Obsolete("Use HutaoException instead")]
|
||||
internal static class ThrowHelper
|
||||
{
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static ArgumentException Argument(string message, string? paramName)
|
||||
{
|
||||
throw new ArgumentException(message, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static DatabaseCorruptedException DatabaseCorrupted(string message, Exception? inner)
|
||||
{
|
||||
throw new DatabaseCorruptedException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static GameFileOperationException GameFileOperation(string message, Exception? inner)
|
||||
{
|
||||
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)
|
||||
{
|
||||
throw new InvalidOperationException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static NotSupportedException NotSupported()
|
||||
{
|
||||
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)
|
||||
{
|
||||
throw new OperationCanceledException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static PackageConvertException PackageConvert(string message, Exception? inner = default)
|
||||
{
|
||||
throw new PackageConvertException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static RuntimeEnvironmentException RuntimeEnvironment(string message, Exception? inner)
|
||||
{
|
||||
throw new RuntimeEnvironmentException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static UserdataCorruptedException UserdataCorrupted(string message, Exception? inner)
|
||||
{
|
||||
throw new UserdataCorruptedException(message, inner);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
/// <summary>
|
||||
/// 用户数据损坏异常
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Obsolete("Use HutaoException instead")]
|
||||
internal sealed class UserdataCorruptedException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的用户数据损坏异常
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="innerException">内部错误</param>
|
||||
public UserdataCorruptedException(string message, Exception? innerException)
|
||||
: base(SH.FormatCoreExceptionServiceUserdataCorruptedMessage($"{message}\n{innerException?.Message}"), innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,16 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.UI;
|
||||
using Snap.Hutao.Win32.System.WinRT;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics.Imaging;
|
||||
using WinRT;
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
namespace Snap.Hutao.Core.Graphics.Imaging;
|
||||
|
||||
/// <summary>
|
||||
/// 软件位图拓展
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class SoftwareBitmapExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 混合模式 正常
|
||||
/// </summary>
|
||||
/// <param name="softwareBitmap">软件位图</param>
|
||||
/// <param name="tint">底色</param>
|
||||
public static unsafe void NormalBlend(this SoftwareBitmap softwareBitmap, Bgra32 tint)
|
||||
{
|
||||
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.ReadWrite))
|
||||
@@ -39,7 +31,7 @@ internal static class SoftwareBitmapExtension
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe Bgra32 GetAccentColor(this SoftwareBitmap softwareBitmap)
|
||||
public static unsafe Bgra32 GetBgra32AccentColor(this SoftwareBitmap softwareBitmap)
|
||||
{
|
||||
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
|
||||
{
|
||||
@@ -59,4 +51,25 @@ internal static class SoftwareBitmapExtension
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe Rgba32 GetRgba32AccentColor(this SoftwareBitmap softwareBitmap)
|
||||
{
|
||||
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
|
||||
{
|
||||
using (IMemoryBufferReference reference = buffer.CreateReference())
|
||||
{
|
||||
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
|
||||
double b = 0, g = 0, r = 0, a = 0;
|
||||
foreach (ref readonly Bgra32 pixel in bytes)
|
||||
{
|
||||
b += pixel.B;
|
||||
g += pixel.G;
|
||||
r += pixel.R;
|
||||
a += pixel.A;
|
||||
}
|
||||
|
||||
return new((byte)(r / bytes.Length), (byte)(g / bytes.Length), (byte)(b / bytes.Length), (byte)(a / bytes.Length));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Snap.Hutao/Snap.Hutao/Core/Graphics/PointInt32Kind.cs
Normal file
17
src/Snap.Hutao/Snap.Hutao/Core/Graphics/PointInt32Kind.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Graphics;
|
||||
|
||||
internal enum PointInt32Kind
|
||||
{
|
||||
TopLeft,
|
||||
TopCenter,
|
||||
TopRight,
|
||||
CenterLeft,
|
||||
Center,
|
||||
CenterRight,
|
||||
BottomLeft,
|
||||
BottomCenter,
|
||||
BottomRight,
|
||||
}
|
||||
10
src/Snap.Hutao/Snap.Hutao/Core/Graphics/PointUInt16.cs
Normal file
10
src/Snap.Hutao/Snap.Hutao/Core/Graphics/PointUInt16.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Graphics;
|
||||
|
||||
internal readonly struct PointUInt16
|
||||
{
|
||||
public readonly ushort X;
|
||||
public readonly ushort Y;
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.Graphics;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
namespace Snap.Hutao.Core.Graphics;
|
||||
|
||||
internal readonly struct CompactRect
|
||||
internal readonly struct RectInt16
|
||||
{
|
||||
private readonly short x;
|
||||
private readonly short y;
|
||||
private readonly short width;
|
||||
private readonly short height;
|
||||
|
||||
private CompactRect(int x, int y, int width, int height)
|
||||
private RectInt16(int x, int y, int width, int height)
|
||||
{
|
||||
this.x = (short)x;
|
||||
this.y = (short)y;
|
||||
@@ -21,24 +20,22 @@ internal readonly struct CompactRect
|
||||
this.height = (short)height;
|
||||
}
|
||||
|
||||
public static implicit operator RectInt32(CompactRect rect)
|
||||
public static implicit operator RectInt32(RectInt16 rect)
|
||||
{
|
||||
return new(rect.x, rect.y, rect.width, rect.height);
|
||||
}
|
||||
|
||||
public static explicit operator CompactRect(RectInt32 rect)
|
||||
public static explicit operator RectInt16(RectInt32 rect)
|
||||
{
|
||||
return new(rect.X, rect.Y, rect.Width, rect.Height);
|
||||
}
|
||||
|
||||
public static unsafe explicit operator CompactRect(ulong value)
|
||||
public static unsafe explicit operator RectInt16(ulong value)
|
||||
{
|
||||
Unsafe.SkipInit(out CompactRect rect);
|
||||
*(ulong*)&rect = value;
|
||||
return rect;
|
||||
return *(RectInt16*)&value;
|
||||
}
|
||||
|
||||
public static unsafe implicit operator ulong(CompactRect rect)
|
||||
public static unsafe implicit operator ulong(RectInt16 rect)
|
||||
{
|
||||
return *(ulong*)▭
|
||||
}
|
||||
35
src/Snap.Hutao/Snap.Hutao/Core/Graphics/RectInt32Convert.cs
Normal file
35
src/Snap.Hutao/Snap.Hutao/Core/Graphics/RectInt32Convert.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using System.Numerics;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics;
|
||||
|
||||
namespace Snap.Hutao.Core.Graphics;
|
||||
|
||||
internal static class RectInt32Convert
|
||||
{
|
||||
public static RectInt32 RectInt32(RECT rect)
|
||||
{
|
||||
return new(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
|
||||
}
|
||||
|
||||
public static RectInt32 RectInt32(Point position, Vector2 size)
|
||||
{
|
||||
return new((int)position.X, (int)position.Y, (int)size.X, (int)size.Y);
|
||||
}
|
||||
|
||||
public static RectInt32 RectInt32(int x, int y, Vector2 size)
|
||||
{
|
||||
return new(x, y, (int)size.X, (int)size.Y);
|
||||
}
|
||||
|
||||
public static unsafe RectInt32 RectInt32(PointInt32 position, SizeInt32 size)
|
||||
{
|
||||
RectInt32View view = default;
|
||||
view.Position = position;
|
||||
view.Size = size;
|
||||
return *(RectInt32*)&view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Windows.Graphics;
|
||||
|
||||
namespace Snap.Hutao.Core.Graphics;
|
||||
|
||||
internal static class RectInt32Extension
|
||||
{
|
||||
public static RectInt32 Scale(this RectInt32 rectInt32, double scale)
|
||||
{
|
||||
return new((int)(rectInt32.X * scale), (int)(rectInt32.Y * scale), (int)(rectInt32.Width * scale), (int)(rectInt32.Height * scale));
|
||||
}
|
||||
|
||||
public static int Size(this RectInt32 rectInt32)
|
||||
{
|
||||
return rectInt32.Width * rectInt32.Height;
|
||||
}
|
||||
|
||||
public static unsafe SizeInt32 GetSizeInt32(this RectInt32 rectInt32)
|
||||
{
|
||||
return ((RectInt32View*)&rectInt32)->Size;
|
||||
}
|
||||
|
||||
public static unsafe PointInt32 GetPointInt32(this RectInt32 rectInt32, PointInt32Kind kind)
|
||||
{
|
||||
RectInt32View* pView = (RectInt32View*)&rectInt32;
|
||||
PointInt32 topLeft = pView->Position;
|
||||
SizeInt32 size = pView->Size;
|
||||
return kind switch
|
||||
{
|
||||
PointInt32Kind.TopLeft => topLeft,
|
||||
PointInt32Kind.TopCenter => new PointInt32(topLeft.X + (size.Width / 2), topLeft.Y),
|
||||
PointInt32Kind.TopRight => new PointInt32(topLeft.X + size.Width, topLeft.Y),
|
||||
PointInt32Kind.CenterLeft => new PointInt32(topLeft.X, topLeft.Y + (size.Height / 2)),
|
||||
PointInt32Kind.Center => new PointInt32(topLeft.X + (size.Width / 2), topLeft.Y + (size.Height / 2)),
|
||||
PointInt32Kind.CenterRight => new PointInt32(topLeft.X + size.Width, topLeft.Y + (size.Height / 2)),
|
||||
PointInt32Kind.BottomLeft => new PointInt32(topLeft.X, topLeft.Y + size.Height),
|
||||
PointInt32Kind.BottomCenter => new PointInt32(topLeft.X + (size.Width / 2), topLeft.Y + size.Height),
|
||||
PointInt32Kind.BottomRight => new PointInt32(topLeft.X + size.Width, topLeft.Y + size.Height),
|
||||
_ => default,
|
||||
};
|
||||
}
|
||||
|
||||
public static RECT ToRECT(this RectInt32 rect)
|
||||
{
|
||||
return new(rect.X, rect.Y, rect.X + rect.Width, rect.Y + rect.Height);
|
||||
}
|
||||
}
|
||||
12
src/Snap.Hutao/Snap.Hutao/Core/Graphics/RectInt32View.cs
Normal file
12
src/Snap.Hutao/Snap.Hutao/Core/Graphics/RectInt32View.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Graphics;
|
||||
|
||||
namespace Snap.Hutao.Core.Graphics;
|
||||
|
||||
internal struct RectInt32View
|
||||
{
|
||||
public PointInt32 Position;
|
||||
public SizeInt32 Size;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Graphics;
|
||||
|
||||
namespace Snap.Hutao.Core.Graphics;
|
||||
|
||||
internal static class SizeInt32Extension
|
||||
{
|
||||
public static SizeInt32 Scale(this SizeInt32 sizeInt32, double scale)
|
||||
{
|
||||
return new((int)(sizeInt32.Width * scale), (int)(sizeInt32.Height * scale));
|
||||
}
|
||||
|
||||
public static int Size(this SizeInt32 sizeInt32)
|
||||
{
|
||||
return sizeInt32.Width * sizeInt32.Height;
|
||||
}
|
||||
|
||||
public static unsafe RectInt32 ToRectInt32(this SizeInt32 sizeInt32)
|
||||
{
|
||||
RectInt32View view = default;
|
||||
view.Size = sizeInt32;
|
||||
return *(RectInt32*)&view;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.DataTransfer;
|
||||
|
||||
/// <summary>
|
||||
/// 剪贴板互操作
|
||||
/// </summary>
|
||||
internal interface IClipboardProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 从剪贴板文本中反序列化
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标类型</typeparam>
|
||||
/// <returns>实例</returns>
|
||||
ValueTask<T?> DeserializeFromJsonAsync<T>()
|
||||
where T : class;
|
||||
|
||||
/// <summary>
|
||||
/// 设置位图
|
||||
/// </summary>
|
||||
/// <param name="stream">图片流</param>
|
||||
/// <returns>是否设置成功</returns>
|
||||
bool SetBitmap(IRandomAccessStream stream);
|
||||
|
||||
/// <summary>
|
||||
/// 设置文本
|
||||
/// </summary>
|
||||
/// <param name="text">文本</param>
|
||||
/// <returns>是否设置成功</returns>
|
||||
bool SetText(string text);
|
||||
}
|
||||
@@ -2,20 +2,57 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.VisualBasic.FileIO;
|
||||
using Snap.Hutao.Win32.System.Com;
|
||||
using Snap.Hutao.Win32.UI.Shell;
|
||||
using System.IO;
|
||||
using static Snap.Hutao.Win32.Macros;
|
||||
using static Snap.Hutao.Win32.Ole32;
|
||||
using static Snap.Hutao.Win32.Shell32;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
internal static class DirectoryOperation
|
||||
{
|
||||
public static bool Move(string sourceDirName, string destDirName)
|
||||
public static bool TryMove(string sourceDirName, string destDirName)
|
||||
{
|
||||
if (!Directory.Exists(sourceDirName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FileSystem.MoveDirectory(sourceDirName, destDirName, true);
|
||||
return true;
|
||||
try
|
||||
{
|
||||
FileSystem.MoveDirectory(sourceDirName, destDirName, true);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe bool UnsafeRename(string path, string name, FILEOPERATION_FLAGS flags = FILEOPERATION_FLAGS.FOF_ALLOWUNDO | FILEOPERATION_FLAGS.FOF_NOCONFIRMMKDIR)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
|
||||
{
|
||||
if (SUCCEEDED(SHCreateItemFromParsingName(path, default, in IShellItem.IID, out IShellItem* pShellItem)))
|
||||
{
|
||||
pFileOperation->SetOperationFlags(flags);
|
||||
pFileOperation->RenameItem(pShellItem, name, default);
|
||||
|
||||
if (SUCCEEDED(pFileOperation->PerformOperations()))
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
|
||||
IUnknownMarshal.Release(pShellItem);
|
||||
}
|
||||
|
||||
IUnknownMarshal.Release(pFileOperation);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using Snap.Hutao.Win32.System.Com;
|
||||
using Snap.Hutao.Win32.UI.Shell;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using static Snap.Hutao.Win32.Macros;
|
||||
using static Snap.Hutao.Win32.Ole32;
|
||||
using static Snap.Hutao.Win32.Shell32;
|
||||
@@ -33,8 +32,15 @@ internal static class FileOperation
|
||||
|
||||
if (overwrite)
|
||||
{
|
||||
File.Move(sourceFileName, destFileName, true);
|
||||
return true;
|
||||
try
|
||||
{
|
||||
File.Move(sourceFileName, destFileName, true);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (File.Exists(destFileName))
|
||||
@@ -42,10 +48,52 @@ internal static class FileOperation
|
||||
return false;
|
||||
}
|
||||
|
||||
File.Move(sourceFileName, destFileName, false);
|
||||
try
|
||||
{
|
||||
File.Move(sourceFileName, destFileName, false);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Delete(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
File.Delete(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static unsafe bool UnsafeDelete(string path)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
|
||||
{
|
||||
if (SUCCEEDED(SHCreateItemFromParsingName(path, default, in IShellItem.IID, out IShellItem* pShellItem)))
|
||||
{
|
||||
pFileOperation->DeleteItem(pShellItem, default);
|
||||
|
||||
if (SUCCEEDED(pFileOperation->PerformOperations()))
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
|
||||
IUnknownMarshal.Release(pShellItem);
|
||||
}
|
||||
|
||||
IUnknownMarshal.Release(pFileOperation);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static unsafe bool UnsafeMove(string sourceFileName, string destFileName)
|
||||
{
|
||||
bool result = false;
|
||||
@@ -74,28 +122,4 @@ internal static class FileOperation
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static unsafe bool UnsafeDelete(string path)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
|
||||
{
|
||||
if (SUCCEEDED(SHCreateItemFromParsingName(path, default, in IShellItem.IID, out IShellItem* pShellItem)))
|
||||
{
|
||||
pFileOperation->DeleteItem(pShellItem, default);
|
||||
|
||||
if (SUCCEEDED(pFileOperation->PerformOperations()))
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
|
||||
IUnknownMarshal.Release(pShellItem);
|
||||
}
|
||||
|
||||
IUnknownMarshal.Release(pFileOperation);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,22 @@ using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Hashing;
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
[Obsolete("Use CryptographicOperations.HashData()")]
|
||||
#endif
|
||||
internal static class Hash
|
||||
{
|
||||
public static string SHA1HexString(string input)
|
||||
public static unsafe string SHA1HexString(string input)
|
||||
{
|
||||
return HashCore(System.Convert.ToHexString, SHA1.HashData, Encoding.UTF8.GetBytes, input);
|
||||
return HashCore(Convert.ToHexString, SHA1.HashData, Encoding.UTF8.GetBytes, input);
|
||||
}
|
||||
|
||||
private static TResult HashCore<TInput, TResult>(Func<byte[], TResult> resultConverter, Func<byte[], byte[]> hashMethod, Func<TInput, byte[]> bytesConverter, TInput input)
|
||||
public static unsafe string MD5HexString(string input)
|
||||
{
|
||||
return HashCore(Convert.ToHexString, System.Security.Cryptography.MD5.HashData, Encoding.UTF8.GetBytes, input);
|
||||
}
|
||||
|
||||
private static unsafe TResult HashCore<TInput, TResult>(Func<byte[], TResult> resultConverter, Func<byte[], byte[]> hashMethod, Func<TInput, byte[]> bytesConverter, TInput input)
|
||||
{
|
||||
return resultConverter(hashMethod(bytesConverter(input)));
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ namespace Snap.Hutao.Core.IO.Hashing;
|
||||
/// <summary>
|
||||
/// 摘要
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
#if NET9_0_OR_GREATER
|
||||
[Obsolete("Use CryptographicOperations.HashData()")]
|
||||
#endif
|
||||
internal static class MD5
|
||||
{
|
||||
/// <summary>
|
||||
@@ -34,6 +36,6 @@ internal static class MD5
|
||||
public static async ValueTask<string> HashAsync(Stream stream, CancellationToken token = default)
|
||||
{
|
||||
byte[] bytes = await System.Security.Cryptography.MD5.HashDataAsync(stream, token).ConfigureAwait(false);
|
||||
return System.Convert.ToHexString(bytes);
|
||||
return Convert.ToHexString(bytes);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Hashing;
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
[Obsolete("Use CryptographicOperations.HashData()")]
|
||||
#endif
|
||||
internal static class SHA256
|
||||
{
|
||||
public static async ValueTask<string> HashFileAsync(string filePath, CancellationToken token = default)
|
||||
@@ -18,6 +21,6 @@ internal static class SHA256
|
||||
public static async ValueTask<string> HashAsync(Stream stream, CancellationToken token = default)
|
||||
{
|
||||
byte[] bytes = await System.Security.Cryptography.SHA256.HashDataAsync(stream, token).ConfigureAwait(false);
|
||||
return System.Convert.ToHexString(bytes);
|
||||
return Convert.ToHexString(bytes);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ namespace Snap.Hutao.Core.IO.Hashing;
|
||||
/// <summary>
|
||||
/// XXH64 摘要
|
||||
/// </summary>
|
||||
#if NET9_0_OR_GREATER
|
||||
[Obsolete("Use CryptographicOperations.HashData()")]
|
||||
#endif
|
||||
internal static class XXH64
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -7,7 +7,8 @@ using Snap.Hutao.Win32.NetworkManagement.WindowsFirewall;
|
||||
using Snap.Hutao.Win32.Security;
|
||||
using System.Runtime.InteropServices;
|
||||
using static Snap.Hutao.Win32.AdvApi32;
|
||||
using static Snap.Hutao.Win32.ApiMsWinNetIsolation;
|
||||
using static Snap.Hutao.Win32.FirewallApi;
|
||||
using static Snap.Hutao.Win32.Kernel32;
|
||||
using static Snap.Hutao.Win32.Macros;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Http.Loopback;
|
||||
@@ -26,45 +27,7 @@ internal sealed unsafe class LoopbackManager : ObservableObject
|
||||
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
|
||||
|
||||
INET_FIREWALL_APP_CONTAINER* pContainers = default;
|
||||
try
|
||||
{
|
||||
{
|
||||
WIN32_ERROR error = NetworkIsolationEnumAppContainers(NETISO_FLAG.NETISO_FLAG_MAX, out uint acCount, out pContainers);
|
||||
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
|
||||
for (uint i = 0; i < acCount; i++)
|
||||
{
|
||||
INET_FIREWALL_APP_CONTAINER* pContainer = pContainers + i;
|
||||
ReadOnlySpan<char> appContainerName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pContainer->appContainerName);
|
||||
if (appContainerName.Equals(runtimeOptions.FamilyName, StringComparison.Ordinal))
|
||||
{
|
||||
ConvertSidToStringSidW(pContainer->appContainerSid, out PWSTR stringSid);
|
||||
hutaoContainerStringSID = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid).ToString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// This function returns 1 rather than 0 specfied in the document.
|
||||
_ = NetworkIsolationFreeAppContainers(pContainers);
|
||||
}
|
||||
|
||||
{
|
||||
WIN32_ERROR error = NetworkIsolationGetAppContainerConfig(out uint accCount, out SID_AND_ATTRIBUTES* pSids);
|
||||
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
|
||||
for (uint i = 0; i < accCount; i++)
|
||||
{
|
||||
ConvertSidToStringSidW((pSids + i)->Sid, out PWSTR stringSid);
|
||||
ReadOnlySpan<char> stringSidSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid);
|
||||
if (stringSidSpan.Equals(hutaoContainerStringSID, StringComparison.Ordinal))
|
||||
{
|
||||
IsLoopbackEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Initialize(out hutaoContainerStringSID);
|
||||
}
|
||||
|
||||
public bool IsLoopbackEnabled { get => isLoopbackEnabled; private set => SetProperty(ref isLoopbackEnabled, value); }
|
||||
@@ -84,4 +47,60 @@ internal sealed unsafe class LoopbackManager : ObservableObject
|
||||
sids.Add(sidAndAttributes);
|
||||
IsLoopbackEnabled = NetworkIsolationSetAppContainerConfig(CollectionsMarshal.AsSpan(sids)) is WIN32_ERROR.ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
private void Initialize(out string hutaoContainerStringSID)
|
||||
{
|
||||
hutaoContainerStringSID = string.Empty;
|
||||
|
||||
INET_FIREWALL_APP_CONTAINER* pContainers = default;
|
||||
try
|
||||
{
|
||||
WIN32_ERROR error = NetworkIsolationEnumAppContainers(NETISO_FLAG.NETISO_FLAG_MAX, out uint acCount, out pContainers);
|
||||
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
|
||||
for (uint i = 0; i < acCount; i++)
|
||||
{
|
||||
INET_FIREWALL_APP_CONTAINER* pContainer = pContainers + i;
|
||||
ReadOnlySpan<char> appContainerName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pContainer->appContainerName);
|
||||
if (appContainerName.Equals(runtimeOptions.FamilyName, StringComparison.Ordinal))
|
||||
{
|
||||
ConvertSidToStringSidW(pContainer->appContainerSid, out PWSTR stringSid);
|
||||
hutaoContainerStringSID = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid).ToString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// This function returns 1 rather than 0 specfied in the document.
|
||||
_ = NetworkIsolationFreeAppContainers(pContainers);
|
||||
}
|
||||
|
||||
SID_AND_ATTRIBUTES* pSids = default;
|
||||
uint count = default;
|
||||
try
|
||||
{
|
||||
WIN32_ERROR error = NetworkIsolationGetAppContainerConfig(out count, out pSids);
|
||||
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
|
||||
|
||||
for (uint i = 0; i < count; i++)
|
||||
{
|
||||
ConvertSidToStringSidW((pSids + i)->Sid, out PWSTR stringSid);
|
||||
ReadOnlySpan<char> stringSidSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid);
|
||||
if (stringSidSpan.Equals(hutaoContainerStringSID, StringComparison.Ordinal))
|
||||
{
|
||||
IsLoopbackEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (uint index = 0; index < count; index++)
|
||||
{
|
||||
HeapFree(GetProcessHeap(), 0, pSids[index].Sid);
|
||||
}
|
||||
|
||||
HeapFree(GetProcessHeap(), 0, pSids);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using System.Reflection;
|
||||
namespace Snap.Hutao.Core.IO.Http.Proxy;
|
||||
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed partial class DynamicHttpProxy : ObservableObject, IWebProxy, IDisposable
|
||||
internal sealed partial class HttpProxyUsingSystemProxy : ObservableObject, IWebProxy, IDisposable
|
||||
{
|
||||
private const string ProxySettingPath = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections";
|
||||
|
||||
@@ -20,13 +20,13 @@ internal sealed partial class DynamicHttpProxy : ObservableObject, IWebProxy, ID
|
||||
|
||||
private IWebProxy innerProxy = default!;
|
||||
|
||||
public DynamicHttpProxy(IServiceProvider serviceProvider)
|
||||
public HttpProxyUsingSystemProxy(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
UpdateInnerProxy();
|
||||
|
||||
watcher = new(ProxySettingPath, OnSystemProxySettingsChanged);
|
||||
watcher.Start();
|
||||
watcher.Start(serviceProvider.GetRequiredService<ILogger<HttpProxyUsingSystemProxy>>());
|
||||
}
|
||||
|
||||
public string CurrentProxyUri
|
||||
@@ -75,8 +75,8 @@ internal sealed partial class DynamicHttpProxy : ObservableObject, IWebProxy, ID
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(innerProxy as IDisposable)?.Dispose();
|
||||
watcher.Dispose();
|
||||
(innerProxy as IDisposable)?.Dispose();
|
||||
}
|
||||
|
||||
public void OnSystemProxySettingsChanged()
|
||||
@@ -9,6 +9,7 @@ using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Http.Sharding;
|
||||
|
||||
// TODO: refactor to use tree structure to calculate shards
|
||||
internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
|
||||
{
|
||||
private const int ShardSize = 4 * 1024 * 1024;
|
||||
@@ -78,7 +79,7 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
|
||||
using (HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent())
|
||||
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(bufferSize))
|
||||
{
|
||||
Memory<byte> buffer = memoryOwner.Memory;
|
||||
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
|
||||
|
||||
@@ -3,7 +3,15 @@
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 流复制状态
|
||||
/// </summary>
|
||||
internal sealed record StreamCopyStatus(long BytesCopied, long TotalBytes);
|
||||
internal sealed class StreamCopyStatus
|
||||
{
|
||||
public StreamCopyStatus(long bytesCopied, long totalBytes)
|
||||
{
|
||||
BytesCopied = bytesCopied;
|
||||
TotalBytes = totalBytes;
|
||||
}
|
||||
|
||||
public long BytesCopied { get; }
|
||||
|
||||
public long TotalBytes { get; }
|
||||
}
|
||||
@@ -27,6 +27,7 @@ internal sealed class StreamReaderWriter : IDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="StreamWriter.WriteAsync(string?)"/>
|
||||
[SuppressMessage("", "SH003")]
|
||||
public Task WriteAsync(string value)
|
||||
{
|
||||
return writer.WriteAsync(value);
|
||||
|
||||
@@ -12,15 +12,8 @@ namespace Snap.Hutao.Core.IO;
|
||||
[HighQuality]
|
||||
internal readonly struct TempFile : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 路径
|
||||
/// </summary>
|
||||
public readonly string Path;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的临时文件
|
||||
/// </summary>
|
||||
/// <param name="delete">是否在创建时删除文件</param>
|
||||
private TempFile(bool delete)
|
||||
{
|
||||
try
|
||||
@@ -38,11 +31,6 @@ internal readonly struct TempFile : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建临时文件并复制内容
|
||||
/// </summary>
|
||||
/// <param name="file">源文件</param>
|
||||
/// <returns>临时文件</returns>
|
||||
public static TempFile? CopyFrom(string file)
|
||||
{
|
||||
TempFile temporaryFile = new(false);
|
||||
@@ -57,9 +45,6 @@ internal readonly struct TempFile : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除临时文件
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -5,76 +5,60 @@ using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 临时文件流
|
||||
/// </summary>
|
||||
internal sealed class TempFileStream : Stream
|
||||
{
|
||||
private readonly string path;
|
||||
private readonly FileStream stream;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的临时的文件流
|
||||
/// </summary>
|
||||
/// <param name="mode">文件模式</param>
|
||||
/// <param name="access">访问方式</param>
|
||||
public TempFileStream(FileMode mode, FileAccess access)
|
||||
{
|
||||
path = Path.GetTempFileName();
|
||||
stream = File.Open(path, mode, access);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanRead { get => stream.CanRead; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanSeek { get => stream.CanSeek; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanWrite { get => stream.CanWrite; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Length { get => stream.Length; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Position { get => stream.Position; set => stream.Position = value; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Flush()
|
||||
{
|
||||
stream.Flush();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return stream.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
return stream.Seek(offset, origin);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
stream.SetLength(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
stream.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (disposing)
|
||||
{
|
||||
stream.Dispose();
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
stream.Dispose();
|
||||
File.Delete(path);
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user