mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
402bb17507 | ||
|
|
dcac7ac792 | ||
|
|
d157476a9a | ||
|
|
506d198167 | ||
|
|
9c2131cb9d | ||
|
|
896e3d49f2 | ||
|
|
7da24790c4 | ||
|
|
ebf163f200 | ||
|
|
26043a6a73 | ||
|
|
ad67001556 | ||
|
|
428cd35a7f | ||
|
|
1cf3450264 | ||
|
|
1849ac16da | ||
|
|
4e9d87af50 | ||
|
|
eb125e547f | ||
|
|
c17798a8c9 | ||
|
|
fa9b39d134 | ||
|
|
8dc85c7a25 | ||
|
|
5845c718e1 | ||
|
|
0c093851ed | ||
|
|
04c187aa16 | ||
|
|
25cf9f5356 | ||
|
|
c30c8b114a | ||
|
|
41f5a04e5a | ||
|
|
4f26d6efc2 | ||
|
|
0d6b5e595e | ||
|
|
e30bebd1a3 | ||
|
|
ffb34c0e93 | ||
|
|
35b35ab649 | ||
|
|
f067350cd8 | ||
|
|
14648dafe8 | ||
|
|
7a7d81cfee | ||
|
|
30c055e7ba | ||
|
|
6a922d9db6 | ||
|
|
02a6e64a8c | ||
|
|
51c4e66472 | ||
|
|
48875195bf | ||
|
|
a179e0e838 | ||
|
|
00c3e94e97 | ||
|
|
eec7224c07 | ||
|
|
85bcf37b1b | ||
|
|
465c7035c0 | ||
|
|
11620816ec | ||
|
|
c5b75d6f82 | ||
|
|
6241bbc997 | ||
|
|
afbd8ec6ad | ||
|
|
288cc841ac | ||
|
|
a9295c0a37 | ||
|
|
29cd690032 | ||
|
|
78d8539ae2 | ||
|
|
7dec87586b | ||
|
|
d169f355f3 | ||
|
|
e1784e2078 | ||
|
|
32b1b698df | ||
|
|
d6c7df1593 | ||
|
|
904fdf7fc9 | ||
|
|
b3e4ebb5d3 | ||
|
|
09b9af4575 | ||
|
|
8930548f44 | ||
|
|
34dbcc6f5d | ||
|
|
75c25cec53 | ||
|
|
22251ca937 | ||
|
|
57c9531db8 | ||
|
|
fd73743159 | ||
|
|
6594d9032d | ||
|
|
ae1b452697 | ||
|
|
bc3df782e4 | ||
|
|
3ef117e683 | ||
|
|
8b1613d46f | ||
|
|
c2708aaedd | ||
|
|
db3de01eec | ||
|
|
635b52980b | ||
|
|
4988c1bd47 | ||
|
|
fd499e372c | ||
|
|
a6c376cd44 | ||
|
|
425b3789bf | ||
|
|
a6986bc201 | ||
|
|
301a14611a | ||
|
|
59db7d968a | ||
|
|
45f4b46e9e | ||
|
|
82b5d9b12a | ||
|
|
6fb5cfe25d | ||
|
|
1df78345c5 | ||
|
|
fd817af9c3 | ||
|
|
3f6efe2e24 | ||
|
|
ef6d88ba2e | ||
|
|
26748e5885 | ||
|
|
5f180846f6 | ||
|
|
57ea6bb34e | ||
|
|
95024e4107 | ||
|
|
678ec191a6 | ||
|
|
40b26d9d3c | ||
|
|
e5677e7de4 | ||
|
|
9ae8b182e7 | ||
|
|
02fe498cce | ||
|
|
f924f78ee2 | ||
|
|
977e749939 | ||
|
|
f8ad3b98fc | ||
|
|
ee39f7fec5 | ||
|
|
c8c1d21c9b | ||
|
|
f06825f246 | ||
|
|
72395fdb89 | ||
|
|
8954f7e325 | ||
|
|
976441de18 | ||
|
|
60a49971f6 | ||
|
|
fb5de92283 | ||
|
|
046f3ace94 | ||
|
|
4c369ad0ad | ||
|
|
7efaaae3e1 | ||
|
|
446bdb2b49 | ||
|
|
20277b8b79 | ||
|
|
7baf125f88 | ||
|
|
a4e782da78 | ||
|
|
d5551e5cdf | ||
|
|
f016a4a27f | ||
|
|
8b931b6d89 | ||
|
|
b942ceb914 | ||
|
|
f7a49e52e0 | ||
|
|
d4bd610fe2 | ||
|
|
a3dcfd3804 | ||
|
|
592525d149 | ||
|
|
83c4598df5 | ||
|
|
31670953b0 | ||
|
|
aa680388ad | ||
|
|
ba4f59de30 | ||
|
|
8780cf385e | ||
|
|
431cdd1253 | ||
|
|
9a8827fb40 | ||
|
|
d88a6ca301 | ||
|
|
5d401794e5 | ||
|
|
26396443dc | ||
|
|
ae2415dbca | ||
|
|
6b755d934d | ||
|
|
7d5b057269 | ||
|
|
917c173eb2 | ||
|
|
28d702422e | ||
|
|
7612ab5da3 | ||
|
|
ab436ecb2f | ||
|
|
457e3ff4d5 | ||
|
|
224c4e52ea | ||
|
|
2a5c7b21fd | ||
|
|
53f8291a66 | ||
|
|
b621d5406a | ||
|
|
12f4847aea | ||
|
|
39a3d31f38 | ||
|
|
70ac0b13a5 | ||
|
|
a3520a4991 | ||
|
|
aed0284e4b | ||
|
|
96ef07cbe5 | ||
|
|
d1f37f37ac | ||
|
|
1fe09f3069 | ||
|
|
5b109013a0 | ||
|
|
32d9355c3a | ||
|
|
71f170d51e | ||
|
|
60015b6354 | ||
|
|
eecae3ea4f | ||
|
|
1831166f1e | ||
|
|
a98915ea24 | ||
|
|
0d46656f57 | ||
|
|
c814a5c28f | ||
|
|
9c3d59cc6f | ||
|
|
890cf3f3ea | ||
|
|
196bbb54c3 | ||
|
|
0481b9e474 | ||
|
|
c4f3eb68e8 | ||
|
|
c2e9f3a926 | ||
|
|
fb1fe3e40f | ||
|
|
75ed512e4a | ||
|
|
dd4dd33d93 | ||
|
|
1e216e9823 | ||
|
|
f823cb5f1a | ||
|
|
2c6d25f0a3 | ||
|
|
817f768263 | ||
|
|
2998fbb167 | ||
|
|
8f0f94054d | ||
|
|
17a5d4d3a2 | ||
|
|
a1c604e68a | ||
|
|
948ec9a822 | ||
|
|
f83174d690 | ||
|
|
d686debbfb | ||
|
|
279e107919 | ||
|
|
45248d75e1 | ||
|
|
22646cfab2 | ||
|
|
d0237a3c89 | ||
|
|
73c80fad10 | ||
|
|
320bed9fcb | ||
|
|
bb12aca3b4 | ||
|
|
c7b5d98fb1 | ||
|
|
7cc96f94f2 | ||
|
|
b35355f9a3 | ||
|
|
745815657d |
17
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
17
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
@@ -18,7 +18,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
|
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
|
- label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -29,17 +29,17 @@ body:
|
|||||||
id: winver
|
id: winver
|
||||||
attributes:
|
attributes:
|
||||||
label: Windows 版本
|
label: Windows 版本
|
||||||
description: |
|
description: |
|
||||||
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
|
`Win+R` 输入 `winver` 回车后在打开的窗口第二行可以找到
|
||||||
placeholder: 例:22000.556
|
placeholder: 例:22000.556
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: shver
|
id: shver
|
||||||
attributes:
|
attributes:
|
||||||
label: Snap Hutao 版本
|
label: Snap Hutao 版本
|
||||||
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
|
description: 在应用标题,应用程序的反馈中心界面中可以找到
|
||||||
placeholder: 例:1.4.15.0
|
placeholder: 例:1.4.15.0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -48,10 +48,10 @@ body:
|
|||||||
id: deviceid
|
id: deviceid
|
||||||
attributes:
|
attributes:
|
||||||
label: 设备 ID
|
label: 设备 ID
|
||||||
description: |
|
description: |
|
||||||
在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID
|
在胡桃工具箱的反馈中心界面,你可以找到并复制你的设备 ID
|
||||||
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
|
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
|
||||||
如果你的程序已经无法启动,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将显示你的设备 ID
|
如果你的程序已经无法启动,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.Diagnostic.Tooling.exe),它将显示你的设备 ID
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ body:
|
|||||||
- 公告
|
- 公告
|
||||||
- 其它
|
- 其它
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: what-happened
|
id: what-happened
|
||||||
@@ -107,4 +107,3 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: 我认为上述的描述已经足以详细,以允许开发人员能复现该问题
|
- label: 我认为上述的描述已经足以详细,以允许开发人员能复现该问题
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
17
.github/ISSUE_TEMPLATE/ENG-bug-report.yml
vendored
17
.github/ISSUE_TEMPLATE/ENG-bug-report.yml
vendored
@@ -18,7 +18,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I have read [FAQ page](https://hut.ao/advanced/FAQ.html) and [Exception page](https://hut.ao/advanced/exceptions.html) in Snap Hutao document, and my issue is not answered
|
- label: I have read [FAQ page](https://hut.ao/advanced/FAQ.html) and [Exception page](https://hut.ao/advanced/exceptions.html) in Snap Hutao document, and my issue is not answered
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- label: I and tried **search feature** in Snap Hutao document site, and no associated article
|
- label: I and tried **search feature** in Snap Hutao document site, and no associated article
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -29,12 +29,12 @@ body:
|
|||||||
id: winver
|
id: winver
|
||||||
attributes:
|
attributes:
|
||||||
label: Windows Version
|
label: Windows Version
|
||||||
description: |
|
description: |
|
||||||
Use `Win+R` and input `winver`, Windows build version is usually at the second line
|
Use `Win+R` and input `winver`, Windows build version is usually at the second line
|
||||||
placeholder: e.g. 22000.556
|
placeholder: e.g. 22000.556
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: shver
|
id: shver
|
||||||
attributes:
|
attributes:
|
||||||
@@ -48,10 +48,10 @@ body:
|
|||||||
id: deviceid
|
id: deviceid
|
||||||
attributes:
|
attributes:
|
||||||
label: Device ID
|
label: Device ID
|
||||||
description: |
|
description: |
|
||||||
In Snap Hutao's settings page, you can find and copy your device ID
|
In Snap Hutao's Feedback Center, you can find and copy your device ID
|
||||||
If your issue is about program crash, please fill this so we can dump the log and locate the source easier
|
If your issue is about program crash, please fill this so we can dump the log and locate the source easier
|
||||||
If your program cannot startup, please download and run [Diagnosis Tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will shows your device ID.
|
If your program cannot startup, please download and run [Diagnostic Tooling](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.Diagnostic.Tooling.exe), it will shows your device ID.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
@@ -74,12 +74,12 @@ body:
|
|||||||
- User Interface
|
- User Interface
|
||||||
- Snap Hutao Cloud
|
- Snap Hutao Cloud
|
||||||
- Snap Hutao Account
|
- Snap Hutao Account
|
||||||
- Checkin
|
- Checkin
|
||||||
- Wiki
|
- Wiki
|
||||||
- Announcement
|
- Announcement
|
||||||
- Other
|
- Other
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: what-happened
|
id: what-happened
|
||||||
@@ -107,4 +107,3 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I believe the description above is detail enough to allow developers to reproduce the issue
|
- label: I believe the description above is detail enough to allow developers to reproduce the issue
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -13,4 +13,8 @@ updates:
|
|||||||
groups:
|
groups:
|
||||||
packages:
|
packages:
|
||||||
patterns:
|
patterns:
|
||||||
- "*"
|
- "*"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/.github/workflows" # GitHub Workflows
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|||||||
23
.github/workflows/alpha.yml
vendored
23
.github/workflows/alpha.yml
vendored
@@ -13,16 +13,28 @@ on:
|
|||||||
- '**.md'
|
- '**.md'
|
||||||
- 'LICENSE'
|
- 'LICENSE'
|
||||||
- '**.yml'
|
- '**.yml'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths-ignore:
|
||||||
|
- '.gitattributes'
|
||||||
|
- '.github/**'
|
||||||
|
- '.gitignore'
|
||||||
|
- '.gitmodules'
|
||||||
|
- '**.md'
|
||||||
|
- 'LICENSE'
|
||||||
|
- '**.yml'
|
||||||
|
- '**.resx'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4.0.0
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0
|
dotnet-version: 8.0
|
||||||
|
|
||||||
@@ -34,20 +46,21 @@ jobs:
|
|||||||
VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }}
|
VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }}
|
||||||
|
|
||||||
- name: Sign Msix
|
- name: Sign Msix
|
||||||
|
if: success() && github.event_name != 'pull_request'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
[System.Convert]::FromBase64String("${{ secrets.CERTIFICATE }}") | Set-Content -AsByteStream temp.pfx
|
[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
|
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
|
||||||
|
|
||||||
- name: Upload signed msix
|
- name: Upload signed msix
|
||||||
if: success()
|
if: success() && github.event_name != 'pull_request'
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}
|
name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}
|
||||||
path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
|
path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
|
||||||
|
|
||||||
- name: Add summary
|
- name: Add summary
|
||||||
if: success()
|
if: success() && github.event_name != 'pull_request'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$summary = "
|
$summary = "
|
||||||
|
|||||||
16
.github/workflows/close_stale.yml
vendored
Normal file
16
.github/workflows/close_stale.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: 'Close stale issues and PRs'
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 1 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- 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.'
|
||||||
|
days-before-stale: 7
|
||||||
|
days-before-close: 3
|
||||||
|
close-issue-reason: not_planned
|
||||||
20
.github/workflows/issue_similarity.yml
vendored
Normal file
20
.github/workflows/issue_similarity.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Issues Similarity Analysis
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
similarity-analysis:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: analysis
|
||||||
|
uses: actions-cool/issues-similarity-analysis@v1
|
||||||
|
with:
|
||||||
|
filter-threshold: 0.5
|
||||||
|
comment-title: '### Probable Similar Topics'
|
||||||
|
title-excludes: '[Publish]:,[Bug]:,[Feat]:,[Network]:,[ENG]'
|
||||||
|
comment-body: '${index}. ${similarity} #${number}'
|
||||||
|
show-footer: false
|
||||||
|
show-mentioned: true
|
||||||
|
since-days: 365
|
||||||
26
.github/workflows/lock_closed_issues.yml
vendored
Normal file
26
.github/workflows/lock_closed_issues.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: 'Lock Threads'
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
discussions: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: lock-threads
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
action:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v5
|
||||||
|
with:
|
||||||
|
issue-inactive-days: '30'
|
||||||
|
issue-comment: 'This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related topic.'
|
||||||
|
issue-lock-reason: 'resolved'
|
||||||
|
process-only: 'issues'
|
||||||
|
log-output: false
|
||||||
@@ -74,3 +74,4 @@ Refresh:
|
|||||||
script:
|
script:
|
||||||
- apt-get install -y curl
|
- apt-get install -y curl
|
||||||
- curl -X PATCH "$PURGE_URL"
|
- curl -X PATCH "$PURGE_URL"
|
||||||
|
- curl -X POST -o /dev/null "$UPLOAD_OSS_URL"
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ Install with Snap Hutao MSIX package, can be installed with Windows built-in App
|
|||||||
|
|
||||||
### 特定的原神项目 / Specific Genshin-related Projects
|
### 特定的原神项目 / Specific Genshin-related Projects
|
||||||
|
|
||||||
* [biuuu/genshin-wish-export](https://github.com/biuuu/genshin-wish-export)
|
* [Scighost/Starward](https://github.com/Scighost/Starward)
|
||||||
* [xunkong/xunkong](https://github.com/xunkong/xunkong)
|
|
||||||
* [YuehaiTeam/cocogoat](https://github.com/YuehaiTeam/cocogoat)
|
|
||||||
|
|
||||||
### 使用的技术栈 / Tech Stack
|
### 使用的技术栈 / Tech Stack
|
||||||
|
|
||||||
@@ -57,7 +55,6 @@ Install with Snap Hutao MSIX package, can be installed with Windows built-in App
|
|||||||
* [dotnet/efcore](https://github.com/dotnet/efcore)
|
* [dotnet/efcore](https://github.com/dotnet/efcore)
|
||||||
* [dotnet/runtime](https://github.com/dotnet/runtime)
|
* [dotnet/runtime](https://github.com/dotnet/runtime)
|
||||||
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
|
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
|
||||||
* [microsoft/CsWin32](https://github.com/microsoft/CsWin32)
|
|
||||||
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
|
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
|
||||||
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
|
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
|
||||||
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
|
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
# CI process script for Snap.Hutao
|
|
||||||
# Usage:
|
|
||||||
# 1. Append the script in Pipelines
|
|
||||||
# 2. Upload the pfx and cer certificates to Pipelines Library secrets
|
|
||||||
# 3. Permit the pfx usage
|
|
||||||
# 4. Add a `pw` variable in the script variables, which is pfx password
|
|
||||||
# 5. Connect the GitHub in project settings
|
|
||||||
# 6. Run
|
|
||||||
|
|
||||||
trigger: none
|
|
||||||
pr: none
|
|
||||||
# trigger:
|
|
||||||
# branches:
|
|
||||||
# include:
|
|
||||||
# - main
|
|
||||||
# - develop
|
|
||||||
# paths:
|
|
||||||
# exclude:
|
|
||||||
# - README.md
|
|
||||||
# - azure-pipelines.yml
|
|
||||||
# - .github/ISSUE_TEMPLATE/*.yml
|
|
||||||
# - .github/workflows/*.yml
|
|
||||||
# - src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
|
|
||||||
# pr:
|
|
||||||
# branches:
|
|
||||||
# include:
|
|
||||||
# - main
|
|
||||||
# paths:
|
|
||||||
# exclude:
|
|
||||||
# - README.md
|
|
||||||
# - azure-pipelines.yml
|
|
||||||
# - .github/ISSUE_TEMPLATE/*.yml
|
|
||||||
# - .github/workflows/*.yml
|
|
||||||
# - src/Snap.Hutao/Snap.Hutao/Resource/Localization/*.resx
|
|
||||||
|
|
||||||
|
|
||||||
pool:
|
|
||||||
name: Default
|
|
||||||
demands: agent.name -equals Hutao-Server
|
|
||||||
|
|
||||||
variables:
|
|
||||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
|
||||||
solution: '$(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao.sln'
|
|
||||||
project: $(Build.SourcesDirectory)/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj'
|
|
||||||
buildPlatform: 'x64'
|
|
||||||
buildConfiguration: 'Release'
|
|
||||||
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: UseDotNet@2
|
|
||||||
displayName: Install dotNet
|
|
||||||
inputs:
|
|
||||||
packageType: 'sdk'
|
|
||||||
version: '8.x'
|
|
||||||
includePreviewVersions: true
|
|
||||||
|
|
||||||
- task: CmdLine@2
|
|
||||||
displayName: dotnet cake
|
|
||||||
inputs:
|
|
||||||
script: dotnet tool restore && dotnet cake
|
|
||||||
|
|
||||||
- task: MsixSigning@1
|
|
||||||
name: signMsix
|
|
||||||
displayName: Sign MSIX package
|
|
||||||
inputs:
|
|
||||||
package: '$(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix'
|
|
||||||
certificate: 'DGP_Studio_CI.pfx'
|
|
||||||
passwordVariable: 'pw'
|
|
||||||
condition: succeeded()
|
|
||||||
|
|
||||||
- task: DownloadSecureFile@1
|
|
||||||
name: cerFile
|
|
||||||
displayName: Download Root CA
|
|
||||||
inputs:
|
|
||||||
secureFile: 'Snap.Hutao.CI.cer'
|
|
||||||
|
|
||||||
- task: PublishPipelineArtifact@1
|
|
||||||
inputs:
|
|
||||||
targetPath: '$(Build.ArtifactStagingDirectory)'
|
|
||||||
artifact: 'Snap.Hutao.Alpha-$(version).msix'
|
|
||||||
publishLocation: 'pipeline'
|
|
||||||
|
|
||||||
#- task: GitHubRelease@1
|
|
||||||
# inputs:
|
|
||||||
# gitHubConnection: 'github.com_Masterain'
|
|
||||||
# repositoryName: 'DGP-Automation/Hutao-Auto-Release'
|
|
||||||
# action: 'create'
|
|
||||||
# target: '$(Build.SourceVersion)'
|
|
||||||
# tagSource: 'userSpecifiedTag'
|
|
||||||
# tag: '$(version)'
|
|
||||||
# title: '$(version)'
|
|
||||||
# releaseNotesSource: 'inline'
|
|
||||||
# releaseNotesInline: |
|
|
||||||
# ## 普通用户请勿下载
|
|
||||||
# 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
|
|
||||||
#
|
|
||||||
# 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
|
|
||||||
#
|
|
||||||
# assets: |
|
|
||||||
# $(Build.ArtifactStagingDirectory)/*
|
|
||||||
# $(cerFile.secureFilePath)
|
|
||||||
# isPreRelease: true
|
|
||||||
# changeLogCompareToRelease: 'lastFullRelease'
|
|
||||||
# changeLogType: 'commitBased'
|
|
||||||
|
|
||||||
|
|
||||||
- task: rclone@1
|
|
||||||
displayName: Upload CI via Rclone
|
|
||||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
||||||
inputs:
|
|
||||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix downloadDGPCN:/releases/Alpha/'
|
|
||||||
configPath: 'C:\agent\_work\_tasks\rclone.conf'
|
|
||||||
|
|
||||||
- task: rclone@1
|
|
||||||
displayName: Upload PR CI via Rclone
|
|
||||||
condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
|
|
||||||
inputs:
|
|
||||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(version).msix downloadDGPCN:/releases/PR/'
|
|
||||||
configPath: 'C:\agent\_work\_tasks\rclone.conf'
|
|
||||||
62
build.cake
62
build.cake
@@ -1,5 +1,5 @@
|
|||||||
#tool "nuget:?package=nuget.commandline&version=6.5.0"
|
#tool "nuget:?package=nuget.commandline&version=6.9.1"
|
||||||
#addin nuget:?package=Cake.Http&version=3.0.2
|
#addin nuget:?package=Cake.Http&version=4.0.0
|
||||||
|
|
||||||
var target = Argument("target", "Build");
|
var target = Argument("target", "Build");
|
||||||
var configuration = Argument("configuration", "Release");
|
var configuration = Argument("configuration", "Release");
|
||||||
@@ -28,43 +28,33 @@ string manifest
|
|||||||
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Package.appxmanifest");
|
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao", "Package.appxmanifest");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AzurePipelines.IsRunningOnAzurePipelines)
|
if (GitHubActions.IsRunningOnGitHubActions)
|
||||||
{
|
|
||||||
repoDir = AzurePipelines.Environment.Build.SourcesDirectory.FullPath;
|
|
||||||
outputPath = AzurePipelines.Environment.Build.ArtifactStagingDirectory.FullPath;
|
|
||||||
|
|
||||||
var versionAuth = HasEnvironmentVariable("VERSION_API_TOKEN") ? EnvironmentVariable("VERSION_API_TOKEN") : throw new Exception("Cannot find VERSION_API_TOKEN");
|
|
||||||
version = HttpGet(
|
|
||||||
"https://internal.snapgenshin.cn/BuildIntergration/RequestNewVersion",
|
|
||||||
new HttpSettings
|
|
||||||
{
|
|
||||||
Headers = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "Authorization", versionAuth }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
Information($"Version: {version}");
|
|
||||||
|
|
||||||
AzurePipelines.Commands.SetVariable("version", version);
|
|
||||||
}
|
|
||||||
else if (GitHubActions.IsRunningOnGitHubActions)
|
|
||||||
{
|
{
|
||||||
repoDir = GitHubActions.Environment.Workflow.Workspace.FullPath;
|
repoDir = GitHubActions.Environment.Workflow.Workspace.FullPath;
|
||||||
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
|
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
|
||||||
|
|
||||||
var versionAuth = HasEnvironmentVariable("VERSION_API_TOKEN") ? EnvironmentVariable("VERSION_API_TOKEN") : throw new Exception("Cannot find VERSION_API_TOKEN");
|
if (GitHubActions.Environment.PullRequest.IsPullRequest)
|
||||||
version = HttpGet(
|
{
|
||||||
"https://internal.snapgenshin.cn/BuildIntergration/RequestNewVersion",
|
version = System.DateTime.Now.ToString("yyyy.M.d.0");
|
||||||
new HttpSettings
|
|
||||||
{
|
Information("Is Pull Request. Skip version.");
|
||||||
Headers = new Dictionary<string, string>
|
}
|
||||||
{
|
else
|
||||||
|
{
|
||||||
|
var versionAuth = HasEnvironmentVariable("VERSION_API_TOKEN") ? EnvironmentVariable("VERSION_API_TOKEN") : throw new Exception("Cannot find VERSION_API_TOKEN");
|
||||||
|
version = HttpGet(
|
||||||
|
"https://internal.snapgenshin.cn/BuildIntergration/RequestNewVersion",
|
||||||
|
new HttpSettings
|
||||||
|
{
|
||||||
|
Headers = new Dictionary<string, string>
|
||||||
|
{
|
||||||
{ "Authorization", versionAuth }
|
{ "Authorization", versionAuth }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
Information($"Version: {version}");
|
|
||||||
|
Information($"Version: {version}");
|
||||||
|
}
|
||||||
|
|
||||||
GitHubActions.Commands.SetOutputParameter("version", version);
|
GitHubActions.Commands.SetOutputParameter("version", version);
|
||||||
}
|
}
|
||||||
@@ -106,7 +96,7 @@ Task("Generate AppxManifest")
|
|||||||
|
|
||||||
var content = System.IO.File.ReadAllText(manifest);
|
var content = System.IO.File.ReadAllText(manifest);
|
||||||
|
|
||||||
if (AzurePipelines.IsRunningOnAzurePipelines || GitHubActions.IsRunningOnGitHubActions)
|
if (GitHubActions.IsRunningOnGitHubActions)
|
||||||
{
|
{
|
||||||
Information("Using CI configuraion");
|
Information("Using CI configuraion");
|
||||||
content = content
|
content = content
|
||||||
@@ -175,7 +165,7 @@ Task("Build MSIX")
|
|||||||
.Does(() =>
|
.Does(() =>
|
||||||
{
|
{
|
||||||
var arguments = "arguments";
|
var arguments = "arguments";
|
||||||
if (AzurePipelines.IsRunningOnAzurePipelines || GitHubActions.IsRunningOnGitHubActions)
|
if (GitHubActions.IsRunningOnGitHubActions)
|
||||||
{
|
{
|
||||||
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix");
|
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,9 +160,39 @@ public sealed class GeniusInvokationDecoding
|
|||||||
|
|
||||||
ushort[] testKnownResult =
|
ushort[] testKnownResult =
|
||||||
[
|
[
|
||||||
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
|
060,
|
||||||
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
|
019,
|
||||||
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
|
001,
|
||||||
|
079,
|
||||||
|
120,
|
||||||
|
120,
|
||||||
|
129,
|
||||||
|
151,
|
||||||
|
151,
|
||||||
|
153,
|
||||||
|
153,
|
||||||
|
181,
|
||||||
|
184,
|
||||||
|
184,
|
||||||
|
185,
|
||||||
|
185,
|
||||||
|
194,
|
||||||
|
194,
|
||||||
|
200,
|
||||||
|
200,
|
||||||
|
201,
|
||||||
|
201,
|
||||||
|
217,
|
||||||
|
217,
|
||||||
|
219,
|
||||||
|
241,
|
||||||
|
241,
|
||||||
|
244,
|
||||||
|
244,
|
||||||
|
245,
|
||||||
|
245,
|
||||||
|
270,
|
||||||
|
270,
|
||||||
];
|
];
|
||||||
|
|
||||||
CollectionAssert.AreEqual(resultArray, testKnownResult);
|
CollectionAssert.AreEqual(resultArray, testKnownResult);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||||
@@ -12,10 +12,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
<PackageReference Include="MSTest.TestAdapter" Version="3.2.1" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
<PackageReference Include="MSTest.TestFramework" Version="3.2.1" />
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
<PackageReference Include="coverlet.collector" Version="6.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
|
|
||||||
"allowMarshaling": true,
|
|
||||||
"useSafeHandles": false,
|
|
||||||
"comInterop": {
|
|
||||||
"preserveSigMethods": [
|
|
||||||
"IFileOpenDialog.Show",
|
|
||||||
"IFileSaveDialog.Show"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
// ADVAPI32
|
|
||||||
RegCloseKey
|
|
||||||
RegOpenKeyExW
|
|
||||||
RegNotifyChangeKeyValue
|
|
||||||
|
|
||||||
// COMCTL32
|
|
||||||
DefSubclassProc
|
|
||||||
RemoveWindowSubclass
|
|
||||||
SetWindowSubclass
|
|
||||||
|
|
||||||
// DWMAPI
|
|
||||||
DwmSetWindowAttribute
|
|
||||||
|
|
||||||
// GDI32
|
|
||||||
GetDeviceCaps
|
|
||||||
|
|
||||||
// KERNEL32
|
|
||||||
AllocConsole
|
|
||||||
CloseHandle
|
|
||||||
CreateEventW
|
|
||||||
CreateRemoteThread
|
|
||||||
FreeConsole
|
|
||||||
GetConsoleMode
|
|
||||||
GetModuleHandleW
|
|
||||||
GetProcAddress
|
|
||||||
GetStdHandle
|
|
||||||
K32EnumProcessModules
|
|
||||||
K32GetModuleBaseNameW
|
|
||||||
K32GetModuleInformation
|
|
||||||
ReadProcessMemory
|
|
||||||
SetConsoleMode
|
|
||||||
SetConsoleTitle
|
|
||||||
SetEvent
|
|
||||||
VirtualAlloc
|
|
||||||
VirtualAllocEx
|
|
||||||
VirtualFree
|
|
||||||
VirtualFreeEx
|
|
||||||
WaitForSingleObject
|
|
||||||
WriteProcessMemory
|
|
||||||
|
|
||||||
// OLE32
|
|
||||||
CoCreateInstance
|
|
||||||
CoWaitForMultipleObjects
|
|
||||||
|
|
||||||
// SHELL32
|
|
||||||
SHCreateItemFromParsingName
|
|
||||||
|
|
||||||
// USER32
|
|
||||||
AttachThreadInput
|
|
||||||
FindWindowExW
|
|
||||||
GetCursorPos
|
|
||||||
GetDC
|
|
||||||
GetDpiForWindow
|
|
||||||
GetForegroundWindow
|
|
||||||
GetWindowLongPtrW
|
|
||||||
GetWindowPlacement
|
|
||||||
GetWindowThreadProcessId
|
|
||||||
ReleaseDC
|
|
||||||
RegisterHotKey
|
|
||||||
SendInput
|
|
||||||
SetForegroundWindow
|
|
||||||
SetWindowLongPtrW
|
|
||||||
UnregisterHotKey
|
|
||||||
|
|
||||||
// COM
|
|
||||||
FileOpenDialog
|
|
||||||
FileSaveDialog
|
|
||||||
IFileOpenDialog
|
|
||||||
IFileSaveDialog
|
|
||||||
IPersistFile
|
|
||||||
IShellLinkDataList
|
|
||||||
IShellLinkW
|
|
||||||
ShellLink
|
|
||||||
SHELL_LINK_DATA_FLAGS
|
|
||||||
|
|
||||||
// WinRT
|
|
||||||
IMemoryBufferByteAccess
|
|
||||||
|
|
||||||
// Macro
|
|
||||||
HRESULT_FROM_WIN32
|
|
||||||
|
|
||||||
// Const value
|
|
||||||
INFINITE
|
|
||||||
MAX_PATH
|
|
||||||
WM_ERASEBKGND
|
|
||||||
WM_GETMINMAXINFO
|
|
||||||
WM_HOTKEY
|
|
||||||
WM_NCRBUTTONDOWN
|
|
||||||
WM_NCRBUTTONUP
|
|
||||||
WM_NULL
|
|
||||||
|
|
||||||
// HKEY
|
|
||||||
HKEY_CLASSES_ROOT
|
|
||||||
HKEY_CURRENT_CONFIG
|
|
||||||
HKEY_CURRENT_USER
|
|
||||||
HKEY_LOCAL_MACHINE
|
|
||||||
HKEY_USERS
|
|
||||||
|
|
||||||
// HRESULT
|
|
||||||
E_FAIL
|
|
||||||
RPC_E_WRONG_THREAD
|
|
||||||
|
|
||||||
// System.Com
|
|
||||||
CWMO_FLAGS
|
|
||||||
|
|
||||||
// System.Registry
|
|
||||||
REG_NOTIFY_FILTER
|
|
||||||
|
|
||||||
// System.Threading
|
|
||||||
LPTHREAD_START_ROUTINE
|
|
||||||
|
|
||||||
// UI.Shell
|
|
||||||
SLGP_FLAGS
|
|
||||||
|
|
||||||
// UI.WindowsAndMessaging
|
|
||||||
MINMAXINFO
|
|
||||||
WINDOW_EX_STYLE
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Windows.Win32.Foundation;
|
|
||||||
using Windows.Win32.System.Com;
|
|
||||||
|
|
||||||
namespace Windows.Win32;
|
|
||||||
|
|
||||||
internal static partial class PInvoke
|
|
||||||
{
|
|
||||||
/// <inheritdoc cref="CoCreateInstance(Guid*, object, CLSCTX, Guid*, out object)"/>
|
|
||||||
internal static unsafe HRESULT CoCreateInstance<TClass, TInterface>(object? pUnkOuter, CLSCTX dwClsContext, out TInterface ppv)
|
|
||||||
where TInterface : class
|
|
||||||
{
|
|
||||||
HRESULT hr = CoCreateInstance(typeof(TClass).GUID, pUnkOuter, dwClsContext, typeof(TInterface).GUID, out object o);
|
|
||||||
ppv = (TInterface)o;
|
|
||||||
return hr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
|
||||||
<ImplicitUsings>disable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Remove="NativeMethods.json" />
|
|
||||||
<None Remove="NativeMethods.txt" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<AdditionalFiles Include="NativeMethods.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using Windows.Win32.UI.WindowsAndMessaging;
|
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("Snap.Hutao")]
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Win32;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 结构体封送
|
|
||||||
/// </summary>
|
|
||||||
internal static class StructMarshal
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 构造一个新的 <see cref="Windows.Win32.UI.WindowsAndMessaging.WINDOWPLACEMENT"/>
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>新的实例</returns>
|
|
||||||
public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
|
|
||||||
{
|
|
||||||
return new() { length = unchecked((uint)sizeof(WINDOWPLACEMENT)) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,8 +13,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Win32", "Snap.Hutao.Win32\Snap.Hutao.Win32.csproj", "{0F7ABEB2-5107-4037-B9DC-84D288FB0801}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -67,32 +65,16 @@ Global
|
|||||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.Build.0 = Release|Any CPU
|
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.ActiveCfg = Release|Any CPU
|
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.Build.0 = Release|Any CPU
|
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.Build.0 = Debug|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.ActiveCfg = Release|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.Build.0 = Release|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
|
|
||||||
RESX_ShowErrorsInErrorList = False
|
|
||||||
RESX_SortFileContentOnSave = True
|
|
||||||
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
|
||||||
RESX_NeutralResourcesLanguage = zh-CN
|
|
||||||
RESX_AutoApplyExistingTranslations = False
|
RESX_AutoApplyExistingTranslations = False
|
||||||
|
RESX_NeutralResourcesLanguage = zh-CN
|
||||||
|
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
||||||
|
RESX_SortFileContentOnSave = True
|
||||||
|
RESX_ShowErrorsInErrorList = False
|
||||||
|
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
|
<ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
|
||||||
<ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
|
<ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
|
||||||
<ResourceDictionary Source="ms-appx:///Control/Theme/SettingsStyle.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/TransitionCollection.xaml"/>
|
||||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
|
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
|
||||||
<ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
|
<ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
x:Key="LargeGridViewItemStyle"
|
x:Key="LargeGridViewItemStyle"
|
||||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||||
TargetType="GridViewItem">
|
TargetType="GridViewItem">
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
<Setter Property="Margin" Value="0,0,12,12"/>
|
<Setter Property="Margin" Value="0,0,12,12"/>
|
||||||
</Style>
|
</Style>
|
||||||
<Style
|
<Style
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Microsoft.Windows.AppLifecycle;
|
|||||||
using Snap.Hutao.Core;
|
using Snap.Hutao.Core;
|
||||||
using Snap.Hutao.Core.ExceptionService;
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
using Snap.Hutao.Core.LifeCycle;
|
using Snap.Hutao.Core.LifeCycle;
|
||||||
|
using Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||||
using Snap.Hutao.Core.Shell;
|
using Snap.Hutao.Core.Shell;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
@@ -36,8 +37,6 @@ public sealed partial class App : Application
|
|||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
""";
|
""";
|
||||||
|
|
||||||
private const string AppInstanceKey = "main";
|
|
||||||
|
|
||||||
private readonly IServiceProvider serviceProvider;
|
private readonly IServiceProvider serviceProvider;
|
||||||
private readonly IActivation activation;
|
private readonly IActivation activation;
|
||||||
private readonly ILogger<App> logger;
|
private readonly ILogger<App> logger;
|
||||||
@@ -50,7 +49,6 @@ public sealed partial class App : Application
|
|||||||
{
|
{
|
||||||
// Load app resource
|
// Load app resource
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
activation = serviceProvider.GetRequiredService<IActivation>();
|
activation = serviceProvider.GetRequiredService<IActivation>();
|
||||||
logger = serviceProvider.GetRequiredService<ILogger<App>>();
|
logger = serviceProvider.GetRequiredService<ILogger<App>>();
|
||||||
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
|
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
|
||||||
@@ -64,25 +62,21 @@ public sealed partial class App : Application
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||||
AppInstance firstInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey);
|
|
||||||
|
|
||||||
if (firstInstance.IsCurrent)
|
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
|
||||||
{
|
{
|
||||||
logger.LogInformation(ConsoleBanner);
|
Exit();
|
||||||
LogDiagnosticInformation();
|
return;
|
||||||
|
|
||||||
// manually invoke
|
|
||||||
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
|
|
||||||
activation.InitializeWith(firstInstance);
|
|
||||||
|
|
||||||
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Redirect the activation (and args) to the "main" instance, and exit.
|
|
||||||
firstInstance.RedirectActivationTo(activatedEventArgs);
|
|
||||||
Process.GetCurrentProcess().Kill();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogInformation(ConsoleBanner);
|
||||||
|
LogDiagnosticInformation();
|
||||||
|
|
||||||
|
// manually invoke
|
||||||
|
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
|
||||||
|
activation.Initialize();
|
||||||
|
|
||||||
|
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ internal static class ControlAnimationConstants
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 图像淡入
|
/// 图像淡入
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly TimeSpan ImageFadeIn = TimeSpan.FromSeconds(0.3);
|
public static readonly TimeSpan ImageScaleFadeIn = TimeSpan.FromSeconds(0.3);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 图像淡出
|
/// 图像淡出
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly TimeSpan ImageFadeOut = TimeSpan.FromSeconds(0.2);
|
public static readonly TimeSpan ImageScaleFadeOut = TimeSpan.FromSeconds(0.2);
|
||||||
|
|
||||||
|
public static readonly TimeSpan ImageOpacityFadeInOut = TimeSpan.FromSeconds(1);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using CommunityToolkit.WinUI.Behaviors;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Behavior;
|
||||||
|
|
||||||
|
[DependencyProperty("Period", typeof(TimeSpan))]
|
||||||
|
[DependencyProperty("Command", typeof(ICommand))]
|
||||||
|
[DependencyProperty("CommandParameter", typeof(object))]
|
||||||
|
internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavior : BehaviorBase<FrameworkElement>, IDisposable
|
||||||
|
{
|
||||||
|
private TaskCompletionSource acutalThemeChangedTaskCompletionSource = new();
|
||||||
|
private CancellationTokenSource periodicTimerCancellationTokenSource = new();
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
periodicTimerCancellationTokenSource.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool Initialize()
|
||||||
|
{
|
||||||
|
AssociatedObject.ActualThemeChanged += OnActualThemeChanged;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnAssociatedObjectLoaded()
|
||||||
|
{
|
||||||
|
RunCoreAsync().SafeForget();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool Uninitialize()
|
||||||
|
{
|
||||||
|
AssociatedObject.ActualThemeChanged -= OnActualThemeChanged;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||||
|
{
|
||||||
|
acutalThemeChangedTaskCompletionSource.TrySetResult();
|
||||||
|
periodicTimerCancellationTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryExecuteCommand()
|
||||||
|
{
|
||||||
|
if (AssociatedObject is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Command is not null && Command.CanExecute(CommandParameter))
|
||||||
|
{
|
||||||
|
Command.Execute(CommandParameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask RunCoreAsync()
|
||||||
|
{
|
||||||
|
using (PeriodicTimer timer = new(Period))
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (!IsAttached)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ITaskContext taskContext = Ioc.Default.GetRequiredService<ITaskContext>();
|
||||||
|
await taskContext.SwitchToMainThreadAsync();
|
||||||
|
TryExecuteCommand();
|
||||||
|
|
||||||
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Task nextTickTask = timer.WaitForNextTickAsync(periodicTimerCancellationTokenSource.Token).AsTask();
|
||||||
|
await Task.WhenAny(nextTickTask, acutalThemeChangedTaskCompletionSource.Task).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
acutalThemeChangedTaskCompletionSource = new();
|
||||||
|
periodicTimerCancellationTokenSource = new();
|
||||||
|
}
|
||||||
|
while (true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Brush;
|
||||||
|
|
||||||
|
internal sealed class ColorSegmentCollection : List<IColorSegment>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -9,5 +9,5 @@ internal interface IColorSegment
|
|||||||
{
|
{
|
||||||
Color Color { get; }
|
Color Color { get; }
|
||||||
|
|
||||||
double Value { get; }
|
double Value { get; set; }
|
||||||
}
|
}
|
||||||
@@ -9,36 +9,48 @@ using System.Runtime.InteropServices;
|
|||||||
|
|
||||||
namespace Snap.Hutao.Control.Brush;
|
namespace Snap.Hutao.Control.Brush;
|
||||||
|
|
||||||
[DependencyProperty("Source", typeof(List<IColorSegment>), default!, nameof(OnSourceChanged))]
|
[DependencyProperty("Source", typeof(ColorSegmentCollection), default!, nameof(OnSourceChanged))]
|
||||||
internal sealed partial class SegmentedBar : ContentControl
|
internal sealed partial class SegmentedBar : ContentControl
|
||||||
{
|
{
|
||||||
private readonly LinearGradientBrush brush = new() { StartPoint = new(0, 0), EndPoint = new(1, 0), };
|
private readonly LinearGradientBrush brush = new() { StartPoint = new(0, 0), EndPoint = new(1, 0), };
|
||||||
|
|
||||||
public SegmentedBar()
|
public SegmentedBar()
|
||||||
{
|
{
|
||||||
|
HorizontalContentAlignment = HorizontalAlignment.Stretch;
|
||||||
|
VerticalContentAlignment = VerticalAlignment.Stretch;
|
||||||
|
|
||||||
Content = new Rectangle()
|
Content = new Rectangle()
|
||||||
{
|
{
|
||||||
Fill = brush,
|
Fill = brush,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
VerticalAlignment = VerticalAlignment.Stretch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
private static void OnSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||||
{
|
{
|
||||||
SegmentedBar segmentedBar = (SegmentedBar)obj;
|
UpdateLinearGradientBrush((SegmentedBar)obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateLinearGradientBrush(SegmentedBar segmentedBar)
|
||||||
|
{
|
||||||
GradientStopCollection collection = segmentedBar.brush.GradientStops;
|
GradientStopCollection collection = segmentedBar.brush.GradientStops;
|
||||||
collection.Clear();
|
collection.Clear();
|
||||||
|
|
||||||
if (args.NewValue as List<IColorSegment> is [_, ..] list)
|
ColorSegmentCollection segmentCollection = segmentedBar.Source;
|
||||||
|
|
||||||
|
double total = segmentCollection.Sum(seg => seg.Value);
|
||||||
|
if (total is 0D)
|
||||||
{
|
{
|
||||||
double total = list.Sum(seg => seg.Value);
|
return;
|
||||||
double offset = 0;
|
}
|
||||||
foreach (ref readonly IColorSegment segment in CollectionsMarshal.AsSpan(list))
|
|
||||||
{
|
double offset = 0;
|
||||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
foreach (ref readonly IColorSegment segment in CollectionsMarshal.AsSpan(segmentCollection))
|
||||||
offset += segment.Value / total;
|
{
|
||||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||||
}
|
offset += segment.Value / total;
|
||||||
|
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,763 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using CommunityToolkit.WinUI.Collections;
|
||||||
|
using CommunityToolkit.WinUI.Helpers;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Windows.Foundation;
|
||||||
|
using Windows.Foundation.Collections;
|
||||||
|
using NotifyCollectionChangedAction = System.Collections.Specialized.NotifyCollectionChangedAction;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
|
||||||
|
|
||||||
|
internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPropertyChanged, ISupportIncrementalLoading, IComparer<object>
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
private readonly List<T> view;
|
||||||
|
private readonly ObservableCollection<SortDescription> sortDescriptions;
|
||||||
|
private readonly Dictionary<string, PropertyInfo?> sortProperties;
|
||||||
|
private readonly bool liveShapingEnabled;
|
||||||
|
private readonly HashSet<string?> observedFilterProperties = [];
|
||||||
|
|
||||||
|
private IList<T> source;
|
||||||
|
private Predicate<T>? filter;
|
||||||
|
private int deferCounter;
|
||||||
|
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
|
||||||
|
|
||||||
|
public AdvancedCollectionView()
|
||||||
|
: this(new List<T>(0))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdvancedCollectionView(IList<T> source, bool isLiveShaping = false)
|
||||||
|
{
|
||||||
|
liveShapingEnabled = isLiveShaping;
|
||||||
|
view = [];
|
||||||
|
sortDescriptions = [];
|
||||||
|
sortDescriptions.CollectionChanged += SortDescriptionsCollectionChanged;
|
||||||
|
sortProperties = [];
|
||||||
|
Source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<object>? CurrentChanged;
|
||||||
|
|
||||||
|
public event CurrentChangingEventHandler? CurrentChanging;
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
public event VectorChangedEventHandler<object>? VectorChanged;
|
||||||
|
|
||||||
|
public IList<T> Source
|
||||||
|
{
|
||||||
|
get => source;
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(source))]
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(source, value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source is not null)
|
||||||
|
{
|
||||||
|
DetachPropertyChangedHandler(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
source = value;
|
||||||
|
AttachPropertyChangedHandler(source);
|
||||||
|
|
||||||
|
sourceWeakEventListener?.Detach();
|
||||||
|
|
||||||
|
if (source is INotifyCollectionChanged sourceNotifyCollectionChanged)
|
||||||
|
{
|
||||||
|
sourceWeakEventListener = new WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>(this)
|
||||||
|
{
|
||||||
|
// Call the actual collection changed event
|
||||||
|
OnEventAction = (source, changed, arg3) => SourceNotifyCollectionChangedCollectionChanged(source, arg3),
|
||||||
|
|
||||||
|
// The source doesn't exist anymore
|
||||||
|
OnDetachAction = (listener) =>
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(sourceWeakEventListener);
|
||||||
|
sourceNotifyCollectionChanged.CollectionChanged -= sourceWeakEventListener.OnEvent;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
sourceNotifyCollectionChanged.CollectionChanged += sourceWeakEventListener.OnEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleSourceChanged();
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get => view.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsReadOnly
|
||||||
|
{
|
||||||
|
get => source is null || source.IsReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IObservableVector<object> CollectionGroups
|
||||||
|
{
|
||||||
|
get => default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? CurrentItem
|
||||||
|
{
|
||||||
|
get => CurrentPosition > -1 && CurrentPosition < view.Count ? view[CurrentPosition] : default;
|
||||||
|
set => MoveCurrentTo(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CurrentPosition { get; private set; }
|
||||||
|
|
||||||
|
public bool HasMoreItems
|
||||||
|
{
|
||||||
|
get => source is ISupportIncrementalLoading { HasMoreItems: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCurrentAfterLast
|
||||||
|
{
|
||||||
|
get => CurrentPosition >= view.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCurrentBeforeFirst
|
||||||
|
{
|
||||||
|
get => CurrentPosition < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanFilter
|
||||||
|
{
|
||||||
|
get => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Predicate<T>? Filter
|
||||||
|
{
|
||||||
|
get => filter;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (filter == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filter = value;
|
||||||
|
HandleFilterChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanSort
|
||||||
|
{
|
||||||
|
get => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<SortDescription> SortDescriptions
|
||||||
|
{
|
||||||
|
get => sortDescriptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<T> SourceCollection
|
||||||
|
{
|
||||||
|
get => source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<T> View
|
||||||
|
{
|
||||||
|
get => view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T this[int index]
|
||||||
|
{
|
||||||
|
get => view[index];
|
||||||
|
set => view[index] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
HandleSourceChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshFilter()
|
||||||
|
{
|
||||||
|
HandleFilterChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshSorting()
|
||||||
|
{
|
||||||
|
HandleSortChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<T> GetEnumerator()
|
||||||
|
{
|
||||||
|
return view.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return view.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(T item)
|
||||||
|
{
|
||||||
|
source.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
source.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(T item)
|
||||||
|
{
|
||||||
|
return view.Contains(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyTo(T[] array, int arrayIndex)
|
||||||
|
{
|
||||||
|
view.CopyTo(array, arrayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(T item)
|
||||||
|
{
|
||||||
|
source.Remove(item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("", "SH007")]
|
||||||
|
public int IndexOf(T? item)
|
||||||
|
{
|
||||||
|
return view.IndexOf(item!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Insert(int index, T item)
|
||||||
|
{
|
||||||
|
source.Insert(index, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveAt(int index)
|
||||||
|
{
|
||||||
|
Remove(view[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveCurrentTo(T? item)
|
||||||
|
{
|
||||||
|
return (item is not null && item.Equals(CurrentItem)) || MoveCurrentToIndex(IndexOf(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveCurrentToPosition(int index)
|
||||||
|
{
|
||||||
|
return MoveCurrentToIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveCurrentToFirst()
|
||||||
|
{
|
||||||
|
return MoveCurrentToIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveCurrentToLast()
|
||||||
|
{
|
||||||
|
return MoveCurrentToIndex(view.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveCurrentToNext()
|
||||||
|
{
|
||||||
|
return MoveCurrentToIndex(CurrentPosition + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveCurrentToPrevious()
|
||||||
|
{
|
||||||
|
return MoveCurrentToIndex(CurrentPosition - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAsyncOperation<LoadMoreItemsResult>? LoadMoreItemsAsync(uint count)
|
||||||
|
{
|
||||||
|
return (source as ISupportIncrementalLoading)?.LoadMoreItemsAsync(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ObserveFilterProperty(string propertyName)
|
||||||
|
{
|
||||||
|
observedFilterProperties.Add(propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearObservedFilterProperties()
|
||||||
|
{
|
||||||
|
observedFilterProperties.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable DeferRefresh()
|
||||||
|
{
|
||||||
|
return new NotificationDeferrer(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
int IComparer<object>.Compare(object? x, object? y)
|
||||||
|
{
|
||||||
|
if (sortProperties.Count <= 0)
|
||||||
|
{
|
||||||
|
Type listType = source.GetType();
|
||||||
|
Type? type;
|
||||||
|
|
||||||
|
if (listType.IsGenericType)
|
||||||
|
{
|
||||||
|
type = listType.GetGenericArguments()[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
type = x?.GetType();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (SortDescription sd in sortDescriptions)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(sd.PropertyName))
|
||||||
|
{
|
||||||
|
sortProperties[sd.PropertyName] = type?.GetProperty(sd.PropertyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (SortDescription sd in sortDescriptions)
|
||||||
|
{
|
||||||
|
object? cx, cy;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(sd.PropertyName))
|
||||||
|
{
|
||||||
|
cx = x;
|
||||||
|
cy = y;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PropertyInfo? pi = sortProperties[sd.PropertyName];
|
||||||
|
|
||||||
|
cx = pi?.GetValue(x);
|
||||||
|
cy = pi?.GetValue(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cmp = sd.Comparer.Compare(cx, cy);
|
||||||
|
|
||||||
|
if (cmp is not 0)
|
||||||
|
{
|
||||||
|
return sd.Direction is SortDirection.Ascending ? +cmp : -cmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void OnPropertyChanged([CallerMemberName] string propertyName = default!)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ItemOnPropertyChanged(object? item, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!liveShapingEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(item);
|
||||||
|
T typedItem = (T)item;
|
||||||
|
|
||||||
|
bool? filterResult = filter?.Invoke(typedItem);
|
||||||
|
|
||||||
|
if (filterResult.HasValue && observedFilterProperties.Contains(e.PropertyName))
|
||||||
|
{
|
||||||
|
int viewIndex = view.IndexOf(typedItem);
|
||||||
|
if (viewIndex != -1 && !filterResult.Value)
|
||||||
|
{
|
||||||
|
RemoveFromView(viewIndex, typedItem);
|
||||||
|
}
|
||||||
|
else if (viewIndex == -1 && filterResult.Value)
|
||||||
|
{
|
||||||
|
int index = source.IndexOf(typedItem);
|
||||||
|
HandleItemAdded(index, typedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((filterResult ?? true) && SortDescriptions.Any(sd => sd.PropertyName == e.PropertyName))
|
||||||
|
{
|
||||||
|
int oldIndex = view.IndexOf(typedItem);
|
||||||
|
|
||||||
|
// Check if item is in view:
|
||||||
|
if (oldIndex < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.RemoveAt(oldIndex);
|
||||||
|
int targetIndex = view.BinarySearch(typedItem, this);
|
||||||
|
if (targetIndex < 0)
|
||||||
|
{
|
||||||
|
targetIndex = ~targetIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only trigger expensive UI updates if the index really changed:
|
||||||
|
if (targetIndex != oldIndex)
|
||||||
|
{
|
||||||
|
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, oldIndex, typedItem));
|
||||||
|
|
||||||
|
view.Insert(targetIndex, typedItem);
|
||||||
|
|
||||||
|
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, targetIndex, typedItem));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
view.Insert(targetIndex, typedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrEmpty(e.PropertyName))
|
||||||
|
{
|
||||||
|
HandleSourceChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AttachPropertyChangedHandler(IEnumerable items)
|
||||||
|
{
|
||||||
|
if (!liveShapingEnabled || items is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (object item in items)
|
||||||
|
{
|
||||||
|
if (item is INotifyPropertyChanged notifyPropertyChanged)
|
||||||
|
{
|
||||||
|
notifyPropertyChanged.PropertyChanged += ItemOnPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DetachPropertyChangedHandler(IEnumerable items)
|
||||||
|
{
|
||||||
|
if (!liveShapingEnabled || items is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (object item in items)
|
||||||
|
{
|
||||||
|
if (item is INotifyPropertyChanged notifyPropertyChanged)
|
||||||
|
{
|
||||||
|
notifyPropertyChanged.PropertyChanged -= ItemOnPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleSortChanged()
|
||||||
|
{
|
||||||
|
sortProperties.Clear();
|
||||||
|
view.Sort(this);
|
||||||
|
sortProperties.Clear();
|
||||||
|
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleFilterChanged()
|
||||||
|
{
|
||||||
|
if (filter is not null)
|
||||||
|
{
|
||||||
|
for (int index = 0; index < view.Count; index++)
|
||||||
|
{
|
||||||
|
T item = view[index];
|
||||||
|
if (filter(item))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveFromView(index, item);
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<T> viewHash = new(view);
|
||||||
|
int viewIndex = 0;
|
||||||
|
for (int index = 0; index < source.Count; index++)
|
||||||
|
{
|
||||||
|
T item = source[index];
|
||||||
|
if (viewHash.Contains(item))
|
||||||
|
{
|
||||||
|
viewIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HandleItemAdded(index, item, viewIndex))
|
||||||
|
{
|
||||||
|
viewIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleSourceChanged()
|
||||||
|
{
|
||||||
|
sortProperties.Clear();
|
||||||
|
T? currentItem = CurrentItem;
|
||||||
|
view.Clear();
|
||||||
|
foreach (T item in Source)
|
||||||
|
{
|
||||||
|
if (filter is not null && !filter(item))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDescriptions.Count > 0)
|
||||||
|
{
|
||||||
|
int targetIndex = view.BinarySearch(item, this);
|
||||||
|
if (targetIndex < 0)
|
||||||
|
{
|
||||||
|
targetIndex = ~targetIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Insert(targetIndex, item);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
view.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortProperties.Clear();
|
||||||
|
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
|
||||||
|
MoveCurrentTo(currentItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SourceNotifyCollectionChangedCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
ArgumentNullException.ThrowIfNull(e.NewItems);
|
||||||
|
AttachPropertyChangedHandler(e.NewItems);
|
||||||
|
if (deferCounter <= 0)
|
||||||
|
{
|
||||||
|
if (e.NewItems?.Count == 1)
|
||||||
|
{
|
||||||
|
object? newItem = e.NewItems[0];
|
||||||
|
ArgumentNullException.ThrowIfNull(newItem);
|
||||||
|
HandleItemAdded(e.NewStartingIndex, (T)newItem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HandleSourceChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
ArgumentNullException.ThrowIfNull(e.OldItems);
|
||||||
|
DetachPropertyChangedHandler(e.OldItems);
|
||||||
|
if (deferCounter <= 0)
|
||||||
|
{
|
||||||
|
if (e.OldItems?.Count == 1)
|
||||||
|
{
|
||||||
|
object? oldItem = e.OldItems[0];
|
||||||
|
ArgumentNullException.ThrowIfNull(oldItem);
|
||||||
|
HandleItemRemoved(e.OldStartingIndex, (T)oldItem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HandleSourceChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Move:
|
||||||
|
case NotifyCollectionChangedAction.Replace:
|
||||||
|
case NotifyCollectionChangedAction.Reset:
|
||||||
|
if (deferCounter <= 0)
|
||||||
|
{
|
||||||
|
HandleSourceChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HandleItemAdded(int newStartingIndex, T newItem, int? viewIndex = null)
|
||||||
|
{
|
||||||
|
if (filter is not null && !filter(newItem))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int newViewIndex = view.Count;
|
||||||
|
|
||||||
|
if (sortDescriptions.Count > 0)
|
||||||
|
{
|
||||||
|
sortProperties.Clear();
|
||||||
|
newViewIndex = view.BinarySearch(newItem, this);
|
||||||
|
if (newViewIndex < 0)
|
||||||
|
{
|
||||||
|
newViewIndex = ~newViewIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (filter is not null)
|
||||||
|
{
|
||||||
|
if (source is null)
|
||||||
|
{
|
||||||
|
HandleSourceChanged();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStartingIndex == 0 || view.Count == 0)
|
||||||
|
{
|
||||||
|
newViewIndex = 0;
|
||||||
|
}
|
||||||
|
else if (newStartingIndex == source.Count - 1)
|
||||||
|
{
|
||||||
|
newViewIndex = view.Count;
|
||||||
|
}
|
||||||
|
else if (viewIndex.HasValue)
|
||||||
|
{
|
||||||
|
newViewIndex = viewIndex.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int i = 0, j = 0; i < source.Count; i++)
|
||||||
|
{
|
||||||
|
if (i == newStartingIndex)
|
||||||
|
{
|
||||||
|
newViewIndex = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Equals(view[j], source[i]))
|
||||||
|
{
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Insert(newViewIndex, newItem);
|
||||||
|
if (newViewIndex <= CurrentPosition)
|
||||||
|
{
|
||||||
|
CurrentPosition++;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, newViewIndex, newItem));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleItemRemoved(int oldStartingIndex, T oldItem)
|
||||||
|
{
|
||||||
|
if (filter is not null && !filter(oldItem))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldStartingIndex < 0 || oldStartingIndex >= view.Count || !Equals(view[oldStartingIndex], oldItem))
|
||||||
|
{
|
||||||
|
oldStartingIndex = view.IndexOf(oldItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldStartingIndex < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveFromView(oldStartingIndex, oldItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveFromView(int itemIndex, T item)
|
||||||
|
{
|
||||||
|
view.RemoveAt(itemIndex);
|
||||||
|
if (itemIndex <= CurrentPosition)
|
||||||
|
{
|
||||||
|
CurrentPosition--;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, itemIndex, item));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SortDescriptionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (deferCounter > 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleSortChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool MoveCurrentToIndex(int i)
|
||||||
|
{
|
||||||
|
if (i < -1 || i >= view.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == CurrentPosition)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentChangingEventArgs e = new();
|
||||||
|
OnCurrentChanging(e);
|
||||||
|
if (e.Cancel)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentPosition = i;
|
||||||
|
OnCurrentChanged(default!);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCurrentChanging(CurrentChangingEventArgs e)
|
||||||
|
{
|
||||||
|
if (deferCounter > 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentChanging?.Invoke(this, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCurrentChanged(object e)
|
||||||
|
{
|
||||||
|
if (deferCounter > 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentChanged?.Invoke(this, e);
|
||||||
|
OnPropertyChanged(nameof(CurrentItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVectorChanged(IVectorChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (deferCounter > 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VectorChanged?.Invoke(this, e);
|
||||||
|
OnPropertyChanged(nameof(Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class NotificationDeferrer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly AdvancedCollectionView<T> advancedCollectionView;
|
||||||
|
private readonly T? currentItem;
|
||||||
|
|
||||||
|
public NotificationDeferrer(AdvancedCollectionView<T> acvs)
|
||||||
|
{
|
||||||
|
advancedCollectionView = acvs;
|
||||||
|
currentItem = advancedCollectionView.CurrentItem;
|
||||||
|
advancedCollectionView.deferCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
advancedCollectionView.MoveCurrentTo(currentItem);
|
||||||
|
advancedCollectionView.deferCounter--;
|
||||||
|
advancedCollectionView.Refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using CommunityToolkit.WinUI.Collections;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
|
||||||
|
|
||||||
|
internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
bool CanFilter { get; }
|
||||||
|
|
||||||
|
bool CanSort { get; }
|
||||||
|
|
||||||
|
object? ICollectionView.CurrentItem
|
||||||
|
{
|
||||||
|
get => CurrentItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
new T? CurrentItem { get; }
|
||||||
|
|
||||||
|
Predicate<T>? Filter { get; set; }
|
||||||
|
|
||||||
|
IList<SortDescription> SortDescriptions { get; }
|
||||||
|
|
||||||
|
IEnumerable<T> SourceCollection { get; }
|
||||||
|
|
||||||
|
object IList<object>.this[int index]
|
||||||
|
{
|
||||||
|
get => this[index];
|
||||||
|
set => this[index] = (T)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
new T this[int index] { get; set; }
|
||||||
|
|
||||||
|
void ICollection<object>.Add(object item)
|
||||||
|
{
|
||||||
|
Add((T)item);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Add(T item);
|
||||||
|
|
||||||
|
void ClearObservedFilterProperties();
|
||||||
|
|
||||||
|
bool ICollection<object>.Contains(object item)
|
||||||
|
{
|
||||||
|
return Contains((T)item);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Contains(T item);
|
||||||
|
|
||||||
|
void ICollection<object>.CopyTo(object[] array, int arrayIndex)
|
||||||
|
{
|
||||||
|
CopyTo((T[])array, arrayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CopyTo(T[] array, int arrayIndex);
|
||||||
|
|
||||||
|
IDisposable DeferRefresh();
|
||||||
|
|
||||||
|
IEnumerator<object> IEnumerable<object>.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
new IEnumerator<T> GetEnumerator();
|
||||||
|
|
||||||
|
int IList<object>.IndexOf(object item)
|
||||||
|
{
|
||||||
|
return IndexOf((T)item);
|
||||||
|
}
|
||||||
|
|
||||||
|
int IndexOf(T item);
|
||||||
|
|
||||||
|
void IList<object>.Insert(int index, object item)
|
||||||
|
{
|
||||||
|
Insert(index, (T)item);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Insert(int index, T item);
|
||||||
|
|
||||||
|
bool ICollectionView.MoveCurrentTo(object item)
|
||||||
|
{
|
||||||
|
return MoveCurrentTo((T)item);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MoveCurrentTo(T item);
|
||||||
|
|
||||||
|
void ObserveFilterProperty(string propertyName);
|
||||||
|
|
||||||
|
void Refresh();
|
||||||
|
|
||||||
|
void RefreshFilter();
|
||||||
|
|
||||||
|
void RefreshSorting();
|
||||||
|
|
||||||
|
bool ICollection<object>.Remove(object item)
|
||||||
|
{
|
||||||
|
return Remove((T)item);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Remove(T item);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Windows.Foundation.Collections;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
|
||||||
|
|
||||||
|
internal sealed class VectorChangedEventArgs : IVectorChangedEventArgs
|
||||||
|
{
|
||||||
|
public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = null!)
|
||||||
|
{
|
||||||
|
CollectionChange = cc;
|
||||||
|
Index = (uint)index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CollectionChange CollectionChange { get; }
|
||||||
|
|
||||||
|
public uint Index { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using CommunityToolkit.WinUI;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Helper;
|
||||||
|
|
||||||
|
[SuppressMessage("", "SH001")]
|
||||||
|
[DependencyProperty("PaneCornerRadius", typeof(CornerRadius), default, nameof(OnPaneCornerRadiusChanged), IsAttached = true, AttachedType = typeof(NavigationView))]
|
||||||
|
public sealed partial class NavigationViewHelper
|
||||||
|
{
|
||||||
|
private static void OnPaneCornerRadiusChanged(DependencyObject dp, DependencyPropertyChangedEventArgs args)
|
||||||
|
{
|
||||||
|
NavigationView navigationView = (NavigationView)dp;
|
||||||
|
CornerRadius newValue = (CornerRadius)args.NewValue;
|
||||||
|
|
||||||
|
if (navigationView.IsLoaded)
|
||||||
|
{
|
||||||
|
SetNavigationViewPaneCornerRadius(navigationView, newValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationView.Loaded += (s, e) =>
|
||||||
|
{
|
||||||
|
NavigationView loadedNavigationView = (NavigationView)s;
|
||||||
|
SetNavigationViewPaneCornerRadius(loadedNavigationView, newValue);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetNavigationViewPaneCornerRadius(NavigationView navigationView, CornerRadius value)
|
||||||
|
{
|
||||||
|
if (navigationView.FindDescendant("RootSplitView") is SplitView splitView)
|
||||||
|
{
|
||||||
|
splitView.CornerRadius = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ using Microsoft.UI.Xaml.Controls;
|
|||||||
namespace Snap.Hutao.Control.Helper;
|
namespace Snap.Hutao.Control.Helper;
|
||||||
|
|
||||||
[SuppressMessage("", "SH001")]
|
[SuppressMessage("", "SH001")]
|
||||||
[DependencyProperty("LeftPanelMaxWidth", typeof(double), IsAttached = true, AttachedType = typeof(ScrollViewer))]
|
|
||||||
[DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))]
|
[DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))]
|
||||||
public sealed partial class ScrollViewerHelper
|
public sealed partial class ScrollViewerHelper
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ public sealed partial class SettingsExpanderHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control;
|
||||||
|
|
||||||
|
internal interface IXamlElementAccessor;
|
||||||
@@ -192,7 +192,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
|||||||
{
|
{
|
||||||
await AnimationBuilder
|
await AnimationBuilder
|
||||||
.Create()
|
.Create()
|
||||||
.Opacity(from: 0D, to: 1D, duration: ControlAnimationConstants.ImageFadeIn)
|
.Opacity(from: 0D, to: 1D, duration: ControlAnimationConstants.ImageScaleFadeIn)
|
||||||
.StartAsync(this, token)
|
.StartAsync(this, token)
|
||||||
.ConfigureAwait(true);
|
.ConfigureAwait(true);
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
|||||||
{
|
{
|
||||||
await AnimationBuilder
|
await AnimationBuilder
|
||||||
.Create()
|
.Create()
|
||||||
.Opacity(from: 1D, to: 0D, duration: ControlAnimationConstants.ImageFadeOut)
|
.Opacity(from: 1D, to: 0D, duration: ControlAnimationConstants.ImageScaleFadeOut)
|
||||||
.StartAsync(this, token)
|
.StartAsync(this, token)
|
||||||
.ConfigureAwait(true);
|
.ConfigureAwait(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ internal struct Bgra32
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public byte A;
|
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>
|
/// <summary>
|
||||||
/// 从 Color 转换
|
/// 从 Color 转换
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -44,4 +54,11 @@ internal struct Bgra32
|
|||||||
*(uint*)&bgra8 = BinaryPrimitives.ReverseEndianness(*(uint*)&color);
|
*(uint*)&bgra8 = BinaryPrimitives.ReverseEndianness(*(uint*)&color);
|
||||||
return bgra8;
|
return bgra8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static unsafe implicit operator Color(Bgra32 bgra8)
|
||||||
|
{
|
||||||
|
Unsafe.SkipInit(out Color color);
|
||||||
|
*(uint*)&color = BinaryPrimitives.ReverseEndianness(*(uint*)&bgra8);
|
||||||
|
return color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Control.Media;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
|
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal struct Hsl32
|
internal struct Hsla32
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Hue in 0..360 range.
|
/// The Hue in 0..360 range.
|
||||||
@@ -46,13 +46,14 @@ internal struct Rgba32
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 使用 RGBA 代码初始化新的结构
|
/// 使用 RGBA 代码初始化新的结构
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code">RGBA 代码</param>
|
/// <param name="xrgbaCode">RGBA 代码</param>
|
||||||
public unsafe Rgba32(uint code)
|
public unsafe Rgba32(uint xrgbaCode)
|
||||||
{
|
{
|
||||||
// RRGGBBAA -> AABBGGRR
|
// uint layout: 0xRRGGBBAA is AABBGGRR
|
||||||
|
// AABBGGRR -> RRGGBBAA
|
||||||
fixed (Rgba32* pSelf = &this)
|
fixed (Rgba32* pSelf = &this)
|
||||||
{
|
{
|
||||||
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(code);
|
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(xrgbaCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,14 +67,14 @@ internal struct Rgba32
|
|||||||
|
|
||||||
public static unsafe implicit operator Color(Rgba32 hexColor)
|
public static unsafe implicit operator Color(Rgba32 hexColor)
|
||||||
{
|
{
|
||||||
// AABBGGRR -> BBGGRRAA
|
// Goal : Rgba32:RRGGBBAA(0xAABBGGRR) -> Color: AARRGGBB(0xBBGGRRAA)
|
||||||
// AABBGGRR -> 000000AA
|
// Step1: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:AA000000(0x000000AA)
|
||||||
uint a = (*(uint*)&hexColor) >> 24;
|
uint a = ((*(uint*)&hexColor) >> 24) & 0x000000FF;
|
||||||
|
|
||||||
// AABBGGRR -> BBGGRR00
|
// Step2: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:00RRGGBB(0xRRGGBB00)
|
||||||
uint rgb = (*(uint*)&hexColor) << 8;
|
uint rgb = ((*(uint*)&hexColor) << 8) & 0xFFFFFF00;
|
||||||
|
|
||||||
// BBGGRR00 + 000000AA
|
// Step2: UInt32:00RRGGBB(0xRRGGBB00) + UInt32:AA000000(0x000000AA) -> UInt32:AARRGGBB(0xRRGGBBAA)
|
||||||
uint rgba = rgb + a;
|
uint rgba = rgb + a;
|
||||||
|
|
||||||
return *(Color*)&rgba;
|
return *(Color*)&rgba;
|
||||||
@@ -84,7 +85,7 @@ internal struct Rgba32
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hsl">HSL 颜色</param>
|
/// <param name="hsl">HSL 颜色</param>
|
||||||
/// <returns>RGBA8颜色</returns>
|
/// <returns>RGBA8颜色</returns>
|
||||||
public static Rgba32 FromHsl(Hsl32 hsl)
|
public static Rgba32 FromHsl(Hsla32 hsl)
|
||||||
{
|
{
|
||||||
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
|
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
|
||||||
double h1 = hsl.H / 60;
|
double h1 = hsl.H / 60;
|
||||||
@@ -141,7 +142,7 @@ internal struct Rgba32
|
|||||||
/// 转换到 HSL 颜色
|
/// 转换到 HSL 颜色
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>HSL 颜色</returns>
|
/// <returns>HSL 颜色</returns>
|
||||||
public readonly Hsl32 ToHsl()
|
public readonly Hsla32 ToHsl()
|
||||||
{
|
{
|
||||||
const double toDouble = 1.0 / 255;
|
const double toDouble = 1.0 / 255;
|
||||||
double r = toDouble * R;
|
double r = toDouble * R;
|
||||||
@@ -174,7 +175,7 @@ internal struct Rgba32
|
|||||||
double lightness = 0.5 * (max + min);
|
double lightness = 0.5 * (max + min);
|
||||||
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
|
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
|
||||||
|
|
||||||
Hsl32 ret;
|
Hsla32 ret;
|
||||||
ret.H = 60 * h1;
|
ret.H = 60 * h1;
|
||||||
ret.S = saturation;
|
ret.S = saturation;
|
||||||
ret.L = lightness;
|
ret.L = lightness;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Win32.System.WinRT;
|
||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
using Windows.Graphics.Imaging;
|
using Windows.Graphics.Imaging;
|
||||||
using Windows.Win32;
|
|
||||||
using Windows.Win32.System.WinRT;
|
|
||||||
using WinRT;
|
using WinRT;
|
||||||
|
|
||||||
namespace Snap.Hutao.Control.Media;
|
namespace Snap.Hutao.Control.Media;
|
||||||
@@ -26,8 +25,7 @@ internal static class SoftwareBitmapExtension
|
|||||||
{
|
{
|
||||||
using (IMemoryBufferReference reference = buffer.CreateReference())
|
using (IMemoryBufferReference reference = buffer.CreateReference())
|
||||||
{
|
{
|
||||||
reference.As<IMemoryBufferByteAccess>().GetBuffer(out byte* data, out uint length);
|
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
|
||||||
Span<Bgra32> bytes = new(data, unchecked((int)length / sizeof(Bgra32)));
|
|
||||||
foreach (ref Bgra32 pixel in bytes)
|
foreach (ref Bgra32 pixel in bytes)
|
||||||
{
|
{
|
||||||
byte baseAlpha = pixel.A;
|
byte baseAlpha = pixel.A;
|
||||||
@@ -40,4 +38,43 @@ internal static class SoftwareBitmapExtension
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static unsafe double Luminance(this SoftwareBitmap softwareBitmap)
|
||||||
|
{
|
||||||
|
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
|
||||||
|
{
|
||||||
|
using (IMemoryBufferReference reference = buffer.CreateReference())
|
||||||
|
{
|
||||||
|
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
|
||||||
|
double sum = 0;
|
||||||
|
foreach (ref readonly Bgra32 pixel in bytes)
|
||||||
|
{
|
||||||
|
sum += pixel.Luminance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum / bytes.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe Bgra32 GetAccentColor(this SoftwareBitmap softwareBitmap)
|
||||||
|
{
|
||||||
|
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
|
||||||
|
{
|
||||||
|
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)(b / bytes.Length), (byte)(g / bytes.Length), (byte)(r / bytes.Length), (byte)(a / bytes.Length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
21
src/Snap.Hutao/Snap.Hutao/Control/Panel/UniformPanel.cs
Normal file
21
src/Snap.Hutao/Snap.Hutao/Control/Panel/UniformPanel.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using CommunityToolkit.WinUI.Controls;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Panel;
|
||||||
|
|
||||||
|
[DependencyProperty("MinItemWidth", typeof(double))]
|
||||||
|
internal sealed partial class UniformPanel : UniformGrid
|
||||||
|
{
|
||||||
|
public UniformPanel()
|
||||||
|
{
|
||||||
|
Columns = 1;
|
||||||
|
SizeChanged += OnSizeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Columns = (int)((e.NewSize.Width + ColumnSpacing) / (MinItemWidth + ColumnSpacing));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,9 @@ using Microsoft.UI.Xaml.Documents;
|
|||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
using Snap.Hutao.Control.Extension;
|
using Snap.Hutao.Control.Extension;
|
||||||
using Snap.Hutao.Control.Media;
|
using Snap.Hutao.Control.Media;
|
||||||
|
using Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||||
using Snap.Hutao.Control.Theme;
|
using Snap.Hutao.Control.Theme;
|
||||||
|
using Snap.Hutao.Metadata;
|
||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
|
|
||||||
@@ -15,20 +17,12 @@ namespace Snap.Hutao.Control.Text;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 专用于呈现描述文本的文本块
|
/// 专用于呈现描述文本的文本块
|
||||||
/// Some part of this file came from:
|
|
||||||
/// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HighQuality]
|
[HighQuality]
|
||||||
[DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))]
|
[DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))]
|
||||||
[DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))]
|
[DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))]
|
||||||
internal sealed partial class DescriptionTextBlock : ContentControl
|
internal sealed partial class DescriptionTextBlock : ContentControl
|
||||||
{
|
{
|
||||||
private static readonly int ColorTagFullLength = "<color=#FFFFFFFF></color>".Length;
|
|
||||||
private static readonly int ColorTagLeftLength = "<color=#FFFFFFFF>".Length;
|
|
||||||
|
|
||||||
private static readonly int ItalicTagFullLength = "<i></i>".Length;
|
|
||||||
private static readonly int ItalicTagLeftLength = "<i>".Length;
|
|
||||||
|
|
||||||
private readonly TypedEventHandler<FrameworkElement, object> actualThemeChangedEventHandler;
|
private readonly TypedEventHandler<FrameworkElement, object> actualThemeChangedEventHandler;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -51,9 +45,15 @@ internal sealed partial class DescriptionTextBlock : ContentControl
|
|||||||
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
|
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
|
||||||
ReadOnlySpan<char> description = (string)e.NewValue;
|
|
||||||
|
|
||||||
UpdateDescription(textBlock, description);
|
try
|
||||||
|
{
|
||||||
|
UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_ = ex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnTextStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
private static void OnTextStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
@@ -62,101 +62,106 @@ internal sealed partial class DescriptionTextBlock : ContentControl
|
|||||||
textBlock.Style = (Style)e.NewValue;
|
textBlock.Style = (Style)e.NewValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateDescription(TextBlock textBlock, in ReadOnlySpan<char> description)
|
private static void UpdateDescription(TextBlock textBlock, MiHoYoSyntaxTree syntaxTree)
|
||||||
{
|
{
|
||||||
textBlock.Inlines.Clear();
|
textBlock.Inlines.Clear();
|
||||||
|
AppendNode(textBlock, textBlock.Inlines, syntaxTree.Root);
|
||||||
|
}
|
||||||
|
|
||||||
int last = 0;
|
private static void AppendNode(TextBlock textBlock, InlineCollection inlines, MiHoYoSyntaxNode node)
|
||||||
for (int i = 0; i < description.Length;)
|
{
|
||||||
|
switch (node.Kind)
|
||||||
{
|
{
|
||||||
// newline
|
case MiHoYoSyntaxKind.Root:
|
||||||
if (description[i..].StartsWith(@"\n"))
|
foreach (MiHoYoSyntaxNode child in ((MiHoYoRootSyntax)node).Children)
|
||||||
{
|
{
|
||||||
AppendText(textBlock, description[last..i]);
|
AppendNode(textBlock, inlines, child);
|
||||||
AppendLineBreak(textBlock);
|
}
|
||||||
i += 2;
|
|
||||||
last = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// color tag
|
break;
|
||||||
else if (description[i..].StartsWith("<c"))
|
case MiHoYoSyntaxKind.PlainText:
|
||||||
{
|
AppendPlainText(textBlock, inlines, (MiHoYoPlainTextSyntax)node);
|
||||||
AppendText(textBlock, description[last..i]);
|
break;
|
||||||
Rgba32 color = new(description.Slice(i + 8, 8).ToString());
|
case MiHoYoSyntaxKind.ColorText:
|
||||||
int length = description[(i + ColorTagLeftLength)..].IndexOf('<');
|
AppendColorText(textBlock, inlines, (MiHoYoColorTextSyntax)node);
|
||||||
AppendColorText(textBlock, description.Slice(i + ColorTagLeftLength, length), color);
|
break;
|
||||||
|
case MiHoYoSyntaxKind.ItalicText:
|
||||||
i += length + ColorTagFullLength;
|
AppendItalicText(textBlock, inlines, (MiHoYoItalicTextSyntax)node);
|
||||||
last = i;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
// italic
|
|
||||||
else if (description[i..].StartsWith("<i"))
|
|
||||||
{
|
|
||||||
AppendText(textBlock, description[last..i]);
|
|
||||||
|
|
||||||
int length = description[(i + ItalicTagLeftLength)..].IndexOf('<');
|
|
||||||
AppendItalicText(textBlock, description.Slice(i + ItalicTagLeftLength, length));
|
|
||||||
|
|
||||||
i += length + ItalicTagFullLength;
|
|
||||||
last = i;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i == description.Length - 1)
|
|
||||||
{
|
|
||||||
AppendText(textBlock, description[last..(i + 1)]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AppendText(TextBlock text, in ReadOnlySpan<char> slice)
|
private static void AppendPlainText(TextBlock textBlock, InlineCollection inlines, MiHoYoPlainTextSyntax plainText)
|
||||||
{
|
{
|
||||||
text.Inlines.Add(new Run { Text = slice.ToString() });
|
// PlainText doesn't have children
|
||||||
|
inlines.Add(new Run { Text = plainText.Span.ToString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AppendColorText(TextBlock text, in ReadOnlySpan<char> slice, Rgba32 color)
|
private static void AppendColorText(TextBlock textBlock, InlineCollection inlines, MiHoYoColorTextSyntax colorText)
|
||||||
{
|
{
|
||||||
|
Rgba32 color = new(colorText.ColorSpan.ToString());
|
||||||
Color targetColor;
|
Color targetColor;
|
||||||
if (ThemeHelper.IsDarkMode(text.ActualTheme))
|
if (ThemeHelper.IsDarkMode(textBlock.ActualTheme))
|
||||||
{
|
{
|
||||||
targetColor = color;
|
targetColor = color;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Make lighter in light mode
|
// Make lighter in light mode
|
||||||
Hsl32 hsl = color.ToHsl();
|
Hsla32 hsl = color.ToHsl();
|
||||||
hsl.L *= 0.3;
|
hsl.L *= 0.3;
|
||||||
targetColor = Rgba32.FromHsl(hsl);
|
targetColor = Rgba32.FromHsl(hsl);
|
||||||
}
|
}
|
||||||
|
|
||||||
text.Inlines.Add(new Run
|
if (colorText.Children.Count > 1)
|
||||||
{
|
{
|
||||||
Text = slice.ToString(),
|
Span span = new()
|
||||||
Foreground = new SolidColorBrush(targetColor),
|
{
|
||||||
});
|
Foreground = new SolidColorBrush(targetColor),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (MiHoYoSyntaxNode child in colorText.Children)
|
||||||
|
{
|
||||||
|
AppendNode(textBlock, span.Inlines, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inlines.Add(new Run
|
||||||
|
{
|
||||||
|
Text = colorText.ContentSpan.ToString(),
|
||||||
|
Foreground = new SolidColorBrush(targetColor),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AppendItalicText(TextBlock text, in ReadOnlySpan<char> slice)
|
private static void AppendItalicText(TextBlock textBlock, InlineCollection inlines, MiHoYoItalicTextSyntax italicText)
|
||||||
{
|
{
|
||||||
text.Inlines.Add(new Run
|
if (italicText.Children.Count > 1)
|
||||||
{
|
{
|
||||||
Text = slice.ToString(),
|
Span span = new()
|
||||||
FontStyle = Windows.UI.Text.FontStyle.Italic,
|
{
|
||||||
});
|
FontStyle = Windows.UI.Text.FontStyle.Italic,
|
||||||
}
|
};
|
||||||
|
|
||||||
private static void AppendLineBreak(TextBlock text)
|
foreach (MiHoYoSyntaxNode child in italicText.Children)
|
||||||
{
|
{
|
||||||
text.Inlines.Add(new LineBreak());
|
AppendNode(textBlock, span.Inlines, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inlines.Add(new Run
|
||||||
|
{
|
||||||
|
Text = italicText.ContentSpan.ToString(),
|
||||||
|
FontStyle = Windows.UI.Text.FontStyle.Italic,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||||
{
|
{
|
||||||
// Simply re-apply texts
|
// Simply re-apply texts
|
||||||
UpdateDescription((TextBlock)Content, Description);
|
UpdateDescription((TextBlock)Content, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle(Description)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ internal sealed partial class HtmlDescriptionTextBlock : ContentControl
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Make lighter in light mode
|
// Make lighter in light mode
|
||||||
Hsl32 hsl = color.ToHsl();
|
Hsla32 hsl = color.ToHsl();
|
||||||
hsl.L *= 0.3;
|
hsl.L *= 0.3;
|
||||||
targetColor = Rgba32.FromHsl(hsl);
|
targetColor = Rgba32.FromHsl(hsl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||||
|
|
||||||
|
internal enum MiHoYoColorKind
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Rgba,
|
||||||
|
Rgb,
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||||
|
|
||||||
|
internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
|
||||||
|
{
|
||||||
|
public MiHoYoColorTextSyntax(MiHoYoColorKind colorKind, string text, int start, int end)
|
||||||
|
: base(MiHoYoSyntaxKind.ColorText, text, start, end)
|
||||||
|
{
|
||||||
|
ColorKind = colorKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MiHoYoColorTextSyntax(MiHoYoColorKind colorKind, string text, in TextPosition position)
|
||||||
|
: base(MiHoYoSyntaxKind.ColorText, text, position)
|
||||||
|
{
|
||||||
|
ColorKind = colorKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MiHoYoColorKind ColorKind { get; }
|
||||||
|
|
||||||
|
public override TextPosition ContentPosition
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ColorKind switch
|
||||||
|
{
|
||||||
|
MiHoYoColorKind.Rgba => new(Position.Start + 17, Position.End - 8),
|
||||||
|
MiHoYoColorKind.Rgb => new(Position.Start + 15, Position.End - 8),
|
||||||
|
_ => throw Must.NeverHappen(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextPosition ColorPosition
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ColorKind switch
|
||||||
|
{
|
||||||
|
MiHoYoColorKind.Rgba => new(Position.Start + 8, Position.Start + 16),
|
||||||
|
MiHoYoColorKind.Rgb => new(Position.Start + 8, Position.Start + 14),
|
||||||
|
_ => throw Must.NeverHappen(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> ColorSpan { get => Text.AsSpan()[ColorPosition.Start..ColorPosition.End]; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||||
|
|
||||||
|
internal sealed class MiHoYoItalicTextSyntax : MiHoYoXmlElementSyntax
|
||||||
|
{
|
||||||
|
public MiHoYoItalicTextSyntax(string text, int start, int end)
|
||||||
|
: base(MiHoYoSyntaxKind.ItalicText, text, start, end)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MiHoYoItalicTextSyntax(string text, in TextPosition position)
|
||||||
|
: base(MiHoYoSyntaxKind.ItalicText, text, position)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override TextPosition ContentPosition { get => new(Position.Start + 3, Position.End - 4); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||||
|
|
||||||
|
internal sealed class MiHoYoPlainTextSyntax : MiHoYoSyntaxNode
|
||||||
|
{
|
||||||
|
public MiHoYoPlainTextSyntax(string text, int start, int end)
|
||||||
|
: base(MiHoYoSyntaxKind.PlainText, text, start, end)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MiHoYoPlainTextSyntax(string text, in TextPosition position)
|
||||||
|
: base(MiHoYoSyntaxKind.PlainText, text, position)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||||
|
|
||||||
|
internal sealed class MiHoYoRootSyntax : MiHoYoSyntaxNode
|
||||||
|
{
|
||||||
|
public MiHoYoRootSyntax(string text, int start, int end)
|
||||||
|
: base(MiHoYoSyntaxKind.Root, text, start, end)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||||
|
|
||||||
|
internal enum MiHoYoSyntaxKind
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Root,
|
||||||
|
PlainText,
|
||||||
|
ColorText,
|
||||||
|
ItalicText,
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||||
|
|
||||||
|
internal abstract class MiHoYoSyntaxNode : SyntaxNode<MiHoYoSyntaxNode, MiHoYoSyntaxKind>
|
||||||
|
{
|
||||||
|
public MiHoYoSyntaxNode(MiHoYoSyntaxKind kind, string text, int start, int end)
|
||||||
|
: base(kind, text, start, end)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MiHoYoSyntaxNode(MiHoYoSyntaxKind kind, string text, in TextPosition position)
|
||||||
|
: base(kind, text, position)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||||
|
|
||||||
|
internal sealed class MiHoYoSyntaxTree
|
||||||
|
{
|
||||||
|
public MiHoYoSyntaxNode Root { get; set; } = default!;
|
||||||
|
|
||||||
|
public string Text { get; set; } = default!;
|
||||||
|
|
||||||
|
public static MiHoYoSyntaxTree Parse(string text)
|
||||||
|
{
|
||||||
|
MiHoYoRootSyntax root = new(text, 0, text.Length);
|
||||||
|
ParseComponents(text, root);
|
||||||
|
|
||||||
|
MiHoYoSyntaxTree tree = new()
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
Root = root,
|
||||||
|
};
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseComponents(string text, MiHoYoSyntaxNode syntax)
|
||||||
|
{
|
||||||
|
TextPosition contentPosition = syntax switch
|
||||||
|
{
|
||||||
|
MiHoYoXmlElementSyntax xmlSyntax => xmlSyntax.ContentPosition,
|
||||||
|
_ => syntax.Position,
|
||||||
|
};
|
||||||
|
ReadOnlySpan<char> contentSpan = text.AsSpan().Slice(contentPosition.Start, contentPosition.Length);
|
||||||
|
|
||||||
|
int endOfProcessedAtContent = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (endOfProcessedAtContent >= contentSpan.Length)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int indexOfXmlLeftOpeningAtUnprocessedContent = contentSpan[endOfProcessedAtContent..].IndexOf('<');
|
||||||
|
|
||||||
|
// End of content
|
||||||
|
if (indexOfXmlLeftOpeningAtUnprocessedContent < 0)
|
||||||
|
{
|
||||||
|
TextPosition position = new(contentPosition.Start + endOfProcessedAtContent, contentPosition.End);
|
||||||
|
MiHoYoPlainTextSyntax plainText = new(text, position);
|
||||||
|
syntax.Children.Add(plainText);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have plain text between xml elements
|
||||||
|
if (indexOfXmlLeftOpeningAtUnprocessedContent > 0)
|
||||||
|
{
|
||||||
|
TextPosition position = new(0, indexOfXmlLeftOpeningAtUnprocessedContent);
|
||||||
|
TextPosition positionAtContent = position.RightShift(endOfProcessedAtContent);
|
||||||
|
TextPosition positionAtText = positionAtContent.RightShift(contentPosition.Start);
|
||||||
|
MiHoYoPlainTextSyntax plainText = new(text, positionAtText);
|
||||||
|
syntax.Children.Add(plainText);
|
||||||
|
endOfProcessedAtContent = positionAtContent.End;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peek the next character after '<'
|
||||||
|
int indexOfXmlLeftOpeningAtContent = endOfProcessedAtContent + indexOfXmlLeftOpeningAtUnprocessedContent;
|
||||||
|
switch (contentSpan[indexOfXmlLeftOpeningAtContent + 1])
|
||||||
|
{
|
||||||
|
case 'c':
|
||||||
|
{
|
||||||
|
int endOfXmlColorRightClosingAtUnprocessedContent = EndOfXmlClosing(contentSpan[indexOfXmlLeftOpeningAtContent..], out int endOfXmlColorLeftClosingAtUnprocessedContent);
|
||||||
|
|
||||||
|
MiHoYoColorKind colorKind = endOfXmlColorLeftClosingAtUnprocessedContent switch
|
||||||
|
{
|
||||||
|
17 => MiHoYoColorKind.Rgba,
|
||||||
|
15 => MiHoYoColorKind.Rgb,
|
||||||
|
_ => throw Must.NeverHappen(),
|
||||||
|
};
|
||||||
|
|
||||||
|
TextPosition position = new(0, endOfXmlColorRightClosingAtUnprocessedContent);
|
||||||
|
TextPosition positionAtContent = position.RightShift(endOfProcessedAtContent);
|
||||||
|
TextPosition positionAtText = positionAtContent.RightShift(contentPosition.Start);
|
||||||
|
|
||||||
|
MiHoYoColorTextSyntax colorText = new(colorKind, text, positionAtText);
|
||||||
|
ParseComponents(text, colorText);
|
||||||
|
syntax.Children.Add(colorText);
|
||||||
|
endOfProcessedAtContent = positionAtContent.End;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'i':
|
||||||
|
{
|
||||||
|
int endOfXmlItalicRightClosingAtUnprocessedContent = EndOfXmlClosing(contentSpan[indexOfXmlLeftOpeningAtContent..], out _);
|
||||||
|
|
||||||
|
TextPosition position = new(0, endOfXmlItalicRightClosingAtUnprocessedContent);
|
||||||
|
TextPosition positionAtContent = position.RightShift(endOfProcessedAtContent);
|
||||||
|
TextPosition positionAtText = positionAtContent.RightShift(contentPosition.Start);
|
||||||
|
|
||||||
|
MiHoYoItalicTextSyntax italicText = new(text, positionAtText);
|
||||||
|
ParseComponents(text, italicText);
|
||||||
|
syntax.Children.Add(italicText);
|
||||||
|
endOfProcessedAtContent = positionAtContent.End;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int EndOfXmlClosing(in ReadOnlySpan<char> span, out int endOfLeftClosing)
|
||||||
|
{
|
||||||
|
endOfLeftClosing = 0;
|
||||||
|
|
||||||
|
int openingCount = 0;
|
||||||
|
int closingCount = 0;
|
||||||
|
|
||||||
|
int current = 0;
|
||||||
|
|
||||||
|
// Considering <i>text1</i>text2<i>text3</i>
|
||||||
|
// Considering <i>text1<span>text2</span>text3</i>
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int leftMarkIndex = span[current..].IndexOf('<');
|
||||||
|
if (span[current..][leftMarkIndex + 1] is '/')
|
||||||
|
{
|
||||||
|
closingCount++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
openingCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
current += span[current..].IndexOf('>') + 1;
|
||||||
|
|
||||||
|
if (openingCount is 1 && closingCount is 0)
|
||||||
|
{
|
||||||
|
endOfLeftClosing = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openingCount == closingCount)
|
||||||
|
{
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
|
||||||
|
|
||||||
|
internal abstract class MiHoYoXmlElementSyntax : MiHoYoSyntaxNode
|
||||||
|
{
|
||||||
|
public MiHoYoXmlElementSyntax(MiHoYoSyntaxKind kind, string text, int start, int end)
|
||||||
|
: base(kind, text, start, end)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MiHoYoXmlElementSyntax(MiHoYoSyntaxKind kind, string text, in TextPosition position)
|
||||||
|
: base(kind, text, position)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract TextPosition ContentPosition { get; }
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> ContentSpan { get => Text.AsSpan(ContentPosition.Start, ContentPosition.Length); }
|
||||||
|
}
|
||||||
33
src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/SyntaxNode.cs
Normal file
33
src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/SyntaxNode.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Text.Syntax;
|
||||||
|
|
||||||
|
internal abstract class SyntaxNode<TSelf, TKind>
|
||||||
|
where TSelf : SyntaxNode<TSelf, TKind>
|
||||||
|
where TKind : struct, Enum
|
||||||
|
{
|
||||||
|
public SyntaxNode(TKind kind, string text, int start, int end)
|
||||||
|
{
|
||||||
|
Kind = kind;
|
||||||
|
Text = text;
|
||||||
|
Position = new(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SyntaxNode(TKind kind, string text, in TextPosition position)
|
||||||
|
{
|
||||||
|
Kind = kind;
|
||||||
|
Text = text;
|
||||||
|
Position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TKind Kind { get; protected set; }
|
||||||
|
|
||||||
|
public List<TSelf> Children { get; } = [];
|
||||||
|
|
||||||
|
public TextPosition Position { get; protected set; }
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> Span { get => Text.AsSpan().Slice(Position.Start, Position.Length); }
|
||||||
|
|
||||||
|
protected string Text { get; set; } = default!;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Control.Text.Syntax;
|
||||||
|
|
||||||
|
[DebuggerDisplay("[{Start}..{End}]")]
|
||||||
|
internal readonly struct TextPosition
|
||||||
|
{
|
||||||
|
public readonly int Start;
|
||||||
|
public readonly int End;
|
||||||
|
|
||||||
|
public TextPosition(int start, int end)
|
||||||
|
{
|
||||||
|
Start = start;
|
||||||
|
End = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly int Length
|
||||||
|
{
|
||||||
|
get => End - Start;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextPosition LeftShift(int offset)
|
||||||
|
{
|
||||||
|
return new(Start - offset, End - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextPosition RightShift(int offset)
|
||||||
|
{
|
||||||
|
return new(Start + offset, End + offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,16 +18,50 @@
|
|||||||
Offset="0,4,0"/>
|
Offset="0,4,0"/>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</ResourceDictionary.ThemeDictionaries>
|
</ResourceDictionary.ThemeDictionaries>
|
||||||
|
|
||||||
<Style x:Key="BorderCardStyle" TargetType="Border">
|
<Style x:Key="BorderCardStyle" TargetType="Border">
|
||||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
||||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style
|
||||||
|
x:Key="AcrylicBorderCardStyle"
|
||||||
|
BasedOn="{StaticResource BorderCardStyle}"
|
||||||
|
TargetType="Border">
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Background" Value="{ThemeResource SystemControlAcrylicElementBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style
|
||||||
|
x:Key="AcrylicSecondaryBorderCardStyle"
|
||||||
|
BasedOn="{StaticResource BorderCardStyle}"
|
||||||
|
TargetType="Border">
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Background" Value="{ThemeResource SystemControlChromeMediumAcrylicElementMediumBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="GridCardStyle" TargetType="Grid">
|
<Style x:Key="GridCardStyle" TargetType="Grid">
|
||||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
||||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||||
</Style>
|
</Style>
|
||||||
</ResourceDictionary>
|
|
||||||
|
<Style
|
||||||
|
x:Key="AcrylicGridCardStyle"
|
||||||
|
BasedOn="{StaticResource GridCardStyle}"
|
||||||
|
TargetType="Grid">
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Background" Value="{ThemeResource SystemControlAcrylicElementBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style
|
||||||
|
x:Key="AcrylicSecondaryGridCardStyle"
|
||||||
|
BasedOn="{StaticResource GridCardStyle}"
|
||||||
|
TargetType="Grid">
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Background" Value="{ThemeResource SystemControlChromeMediumAcrylicElementMediumBrush}"/>
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -2,6 +2,53 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:shch="using:Snap.Hutao.Control.Helper">
|
xmlns:shch="using:Snap.Hutao.Control.Helper">
|
||||||
|
<ResourceDictionary.ThemeDictionaries>
|
||||||
|
<ResourceDictionary x:Key="Light">
|
||||||
|
<AcrylicBrush
|
||||||
|
x:Key="InfoBarErrorSeverityBackgroundBrush"
|
||||||
|
FallbackColor="#FDE7E9"
|
||||||
|
TintColor="#FDE7E9"
|
||||||
|
TintOpacity="0.6"/>
|
||||||
|
<AcrylicBrush
|
||||||
|
x:Key="InfoBarWarningSeverityBackgroundBrush"
|
||||||
|
FallbackColor="#FFF4CE"
|
||||||
|
TintColor="#FFF4CE"
|
||||||
|
TintOpacity="0.6"/>
|
||||||
|
<AcrylicBrush
|
||||||
|
x:Key="InfoBarSuccessSeverityBackgroundBrush"
|
||||||
|
FallbackColor="#DFF6DD"
|
||||||
|
TintColor="#DFF6DD"
|
||||||
|
TintOpacity="0.6"/>
|
||||||
|
<AcrylicBrush
|
||||||
|
x:Key="InfoBarInformationalSeverityBackgroundBrush"
|
||||||
|
FallbackColor="#80F6F6F6"
|
||||||
|
TintColor="#80F6F6F6"
|
||||||
|
TintOpacity="0.6"/>
|
||||||
|
</ResourceDictionary>
|
||||||
|
<ResourceDictionary x:Key="Dark">
|
||||||
|
<AcrylicBrush
|
||||||
|
x:Key="InfoBarErrorSeverityBackgroundBrush"
|
||||||
|
FallbackColor="#442726"
|
||||||
|
TintColor="#442726"
|
||||||
|
TintOpacity="0.6"/>
|
||||||
|
<AcrylicBrush
|
||||||
|
x:Key="InfoBarWarningSeverityBackgroundBrush"
|
||||||
|
FallbackColor="#433519"
|
||||||
|
TintColor="#433519"
|
||||||
|
TintOpacity="0.6"/>
|
||||||
|
<AcrylicBrush
|
||||||
|
x:Key="InfoBarSuccessSeverityBackgroundBrush"
|
||||||
|
FallbackColor="#393D1B"
|
||||||
|
TintColor="#393D1B"
|
||||||
|
TintOpacity="0.6"/>
|
||||||
|
<AcrylicBrush
|
||||||
|
x:Key="InfoBarInformationalSeverityBackgroundBrush"
|
||||||
|
FallbackColor="#34424d"
|
||||||
|
TintColor="#34424d"
|
||||||
|
TintOpacity="0.6"/>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ResourceDictionary.ThemeDictionaries>
|
||||||
|
|
||||||
<Thickness x:Key="InfoBarIconMargin">19,16,19,16</Thickness>
|
<Thickness x:Key="InfoBarIconMargin">19,16,19,16</Thickness>
|
||||||
<Thickness x:Key="InfoBarContentRootPadding">0,0,0,0</Thickness>
|
<Thickness x:Key="InfoBarContentRootPadding">0,0,0,0</Thickness>
|
||||||
<x:Double x:Key="InfoBarIconFontSize">20</x:Double>
|
<x:Double x:Key="InfoBarIconFontSize">20</x:Double>
|
||||||
|
|||||||
14
src/Snap.Hutao/Snap.Hutao/Control/Theme/KnownColors.cs
Normal file
14
src/Snap.Hutao/Snap.Hutao/Control/Theme/KnownColors.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
<x:Double x:Key="ContentDialogMinHeight">64</x:Double>
|
<x:Double x:Key="ContentDialogMinHeight">64</x:Double>
|
||||||
<x:Double x:Key="LargeAppBarButtonWidth">100</x:Double>
|
<x:Double x:Key="LargeAppBarButtonWidth">100</x:Double>
|
||||||
|
|
||||||
|
<x:Double x:Key="AppBarThemeCompactActualHeight">50</x:Double>
|
||||||
|
|
||||||
<!-- ProgressBar -->
|
<!-- ProgressBar -->
|
||||||
<x:Double x:Key="LargeBackgroundProgressBarOpacity">0.2</x:Double>
|
<x:Double x:Key="LargeBackgroundProgressBarOpacity">0.2</x:Double>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -1,5 +1,758 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
<ResourceDictionary
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:cw="using:CommunityToolkit.WinUI">
|
||||||
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
|
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
|
||||||
<Thickness x:Key="PivotHeaderItemMargin">16,0,0,0</Thickness>
|
<Thickness x:Key="PivotHeaderItemMargin">16,0,0,0</Thickness>
|
||||||
<Thickness x:Key="PivotItemMargin">0</Thickness>
|
<Thickness x:Key="PivotItemMargin">0</Thickness>
|
||||||
|
|
||||||
|
<Style x:Key="CardPivotStyle" TargetType="Pivot">
|
||||||
|
<Setter Property="Margin" Value="0"/>
|
||||||
|
<Setter Property="Padding" Value="0"/>
|
||||||
|
<Setter Property="Background" Value="{ThemeResource PivotBackground}"/>
|
||||||
|
<Setter Property="IsTabStop" Value="False"/>
|
||||||
|
<Setter Property="ItemsPanel">
|
||||||
|
<Setter.Value>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<Grid/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Pivot">
|
||||||
|
<Grid
|
||||||
|
x:Name="RootElement"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||||
|
Background="{TemplateBinding Background}">
|
||||||
|
<Grid.Resources>
|
||||||
|
<Style x:Key="BaseContentControlStyle" TargetType="ContentControl">
|
||||||
|
<Setter Property="FontFamily" Value="XamlAutoFontFamily"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ContentControl">
|
||||||
|
<ContentPresenter
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||||
|
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||||
|
OpticalMarginAlignment="TrimSideBearings"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style
|
||||||
|
x:Key="TitleContentControlStyle"
|
||||||
|
BasedOn="{StaticResource BaseContentControlStyle}"
|
||||||
|
TargetType="ContentControl">
|
||||||
|
<Setter Property="FontFamily" Value="{ThemeResource PivotTitleFontFamily}"/>
|
||||||
|
<Setter Property="FontWeight" Value="{ThemeResource PivotTitleThemeFontWeight}"/>
|
||||||
|
<Setter Property="FontSize" Value="{ThemeResource PivotTitleFontSize}"/>
|
||||||
|
</Style>
|
||||||
|
</Grid.Resources>
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<ContentControl
|
||||||
|
x:Name="TitleContentControl"
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="{StaticResource PivotPortraitThemePadding}"
|
||||||
|
Content="{TemplateBinding Title}"
|
||||||
|
ContentTemplate="{TemplateBinding TitleTemplate}"
|
||||||
|
IsTabStop="False"
|
||||||
|
Style="{StaticResource TitleContentControlStyle}"
|
||||||
|
Visibility="Collapsed"/>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.Resources>
|
||||||
|
<ControlTemplate x:Key="NextTemplate" TargetType="Button">
|
||||||
|
<Border
|
||||||
|
x:Name="Root"
|
||||||
|
Background="{ThemeResource PivotNextButtonBackground}"
|
||||||
|
BorderBrush="{ThemeResource PivotNextButtonBorderBrush}"
|
||||||
|
BorderThickness="{ThemeResource PivotNavButtonBorderThemeThickness}">
|
||||||
|
<FontIcon
|
||||||
|
x:Name="Arrow"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{ThemeResource PivotNextButtonForeground}"
|
||||||
|
Glyph=""
|
||||||
|
MirroredWhenRightToLeft="True"
|
||||||
|
UseLayoutRounding="False"/>
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Normal"/>
|
||||||
|
<VisualState x:Name="PointerOver">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBackgroundPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBorderBrushPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonForegroundPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Pressed">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBackgroundPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBorderBrushPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonForegroundPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
<ControlTemplate x:Key="PreviousTemplate" TargetType="Button">
|
||||||
|
<Border
|
||||||
|
x:Name="Root"
|
||||||
|
Background="{ThemeResource PivotPreviousButtonBackground}"
|
||||||
|
BorderBrush="{ThemeResource PivotPreviousButtonBorderBrush}"
|
||||||
|
BorderThickness="{ThemeResource PivotNavButtonBorderThemeThickness}">
|
||||||
|
<FontIcon
|
||||||
|
x:Name="Arrow"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{ThemeResource PivotPreviousButtonForeground}"
|
||||||
|
Glyph=""
|
||||||
|
MirroredWhenRightToLeft="True"
|
||||||
|
UseLayoutRounding="False"/>
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Normal"/>
|
||||||
|
<VisualState x:Name="PointerOver">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBackgroundPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBorderBrushPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonForegroundPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Pressed">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBackgroundPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBorderBrushPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonForegroundPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Grid.Resources>
|
||||||
|
<ScrollViewer
|
||||||
|
x:Name="ScrollViewer"
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
VerticalContentAlignment="Stretch"
|
||||||
|
BringIntoViewOnFocusChange="False"
|
||||||
|
HorizontalScrollBarVisibility="Hidden"
|
||||||
|
HorizontalSnapPointsAlignment="Center"
|
||||||
|
HorizontalSnapPointsType="MandatorySingle"
|
||||||
|
Template="{StaticResource ScrollViewerScrollBarlessTemplate}"
|
||||||
|
VerticalScrollBarVisibility="Disabled"
|
||||||
|
VerticalScrollMode="Disabled"
|
||||||
|
VerticalSnapPointsType="None"
|
||||||
|
ZoomMode="Disabled">
|
||||||
|
<PivotPanel x:Name="Panel" VerticalAlignment="Stretch">
|
||||||
|
<Grid x:Name="PivotLayoutElement">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.RenderTransform>
|
||||||
|
<CompositeTransform x:Name="PivotLayoutElementTranslateTransform"/>
|
||||||
|
</Grid.RenderTransform>
|
||||||
|
|
||||||
|
<ItemsPresenter x:Name="PivotItemPresenter" Grid.Row="1">
|
||||||
|
<ItemsPresenter.RenderTransform>
|
||||||
|
<TransformGroup>
|
||||||
|
<TranslateTransform x:Name="ItemsPresenterTranslateTransform"/>
|
||||||
|
<CompositeTransform x:Name="ItemsPresenterCompositeTransform"/>
|
||||||
|
</TransformGroup>
|
||||||
|
</ItemsPresenter.RenderTransform>
|
||||||
|
</ItemsPresenter>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="16,16,16,0"
|
||||||
|
cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
|
||||||
|
<Grid Style="{ThemeResource AcrylicSecondaryGridCardStyle}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<ContentPresenter
|
||||||
|
x:Name="LeftHeaderPresenter"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Content="{TemplateBinding LeftHeader}"
|
||||||
|
ContentTemplate="{TemplateBinding LeftHeaderTemplate}"/>
|
||||||
|
<ContentControl
|
||||||
|
x:Name="HeaderClipper"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
UseSystemFocusVisuals="{StaticResource UseSystemFocusVisuals}">
|
||||||
|
<ContentControl.Clip>
|
||||||
|
<RectangleGeometry x:Name="HeaderClipperGeometry"/>
|
||||||
|
</ContentControl.Clip>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RenderTransform>
|
||||||
|
<CompositeTransform x:Name="HeaderOffsetTranslateTransform"/>
|
||||||
|
</Grid.RenderTransform>
|
||||||
|
<PivotHeaderPanel x:Name="StaticHeader" Visibility="Collapsed">
|
||||||
|
<PivotHeaderPanel.RenderTransform>
|
||||||
|
<CompositeTransform x:Name="StaticHeaderTranslateTransform"/>
|
||||||
|
</PivotHeaderPanel.RenderTransform>
|
||||||
|
</PivotHeaderPanel>
|
||||||
|
<PivotHeaderPanel x:Name="Header">
|
||||||
|
<PivotHeaderPanel.RenderTransform>
|
||||||
|
<CompositeTransform x:Name="HeaderTranslateTransform"/>
|
||||||
|
</PivotHeaderPanel.RenderTransform>
|
||||||
|
</PivotHeaderPanel>
|
||||||
|
<Rectangle
|
||||||
|
x:Name="FocusFollower"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Control.IsTemplateFocusTarget="True"
|
||||||
|
Fill="Transparent"
|
||||||
|
IsHitTestVisible="False"/>
|
||||||
|
</Grid>
|
||||||
|
</ContentControl>
|
||||||
|
<Button
|
||||||
|
x:Name="PreviousButton"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="20"
|
||||||
|
Height="36"
|
||||||
|
Margin="{ThemeResource PivotNavButtonMargin}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="Transparent"
|
||||||
|
IsEnabled="False"
|
||||||
|
IsTabStop="False"
|
||||||
|
Opacity="0"
|
||||||
|
Template="{StaticResource PreviousTemplate}"
|
||||||
|
UseSystemFocusVisuals="False"/>
|
||||||
|
<Button
|
||||||
|
x:Name="NextButton"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="20"
|
||||||
|
Height="36"
|
||||||
|
Margin="{ThemeResource PivotNavButtonMargin}"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="Transparent"
|
||||||
|
IsEnabled="False"
|
||||||
|
IsTabStop="False"
|
||||||
|
Opacity="0"
|
||||||
|
Template="{StaticResource NextTemplate}"
|
||||||
|
UseSystemFocusVisuals="False"/>
|
||||||
|
<ContentPresenter
|
||||||
|
x:Name="RightHeaderPresenter"
|
||||||
|
Grid.Column="2"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Content="{TemplateBinding RightHeader}"
|
||||||
|
ContentTemplate="{TemplateBinding RightHeaderTemplate}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</PivotPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="Orientation">
|
||||||
|
<VisualState x:Name="Portrait">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl" Storyboard.TargetProperty="Margin">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPortraitThemePadding}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Landscape">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl" Storyboard.TargetProperty="Margin">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotLandscapeThemePadding}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
|
||||||
|
</VisualStateGroup>
|
||||||
|
<VisualStateGroup x:Name="NavigationButtonsVisibility">
|
||||||
|
<VisualState x:Name="NavigationButtonsHidden"/>
|
||||||
|
<VisualState x:Name="NavigationButtonsVisible">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="Opacity">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="IsEnabled">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="Opacity">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="IsEnabled">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="PreviousButtonVisible">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="Opacity">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="IsEnabled">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="NextButtonVisible">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="Opacity">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="IsEnabled">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
|
||||||
|
</VisualStateGroup>
|
||||||
|
<VisualStateGroup x:Name="HeaderStates">
|
||||||
|
<VisualState x:Name="HeaderDynamic"/>
|
||||||
|
<VisualState x:Name="HeaderStatic">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Header" Storyboard.TargetProperty="Visibility">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="StaticHeader" Storyboard.TargetProperty="Visibility">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
|
||||||
|
</VisualStateGroup>
|
||||||
|
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="CardPivotStyle2" TargetType="Pivot">
|
||||||
|
<Setter Property="Margin" Value="0"/>
|
||||||
|
<Setter Property="Padding" Value="0"/>
|
||||||
|
<Setter Property="Background" Value="{ThemeResource PivotBackground}"/>
|
||||||
|
<Setter Property="IsTabStop" Value="False"/>
|
||||||
|
<Setter Property="ItemsPanel">
|
||||||
|
<Setter.Value>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<Grid/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Pivot">
|
||||||
|
<Grid
|
||||||
|
x:Name="RootElement"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||||
|
Background="{TemplateBinding Background}">
|
||||||
|
<Grid.Resources>
|
||||||
|
<Style x:Key="BaseContentControlStyle" TargetType="ContentControl">
|
||||||
|
<Setter Property="FontFamily" Value="XamlAutoFontFamily"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ContentControl">
|
||||||
|
<ContentPresenter
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||||
|
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||||
|
OpticalMarginAlignment="TrimSideBearings"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style
|
||||||
|
x:Key="TitleContentControlStyle"
|
||||||
|
BasedOn="{StaticResource BaseContentControlStyle}"
|
||||||
|
TargetType="ContentControl">
|
||||||
|
<Setter Property="FontFamily" Value="{ThemeResource PivotTitleFontFamily}"/>
|
||||||
|
<Setter Property="FontWeight" Value="{ThemeResource PivotTitleThemeFontWeight}"/>
|
||||||
|
<Setter Property="FontSize" Value="{ThemeResource PivotTitleFontSize}"/>
|
||||||
|
</Style>
|
||||||
|
</Grid.Resources>
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<ContentControl
|
||||||
|
x:Name="TitleContentControl"
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="{StaticResource PivotPortraitThemePadding}"
|
||||||
|
Content="{TemplateBinding Title}"
|
||||||
|
ContentTemplate="{TemplateBinding TitleTemplate}"
|
||||||
|
IsTabStop="False"
|
||||||
|
Style="{StaticResource TitleContentControlStyle}"
|
||||||
|
Visibility="Collapsed"/>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.Resources>
|
||||||
|
<ControlTemplate x:Key="NextTemplate" TargetType="Button">
|
||||||
|
<Border
|
||||||
|
x:Name="Root"
|
||||||
|
Background="{ThemeResource PivotNextButtonBackground}"
|
||||||
|
BorderBrush="{ThemeResource PivotNextButtonBorderBrush}"
|
||||||
|
BorderThickness="{ThemeResource PivotNavButtonBorderThemeThickness}">
|
||||||
|
<FontIcon
|
||||||
|
x:Name="Arrow"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{ThemeResource PivotNextButtonForeground}"
|
||||||
|
Glyph=""
|
||||||
|
MirroredWhenRightToLeft="True"
|
||||||
|
UseLayoutRounding="False"/>
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Normal"/>
|
||||||
|
<VisualState x:Name="PointerOver">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBackgroundPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBorderBrushPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonForegroundPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Pressed">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBackgroundPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonBorderBrushPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotNextButtonForegroundPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
<ControlTemplate x:Key="PreviousTemplate" TargetType="Button">
|
||||||
|
<Border
|
||||||
|
x:Name="Root"
|
||||||
|
Background="{ThemeResource PivotPreviousButtonBackground}"
|
||||||
|
BorderBrush="{ThemeResource PivotPreviousButtonBorderBrush}"
|
||||||
|
BorderThickness="{ThemeResource PivotNavButtonBorderThemeThickness}">
|
||||||
|
<FontIcon
|
||||||
|
x:Name="Arrow"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{ThemeResource PivotPreviousButtonForeground}"
|
||||||
|
Glyph=""
|
||||||
|
MirroredWhenRightToLeft="True"
|
||||||
|
UseLayoutRounding="False"/>
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Normal"/>
|
||||||
|
<VisualState x:Name="PointerOver">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBackgroundPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBorderBrushPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonForegroundPointerOver}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Pressed">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBackgroundPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Root" Storyboard.TargetProperty="BorderBrush">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonBorderBrushPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Arrow" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPreviousButtonForegroundPressed}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Grid.Resources>
|
||||||
|
<ScrollViewer
|
||||||
|
x:Name="ScrollViewer"
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
VerticalContentAlignment="Stretch"
|
||||||
|
BringIntoViewOnFocusChange="False"
|
||||||
|
HorizontalScrollBarVisibility="Hidden"
|
||||||
|
HorizontalSnapPointsAlignment="Center"
|
||||||
|
HorizontalSnapPointsType="MandatorySingle"
|
||||||
|
Template="{StaticResource ScrollViewerScrollBarlessTemplate}"
|
||||||
|
VerticalScrollBarVisibility="Disabled"
|
||||||
|
VerticalScrollMode="Disabled"
|
||||||
|
VerticalSnapPointsType="None"
|
||||||
|
ZoomMode="Disabled">
|
||||||
|
<PivotPanel x:Name="Panel" VerticalAlignment="Stretch">
|
||||||
|
<Grid x:Name="PivotLayoutElement">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.RenderTransform>
|
||||||
|
<CompositeTransform x:Name="PivotLayoutElementTranslateTransform"/>
|
||||||
|
</Grid.RenderTransform>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ItemsPresenter x:Name="PivotItemPresenter" Grid.Row="1">
|
||||||
|
<ItemsPresenter.RenderTransform>
|
||||||
|
<TransformGroup>
|
||||||
|
<TranslateTransform x:Name="ItemsPresenterTranslateTransform"/>
|
||||||
|
<CompositeTransform x:Name="ItemsPresenterCompositeTransform"/>
|
||||||
|
</TransformGroup>
|
||||||
|
</ItemsPresenter.RenderTransform>
|
||||||
|
</ItemsPresenter>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="16,16,16,0"
|
||||||
|
cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
|
||||||
|
<Grid Style="{ThemeResource AcrylicSecondaryGridCardStyle}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<ContentPresenter
|
||||||
|
x:Name="LeftHeaderPresenter"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Content="{TemplateBinding LeftHeader}"
|
||||||
|
ContentTemplate="{TemplateBinding LeftHeaderTemplate}"/>
|
||||||
|
<ContentControl
|
||||||
|
x:Name="HeaderClipper"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
UseSystemFocusVisuals="{StaticResource UseSystemFocusVisuals}">
|
||||||
|
<ContentControl.Clip>
|
||||||
|
<RectangleGeometry x:Name="HeaderClipperGeometry"/>
|
||||||
|
</ContentControl.Clip>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RenderTransform>
|
||||||
|
<CompositeTransform x:Name="HeaderOffsetTranslateTransform"/>
|
||||||
|
</Grid.RenderTransform>
|
||||||
|
<PivotHeaderPanel x:Name="StaticHeader" Visibility="Collapsed">
|
||||||
|
<PivotHeaderPanel.RenderTransform>
|
||||||
|
<CompositeTransform x:Name="StaticHeaderTranslateTransform"/>
|
||||||
|
</PivotHeaderPanel.RenderTransform>
|
||||||
|
</PivotHeaderPanel>
|
||||||
|
<PivotHeaderPanel x:Name="Header">
|
||||||
|
<PivotHeaderPanel.RenderTransform>
|
||||||
|
<CompositeTransform x:Name="HeaderTranslateTransform"/>
|
||||||
|
</PivotHeaderPanel.RenderTransform>
|
||||||
|
</PivotHeaderPanel>
|
||||||
|
<Rectangle
|
||||||
|
x:Name="FocusFollower"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Control.IsTemplateFocusTarget="True"
|
||||||
|
Fill="Transparent"
|
||||||
|
IsHitTestVisible="False"/>
|
||||||
|
</Grid>
|
||||||
|
</ContentControl>
|
||||||
|
<Button
|
||||||
|
x:Name="PreviousButton"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="20"
|
||||||
|
Height="36"
|
||||||
|
Margin="{ThemeResource PivotNavButtonMargin}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="Transparent"
|
||||||
|
IsEnabled="False"
|
||||||
|
IsTabStop="False"
|
||||||
|
Opacity="0"
|
||||||
|
Template="{StaticResource PreviousTemplate}"
|
||||||
|
UseSystemFocusVisuals="False"/>
|
||||||
|
<Button
|
||||||
|
x:Name="NextButton"
|
||||||
|
Grid.Column="1"
|
||||||
|
Width="20"
|
||||||
|
Height="36"
|
||||||
|
Margin="{ThemeResource PivotNavButtonMargin}"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="Transparent"
|
||||||
|
IsEnabled="False"
|
||||||
|
IsTabStop="False"
|
||||||
|
Opacity="0"
|
||||||
|
Template="{StaticResource NextTemplate}"
|
||||||
|
UseSystemFocusVisuals="False"/>
|
||||||
|
<ContentPresenter
|
||||||
|
x:Name="RightHeaderPresenter"
|
||||||
|
Grid.Column="2"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Content="{TemplateBinding RightHeader}"
|
||||||
|
ContentTemplate="{TemplateBinding RightHeaderTemplate}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</PivotPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="Orientation">
|
||||||
|
<VisualState x:Name="Portrait">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl" Storyboard.TargetProperty="Margin">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotPortraitThemePadding}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Landscape">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TitleContentControl" Storyboard.TargetProperty="Margin">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource PivotLandscapeThemePadding}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
|
||||||
|
</VisualStateGroup>
|
||||||
|
<VisualStateGroup x:Name="NavigationButtonsVisibility">
|
||||||
|
<VisualState x:Name="NavigationButtonsHidden"/>
|
||||||
|
<VisualState x:Name="NavigationButtonsVisible">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="Opacity">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="IsEnabled">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="Opacity">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="IsEnabled">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="PreviousButtonVisible">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="Opacity">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PreviousButton" Storyboard.TargetProperty="IsEnabled">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="NextButtonVisible">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="Opacity">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="NextButton" Storyboard.TargetProperty="IsEnabled">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="True"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
|
||||||
|
</VisualStateGroup>
|
||||||
|
<VisualStateGroup x:Name="HeaderStates">
|
||||||
|
<VisualState x:Name="HeaderDynamic"/>
|
||||||
|
<VisualState x:Name="HeaderStatic">
|
||||||
|
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Header" Storyboard.TargetProperty="Visibility">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="StaticHeader" Storyboard.TargetProperty="Visibility">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
|
||||||
|
</VisualStateGroup>
|
||||||
|
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
Grid.ColumnSpan="2"
|
Grid.ColumnSpan="2"
|
||||||
Margin="{TemplateBinding Padding}">
|
Margin="{TemplateBinding Padding}">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" MaxWidth="{Binding Path=(shch:ScrollViewerHelper.LeftPanelMaxWidth), RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<ScrollContentPresenter x:Name="ScrollContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}"/>
|
<ScrollContentPresenter x:Name="ScrollContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}"/>
|
||||||
|
|||||||
@@ -21,6 +21,14 @@
|
|||||||
<Setter Property="Margin" Value="1,29,0,5"/>
|
<Setter Property="Margin" Value="1,29,0,5"/>
|
||||||
</Style.Setters>
|
</Style.Setters>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style
|
||||||
|
x:Key="SettingsCardHeaderTextBlockStyle"
|
||||||
|
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
|
||||||
|
TargetType="TextBlock">
|
||||||
|
<Style.Setters>
|
||||||
|
<Setter Property="Margin" Value="1,0,0,5"/>
|
||||||
|
</Style.Setters>
|
||||||
|
</Style>
|
||||||
<Style
|
<Style
|
||||||
x:Key="SettingsContentComboBoxStyle"
|
x:Key="SettingsContentComboBoxStyle"
|
||||||
BasedOn="{StaticResource DefaultComboBoxStyle}"
|
BasedOn="{StaticResource DefaultComboBoxStyle}"
|
||||||
|
|||||||
5
src/Snap.Hutao/Snap.Hutao/Control/Theme/Thickness.xaml
Normal file
5
src/Snap.Hutao/Snap.Hutao/Control/Theme/Thickness.xaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Thickness x:Key="ListViewInSplitPanePadding">0,2</Thickness>
|
||||||
|
<!-- https://github.com/microsoft/microsoft-ui-xaml/issues/4811 -->
|
||||||
|
<x:Int32 x:Key="__DiscardPageOverride">0</x:Int32>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<x:String x:Key="DocumentLink_Home">https://hut.ao</x:String>
|
<x:String x:Key="DocumentLink_Home">https://hut.ao</x:String>
|
||||||
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String>
|
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String>
|
||||||
<x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String>
|
<x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String>
|
||||||
|
<x:String x:Key="DocumentLink_Loopback">https://hut.ao/zh/advanced/FAQ.html#%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87%E7%BD%91%E7%BB%9C%E4%BB%A3%E7%90%86%E4%BD%BF%E7%94%A8%E8%83%A1%E6%A1%83%E5%B7%A5%E5%85%B7%E7%AE%B1</x:String>
|
||||||
|
|
||||||
<!-- Other -->
|
<!-- Other -->
|
||||||
<x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String>
|
<x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String>
|
||||||
@@ -33,5 +34,6 @@
|
|||||||
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
|
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
||||||
|
<x:String x:Key="UI_EmotionIcon433">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon433.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon445">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
|
<x:String x:Key="UI_EmotionIcon445">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
|
||||||
using Snap.Hutao.Core.IO;
|
using Snap.Hutao.Core.IO;
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.Caching;
|
namespace Snap.Hutao.Core.Caching;
|
||||||
@@ -10,7 +9,7 @@ namespace Snap.Hutao.Core.Caching;
|
|||||||
/// 为图像缓存提供抽象
|
/// 为图像缓存提供抽象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HighQuality]
|
[HighQuality]
|
||||||
internal interface IImageCache : ICastService
|
internal interface IImageCache
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the file path containing cached item for given Uri
|
/// Gets the file path containing cached item for given Uri
|
||||||
|
|||||||
@@ -42,10 +42,20 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
|||||||
private string? baseFolder;
|
private string? baseFolder;
|
||||||
private string? cacheFolder;
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void RemoveInvalid()
|
public void RemoveInvalid()
|
||||||
{
|
{
|
||||||
RemoveInternal(Directory.GetFiles(GetCacheFolder()).Where(file => IsFileInvalid(file, false)));
|
RemoveCore(Directory.GetFiles(CacheFolder).Where(file => IsFileInvalid(file, false)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -62,7 +72,7 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string folder = GetCacheFolder();
|
string folder = CacheFolder;
|
||||||
string[] files = Directory.GetFiles(folder);
|
string[] files = Directory.GetFiles(folder);
|
||||||
|
|
||||||
List<string> filesToDelete = [];
|
List<string> filesToDelete = [];
|
||||||
@@ -75,16 +85,16 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveInternal(filesToDelete);
|
RemoveCore(filesToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri)
|
public async ValueTask<ValueFile> GetFileFromCacheAsync(Uri uri)
|
||||||
{
|
{
|
||||||
string fileName = GetCacheFileName(uri);
|
string fileName = GetCacheFileName(uri);
|
||||||
string filePath = Path.Combine(GetCacheFolder(), fileName);
|
string filePath = Path.Combine(CacheFolder, fileName);
|
||||||
|
|
||||||
if (File.Exists(filePath) && new FileInfo(filePath).Length != 0)
|
if (!IsFileInvalid(filePath))
|
||||||
{
|
{
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
@@ -94,10 +104,12 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
|||||||
{
|
{
|
||||||
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
|
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
|
||||||
{
|
{
|
||||||
|
logger.LogDebug("Begin downloading image file from '{Uri}' to '{File}'", uri, filePath);
|
||||||
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
|
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
|
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
|
||||||
{
|
{
|
||||||
|
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", uri);
|
||||||
await task.ConfigureAwait(false);
|
await task.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +127,7 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
|||||||
public ValueFile GetFileFromCategoryAndName(string category, string fileName)
|
public ValueFile GetFileFromCategoryAndName(string category, string fileName)
|
||||||
{
|
{
|
||||||
Uri dummyUri = Web.HutaoEndpoints.StaticRaw(category, fileName).ToUri();
|
Uri dummyUri = Web.HutaoEndpoints.StaticRaw(category, fileName).ToUri();
|
||||||
return Path.Combine(GetCacheFolder(), GetCacheFileName(dummyUri));
|
return Path.Combine(CacheFolder, GetCacheFileName(dummyUri));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetCacheFileName(Uri uri)
|
private static string GetCacheFileName(Uri uri)
|
||||||
@@ -137,17 +149,18 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
|||||||
return fileInfo.Length == 0;
|
return fileInfo.Length == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveInternal(IEnumerable<string> filePaths)
|
private void RemoveCore(IEnumerable<string> filePaths)
|
||||||
{
|
{
|
||||||
foreach (string filePath in filePaths)
|
foreach (string filePath in filePaths)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.Delete(filePath);
|
File.Delete(filePath);
|
||||||
|
logger.LogInformation("Remove cached image succeed:{File}", filePath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Remove Cache Image Failed:{File}", filePath);
|
logger.LogWarning(ex, "Remove cached image failed:{File}", filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,14 +168,17 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
|||||||
[SuppressMessage("", "SH003")]
|
[SuppressMessage("", "SH003")]
|
||||||
private async Task DownloadFileAsync(Uri uri, string baseFile)
|
private async Task DownloadFileAsync(Uri uri, string baseFile)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Begin downloading for {Uri}", uri);
|
|
||||||
|
|
||||||
int retryCount = 0;
|
int retryCount = 0;
|
||||||
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
|
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
|
||||||
while (retryCount < 3)
|
while (retryCount < 3)
|
||||||
{
|
{
|
||||||
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
if (message.RequestMessage is { RequestUri: { } target } && target != uri)
|
||||||
|
{
|
||||||
|
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
|
||||||
|
}
|
||||||
|
|
||||||
if (message.IsSuccessStatusCode)
|
if (message.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||||
@@ -181,7 +197,7 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
|||||||
{
|
{
|
||||||
retryCount++;
|
retryCount++;
|
||||||
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
|
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
|
||||||
logger.LogInformation("Retry {Uri} after {Delay}.", uri, delay);
|
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
|
||||||
await Task.Delay(delay).ConfigureAwait(false);
|
await Task.Delay(delay).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -192,18 +208,4 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetCacheFolder()
|
|
||||||
{
|
|
||||||
if (cacheFolder is not null)
|
|
||||||
{
|
|
||||||
return cacheFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
baseFolder ??= serviceProvider.GetRequiredService<RuntimeOptions>().LocalCache;
|
|
||||||
DirectoryInfo info = Directory.CreateDirectory(Path.Combine(baseFolder, CacheFolderName));
|
|
||||||
cacheFolder = info.FullName;
|
|
||||||
|
|
||||||
return cacheFolder;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,29 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using CommunityToolkit.WinUI.Collections;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Snap.Hutao.Model;
|
||||||
|
using Snap.Hutao.Model.Entity.Database;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.Database;
|
namespace Snap.Hutao.Core.Database;
|
||||||
|
|
||||||
internal sealed class ObservableReorderableDbCollection<T> : ObservableCollection<T>
|
internal sealed class ObservableReorderableDbCollection<TEntity> : ObservableCollection<TEntity>
|
||||||
where T : class, IReorderable
|
where TEntity : class, IReorderable
|
||||||
{
|
{
|
||||||
private readonly DbContext dbContext;
|
private readonly IServiceProvider serviceProvider;
|
||||||
private bool previousChangeIsRemoved;
|
|
||||||
|
|
||||||
public ObservableReorderableDbCollection(List<T> items, DbContext dbContext)
|
public ObservableReorderableDbCollection(List<TEntity> items, IServiceProvider serviceProvider)
|
||||||
: base(AdjustIndex(items))
|
: base(AdjustIndex(items.SortBy(x => x.Index)))
|
||||||
{
|
{
|
||||||
this.dbContext = dbContext;
|
this.serviceProvider = serviceProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IAdvancedCollectionView? View { get; set; }
|
||||||
|
|
||||||
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnCollectionChanged(e);
|
base.OnCollectionChanged(e);
|
||||||
@@ -27,26 +31,18 @@ internal sealed class ObservableReorderableDbCollection<T> : ObservableCollectio
|
|||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case NotifyCollectionChangedAction.Remove:
|
case NotifyCollectionChangedAction.Remove:
|
||||||
previousChangeIsRemoved = true;
|
|
||||||
break;
|
|
||||||
case NotifyCollectionChangedAction.Add:
|
case NotifyCollectionChangedAction.Add:
|
||||||
if (!previousChangeIsRemoved)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnReorder();
|
OnReorder();
|
||||||
previousChangeIsRemoved = false;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<T> AdjustIndex(List<T> list)
|
private static List<TEntity> AdjustIndex(List<TEntity> list)
|
||||||
{
|
{
|
||||||
Span<T> span = CollectionsMarshal.AsSpan(list);
|
Span<TEntity> span = CollectionsMarshal.AsSpan(list);
|
||||||
for (int i = 0; i < list.Count; i++)
|
for (int i = 0; i < list.Count; i++)
|
||||||
{
|
{
|
||||||
ref readonly T item = ref span[i];
|
ref readonly TEntity item = ref span[i];
|
||||||
item.Index = i;
|
item.Index = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,12 +51,79 @@ internal sealed class ObservableReorderableDbCollection<T> : ObservableCollectio
|
|||||||
|
|
||||||
private void OnReorder()
|
private void OnReorder()
|
||||||
{
|
{
|
||||||
AdjustIndex((List<T>)Items);
|
using (View?.DeferRefresh())
|
||||||
|
|
||||||
DbSet<T> dbSet = dbContext.Set<T>();
|
|
||||||
foreach (ref readonly T item in CollectionsMarshal.AsSpan((List<T>)Items))
|
|
||||||
{
|
{
|
||||||
dbSet.UpdateAndSave(item);
|
AdjustIndex((List<TEntity>)Items);
|
||||||
|
|
||||||
|
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||||
|
foreach (ref readonly TEntity item in CollectionsMarshal.AsSpan((List<TEntity>)Items))
|
||||||
|
{
|
||||||
|
dbSet.UpdateAndSave(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("", "SA1402")]
|
||||||
|
internal sealed class ObservableReorderableDbCollection<TEntityOnly, TEntity> : ObservableCollection<TEntityOnly>
|
||||||
|
where TEntityOnly : class, IEntityOnly<TEntity>
|
||||||
|
where TEntity : class, IReorderable
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider serviceProvider;
|
||||||
|
|
||||||
|
public ObservableReorderableDbCollection(List<TEntityOnly> items, IServiceProvider serviceProvider)
|
||||||
|
: base(AdjustIndex(items.SortBy(x => x.Entity.Index)))
|
||||||
|
{
|
||||||
|
this.serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdvancedCollectionView? View { get; set; }
|
||||||
|
|
||||||
|
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnCollectionChanged(e);
|
||||||
|
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
OnReorder();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<TEntityOnly> AdjustIndex(List<TEntityOnly> list)
|
||||||
|
{
|
||||||
|
Span<TEntityOnly> span = CollectionsMarshal.AsSpan(list);
|
||||||
|
for (int i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
ref readonly TEntityOnly item = ref span[i];
|
||||||
|
item.Entity.Index = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReorder()
|
||||||
|
{
|
||||||
|
using (View?.DeferRefresh())
|
||||||
|
{
|
||||||
|
AdjustIndex((List<TEntityOnly>)Items);
|
||||||
|
|
||||||
|
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
|
||||||
|
DbSet<TEntity> dbSet = appDbContext.Set<TEntity>();
|
||||||
|
foreach (ref readonly TEntityOnly item in CollectionsMarshal.AsSpan((List<TEntityOnly>)Items))
|
||||||
|
{
|
||||||
|
dbSet.UpdateAndSave(item.Entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ namespace Snap.Hutao.Core.Database;
|
|||||||
internal static class QueryableExtension
|
internal static class QueryableExtension
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// source.Where(predicate).ExecuteDelete()
|
/// <code>source.Where(predicate).ExecuteDelete()</code>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TSource">源类型</typeparam>
|
/// <typeparam name="TSource">源类型</typeparam>
|
||||||
/// <param name="source">源</param>
|
/// <param name="source">源</param>
|
||||||
@@ -27,7 +27,7 @@ internal static class QueryableExtension
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// source.Where(predicate).ExecuteDeleteAsync(token)
|
/// <code>source.Where(predicate).ExecuteDeleteAsync(token)</code>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TSource">源类型</typeparam>
|
/// <typeparam name="TSource">源类型</typeparam>
|
||||||
/// <param name="source">源</param>
|
/// <param name="source">源</param>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 可转换类型服务
|
/// 可转换类型服务
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Not useful anymore")]
|
||||||
internal interface ICastService
|
internal interface ICastService
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 有名称的对象
|
|
||||||
/// 指示该对象可通过名称区分
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("无意义的接口")]
|
|
||||||
[HighQuality]
|
|
||||||
internal interface INamedService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 名称
|
|
||||||
/// </summary>
|
|
||||||
string Name { get; }
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 海外服/HoYoLAB 可区分
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Use IOverseaSupportFactory instead")]
|
|
||||||
internal interface IOverseaSupport
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 是否为 海外服/HoYoLAB
|
|
||||||
/// </summary>
|
|
||||||
public bool IsOversea { get; }
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,7 @@ internal static class CastServiceExtension
|
|||||||
/// <typeparam name="T">目标转换类型</typeparam>
|
/// <typeparam name="T">目标转换类型</typeparam>
|
||||||
/// <param name="service">对象</param>
|
/// <param name="service">对象</param>
|
||||||
/// <returns>转换类型后的对象</returns>
|
/// <returns>转换类型后的对象</returns>
|
||||||
|
[Obsolete("Not useful anymore")]
|
||||||
public static T? As<T>(this ICastService service)
|
public static T? As<T>(this ICastService service)
|
||||||
where T : class
|
where T : class
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Snap.Hutao.Core.IO.Http.DynamicProxy;
|
|
||||||
using Snap.Hutao.Core.Logging;
|
using Snap.Hutao.Core.Logging;
|
||||||
using Snap.Hutao.Service;
|
using Snap.Hutao.Service;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Windows.Globalization;
|
using Windows.Globalization;
|
||||||
|
|
||||||
@@ -33,7 +31,7 @@ internal static class DependencyInjection
|
|||||||
.AddJsonOptions()
|
.AddJsonOptions()
|
||||||
.AddDatabase()
|
.AddDatabase()
|
||||||
.AddInjections()
|
.AddInjections()
|
||||||
.AddHttpClients()
|
.AddAllHttpClients()
|
||||||
|
|
||||||
// Discrete services
|
// Discrete services
|
||||||
.AddSingleton<IMessenger, WeakReferenceMessenger>()
|
.AddSingleton<IMessenger, WeakReferenceMessenger>()
|
||||||
@@ -43,7 +41,6 @@ internal static class DependencyInjection
|
|||||||
|
|
||||||
serviceProvider.InitializeConsoleWindow();
|
serviceProvider.InitializeConsoleWindow();
|
||||||
serviceProvider.InitializeCulture();
|
serviceProvider.InitializeCulture();
|
||||||
serviceProvider.InitializedDynamicHttpProxy();
|
|
||||||
|
|
||||||
return serviceProvider;
|
return serviceProvider;
|
||||||
}
|
}
|
||||||
@@ -54,8 +51,13 @@ internal static class DependencyInjection
|
|||||||
CultureOptions cultureOptions = serviceProvider.GetRequiredService<CultureOptions>();
|
CultureOptions cultureOptions = serviceProvider.GetRequiredService<CultureOptions>();
|
||||||
cultureOptions.SystemCulture = CultureInfo.CurrentCulture;
|
cultureOptions.SystemCulture = CultureInfo.CurrentCulture;
|
||||||
|
|
||||||
|
ILogger<CultureOptions> logger = serviceProvider.GetRequiredService<ILogger<CultureOptions>>();
|
||||||
|
logger.LogDebug("System Culture: {System}", cultureOptions.SystemCulture);
|
||||||
|
|
||||||
CultureInfo cultureInfo = cultureOptions.CurrentCulture;
|
CultureInfo cultureInfo = cultureOptions.CurrentCulture;
|
||||||
|
|
||||||
|
logger.LogDebug("Current Culture: {Current}", cultureInfo);
|
||||||
|
|
||||||
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
||||||
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
||||||
CultureInfo.CurrentCulture = cultureInfo;
|
CultureInfo.CurrentCulture = cultureInfo;
|
||||||
@@ -66,13 +68,9 @@ internal static class DependencyInjection
|
|||||||
SH.Culture = cultureInfo;
|
SH.Culture = cultureInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void InitializeConsoleWindow(this IServiceProvider serviceProvider)
|
private static void InitializeConsoleWindow(this IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
_ = serviceProvider.GetRequiredService<ConsoleWindowLifeTime>();
|
_ = serviceProvider.GetRequiredService<ConsoleWindowLifeTime>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void InitializedDynamicHttpProxy(this IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
HttpClient.DefaultProxy = serviceProvider.GetRequiredService<DynamicHttpProxy>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -47,17 +47,13 @@ internal static class IocConfiguration
|
|||||||
{
|
{
|
||||||
if (context.Database.GetPendingMigrations().Any())
|
if (context.Database.GetPendingMigrations().Any())
|
||||||
{
|
{
|
||||||
#if DEBUG
|
|
||||||
System.Diagnostics.Debug.WriteLine("[Database] Performing AppDbContext Migrations");
|
System.Diagnostics.Debug.WriteLine("[Database] Performing AppDbContext Migrations");
|
||||||
#endif
|
|
||||||
context.Database.Migrate();
|
context.Database.Migrate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
builder
|
builder
|
||||||
#if DEBUG
|
|
||||||
.EnableSensitiveDataLogging()
|
.EnableSensitiveDataLogging()
|
||||||
#endif
|
|
||||||
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
|
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
|
||||||
.UseSqlite(sqlConnectionString);
|
.UseSqlite(sqlConnectionString);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core.IO.Http.Proxy;
|
||||||
using Snap.Hutao.Web.Hoyolab;
|
using Snap.Hutao.Web.Hoyolab;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
||||||
@@ -14,12 +15,27 @@ internal static partial class IocHttpClientConfiguration
|
|||||||
{
|
{
|
||||||
private const string ApplicationJson = "application/json";
|
private const string ApplicationJson = "application/json";
|
||||||
|
|
||||||
/// <summary>
|
public static IServiceCollection AddAllHttpClients(this IServiceCollection services)
|
||||||
/// 添加 <see cref="HttpClient"/>
|
{
|
||||||
/// 此方法将会自动生成
|
services
|
||||||
/// </summary>
|
.ConfigureHttpClientDefaults(clientBuilder =>
|
||||||
/// <param name="services">集合</param>
|
{
|
||||||
/// <returns>可继续操作的集合</returns>
|
clientBuilder
|
||||||
|
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler())
|
||||||
|
.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
|
||||||
|
{
|
||||||
|
HttpClientHandler clientHandler = (HttpClientHandler)handler;
|
||||||
|
clientHandler.AllowAutoRedirect = true;
|
||||||
|
clientHandler.UseProxy = true;
|
||||||
|
clientHandler.Proxy = provider.GetRequiredService<DynamicHttpProxy>();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.AddHttpClients();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public static partial IServiceCollection AddHttpClients(this IServiceCollection services);
|
public static partial IServiceCollection AddHttpClients(this IServiceCollection services);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -89,6 +105,7 @@ internal static partial class IocHttpClientConfiguration
|
|||||||
/// HoYoLAB web
|
/// HoYoLAB web
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="client">配置后的客户端</param>
|
/// <param name="client">配置后的客户端</param>
|
||||||
|
[SuppressMessage("", "IDE0051")]
|
||||||
private static void XRpc4Configuration(HttpClient client)
|
private static void XRpc4Configuration(HttpClient client)
|
||||||
{
|
{
|
||||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ internal readonly struct MeasureExecutionToken : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
logger.LogInformation("{Caller} toke {Time} ms", callerName, stopwatch.GetElapsedTime().TotalMilliseconds);
|
logger.LogDebug("{Caller} toke {Time} ms", callerName, stopwatch.GetElapsedTime().TotalMilliseconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ namespace Snap.Hutao.Core.ExceptionService;
|
|||||||
/// 数据库损坏异常
|
/// 数据库损坏异常
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HighQuality]
|
[HighQuality]
|
||||||
|
[Obsolete("Use HutaoException instead")]
|
||||||
internal sealed class DatabaseCorruptedException : Exception
|
internal sealed class DatabaseCorruptedException : Exception
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -23,8 +23,16 @@ internal sealed partial class ExceptionRecorder
|
|||||||
public void Record(Application app)
|
public void Record(Application app)
|
||||||
{
|
{
|
||||||
app.UnhandledException += OnAppUnhandledException;
|
app.UnhandledException += OnAppUnhandledException;
|
||||||
|
app.DebugSettings.FailFastOnErrors = false;
|
||||||
|
|
||||||
|
app.DebugSettings.IsBindingTracingEnabled = true;
|
||||||
app.DebugSettings.BindingFailed += OnXamlBindingFailed;
|
app.DebugSettings.BindingFailed += OnXamlBindingFailed;
|
||||||
|
|
||||||
|
app.DebugSettings.IsXamlResourceReferenceTracingEnabled = true;
|
||||||
app.DebugSettings.XamlResourceReferenceFailed += OnXamlResourceReferenceFailed;
|
app.DebugSettings.XamlResourceReferenceFailed += OnXamlResourceReferenceFailed;
|
||||||
|
|
||||||
|
app.DebugSettings.LayoutCycleTracingLevel = LayoutCycleTracingLevel.High;
|
||||||
|
app.DebugSettings.LayoutCycleDebugBreakLevel = LayoutCycleDebugBreakLevel.High;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("", "CA2012")]
|
[SuppressMessage("", "CA2012")]
|
||||||
|
|||||||
@@ -11,10 +11,30 @@ internal sealed class HutaoException : Exception
|
|||||||
Kind = kind;
|
Kind = kind;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HutaoException(string message, Exception? innerException)
|
private HutaoException(string message, Exception? innerException)
|
||||||
: base($"{message}\n{innerException?.Message}", innerException)
|
: base($"{message}\n{innerException?.Message}", innerException)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public HutaoExceptionKind Kind { get; private set; }
|
public HutaoExceptionKind Kind { get; private set; }
|
||||||
|
|
||||||
|
[DoesNotReturn]
|
||||||
|
public static HutaoException Throw(HutaoExceptionKind kind, string message, Exception? innerException = default)
|
||||||
|
{
|
||||||
|
throw new HutaoException(kind, message, innerException);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ThrowIf(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
{
|
||||||
|
throw new HutaoException(kind, message, innerException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HutaoException ServiceTypeCastFailed<TFrom, TTo>(string name, Exception? innerException = default)
|
||||||
|
{
|
||||||
|
string message = $"This instance of '{typeof(TFrom).FullName}' '{name}' doesn't implement '{typeof(TTo).FullName}'";
|
||||||
|
throw new HutaoException(HutaoExceptionKind.ServiceTypeCastFailed, message, innerException);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,4 +6,7 @@ namespace Snap.Hutao.Core.ExceptionService;
|
|||||||
internal enum HutaoExceptionKind
|
internal enum HutaoExceptionKind
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
|
ServiceTypeCastFailed,
|
||||||
|
FileSystemCreateFileInsufficientPermissions,
|
||||||
|
PrivateNamedPipeContentHashIncorrect,
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ namespace Snap.Hutao.Core.ExceptionService;
|
|||||||
/// 用户的计算机中的某些设置不符合要求
|
/// 用户的计算机中的某些设置不符合要求
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HighQuality]
|
[HighQuality]
|
||||||
|
[Obsolete("Use HutaoException instead")]
|
||||||
internal sealed class RuntimeEnvironmentException : Exception
|
internal sealed class RuntimeEnvironmentException : Exception
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using Snap.Hutao.Service.Game;
|
using Snap.Hutao.Service.Game;
|
||||||
using Snap.Hutao.Service.Game.Package;
|
using Snap.Hutao.Service.Game.Package;
|
||||||
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.ExceptionService;
|
namespace Snap.Hutao.Core.ExceptionService;
|
||||||
@@ -12,6 +13,7 @@ namespace Snap.Hutao.Core.ExceptionService;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[HighQuality]
|
[HighQuality]
|
||||||
[System.Diagnostics.StackTraceHidden]
|
[System.Diagnostics.StackTraceHidden]
|
||||||
|
[Obsolete("Use HutaoException instead")]
|
||||||
internal static class ThrowHelper
|
internal static class ThrowHelper
|
||||||
{
|
{
|
||||||
[DoesNotReturn]
|
[DoesNotReturn]
|
||||||
@@ -35,6 +37,22 @@ internal static class ThrowHelper
|
|||||||
throw new GameFileOperationException(message, 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]
|
[DoesNotReturn]
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
public static InvalidOperationException InvalidOperation(string message, Exception? inner = default)
|
public static InvalidOperationException InvalidOperation(string message, Exception? inner = default)
|
||||||
@@ -56,6 +74,15 @@ internal static class ThrowHelper
|
|||||||
throw new NotSupportedException(message);
|
throw new NotSupportedException(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
public static void NotSupportedIf(bool condition, string message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[DoesNotReturn]
|
[DoesNotReturn]
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
public static OperationCanceledException OperationCanceled(string message, Exception? inner = default)
|
public static OperationCanceledException OperationCanceled(string message, Exception? inner = default)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Snap.Hutao.Core.ExceptionService;
|
|||||||
/// 用户数据损坏异常
|
/// 用户数据损坏异常
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HighQuality]
|
[HighQuality]
|
||||||
|
[Obsolete("Use HutaoException instead")]
|
||||||
internal sealed class UserdataCorruptedException : Exception
|
internal sealed class UserdataCorruptedException : Exception
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Snap.Hutao.Win32.Foundation;
|
||||||
|
using Snap.Hutao.Win32.NetworkManagement.WindowsFirewall;
|
||||||
|
using Snap.Hutao.Win32.Security;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using static Snap.Hutao.Win32.AdvApi32;
|
||||||
|
using static Snap.Hutao.Win32.ApiMsWinNetIsolation;
|
||||||
|
using static Snap.Hutao.Win32.Macros;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.IO.Http.Loopback;
|
||||||
|
|
||||||
|
[Injection(InjectAs.Singleton)]
|
||||||
|
internal sealed unsafe class LoopbackManager : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly RuntimeOptions runtimeOptions;
|
||||||
|
private readonly ITaskContext taskContext;
|
||||||
|
|
||||||
|
private readonly string hutaoContainerStringSID = default!;
|
||||||
|
private bool isLoopbackEnabled;
|
||||||
|
|
||||||
|
public LoopbackManager(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||||
|
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
|
||||||
|
|
||||||
|
INET_FIREWALL_APP_CONTAINER* pContainers = default;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
{
|
||||||
|
WIN32_ERROR error = NetworkIsolationEnumAppContainers(NETISO_FLAG.NETISO_FLAG_MAX, out uint acCount, out pContainers);
|
||||||
|
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
|
||||||
|
for (uint i = 0; i < acCount; i++)
|
||||||
|
{
|
||||||
|
INET_FIREWALL_APP_CONTAINER* pContainer = pContainers + i;
|
||||||
|
ReadOnlySpan<char> appContainerName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pContainer->appContainerName);
|
||||||
|
if (appContainerName.Equals(runtimeOptions.FamilyName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
ConvertSidToStringSidW(pContainer->appContainerSid, out PWSTR stringSid);
|
||||||
|
hutaoContainerStringSID = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid).ToString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// This function returns 1 rather than 0 specfied in the document.
|
||||||
|
_ = NetworkIsolationFreeAppContainers(pContainers);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
WIN32_ERROR 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsLoopbackEnabled { get => isLoopbackEnabled; private set => SetProperty(ref isLoopbackEnabled, value); }
|
||||||
|
|
||||||
|
public void EnableLoopback()
|
||||||
|
{
|
||||||
|
NetworkIsolationGetAppContainerConfig(out uint accCount, out SID_AND_ATTRIBUTES* pSids);
|
||||||
|
List<SID_AND_ATTRIBUTES> sids = new((int)(accCount + 1));
|
||||||
|
for (uint i = 0; i < accCount; i++)
|
||||||
|
{
|
||||||
|
sids.Add(*(pSids + i));
|
||||||
|
}
|
||||||
|
|
||||||
|
ConvertStringSidToSidW(hutaoContainerStringSID, out PSID pSid);
|
||||||
|
SID_AND_ATTRIBUTES sidAndAttributes = default;
|
||||||
|
sidAndAttributes.Sid = pSid;
|
||||||
|
sids.Add(sidAndAttributes);
|
||||||
|
IsLoopbackEnabled = NetworkIsolationSetAppContainerConfig(CollectionsMarshal.AsSpan(sids)) is WIN32_ERROR.ERROR_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +1,46 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using Snap.Hutao.Win32.Registry;
|
using Snap.Hutao.Win32.Registry;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.IO.Http.DynamicProxy;
|
namespace Snap.Hutao.Core.IO.Http.Proxy;
|
||||||
|
|
||||||
[Injection(InjectAs.Singleton)]
|
[Injection(InjectAs.Singleton)]
|
||||||
internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable
|
internal sealed partial class DynamicHttpProxy : ObservableObject, IWebProxy, IDisposable
|
||||||
{
|
{
|
||||||
private const string ProxySettingPath = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections";
|
private const string ProxySettingPath = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections";
|
||||||
|
|
||||||
private static readonly MethodInfo ConstructSystemProxyMethod;
|
private static readonly Lazy<MethodInfo> LazyConstructSystemProxyMethod = new(GetConstructSystemProxyMethod);
|
||||||
|
|
||||||
|
private readonly IServiceProvider serviceProvider;
|
||||||
private readonly RegistryWatcher watcher;
|
private readonly RegistryWatcher watcher;
|
||||||
|
|
||||||
private IWebProxy innerProxy = default!;
|
private IWebProxy innerProxy = default!;
|
||||||
|
|
||||||
static DynamicHttpProxy()
|
public DynamicHttpProxy(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
Type? systemProxyInfoType = typeof(System.Net.Http.SocketsHttpHandler).Assembly.GetType("System.Net.Http.SystemProxyInfo");
|
this.serviceProvider = serviceProvider;
|
||||||
ArgumentNullException.ThrowIfNull(systemProxyInfoType);
|
UpdateInnerProxy();
|
||||||
|
|
||||||
MethodInfo? constructSystemProxyMethod = systemProxyInfoType.GetMethod("ConstructSystemProxy", BindingFlags.Static | BindingFlags.Public);
|
watcher = new(ProxySettingPath, OnSystemProxySettingsChanged);
|
||||||
ArgumentNullException.ThrowIfNull(constructSystemProxyMethod);
|
|
||||||
ConstructSystemProxyMethod = constructSystemProxyMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DynamicHttpProxy()
|
|
||||||
{
|
|
||||||
UpdateProxy();
|
|
||||||
|
|
||||||
watcher = new(ProxySettingPath, UpdateProxy);
|
|
||||||
watcher.Start();
|
watcher.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
public string CurrentProxyUri
|
||||||
public ICredentials? Credentials
|
|
||||||
{
|
{
|
||||||
get => InnerProxy.Credentials;
|
get
|
||||||
set => InnerProxy.Credentials = value;
|
{
|
||||||
|
Uri? proxyUri = GetProxy("https://hut.ao".ToUri());
|
||||||
|
return proxyUri is null
|
||||||
|
? SH.ViewPageFeedbackCurrentProxyNoProxyDescription
|
||||||
|
: proxyUri.AbsoluteUri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IWebProxy InnerProxy
|
public IWebProxy InnerProxy
|
||||||
{
|
{
|
||||||
get => innerProxy;
|
get => innerProxy;
|
||||||
|
|
||||||
@@ -60,13 +57,10 @@ internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[MemberNotNull(nameof(innerProxy))]
|
public ICredentials? Credentials
|
||||||
public void UpdateProxy()
|
|
||||||
{
|
{
|
||||||
IWebProxy? proxy = ConstructSystemProxyMethod.Invoke(default, default) as IWebProxy;
|
get => InnerProxy.Credentials;
|
||||||
ArgumentNullException.ThrowIfNull(proxy);
|
set => InnerProxy.Credentials = value;
|
||||||
|
|
||||||
InnerProxy = proxy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uri? GetProxy(Uri destination)
|
public Uri? GetProxy(Uri destination)
|
||||||
@@ -84,4 +78,33 @@ internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable
|
|||||||
(innerProxy as IDisposable)?.Dispose();
|
(innerProxy as IDisposable)?.Dispose();
|
||||||
watcher.Dispose();
|
watcher.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnSystemProxySettingsChanged()
|
||||||
|
{
|
||||||
|
UpdateInnerProxy();
|
||||||
|
|
||||||
|
// TaskContext can't be injected directly since there are some recursive dependencies.
|
||||||
|
ITaskContext taskContext = serviceProvider.GetRequiredService<ITaskContext>();
|
||||||
|
taskContext.BeginInvokeOnMainThread(() => OnPropertyChanged(nameof(CurrentProxyUri)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodInfo GetConstructSystemProxyMethod()
|
||||||
|
{
|
||||||
|
Type? systemProxyInfoType = typeof(System.Net.Http.SocketsHttpHandler).Assembly.GetType("System.Net.Http.SystemProxyInfo");
|
||||||
|
ArgumentNullException.ThrowIfNull(systemProxyInfoType);
|
||||||
|
|
||||||
|
MethodInfo? constructSystemProxyMethod = systemProxyInfoType.GetMethod("ConstructSystemProxy", BindingFlags.Static | BindingFlags.Public);
|
||||||
|
ArgumentNullException.ThrowIfNull(constructSystemProxyMethod);
|
||||||
|
|
||||||
|
return constructSystemProxyMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(innerProxy))]
|
||||||
|
private void UpdateInnerProxy()
|
||||||
|
{
|
||||||
|
IWebProxy? proxy = LazyConstructSystemProxyMethod.Value.Invoke(default, default) as IWebProxy;
|
||||||
|
ArgumentNullException.ThrowIfNull(proxy);
|
||||||
|
|
||||||
|
InnerProxy = proxy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
|
|||||||
private readonly long contentLength;
|
private readonly long contentLength;
|
||||||
private readonly int bufferSize;
|
private readonly int bufferSize;
|
||||||
private readonly SafeFileHandle destFileHandle;
|
private readonly SafeFileHandle destFileHandle;
|
||||||
|
private readonly int maxDegreeOfParallelism;
|
||||||
private readonly List<Shard> shards;
|
private readonly List<Shard> shards;
|
||||||
|
|
||||||
private HttpShardCopyWorker(HttpShardCopyWorkerOptions<TStatus> options)
|
private HttpShardCopyWorker(HttpShardCopyWorkerOptions<TStatus> options)
|
||||||
@@ -28,6 +29,7 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
|
|||||||
contentLength = options.ContentLength;
|
contentLength = options.ContentLength;
|
||||||
bufferSize = options.BufferSize;
|
bufferSize = options.BufferSize;
|
||||||
destFileHandle = options.GetFileHandle();
|
destFileHandle = options.GetFileHandle();
|
||||||
|
maxDegreeOfParallelism = options.MaxDegreeOfParallelism;
|
||||||
shards = CalculateShards(contentLength);
|
shards = CalculateShards(contentLength);
|
||||||
|
|
||||||
static List<Shard> CalculateShards(long contentLength)
|
static List<Shard> CalculateShards(long contentLength)
|
||||||
@@ -56,7 +58,11 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
|
|||||||
public Task CopyAsync(IProgress<TStatus> progress, CancellationToken token = default)
|
public Task CopyAsync(IProgress<TStatus> progress, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
ShardProgress shardProgress = new(progress, statusFactory, contentLength);
|
ShardProgress shardProgress = new(progress, statusFactory, contentLength);
|
||||||
return Parallel.ForEachAsync(shards, token, (shard, token) => CopyShardAsync(shard, shardProgress, token));
|
ParallelOptions parallelOptions = new()
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = maxDegreeOfParallelism,
|
||||||
|
};
|
||||||
|
return Parallel.ForEachAsync(shards, parallelOptions, (shard, token) => CopyShardAsync(shard, shardProgress, token));
|
||||||
|
|
||||||
async ValueTask CopyShardAsync(Shard shard, IProgress<ShardStatus> progress, CancellationToken token)
|
async ValueTask CopyShardAsync(Shard shard, IProgress<ShardStatus> progress, CancellationToken token)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ internal sealed class HttpShardCopyWorkerOptions<TStatus>
|
|||||||
|
|
||||||
public int BufferSize { get; set; } = 80 * 1024;
|
public int BufferSize { get; set; } = 80 * 1024;
|
||||||
|
|
||||||
|
public int MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount;
|
||||||
|
|
||||||
public SafeFileHandle GetFileHandle()
|
public SafeFileHandle GetFileHandle()
|
||||||
{
|
{
|
||||||
return File.OpenHandle(DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.RandomAccess | FileOptions.Asynchronous, ContentLength);
|
return File.OpenHandle(DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.RandomAccess | FileOptions.Asynchronous, ContentLength);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ internal readonly struct TempFile : IDisposable
|
|||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
ThrowHelper.RuntimeEnvironment(SH.CoreIOTempFileCreateFail, ex);
|
HutaoException.Throw(HutaoExceptionKind.FileSystemCreateFileInsufficientPermissions, SH.CoreIOTempFileCreateFail, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delete)
|
if (delete)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
using CommunityToolkit.WinUI.Notifications;
|
using CommunityToolkit.WinUI.Notifications;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Windows.AppLifecycle;
|
using Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||||
using Snap.Hutao.Core.Setting;
|
using Snap.Hutao.Core.Setting;
|
||||||
using Snap.Hutao.Service.DailyNote;
|
using Snap.Hutao.Service.DailyNote;
|
||||||
using Snap.Hutao.Service.Discord;
|
using Snap.Hutao.Service.Discord;
|
||||||
@@ -24,24 +24,9 @@ namespace Snap.Hutao.Core.LifeCycle;
|
|||||||
[SuppressMessage("", "CA1001")]
|
[SuppressMessage("", "CA1001")]
|
||||||
internal sealed partial class Activation : IActivation
|
internal sealed partial class Activation : IActivation
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 操作
|
|
||||||
/// </summary>
|
|
||||||
public const string Action = nameof(Action);
|
public const string Action = nameof(Action);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Uid
|
|
||||||
/// </summary>
|
|
||||||
public const string Uid = nameof(Uid);
|
public const string Uid = nameof(Uid);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 启动游戏启动参数
|
|
||||||
/// </summary>
|
|
||||||
public const string LaunchGame = nameof(LaunchGame);
|
public const string LaunchGame = nameof(LaunchGame);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 从剪贴板导入成就
|
|
||||||
/// </summary>
|
|
||||||
public const string ImportUIAFFromClipboard = nameof(ImportUIAFFromClipboard);
|
public const string ImportUIAFFromClipboard = nameof(ImportUIAFFromClipboard);
|
||||||
|
|
||||||
private const string CategoryAchievement = "ACHIEVEMENT";
|
private const string CategoryAchievement = "ACHIEVEMENT";
|
||||||
@@ -55,29 +40,20 @@ internal sealed partial class Activation : IActivation
|
|||||||
private readonly SemaphoreSlim activateSemaphore = new(1);
|
private readonly SemaphoreSlim activateSemaphore = new(1);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Activate(object? sender, AppActivationArguments args)
|
public void Activate(HutaoActivationArguments args)
|
||||||
{
|
{
|
||||||
_ = sender;
|
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
|
||||||
if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
|
|
||||||
{
|
{
|
||||||
HandleActivationAsync(args, true).SafeForget();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HandleActivationAsync(args).SafeForget();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void NonRedirectToActivate(object? sender, AppActivationArguments args)
|
public void Initialize()
|
||||||
{
|
{
|
||||||
_ = sender;
|
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget();
|
||||||
if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
|
|
||||||
{
|
|
||||||
HandleActivationAsync(args, false).SafeForget();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void InitializeWith(AppInstance appInstance)
|
|
||||||
{
|
|
||||||
appInstance.Activated += Activate;
|
|
||||||
ToastNotificationManagerCompat.OnActivated += NotificationActivate;
|
ToastNotificationManagerCompat.OnActivated += NotificationActivate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,44 +71,40 @@ internal sealed partial class Activation : IActivation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask HandleActivationAsync(AppActivationArguments args, bool isRedirected)
|
private async ValueTask HandleActivationAsync(HutaoActivationArguments args)
|
||||||
{
|
{
|
||||||
if (activateSemaphore.CurrentCount > 0)
|
if (activateSemaphore.CurrentCount > 0)
|
||||||
{
|
{
|
||||||
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
|
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await HandleActivationCoreAsync(args, isRedirected).ConfigureAwait(false);
|
await HandleActivationCoreAsync(args).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask HandleActivationCoreAsync(AppActivationArguments args, bool isRedirected)
|
private async ValueTask HandleActivationCoreAsync(HutaoActivationArguments args)
|
||||||
{
|
{
|
||||||
if (args.Kind == ExtendedActivationKind.Protocol)
|
if (args.Kind is HutaoActivationKind.Protocol)
|
||||||
{
|
{
|
||||||
if (args.TryGetProtocolActivatedUri(out Uri? uri))
|
ArgumentNullException.ThrowIfNull(args.ProtocolActivatedUri);
|
||||||
{
|
await HandleUrlActivationAsync(args.ProtocolActivatedUri, args.IsRedirectTo).ConfigureAwait(false);
|
||||||
await HandleUrlActivationAsync(uri, isRedirected).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (args.Kind == ExtendedActivationKind.Launch)
|
else if (args.Kind is HutaoActivationKind.Launch)
|
||||||
{
|
{
|
||||||
if (args.TryGetLaunchActivatedArgument(out string? arguments))
|
ArgumentNullException.ThrowIfNull(args.LaunchActivatedArguments);
|
||||||
|
switch (args.LaunchActivatedArguments)
|
||||||
{
|
{
|
||||||
switch (arguments)
|
case LaunchGame:
|
||||||
{
|
{
|
||||||
case LaunchGame:
|
await HandleLaunchGameActionAsync().ConfigureAwait(false);
|
||||||
{
|
break;
|
||||||
await HandleLaunchGameActionAsync().ConfigureAwait(false);
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
|
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,17 +148,15 @@ internal sealed partial class Activation : IActivation
|
|||||||
|
|
||||||
await taskContext.SwitchToBackgroundAsync();
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
|
|
||||||
serviceProvider
|
if (serviceProvider.GetRequiredService<IMetadataService>() is IMetadataServiceInitialization metadataServiceInitialization)
|
||||||
.GetRequiredService<IMetadataService>()
|
{
|
||||||
.As<IMetadataServiceInitialization>()?
|
metadataServiceInitialization.InitializeInternalAsync().SafeForget();
|
||||||
.InitializeInternalAsync()
|
}
|
||||||
.SafeForget();
|
|
||||||
|
|
||||||
serviceProvider
|
if (serviceProvider.GetRequiredService<IHutaoUserService>() is IHutaoUserServiceInitialization hutaoUserServiceInitialization)
|
||||||
.GetRequiredService<IHutaoUserService>()
|
{
|
||||||
.As<IHutaoUserServiceInitialization>()?
|
hutaoUserServiceInitialization.InitializeInternalAsync().SafeForget();
|
||||||
.InitializeInternalAsync()
|
}
|
||||||
.SafeForget();
|
|
||||||
|
|
||||||
serviceProvider
|
serviceProvider
|
||||||
.GetRequiredService<IDiscordService>()
|
.GetRequiredService<IDiscordService>()
|
||||||
@@ -194,7 +164,7 @@ internal sealed partial class Activation : IActivation
|
|||||||
.SafeForget();
|
.SafeForget();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirected)
|
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirectTo)
|
||||||
{
|
{
|
||||||
UriBuilder builder = new(uri);
|
UriBuilder builder = new(uri);
|
||||||
|
|
||||||
@@ -207,13 +177,13 @@ internal sealed partial class Activation : IActivation
|
|||||||
case CategoryAchievement:
|
case CategoryAchievement:
|
||||||
{
|
{
|
||||||
await WaitMainWindowAsync().ConfigureAwait(false);
|
await WaitMainWindowAsync().ConfigureAwait(false);
|
||||||
await HandleAchievementActionAsync(action, parameter, isRedirected).ConfigureAwait(false);
|
await HandleAchievementActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case CategoryDailyNote:
|
case CategoryDailyNote:
|
||||||
{
|
{
|
||||||
await HandleDailyNoteActionAsync(action, parameter, isRedirected).ConfigureAwait(false);
|
await HandleDailyNoteActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,10 +195,10 @@ internal sealed partial class Activation : IActivation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirected)
|
private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirectTo)
|
||||||
{
|
{
|
||||||
_ = parameter;
|
_ = parameter;
|
||||||
_ = isRedirected;
|
_ = isRedirectTo;
|
||||||
switch (action)
|
switch (action)
|
||||||
{
|
{
|
||||||
case UrlActionImport:
|
case UrlActionImport:
|
||||||
@@ -245,7 +215,7 @@ internal sealed partial class Activation : IActivation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirected)
|
private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirectTo)
|
||||||
{
|
{
|
||||||
_ = parameter;
|
_ = parameter;
|
||||||
switch (action)
|
switch (action)
|
||||||
@@ -264,7 +234,7 @@ internal sealed partial class Activation : IActivation
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's redirected.
|
// Check if it's redirected.
|
||||||
if (!isRedirected)
|
if (!isRedirectTo)
|
||||||
{
|
{
|
||||||
// It's a direct open process, should exit immediately.
|
// It's a direct open process, should exit immediately.
|
||||||
Process.GetCurrentProcess().Kill();
|
Process.GetCurrentProcess().Kill();
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ internal static class AppActivationArgumentsExtensions
|
|||||||
/// <param name="activatedEventArgs">应用程序激活参数</param>
|
/// <param name="activatedEventArgs">应用程序激活参数</param>
|
||||||
/// <param name="arguments">参数</param>
|
/// <param name="arguments">参数</param>
|
||||||
/// <returns>是否存在参数</returns>
|
/// <returns>是否存在参数</returns>
|
||||||
public static bool TryGetLaunchActivatedArgument(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
|
public static bool TryGetLaunchActivatedArguments(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
|
||||||
{
|
{
|
||||||
arguments = null;
|
arguments = null;
|
||||||
if (activatedEventArgs.Data is not ILaunchActivatedEventArgs launchArgs)
|
if (activatedEventArgs.Data is not ILaunchActivatedEventArgs launchArgs)
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Microsoft.Windows.AppLifecycle;
|
using Microsoft.Windows.AppLifecycle;
|
||||||
using Windows.Win32.Foundation;
|
using Snap.Hutao.Win32.Foundation;
|
||||||
using Windows.Win32.Security;
|
using Snap.Hutao.Win32.System.Com;
|
||||||
using Windows.Win32.System.Com;
|
using static Snap.Hutao.Win32.ConstValues;
|
||||||
using static Windows.Win32.PInvoke;
|
using static Snap.Hutao.Win32.Kernel32;
|
||||||
|
using static Snap.Hutao.Win32.Ole32;
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.LifeCycle;
|
namespace Snap.Hutao.Core.LifeCycle;
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ internal static class AppInstanceExtension
|
|||||||
private static readonly WaitCallback RunActionWaitCallback = RunAction;
|
private static readonly WaitCallback RunActionWaitCallback = RunAction;
|
||||||
|
|
||||||
// Hold the reference here to prevent memory corruption.
|
// Hold the reference here to prevent memory corruption.
|
||||||
private static HANDLE redirectEventHandle = HANDLE.Null;
|
private static HANDLE redirectEventHandle;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 同步非阻塞重定向
|
/// 同步非阻塞重定向
|
||||||
@@ -27,19 +28,24 @@ internal static class AppInstanceExtension
|
|||||||
/// <param name="args">参数</param>
|
/// <param name="args">参数</param>
|
||||||
public static unsafe void RedirectActivationTo(this AppInstance appInstance, AppActivationArguments args)
|
public static unsafe void RedirectActivationTo(this AppInstance appInstance, AppActivationArguments args)
|
||||||
{
|
{
|
||||||
redirectEventHandle = CreateEvent(default(SECURITY_ATTRIBUTES*), true, false, null);
|
try
|
||||||
|
|
||||||
// use ThreadPool.UnsafeQueueUserWorkItem to cancel stacktrace
|
|
||||||
// like ExecutionContext.SuppressFlow
|
|
||||||
ThreadPool.UnsafeQueueUserWorkItem(RunActionWaitCallback, () =>
|
|
||||||
{
|
{
|
||||||
appInstance.RedirectActivationToAsync(args).AsTask().Wait();
|
redirectEventHandle = CreateEventW(default, true, false, default);
|
||||||
SetEvent(redirectEventHandle);
|
|
||||||
});
|
|
||||||
|
|
||||||
ReadOnlySpan<HANDLE> handles = new(ref redirectEventHandle);
|
// use ThreadPool.UnsafeQueueUserWorkItem to cancel stacktrace
|
||||||
CoWaitForMultipleObjects((uint)CWMO_FLAGS.CWMO_DEFAULT, INFINITE, handles, out uint _);
|
// like ExecutionContext.SuppressFlow
|
||||||
CloseHandle(redirectEventHandle);
|
ThreadPool.UnsafeQueueUserWorkItem(RunActionWaitCallback, () =>
|
||||||
|
{
|
||||||
|
appInstance.RedirectActivationToAsync(args).AsTask().Wait();
|
||||||
|
SetEvent(redirectEventHandle);
|
||||||
|
});
|
||||||
|
|
||||||
|
CoWaitForMultipleObjects(CWMO_FLAGS.CWMO_DEFAULT, INFINITE, [redirectEventHandle], out uint _);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseHandle(redirectEventHandle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("", "SH007")]
|
[SuppressMessage("", "SH007")]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Snap.Hutao.Core.Windowing;
|
using Snap.Hutao.Core.Windowing;
|
||||||
using Windows.Win32.Foundation;
|
using Snap.Hutao.Win32.Foundation;
|
||||||
using WinRT.Interop;
|
using WinRT.Interop;
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.LifeCycle;
|
namespace Snap.Hutao.Core.LifeCycle;
|
||||||
@@ -19,6 +19,6 @@ internal static class CurrentWindowReferenceExtension
|
|||||||
{
|
{
|
||||||
return reference.Window is IWindowOptionsSource optionsSource
|
return reference.Window is IWindowOptionsSource optionsSource
|
||||||
? optionsSource.WindowOptions.Hwnd
|
? optionsSource.WindowOptions.Hwnd
|
||||||
: (HWND)WindowNative.GetWindowHandle(reference.Window);
|
: WindowNative.GetWindowHandle(reference.Window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.Windows.AppLifecycle;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.LifeCycle;
|
||||||
|
|
||||||
|
internal sealed class HutaoActivationArguments
|
||||||
|
{
|
||||||
|
public bool IsRedirectTo { get; set; }
|
||||||
|
|
||||||
|
public HutaoActivationKind Kind { get; set; }
|
||||||
|
|
||||||
|
public Uri? ProtocolActivatedUri { get; set; }
|
||||||
|
|
||||||
|
public string? LaunchActivatedArguments { get; set; }
|
||||||
|
|
||||||
|
public static HutaoActivationArguments FromAppActivationArguments(AppActivationArguments args, bool isRedirected = false)
|
||||||
|
{
|
||||||
|
HutaoActivationArguments result = new()
|
||||||
|
{
|
||||||
|
IsRedirectTo = isRedirected,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (args.Kind)
|
||||||
|
{
|
||||||
|
case ExtendedActivationKind.Launch:
|
||||||
|
{
|
||||||
|
result.Kind = HutaoActivationKind.Launch;
|
||||||
|
if (args.TryGetLaunchActivatedArguments(out string? arguments))
|
||||||
|
{
|
||||||
|
result.LaunchActivatedArguments = arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ExtendedActivationKind.Protocol:
|
||||||
|
{
|
||||||
|
result.Kind = HutaoActivationKind.Protocol;
|
||||||
|
if (args.TryGetProtocolActivatedUri(out Uri? uri))
|
||||||
|
{
|
||||||
|
result.ProtocolActivatedUri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.LifeCycle;
|
||||||
|
|
||||||
|
internal enum HutaoActivationKind
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Launch,
|
||||||
|
Protocol,
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Microsoft.Windows.AppLifecycle;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.LifeCycle;
|
namespace Snap.Hutao.Core.LifeCycle;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -10,24 +8,7 @@ namespace Snap.Hutao.Core.LifeCycle;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal interface IActivation
|
internal interface IActivation
|
||||||
{
|
{
|
||||||
/// <summary>
|
void Activate(HutaoActivationArguments args);
|
||||||
/// 响应激活事件
|
|
||||||
/// 激活事件一般不会在UI线程上触发
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender">发送方</param>
|
|
||||||
/// <param name="args">激活参数</param>
|
|
||||||
void Activate(object? sender, AppActivationArguments args);
|
|
||||||
|
|
||||||
/// <summary>
|
void Initialize();
|
||||||
/// 使用当前 App 实例初始化激活
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="appInstance">App 实例</param>
|
|
||||||
void InitializeWith(AppInstance appInstance);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 无转发触发激活事件
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender">发送方</param>
|
|
||||||
/// <param name="args">激活参数</param>
|
|
||||||
void NonRedirectToActivate(object? sender, AppActivationArguments args);
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.IO.Pipes;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||||
|
|
||||||
|
internal static class NamedPipeClientStreamExtension
|
||||||
|
{
|
||||||
|
public static bool TryConnectOnce(this NamedPipeClientStream clientStream)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
clientStream.Connect(TimeSpan.Zero);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (TimeoutException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||||
|
|
||||||
|
internal enum PipePacketCommand : byte
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
RedirectActivation = 10,
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||||
|
|
||||||
|
internal enum PipePacketContentType : byte
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Json = 1,
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||||
|
internal struct PipePacketHeader
|
||||||
|
{
|
||||||
|
public byte Version;
|
||||||
|
public PipePacketType Type;
|
||||||
|
public PipePacketCommand Command;
|
||||||
|
public PipePacketContentType ContentType;
|
||||||
|
public int ContentLength;
|
||||||
|
public ulong Checksum;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||||
|
|
||||||
|
internal enum PipePacketType : byte
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Request = 1,
|
||||||
|
Response = 2,
|
||||||
|
Termination = 3,
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.Windows.AppLifecycle;
|
||||||
|
using System.IO.Hashing;
|
||||||
|
using System.IO.Pipes;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||||
|
|
||||||
|
[Injection(InjectAs.Singleton)]
|
||||||
|
internal sealed class PrivateNamedPipeClient : IDisposable
|
||||||
|
{
|
||||||
|
private readonly NamedPipeClientStream clientStream = new(".", "Snap.Hutao.PrivateNamedPipe", PipeDirection.InOut, PipeOptions.Asynchronous | PipeOptions.WriteThrough);
|
||||||
|
|
||||||
|
public unsafe bool TryRedirectActivationTo(AppActivationArguments args)
|
||||||
|
{
|
||||||
|
if (clientStream.TryConnectOnce())
|
||||||
|
{
|
||||||
|
{
|
||||||
|
PipePacketHeader redirectActivationPacket = default;
|
||||||
|
redirectActivationPacket.Version = 1;
|
||||||
|
redirectActivationPacket.Type = PipePacketType.Request;
|
||||||
|
redirectActivationPacket.Command = PipePacketCommand.RedirectActivation;
|
||||||
|
redirectActivationPacket.ContentType = PipePacketContentType.Json;
|
||||||
|
|
||||||
|
HutaoActivationArguments hutaoArgs = HutaoActivationArguments.FromAppActivationArguments(args, isRedirected: true);
|
||||||
|
byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(hutaoArgs);
|
||||||
|
|
||||||
|
redirectActivationPacket.ContentLength = jsonBytes.Length;
|
||||||
|
redirectActivationPacket.Checksum = XxHash64.HashToUInt64(jsonBytes);
|
||||||
|
|
||||||
|
clientStream.Write(new(&redirectActivationPacket, sizeof(PipePacketHeader)));
|
||||||
|
clientStream.Write(jsonBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
PipePacketHeader terminationPacket = default;
|
||||||
|
terminationPacket.Version = 1;
|
||||||
|
terminationPacket.Type = PipePacketType.Termination;
|
||||||
|
|
||||||
|
clientStream.Write(new(&terminationPacket, sizeof(PipePacketHeader)));
|
||||||
|
}
|
||||||
|
|
||||||
|
clientStream.Flush();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
clientStream.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||||
|
|
||||||
|
[Injection(InjectAs.Singleton)]
|
||||||
|
[ConstructorGenerated]
|
||||||
|
internal sealed partial class PrivateNamedPipeMessageDispatcher
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider serviceProvider;
|
||||||
|
|
||||||
|
public void RedirectActivation(HutaoActivationArguments? args)
|
||||||
|
{
|
||||||
|
if (args is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceProvider.GetRequiredService<IActivation>().Activate(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
|
using System.IO.Hashing;
|
||||||
|
using System.IO.Pipes;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||||
|
|
||||||
|
[Injection(InjectAs.Singleton)]
|
||||||
|
[ConstructorGenerated]
|
||||||
|
internal sealed partial class PrivateNamedPipeServer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly PrivateNamedPipeMessageDispatcher messageDispatcher;
|
||||||
|
|
||||||
|
private readonly NamedPipeServerStream serverStream = new("Snap.Hutao.PrivateNamedPipe", PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.WriteThrough);
|
||||||
|
private readonly CancellationTokenSource serverTokenSource = new();
|
||||||
|
private readonly SemaphoreSlim serverSemaphore = new(1);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
serverTokenSource.Cancel();
|
||||||
|
serverSemaphore.Wait();
|
||||||
|
serverSemaphore.Dispose();
|
||||||
|
serverTokenSource.Dispose();
|
||||||
|
|
||||||
|
serverStream.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask RunAsync()
|
||||||
|
{
|
||||||
|
using (await serverSemaphore.EnterAsync(serverTokenSource.Token).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
while (!serverTokenSource.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await serverStream.WaitForConnectionAsync(serverTokenSource.Token).ConfigureAwait(false);
|
||||||
|
RunPacketSession(serverStream, serverTokenSource.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe byte[] GetValidatedContent(NamedPipeServerStream serverStream, PipePacketHeader* header)
|
||||||
|
{
|
||||||
|
byte[] content = new byte[header->ContentLength];
|
||||||
|
serverStream.ReadAtLeast(content, header->ContentLength, false);
|
||||||
|
HutaoException.ThrowIf(XxHash64.HashToUInt64(content) != header->Checksum, HutaoExceptionKind.PrivateNamedPipeContentHashIncorrect, "PipePacket Content Hash incorrect");
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void RunPacketSession(NamedPipeServerStream serverStream, CancellationToken token)
|
||||||
|
{
|
||||||
|
Span<byte> headerSpan = stackalloc byte[sizeof(PipePacketHeader)];
|
||||||
|
bool sessionTerminated = false;
|
||||||
|
while (serverStream.IsConnected && !sessionTerminated && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
serverStream.ReadExactly(headerSpan);
|
||||||
|
fixed (byte* pHeader = headerSpan)
|
||||||
|
{
|
||||||
|
PipePacketHeader* header = (PipePacketHeader*)pHeader;
|
||||||
|
|
||||||
|
switch ((header->Type, header->Command, header->ContentType))
|
||||||
|
{
|
||||||
|
case (PipePacketType.Request, PipePacketCommand.RedirectActivation, PipePacketContentType.Json):
|
||||||
|
ReadOnlySpan<byte> content = GetValidatedContent(serverStream, header);
|
||||||
|
messageDispatcher.RedirectActivation(JsonSerializer.Deserialize<HutaoActivationArguments>(content));
|
||||||
|
break;
|
||||||
|
case (PipePacketType.Termination, _, _):
|
||||||
|
serverStream.Disconnect();
|
||||||
|
sessionTerminated = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user