mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
179 Commits
fix/1208
...
feat/reord
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceb26bb669 | ||
|
|
a4e782da78 | ||
|
|
d5551e5cdf | ||
|
|
f016a4a27f | ||
|
|
8b931b6d89 | ||
|
|
b942ceb914 | ||
|
|
f7a49e52e0 | ||
|
|
d4bd610fe2 | ||
|
|
a3dcfd3804 | ||
|
|
592525d149 | ||
|
|
83c4598df5 | ||
|
|
aa680388ad | ||
|
|
ba4f59de30 | ||
|
|
8780cf385e | ||
|
|
431cdd1253 | ||
|
|
9a8827fb40 | ||
|
|
d88a6ca301 | ||
|
|
5d401794e5 | ||
|
|
26396443dc | ||
|
|
6b755d934d | ||
|
|
7d5b057269 | ||
|
|
917c173eb2 | ||
|
|
28d702422e | ||
|
|
7612ab5da3 | ||
|
|
ab436ecb2f | ||
|
|
457e3ff4d5 | ||
|
|
224c4e52ea | ||
|
|
2a5c7b21fd | ||
|
|
53f8291a66 | ||
|
|
b621d5406a | ||
|
|
12f4847aea | ||
|
|
39a3d31f38 | ||
|
|
70ac0b13a5 | ||
|
|
a3520a4991 | ||
|
|
aed0284e4b | ||
|
|
96ef07cbe5 | ||
|
|
d1f37f37ac | ||
|
|
1fe09f3069 | ||
|
|
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 | ||
|
|
45248d75e1 | ||
|
|
22646cfab2 | ||
|
|
d0237a3c89 | ||
|
|
73c80fad10 | ||
|
|
320bed9fcb | ||
|
|
bb12aca3b4 | ||
|
|
c7b5d98fb1 | ||
|
|
7cc96f94f2 | ||
|
|
b35355f9a3 | ||
|
|
745815657d | ||
|
|
d93a9f41f3 | ||
|
|
910f099c6d | ||
|
|
e68449ec0c | ||
|
|
e484fbed21 | ||
|
|
88af6d28a9 | ||
|
|
3ab34f0992 | ||
|
|
5e875a7f18 | ||
|
|
89d98748e8 | ||
|
|
d33cd894b9 | ||
|
|
f0c19b419e | ||
|
|
f1d9787e45 | ||
|
|
5f9b4a7cb2 | ||
|
|
8710150897 | ||
|
|
92c1b12764 | ||
|
|
d73bd557f3 | ||
|
|
777d7d1056 | ||
|
|
1a944dae9c | ||
|
|
a26c52ba97 | ||
|
|
5fab03d57e | ||
|
|
e8a459cb41 | ||
|
|
04df5a7bf1 | ||
|
|
1ebcc2fc89 | ||
|
|
e9917e788d | ||
|
|
9665876d52 | ||
|
|
8921816873 | ||
|
|
2698761594 | ||
|
|
3ae4210ca0 | ||
|
|
2f5e0cbe39 | ||
|
|
d3444a9435 | ||
|
|
8b6f95c3d9 | ||
|
|
88b8335e5b | ||
|
|
061aba715b | ||
|
|
da80631b72 | ||
|
|
97acf872bc | ||
|
|
addaf1a9e3 | ||
|
|
76183901da | ||
|
|
87ee81e7fa | ||
|
|
f2f858de15 | ||
|
|
c434521004 | ||
|
|
27ed2cefc1 | ||
|
|
6dc1e664b0 | ||
|
|
51c3dde24b | ||
|
|
2d497faaa5 | ||
|
|
4783934b92 | ||
|
|
03d235876a | ||
|
|
f49e9669af | ||
|
|
533c70caaa | ||
|
|
dd59302bb3 | ||
|
|
96e42f51f0 | ||
|
|
5a19c19759 | ||
|
|
8fb831ef7c | ||
|
|
a30c8d8678 | ||
|
|
2655e835f8 | ||
|
|
ffd74703cd | ||
|
|
584465dc45 | ||
|
|
a1e751160d | ||
|
|
d78d2cf51a | ||
|
|
24709bfbf9 | ||
|
|
9be396b175 | ||
|
|
bb83e76d33 | ||
|
|
1ca24c8a78 | ||
|
|
3d56aef221 | ||
|
|
d43f2e76c4 | ||
|
|
104fb9a3b0 | ||
|
|
d6b79584b6 | ||
|
|
fcd0b65257 | ||
|
|
802951edd7 | ||
|
|
79fc42aa3b | ||
|
|
fb0491dc57 | ||
|
|
b81d088379 | ||
|
|
553d267625 | ||
|
|
199e753103 | ||
|
|
48774960a7 | ||
|
|
7bfea0e090 | ||
|
|
f0f9e387a8 | ||
|
|
f71a34a6be | ||
|
|
e6fd0b833b | ||
|
|
d2c33cf19c | ||
|
|
1d074f5313 | ||
|
|
769a1c3812 | ||
|
|
b54717fa9b | ||
|
|
ffa0b05a12 | ||
|
|
d07a33f3e4 | ||
|
|
bbd274c391 | ||
|
|
f8a8a929ac | ||
|
|
cf3298dbd0 | ||
|
|
a8b887def2 | ||
|
|
5a937b0838 | ||
|
|
c016ae1cb8 | ||
|
|
c7fdf8001d | ||
|
|
b11526761e | ||
|
|
6ee823094a | ||
|
|
14894b0b47 | ||
|
|
6834073603 | ||
|
|
911fe57fb2 | ||
|
|
7320cf7dd0 | ||
|
|
bc6d03e442 | ||
|
|
307a49b346 | ||
|
|
9f8d80ff43 | ||
|
|
d34130b6c0 | ||
|
|
2161f12069 | ||
|
|
0bedd1894c | ||
|
|
442db0bae4 |
6
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
6
.github/ISSUE_TEMPLATE/CHS-bug-report.yml
vendored
@@ -22,7 +22,7 @@ body:
|
||||
- label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
|
||||
required: true
|
||||
|
||||
- label: 我的问题不是[已修复](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E4%BF%AE%E5%A4%8D)的问题也不是一个别人已发布的**重复的**问题
|
||||
- label: 我的问题不是[已完成](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90)的问题也不是一个别人已发布的**重复的**问题
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
@@ -51,7 +51,7 @@ body:
|
||||
description: |
|
||||
在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID
|
||||
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
|
||||
如果你的程序已经无法启动,请下载并运行[此PowerShell脚本](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1),它将显示你的设备 ID
|
||||
如果你的程序已经无法启动,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将显示你的设备 ID
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -87,7 +87,7 @@ body:
|
||||
label: 发生了什么?
|
||||
description: |
|
||||
详细的描述问题发生前后的行为,以便我们解决问题。**如果你的问题涉及程序崩溃,你应当检查 Windows 事件查看器,并将相关的 `.Net 错误`详情附上**
|
||||
如果你无法找到该日志,请下载并运行[此PowerShell脚本](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/dump_log_script/dump_log_zh.ps1),它将输出错误日志
|
||||
如果你无法找到该日志,请下载并运行[诊断工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将转储问题日志至工具运行目录中的 `Snap.Hutao Error Log.txt`
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/ENG-bug-report.yml
vendored
6
.github/ISSUE_TEMPLATE/ENG-bug-report.yml
vendored
@@ -22,7 +22,7 @@ body:
|
||||
- label: I and tried **search feature** in Snap Hutao document site, and no associated article
|
||||
required: true
|
||||
|
||||
- label: My issue is not a [fixed issue](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E4%BF%AE%E5%A4%8D), and it's not a duplicated issue
|
||||
- label: My issue is not a [finished issue](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90), and it's not a duplicated issue
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
@@ -51,7 +51,7 @@ body:
|
||||
description: |
|
||||
In Snap Hutao's settings page, you can find and copy your device ID
|
||||
If your issue is about program crash, please fill this so we can dump the log and locate the source easier
|
||||
If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID.
|
||||
If your program cannot startup, please download and run [Diagnosis Tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will shows your device ID.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -87,7 +87,7 @@ body:
|
||||
label: What Happened?
|
||||
description: |
|
||||
Describe your issue in detail to help us identify the issue. **If your issue is about program crash, you should check Windows Event Viewer, and attach associated `.Net Error` details here**If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID.
|
||||
If you cannot find it, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/dump_log_script/dump_log_en.ps1), it will dump the error log.
|
||||
If you cannot find it, please download and run [Diagnosis Tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will dump the error log to `Snap.Hutao Error Log.txt` in the working directory of the tool.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
40
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
40
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
@@ -12,50 +12,26 @@ body:
|
||||
value: |
|
||||
|
||||
## 创建版本
|
||||
|
||||
|
||||
- [ ] 同步一次 [Crowdin](https://crowdin.com/project/snap-hutao) 翻译
|
||||
- [ ] 发布 RC 版本(Optional)
|
||||
- [ ] 合并入主分支
|
||||
- [ ] 整理更新内容,等待翻译
|
||||
- [ ] 打包
|
||||
- [ ] 提交微软商店
|
||||
- [ ] 包含更新日志
|
||||
- [ ] 在 [Snap.Hutao.Docs@next-patch](https://github.com/DGP-Studio/Snap.Hutao.Docs/tree/next-patch) 分支更新文档并直接开 PR
|
||||
- [ ] 更新日志
|
||||
- [ ] 功能文档更新
|
||||
|
||||
## 发布版本 [半自动]
|
||||
|
||||
- [ ] 在 GitHub 个人设置中更新 [Publish-Automate Beta PAT](https://github.com/settings/tokens?type=beta),有效期需小于预计发版需要天数
|
||||
- [ ] 将更新的 PAT 更新至 Publish-Automate 库的 [Actions Secrets](https://github.com/DGP-Studio/Publish-Automate/settings/secrets/actions) 中
|
||||
|
||||
***
|
||||
***
|
||||
|
||||
- [ ] 运行 [Auto Publish Action](https://github.com/DGP-Studio/Publish-Automate/actions/workflows/auto-publish.yml)
|
||||
- [ ] 在 https://store.rg-adguard.net/ 下载新版本安装包
|
||||
- [ ] Store URL: https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52
|
||||
- [ ] 命名格式为 `Snap.Hutao x.x.x.msix`
|
||||
- [ ] Merge 文档 PR
|
||||
- [ ] 发布 Release
|
||||
- [ ] 更新日志格式(以 1.6.2 版本为例)
|
||||
|
||||
```jsx
|
||||
## Update log
|
||||
https://hut.ao/en/statements/update-log.html#_1-6-2
|
||||
|
||||
## 更新日志
|
||||
[此处从文档复制]
|
||||
|
||||
## What's Changed
|
||||
**Full Changelog**: https://github.com/DGP-Studio/Snap.Hutao/compare/1.6.0...1.6.2
|
||||
```
|
||||
|
||||
- [ ] 通知用户
|
||||
- [ ] 主分支合并入 release 分支
|
||||
- [ ] 等待 Release 自动发布
|
||||
- [ ] 检查极狐是否同步完成 Release
|
||||
- [ ] 通知用户
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: Final Check
|
||||
description: Understand what you are doing
|
||||
description: Understand what you are doing
|
||||
options:
|
||||
- label: I understand that I will get banned from repository if I don't have permission to use this template
|
||||
required: true
|
||||
required: true
|
||||
|
||||
1
.github/workflows/alpha.yml
vendored
1
.github/workflows/alpha.yml
vendored
@@ -12,6 +12,7 @@ on:
|
||||
- '.gitmodules'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- '**.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
76
.gitlab-ci.yml
Normal file
76
.gitlab-ci.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
stages:
|
||||
- fetch
|
||||
- release
|
||||
- refresh
|
||||
|
||||
Fetch:
|
||||
stage: fetch
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
tags:
|
||||
- us3
|
||||
script:
|
||||
- apt-get update -qy
|
||||
- apt-get install -y curl jq
|
||||
- RELEASE_INFO=$(curl -sSL "https://api.github.com/repos/$CI_PROJECT_PATH/releases/latest")
|
||||
- ASSET_URL=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.name | endswith(".msix")) | .browser_download_url')
|
||||
- SHA256SUMS_URL=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.name == "SHA256SUMS") | .browser_download_url')
|
||||
- curl -LJO "$ASSET_URL"
|
||||
- curl -LJO "$SHA256SUMS_URL"
|
||||
- FILE_NAME=$(basename "$ASSET_URL")
|
||||
- SHA256SUMS_NAME=$(basename "$SHA256SUMS_URL")
|
||||
- echo "File name at script stage is $FILE_NAME"
|
||||
- echo "SHA256SUMS name at script stage is $SHA256SUMS_NAME"
|
||||
- echo "THIS_FILE_NAME=$FILE_NAME" >> next.env
|
||||
- echo "THIS_SHA256SUMS_NAME=$SHA256SUMS_NAME" >> next.env
|
||||
after_script:
|
||||
- echo "Current Job ID is $CI_JOB_ID"
|
||||
- echo "THIS_JOB_ID=$CI_JOB_ID" >> next.env
|
||||
artifacts:
|
||||
paths:
|
||||
- "*.msix"
|
||||
- "SHA256SUMS"
|
||||
expire_in: 180 days
|
||||
reports:
|
||||
dotenv: next.env
|
||||
|
||||
release:
|
||||
stage: release
|
||||
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
needs:
|
||||
- job: Fetch
|
||||
artifacts: true
|
||||
variables:
|
||||
TAG: '$CI_COMMIT_TAG'
|
||||
script:
|
||||
- echo "Create Release $TAG"
|
||||
- echo "$THIS_JOB_ID"
|
||||
- echo "$THIS_FILE_NAME"
|
||||
release:
|
||||
name: '$TAG'
|
||||
tag_name: '$TAG'
|
||||
ref: '$TAG'
|
||||
description: 'Release $TAG by CI'
|
||||
assets:
|
||||
links:
|
||||
- name: "$THIS_FILE_NAME"
|
||||
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_FILE_NAME?inline=false"
|
||||
link_type: package
|
||||
- name: "$THIS_SHA256SUMS_NAME"
|
||||
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_SHA256SUMS_NAME?inline=false"
|
||||
link_type: other
|
||||
- name: "artifact_archive"
|
||||
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/download?file_type=archive"
|
||||
link_type: other
|
||||
|
||||
Refresh:
|
||||
stage: refresh
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
needs:
|
||||
- job: release
|
||||
script:
|
||||
- apt-get install -y curl
|
||||
- curl -X PATCH "$PURGE_URL"
|
||||
@@ -4,13 +4,15 @@
|
||||
|
||||
### Setup Snap.Hutao Project
|
||||
|
||||
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/)
|
||||
2. Open Visual Studio Installer to complete Visual Studio installation
|
||||
- You need to install `.NET desktop development`, `Desktop development with C++` and `Universal Windows Platform development` components
|
||||
3. Install `Single-project MSIX Packaging Tools for VS 2022` provided by Microsoft in Visual Studio marketplace
|
||||
4. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device
|
||||
5. Switch git branch to `develop`
|
||||
6. Open project solution with your Visual Studio and then you are ready to go
|
||||
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/).
|
||||
- No need to select workloads; Visual Studio will handle it automatically.
|
||||
- Close Visual Studio Installer to ensure a smooth installation experience for workloads.
|
||||
- If using Visual Studio 2022 17.9 preview, skip step 5, as automatic extension installation is supported in this version.
|
||||
2. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device.
|
||||
3. Switch to the`develop` branch using git.
|
||||
4. Open the project solution with your Visual Studio. Visual Studio will prompt you to install the necessary workloads, closing and reopening automatically.
|
||||
5. (For Visual Studio 2022 17.8) Install the [Single-project MSIX Packaging Tools for VS 2022](https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17) provided by Microsoft in Visual Studio marketplace.
|
||||
6. Open the project solution with your Visual Studio, and you are ready to go.
|
||||
|
||||
### Start Pull Request
|
||||
|
||||
|
||||
31
README.md
31
README.md
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
|
||||
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新 功能相结合,它提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
|
||||
@@ -7,7 +7,29 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
||||
|
||||
## 下载使用 / Download
|
||||
|
||||
[<img src="https://get.microsoft.com/images/zh-cn%20light.svg" width="30%" height="30%">](https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52)
|
||||
 [](https://github.com/DGP-Studio/Snap.Hutao/releases/latest) []()
|
||||
|
||||
---
|
||||
|
||||
#### 使用安装器安装 / Install with Snap.Hutao.Depolyment Installer
|
||||
|
||||
Snap.Hutao.Depolyment 是一个由 DGP-Studio 重新包装的 Windows 应用安装器,适用于缺少专业计算机知识的一般用户,可以在安装时同时解决缺少必要系统环境的问题。
|
||||
|
||||
Snap.Hutao.Depolyment is a Windows application installer repackaged by DGP-Studio for the users who lacks computer knowledge and can solve the problem of missing necessary system environment at the same time as the installation.
|
||||
|
||||
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao.Deployment/releases/latest)
|
||||
|
||||
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao.Deployment/-/releases)
|
||||
|
||||
#### 使用 MSIX 包安装 / Install with MSIX Package
|
||||
|
||||
直接使用 Snap Hutao MSIX 安装包,使用 Windows 内置的 App Installer 即可安装。如在安装中出现问题,请查阅我们的[常见问题](https://hut.ao/zh/advanced/FAQ.html)文档
|
||||
|
||||
Install with Snap Hutao MSIX package, can be installed with Windows built-in App Installer. If you faced any issue, please check our [FAQ](https://hut.ao/en/advanced/FAQ.html) document.
|
||||
|
||||
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao/releases/latest)
|
||||
|
||||
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao/-/releases)
|
||||
|
||||
## 贡献 / Contribute
|
||||
|
||||
@@ -22,9 +44,7 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
||||
|
||||
### 特定的原神项目 / Specific Genshin-related Projects
|
||||
|
||||
* [biuuu/genshin-wish-export](https://github.com/biuuu/genshin-wish-export)
|
||||
* [xunkong/xunkong](https://github.com/xunkong/xunkong)
|
||||
* [YuehaiTeam/cocogoat](https://github.com/YuehaiTeam/cocogoat)
|
||||
* [Scighost/Starward](https://github.com/Scighost/Starward)
|
||||
|
||||
### 使用的技术栈 / Tech Stack
|
||||
|
||||
@@ -35,7 +55,6 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
||||
* [dotnet/efcore](https://github.com/dotnet/efcore)
|
||||
* [dotnet/runtime](https://github.com/dotnet/runtime)
|
||||
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
|
||||
* [microsoft/CsWin32](https://github.com/microsoft/CsWin32)
|
||||
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
|
||||
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
|
||||
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
|
||||
|
||||
@@ -124,9 +124,6 @@ dotnet_diagnostic.SA1623.severity = none
|
||||
# SA1636: File header copyright text should match
|
||||
dotnet_diagnostic.SA1636.severity = none
|
||||
|
||||
# SA1414: Tuple types in signatures should have element names
|
||||
dotnet_diagnostic.SA1414.severity = none
|
||||
|
||||
# SA0001: XML comment analysis disabled
|
||||
dotnet_diagnostic.SA0001.severity = none
|
||||
csharp_style_prefer_parameter_null_checking = true:suggestion
|
||||
@@ -325,7 +322,6 @@ dotnet_diagnostic.CA2227.severity = suggestion
|
||||
# CA2251: 使用 “string.Equals”
|
||||
dotnet_diagnostic.CA2251.severity = suggestion
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
dotnet_diagnostic.SA1010.severity = none
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
11
src/Snap.Hutao/.vsconfig
Normal file
11
src/Snap.Hutao/.vsconfig
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"components": [
|
||||
"Microsoft.VisualStudio.Workload.ManagedDesktop",
|
||||
"Microsoft.VisualStudio.Workload.NativeDesktop",
|
||||
"Microsoft.VisualStudio.Workload.Universal"
|
||||
],
|
||||
"extensions": [
|
||||
"https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Snap.Hutao.Test.IncomingFeature;
|
||||
|
||||
[TestClass]
|
||||
public class GameRegistryContentTest
|
||||
{
|
||||
private static readonly JsonSerializerOptions RegistryContentSerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
};
|
||||
|
||||
[TestMethod]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void GetRegistryContent()
|
||||
{
|
||||
GetRegistryContentCore(@"Software\miHoYo\原神");
|
||||
GetRegistryContentCore(@"Software\miHoYo\Genshin Impact");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void GetRegistryContentCore(string subkey)
|
||||
{
|
||||
using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64))
|
||||
{
|
||||
RegistryKey? gameKey = key.OpenSubKey(subkey);
|
||||
Assert.IsNotNull(gameKey);
|
||||
|
||||
Dictionary<string, object> data = [];
|
||||
foreach (string valueName in gameKey.GetValueNames())
|
||||
{
|
||||
data[valueName] = gameKey.GetValueKind(valueName) switch
|
||||
{
|
||||
RegistryValueKind.DWord => (int)gameKey.GetValue(valueName)!,
|
||||
RegistryValueKind.Binary => GetStringOrObject((byte[])gameKey.GetValue(valueName)!),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
|
||||
Console.WriteLine($"Subkey: {subkey}");
|
||||
Console.WriteLine(JsonSerializer.Serialize(data, RegistryContentSerializerOptions));
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe object GetStringOrObject(byte[] bytes)
|
||||
{
|
||||
fixed (byte* pByte = bytes)
|
||||
{
|
||||
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
|
||||
string temp = Encoding.UTF8.GetString(span);
|
||||
|
||||
if (temp.AsSpan()[0] is '{' or '[')
|
||||
{
|
||||
return JsonSerializer.Deserialize<JsonElement>(temp);
|
||||
}
|
||||
|
||||
return temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,9 +160,39 @@ public sealed class GeniusInvokationDecoding
|
||||
|
||||
ushort[] testKnownResult =
|
||||
[
|
||||
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
|
||||
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
|
||||
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
|
||||
060,
|
||||
019,
|
||||
001,
|
||||
079,
|
||||
120,
|
||||
120,
|
||||
129,
|
||||
151,
|
||||
151,
|
||||
153,
|
||||
153,
|
||||
181,
|
||||
184,
|
||||
184,
|
||||
185,
|
||||
185,
|
||||
194,
|
||||
194,
|
||||
200,
|
||||
200,
|
||||
201,
|
||||
201,
|
||||
217,
|
||||
217,
|
||||
219,
|
||||
241,
|
||||
241,
|
||||
244,
|
||||
244,
|
||||
245,
|
||||
245,
|
||||
270,
|
||||
270,
|
||||
];
|
||||
|
||||
CollectionAssert.AreEqual(resultArray, testKnownResult);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.2.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -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,94 +0,0 @@
|
||||
// COMCTL32
|
||||
DefSubclassProc
|
||||
RemoveWindowSubclass
|
||||
SetWindowSubclass
|
||||
|
||||
// DWMAPI
|
||||
DwmSetWindowAttribute
|
||||
|
||||
// GDI32
|
||||
GetDeviceCaps
|
||||
|
||||
// KERNEL32
|
||||
AllocConsole
|
||||
CloseHandle
|
||||
CreateEventW
|
||||
CreateRemoteThread
|
||||
FreeConsole
|
||||
GetConsoleMode
|
||||
GetModuleHandleW
|
||||
GetProcAddress
|
||||
GetStdHandle
|
||||
K32EnumProcessModules
|
||||
K32GetModuleBaseNameW
|
||||
K32GetModuleInformation
|
||||
ReadProcessMemory
|
||||
SetConsoleMode
|
||||
SetConsoleTitle
|
||||
SetEvent
|
||||
VirtualAlloc
|
||||
VirtualAllocEx
|
||||
VirtualFree
|
||||
VirtualFreeEx
|
||||
WaitForSingleObject
|
||||
WriteProcessMemory
|
||||
|
||||
// OLE32
|
||||
CoCreateInstance
|
||||
CoWaitForMultipleObjects
|
||||
|
||||
// SHELL32
|
||||
SHCreateItemFromParsingName
|
||||
|
||||
// USER32
|
||||
AttachThreadInput
|
||||
FindWindowExW
|
||||
GetCursorPos
|
||||
GetDC
|
||||
GetDpiForWindow
|
||||
GetForegroundWindow
|
||||
GetWindowPlacement
|
||||
GetWindowThreadProcessId
|
||||
ReleaseDC
|
||||
RegisterHotKey
|
||||
SendInput
|
||||
SetForegroundWindow
|
||||
UnregisterHotKey
|
||||
|
||||
// COM
|
||||
FileOpenDialog
|
||||
FileSaveDialog
|
||||
IFileOpenDialog
|
||||
IFileSaveDialog
|
||||
IPersistFile
|
||||
IShellLinkDataList
|
||||
IShellLinkW
|
||||
ShellLink
|
||||
SHELL_LINK_DATA_FLAGS
|
||||
|
||||
// WinRT
|
||||
IMemoryBufferByteAccess
|
||||
|
||||
// Const value
|
||||
E_FAIL
|
||||
INFINITE
|
||||
RPC_E_WRONG_THREAD
|
||||
MAX_PATH
|
||||
WM_GETMINMAXINFO
|
||||
WM_HOTKEY
|
||||
WM_NCRBUTTONDOWN
|
||||
WM_NCRBUTTONUP
|
||||
WM_NULL
|
||||
|
||||
// Type & Enum definition
|
||||
HRESULT_FROM_WIN32
|
||||
SLGP_FLAGS
|
||||
|
||||
// System.Threading
|
||||
LPTHREAD_START_ROUTINE
|
||||
|
||||
// UI.WindowsAndMessaging
|
||||
MINMAXINFO
|
||||
|
||||
// System.Com
|
||||
CWMO_FLAGS
|
||||
@@ -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,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="NativeMethods.json" />
|
||||
<None Remove="NativeMethods.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="NativeMethods.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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)) };
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Windows.Win32.CsWin32.InteropServices;
|
||||
|
||||
internal class WinRTCustomMarshaler : ICustomMarshaler
|
||||
{
|
||||
private static readonly string? AssemblyFullName = typeof(Windows.Foundation.IMemoryBuffer).Assembly.FullName;
|
||||
|
||||
private readonly string className;
|
||||
private bool lookedForFromAbi;
|
||||
private MethodInfo? fromAbiMethod;
|
||||
|
||||
private WinRTCustomMarshaler(string className)
|
||||
{
|
||||
this.className = className;
|
||||
}
|
||||
|
||||
public static ICustomMarshaler GetInstance(string cookie)
|
||||
{
|
||||
return new WinRTCustomMarshaler(cookie);
|
||||
}
|
||||
|
||||
public void CleanUpManagedData(object ManagedObj)
|
||||
{
|
||||
}
|
||||
|
||||
public void CleanUpNativeData(nint pNativeData)
|
||||
{
|
||||
Marshal.Release(pNativeData);
|
||||
}
|
||||
|
||||
public int GetNativeDataSize()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public nint MarshalManagedToNative(object ManagedObj)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public object MarshalNativeToManaged(nint thisPtr)
|
||||
{
|
||||
return className switch
|
||||
{
|
||||
"Windows.System.DispatcherQueueController" => Windows.System.DispatcherQueueController.FromAbi(thisPtr),
|
||||
_ => MarshalNativeToManagedSlow(thisPtr),
|
||||
};
|
||||
}
|
||||
|
||||
private object MarshalNativeToManagedSlow(nint pNativeData)
|
||||
{
|
||||
if (!lookedForFromAbi)
|
||||
{
|
||||
Type? type = Type.GetType($"{className}, {AssemblyFullName}");
|
||||
|
||||
fromAbiMethod = type?.GetMethod("FromAbi");
|
||||
lookedForFromAbi = true;
|
||||
}
|
||||
|
||||
if (fromAbiMethod is not null)
|
||||
{
|
||||
return fromAbiMethod.Invoke(default, new object[] { pNativeData })!;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Marshal.GetObjectForIUnknown(pNativeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,11 @@ EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A95A964-04B1-477A-BDE7-505525B3CAD8}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.vsconfig = .vsconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Win32", "Snap.Hutao.Win32\Snap.Hutao.Win32.csproj", "{0F7ABEB2-5107-4037-B9DC-84D288FB0801}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -66,22 +65,6 @@ Global
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0F7ABEB2-5107-4037-B9DC-84D288FB0801}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
x:Key="LargeGridViewItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
TargetType="GridViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Margin" Value="0,0,12,12"/>
|
||||
</Style>
|
||||
<Style
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||
using Snap.Hutao.Core.Shell;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -36,8 +37,6 @@ public sealed partial class App : Application
|
||||
----------------------------------------------------------------
|
||||
""";
|
||||
|
||||
private const string AppInstanceKey = "main";
|
||||
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IActivation activation;
|
||||
private readonly ILogger<App> logger;
|
||||
@@ -50,7 +49,6 @@ public sealed partial class App : Application
|
||||
{
|
||||
// Load app resource
|
||||
InitializeComponent();
|
||||
|
||||
activation = serviceProvider.GetRequiredService<IActivation>();
|
||||
logger = serviceProvider.GetRequiredService<ILogger<App>>();
|
||||
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
|
||||
@@ -64,25 +62,21 @@ public sealed partial class App : Application
|
||||
try
|
||||
{
|
||||
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
AppInstance firstInstance = AppInstance.FindOrRegisterForKey(AppInstanceKey);
|
||||
|
||||
if (firstInstance.IsCurrent)
|
||||
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
|
||||
{
|
||||
logger.LogInformation(ConsoleBanner);
|
||||
|
||||
// manually invoke
|
||||
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
|
||||
activation.InitializeWith(firstInstance);
|
||||
|
||||
LogDiagnosticInformation();
|
||||
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Redirect the activation (and args) to the "main" instance, and exit.
|
||||
firstInstance.RedirectActivationTo(activatedEventArgs);
|
||||
Process.GetCurrentProcess().Kill();
|
||||
Exit();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation(ConsoleBanner);
|
||||
LogDiagnosticInformation();
|
||||
|
||||
// manually invoke
|
||||
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
|
||||
activation.Initialize();
|
||||
|
||||
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
[SuppressMessage("", "SH001")]
|
||||
[DependencyProperty("LeftPanelMaxWidth", typeof(double), IsAttached = true, AttachedType = typeof(ScrollViewer))]
|
||||
[DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))]
|
||||
public sealed partial class ScrollViewerHelper
|
||||
{
|
||||
|
||||
@@ -20,4 +20,4 @@ public sealed partial class SettingsExpanderHelper
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,12 +40,7 @@ internal sealed class CachedImage : Implementation.ImageEx
|
||||
{
|
||||
// The image is corrupted, remove it.
|
||||
imageCache.Remove(imageUri);
|
||||
return null;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// task was explicitly canceled
|
||||
return null;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Image
|
||||
Name="PlaceholderImage"
|
||||
Margin="{TemplateBinding PlaceholderMargin}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
Source="{TemplateBinding PlaceholderSource}"
|
||||
|
||||
@@ -11,10 +11,6 @@ using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
internal delegate void ImageExFailedEventHandler(object sender, ImageExFailedEventArgs e);
|
||||
|
||||
internal delegate void ImageExOpenedEventHandler(object sender, ImageExOpenedEventArgs e);
|
||||
|
||||
[SuppressMessage("", "CA1001")]
|
||||
[SuppressMessage("", "SH003")]
|
||||
[TemplateVisualState(Name = LoadingState, GroupName = CommonGroup)]
|
||||
@@ -22,98 +18,34 @@ internal delegate void ImageExOpenedEventHandler(object sender, ImageExOpenedEve
|
||||
[TemplateVisualState(Name = UnloadedState, GroupName = CommonGroup)]
|
||||
[TemplateVisualState(Name = FailedState, GroupName = CommonGroup)]
|
||||
[TemplatePart(Name = PartImage, Type = typeof(object))]
|
||||
internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
|
||||
[TemplatePart(Name = PartPlaceholderImage, Type = typeof(object))]
|
||||
[DependencyProperty("Stretch", typeof(Stretch), Stretch.Uniform)]
|
||||
[DependencyProperty("DecodePixelHeight", typeof(int), 0)]
|
||||
[DependencyProperty("DecodePixelWidth", typeof(int), 0)]
|
||||
[DependencyProperty("DecodePixelType", typeof(DecodePixelType), DecodePixelType.Physical)]
|
||||
[DependencyProperty("IsCacheEnabled", typeof(bool), false)]
|
||||
[DependencyProperty("EnableLazyLoading", typeof(bool), false, nameof(EnableLazyLoadingChanged))]
|
||||
[DependencyProperty("LazyLoadingThreshold", typeof(double), default(double), nameof(LazyLoadingThresholdChanged))]
|
||||
[DependencyProperty("PlaceholderSource", typeof(object), default(object))]
|
||||
[DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)]
|
||||
[DependencyProperty("PlaceholderMargin", typeof(Thickness))]
|
||||
[DependencyProperty("Source", typeof(object), default(object), nameof(SourceChanged))]
|
||||
internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
|
||||
{
|
||||
protected const string PartImage = "Image";
|
||||
protected const string PartPlaceholderImage = "PlaceholderImage";
|
||||
protected const string CommonGroup = "CommonStates";
|
||||
protected const string LoadingState = "Loading";
|
||||
protected const string LoadedState = "Loaded";
|
||||
protected const string UnloadedState = "Unloaded";
|
||||
protected const string FailedState = "Failed";
|
||||
|
||||
private static readonly DependencyProperty StretchProperty = DependencyProperty.Register(nameof(Stretch), typeof(Stretch), typeof(ImageExBase), new PropertyMetadata(Stretch.Uniform));
|
||||
private static readonly DependencyProperty DecodePixelHeightProperty = DependencyProperty.Register(nameof(DecodePixelHeight), typeof(int), typeof(ImageExBase), new PropertyMetadata(0));
|
||||
private static readonly DependencyProperty DecodePixelTypeProperty = DependencyProperty.Register(nameof(DecodePixelType), typeof(int), typeof(ImageExBase), new PropertyMetadata(DecodePixelType.Physical));
|
||||
private static readonly DependencyProperty DecodePixelWidthProperty = DependencyProperty.Register(nameof(DecodePixelWidth), typeof(int), typeof(ImageExBase), new PropertyMetadata(0));
|
||||
private static readonly DependencyProperty IsCacheEnabledProperty = DependencyProperty.Register(nameof(IsCacheEnabled), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false));
|
||||
private static readonly DependencyProperty EnableLazyLoadingProperty = DependencyProperty.Register(nameof(EnableLazyLoading), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false, EnableLazyLoadingChanged));
|
||||
private static readonly DependencyProperty LazyLoadingThresholdProperty = DependencyProperty.Register(nameof(LazyLoadingThreshold), typeof(double), typeof(ImageExBase), new PropertyMetadata(default(double), LazyLoadingThresholdChanged));
|
||||
private static readonly DependencyProperty PlaceholderSourceProperty = DependencyProperty.Register(nameof(PlaceholderSource), typeof(ImageSource), typeof(ImageExBase), new PropertyMetadata(default(ImageSource), PlaceholderSourceChanged));
|
||||
private static readonly DependencyProperty PlaceholderStretchProperty = DependencyProperty.Register(nameof(PlaceholderStretch), typeof(Stretch), typeof(ImageExBase), new PropertyMetadata(default(Stretch)));
|
||||
private static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(ImageExBase), new PropertyMetadata(null, SourceChanged));
|
||||
|
||||
private CancellationTokenSource? tokenSource;
|
||||
private object? lazyLoadingSource;
|
||||
private bool isInViewport;
|
||||
|
||||
public event ImageExFailedEventHandler? ImageExFailed;
|
||||
|
||||
public event ImageExOpenedEventHandler? ImageExOpened;
|
||||
|
||||
public event EventHandler? ImageExInitialized;
|
||||
|
||||
public bool IsInitialized { get; private set; }
|
||||
|
||||
public int DecodePixelHeight
|
||||
{
|
||||
get => (int)GetValue(DecodePixelHeightProperty);
|
||||
set => SetValue(DecodePixelHeightProperty, value);
|
||||
}
|
||||
|
||||
public DecodePixelType DecodePixelType
|
||||
{
|
||||
get => (DecodePixelType)GetValue(DecodePixelTypeProperty);
|
||||
set => SetValue(DecodePixelTypeProperty, value);
|
||||
}
|
||||
|
||||
public int DecodePixelWidth
|
||||
{
|
||||
get => (int)GetValue(DecodePixelWidthProperty);
|
||||
set => SetValue(DecodePixelWidthProperty, value);
|
||||
}
|
||||
|
||||
public Stretch Stretch
|
||||
{
|
||||
get => (Stretch)GetValue(StretchProperty);
|
||||
set => SetValue(StretchProperty, value);
|
||||
}
|
||||
|
||||
public bool IsCacheEnabled
|
||||
{
|
||||
get => (bool)GetValue(IsCacheEnabledProperty);
|
||||
set => SetValue(IsCacheEnabledProperty, value);
|
||||
}
|
||||
|
||||
public bool EnableLazyLoading
|
||||
{
|
||||
get => (bool)GetValue(EnableLazyLoadingProperty);
|
||||
set => SetValue(EnableLazyLoadingProperty, value);
|
||||
}
|
||||
|
||||
public double LazyLoadingThreshold
|
||||
{
|
||||
get => (double)GetValue(LazyLoadingThresholdProperty);
|
||||
set => SetValue(LazyLoadingThresholdProperty, value);
|
||||
}
|
||||
|
||||
public ImageSource PlaceholderSource
|
||||
{
|
||||
get => (ImageSource)GetValue(PlaceholderSourceProperty);
|
||||
set => SetValue(PlaceholderSourceProperty, value);
|
||||
}
|
||||
|
||||
public Stretch PlaceholderStretch
|
||||
{
|
||||
get => (Stretch)GetValue(PlaceholderStretchProperty);
|
||||
set => SetValue(PlaceholderStretchProperty, value);
|
||||
}
|
||||
|
||||
public object Source
|
||||
{
|
||||
get => GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
public bool WaitUntilLoaded
|
||||
{
|
||||
get => true;
|
||||
@@ -121,11 +53,9 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
|
||||
protected object? Image { get; private set; }
|
||||
|
||||
public abstract CompositionBrush GetAlphaMask();
|
||||
protected object? PlaceholderImage { get; private set; }
|
||||
|
||||
protected virtual void OnPlaceholderSourceChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
}
|
||||
public abstract CompositionBrush GetAlphaMask();
|
||||
|
||||
protected virtual Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
@@ -136,61 +66,11 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, LoadedState, true);
|
||||
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
|
||||
}
|
||||
|
||||
protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new FileNotFoundException(e.ErrorMessage)));
|
||||
}
|
||||
|
||||
protected void AttachImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened += handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected void RemoveImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected void AttachImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed += handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected void RemoveImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
@@ -199,11 +79,10 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
RemoveImageFailed(OnImageFailed);
|
||||
|
||||
Image = GetTemplateChild(PartImage);
|
||||
PlaceholderImage = GetTemplateChild(PartPlaceholderImage);
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
ImageExInitialized?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
if (Source is null || !EnableLazyLoading || isInViewport)
|
||||
{
|
||||
lazyLoadingSource = null;
|
||||
@@ -218,23 +97,73 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
AttachImageFailed(OnImageFailed);
|
||||
|
||||
base.OnApplyTemplate();
|
||||
|
||||
void AttachImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened += handler;
|
||||
}
|
||||
}
|
||||
|
||||
void AttachImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed += handler;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed -= handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnableLazyLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ImageExBase control)
|
||||
if (d is not ImageExBase control)
|
||||
{
|
||||
bool value = (bool)e.NewValue;
|
||||
if (value)
|
||||
{
|
||||
control.LayoutUpdated += control.ImageExBase_LayoutUpdated;
|
||||
return;
|
||||
}
|
||||
|
||||
control.InvalidateLazyLoading();
|
||||
}
|
||||
else
|
||||
{
|
||||
control.LayoutUpdated -= control.ImageExBase_LayoutUpdated;
|
||||
}
|
||||
bool value = (bool)e.NewValue;
|
||||
if (value)
|
||||
{
|
||||
control.LayoutUpdated += control.OnImageExBaseLayoutUpdated;
|
||||
|
||||
control.InvalidateLazyLoading();
|
||||
}
|
||||
else
|
||||
{
|
||||
control.LayoutUpdated -= control.OnImageExBaseLayoutUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,14 +175,6 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
}
|
||||
}
|
||||
|
||||
private static void PlaceholderSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ImageExBase control)
|
||||
{
|
||||
control.OnPlaceholderSourceChanged(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not ImageExBase control)
|
||||
@@ -261,17 +182,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.OldValue is null || e.NewValue is null || !e.OldValue.Equals(e.NewValue))
|
||||
if (e.OldValue is not null && e.NewValue is not null && e.OldValue.Equals(e.NewValue))
|
||||
{
|
||||
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
|
||||
{
|
||||
control.lazyLoadingSource = null;
|
||||
control.SetSource(e.NewValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
control.lazyLoadingSource = e.NewValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
|
||||
{
|
||||
control.lazyLoadingSource = null;
|
||||
control.SetSource(e.NewValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
control.lazyLoadingSource = e.NewValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,11 +224,24 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
|
||||
{
|
||||
VisualStateManager.GoToState(this, LoadedState, true);
|
||||
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "IDE0019")]
|
||||
private void AttachPlaceholderSource(ImageSource? source)
|
||||
{
|
||||
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
|
||||
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
|
||||
// We only need to call those methods if we fail in other cases before we get here.
|
||||
if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.Source = source;
|
||||
}
|
||||
else if (PlaceholderImage is ImageBrush brush)
|
||||
{
|
||||
brush.ImageSource = source;
|
||||
}
|
||||
}
|
||||
|
||||
private async void SetSource(object? source)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
@@ -326,22 +262,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
|
||||
VisualStateManager.GoToState(this, LoadingState, true);
|
||||
|
||||
ImageSource? imageSource = source as ImageSource;
|
||||
if (imageSource is not null)
|
||||
if (source as ImageSource is { } imageSource)
|
||||
{
|
||||
AttachSource(imageSource);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Uri? uri = source as Uri;
|
||||
if (uri is null)
|
||||
if (source as Uri is not { } uri)
|
||||
{
|
||||
string? url = source as string ?? source.ToString();
|
||||
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new UriFormatException("Invalid uri specified.")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -355,61 +288,131 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
{
|
||||
await LoadImageAsync(uri, tokenSource.Token).ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetPlaceholderSource(PlaceholderSource);
|
||||
|
||||
if (ex is OperationCanceledException)
|
||||
{
|
||||
// nothing to do as cancellation has been requested.
|
||||
}
|
||||
else
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void SetPlaceholderSource(object? source)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tokenSource?.Cancel();
|
||||
|
||||
tokenSource = new CancellationTokenSource();
|
||||
|
||||
AttachPlaceholderSource(null);
|
||||
|
||||
if (source is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (source as ImageSource is { } imageSource)
|
||||
{
|
||||
AttachPlaceholderSource(imageSource);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (source as Uri is not { } uri)
|
||||
{
|
||||
string? url = source as string ?? source.ToString();
|
||||
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsHttpUri(uri) && !uri.IsAbsoluteUri)
|
||||
{
|
||||
uri = new Uri("ms-appx:///" + uri.OriginalString.TrimStart('/'));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (uri is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImageSource? img = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
// Only attach our image if we still have a valid request.
|
||||
AttachPlaceholderSource(img);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// nothing to do as cancellation has been requested.
|
||||
}
|
||||
catch (Exception e)
|
||||
catch
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadImageAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
if (imageUri is not null)
|
||||
if (imageUri is null)
|
||||
{
|
||||
if (IsCacheEnabled)
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsCacheEnabled)
|
||||
{
|
||||
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
|
||||
// Only attach our image if we still have a valid request.
|
||||
AttachSource(img);
|
||||
}
|
||||
}
|
||||
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string source = imageUri.OriginalString;
|
||||
const string base64Head = "base64,";
|
||||
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
|
||||
BitmapImage bitmap = new();
|
||||
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
// Only attach our image if we still have a valid request.
|
||||
AttachSource(img);
|
||||
AttachSource(bitmap);
|
||||
}
|
||||
}
|
||||
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
|
||||
}
|
||||
else
|
||||
{
|
||||
AttachSource(new BitmapImage(imageUri)
|
||||
{
|
||||
string source = imageUri.OriginalString;
|
||||
const string base64Head = "base64,";
|
||||
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
|
||||
BitmapImage bitmap = new();
|
||||
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
AttachSource(bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AttachSource(new BitmapImage(imageUri)
|
||||
{
|
||||
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
|
||||
});
|
||||
}
|
||||
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ImageExBase_LayoutUpdated(object? sender, object e)
|
||||
private void OnImageExBaseLayoutUpdated(object? sender, object e)
|
||||
{
|
||||
InvalidateLazyLoading();
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
internal sealed class ImageExFailedEventArgs : EventArgs
|
||||
{
|
||||
public ImageExFailedEventArgs(Exception errorException)
|
||||
{
|
||||
ErrorMessage = ErrorException?.Message;
|
||||
ErrorException = errorException;
|
||||
}
|
||||
|
||||
public Exception? ErrorException { get; private set; }
|
||||
|
||||
public string? ErrorMessage { get; private set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
internal sealed class ImageExOpenedEventArgs : EventArgs
|
||||
{
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace Snap.Hutao.Control;
|
||||
|
||||
[TemplateVisualState(Name = "LoadingIn", GroupName = "CommonStates")]
|
||||
[TemplateVisualState(Name = "LoadingOut", GroupName = "CommonStates")]
|
||||
[TemplatePart(Name = "ContentGrid", Type = typeof(FrameworkElement))]
|
||||
internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
|
||||
{
|
||||
public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(Loading), new PropertyMetadata(default(bool), IsLoadingPropertyChanged));
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:shc="using:Snap.Hutao.Control">
|
||||
|
||||
<Style TargetType="shc:Loading">
|
||||
<Style BasedOn="{StaticResource DefaultLoadingStyle}" TargetType="shc:Loading"/>
|
||||
|
||||
<Style x:Key="DefaultLoadingStyle" TargetType="shc:Loading">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
|
||||
@@ -18,12 +18,15 @@ internal sealed class FontIconExtension : MarkupExtension
|
||||
/// </summary>
|
||||
public string Glyph { get; set; } = default!;
|
||||
|
||||
public double FontSize { get; set; } = 12;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override object ProvideValue()
|
||||
{
|
||||
return new FontIcon()
|
||||
{
|
||||
Glyph = Glyph,
|
||||
FontSize = FontSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,8 @@ internal struct Rgba32
|
||||
/// <param name="code">RGBA 代码</param>
|
||||
public unsafe Rgba32(uint code)
|
||||
{
|
||||
// RRGGBBAA -> AABBGGRR
|
||||
// uint layout: 0xRRGGBBAA -> AABBGGRR
|
||||
// AABBGGRR -> RRGGBBAA
|
||||
fixed (Rgba32* pSelf = &this)
|
||||
{
|
||||
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(code);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32.System.WinRT;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.System.WinRT;
|
||||
using WinRT;
|
||||
|
||||
namespace Snap.Hutao.Control.Media;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Control.Media;
|
||||
using Snap.Hutao.Control.Theme;
|
||||
using Snap.Hutao.Metadata;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
|
||||
@@ -23,8 +24,11 @@ namespace Snap.Hutao.Control.Text;
|
||||
[DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))]
|
||||
internal sealed partial class DescriptionTextBlock : ContentControl
|
||||
{
|
||||
private static readonly int ColorTagFullLength = "<color=#FFFFFFFF></color>".Length;
|
||||
private static readonly int ColorTagLeftLength = "<color=#FFFFFFFF>".Length;
|
||||
private static readonly int RgbaColorTagFullLength = "<color=#FFFFFFFF></color>".Length;
|
||||
private static readonly int RgbaColorTagLeftLength = "<color=#FFFFFFFF>".Length;
|
||||
|
||||
private static readonly int RgbColorTagFullLength = "<color=#FFFFFF></color>".Length;
|
||||
private static readonly int RgbColorTagLeftLength = "<color=#FFFFFF>".Length;
|
||||
|
||||
private static readonly int ItalicTagFullLength = "<i></i>".Length;
|
||||
private static readonly int ItalicTagLeftLength = "<i>".Length;
|
||||
@@ -51,9 +55,16 @@ internal sealed partial class DescriptionTextBlock : ContentControl
|
||||
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
|
||||
ReadOnlySpan<char> description = (string)e.NewValue;
|
||||
ReadOnlySpan<char> description = SpecialNameHandler.Handle((string)e.NewValue);
|
||||
|
||||
UpdateDescription(textBlock, description);
|
||||
try
|
||||
{
|
||||
UpdateDescription(textBlock, description);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ = ex;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnTextStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
@@ -81,13 +92,32 @@ internal sealed partial class DescriptionTextBlock : ContentControl
|
||||
// color tag
|
||||
else if (description[i..].StartsWith("<c"))
|
||||
{
|
||||
AppendText(textBlock, description[last..i]);
|
||||
Rgba32 color = new(description.Slice(i + 8, 8).ToString());
|
||||
int length = description[(i + ColorTagLeftLength)..].IndexOf('<');
|
||||
AppendColorText(textBlock, description.Slice(i + ColorTagLeftLength, length), color);
|
||||
switch (description[i..].IndexOf('>'))
|
||||
{
|
||||
case 16: // RgbaColorTag
|
||||
{
|
||||
AppendText(textBlock, description[last..i]);
|
||||
Rgba32 color = new(description.Slice(i + 8, 8).ToString());
|
||||
int length = description[(i + RgbaColorTagLeftLength)..].IndexOf('<');
|
||||
AppendColorText(textBlock, description.Slice(i + RgbaColorTagLeftLength, length), color);
|
||||
|
||||
i += length + ColorTagFullLength;
|
||||
last = i;
|
||||
i += length + RgbaColorTagFullLength;
|
||||
last = i;
|
||||
break;
|
||||
}
|
||||
|
||||
case 14: // RgbColorTag
|
||||
{
|
||||
AppendText(textBlock, description[last..i]);
|
||||
Rgba32 color = new(description.Slice(i + 8, 6).ToString());
|
||||
int length = description[(i + RgbColorTagLeftLength)..].IndexOf('<');
|
||||
AppendColorText(textBlock, description.Slice(i + RgbColorTagLeftLength, length), color);
|
||||
|
||||
i += length + RgbColorTagFullLength;
|
||||
last = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// italic
|
||||
|
||||
@@ -156,7 +156,7 @@ internal sealed partial class HtmlDescriptionTextBlock : ContentControl
|
||||
text.Inlines.Add(new Run
|
||||
{
|
||||
Text = slice.ToString(),
|
||||
FontWeight = FontWeights.Bold,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,43 @@
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="AcrylicBaseHighBorderCardStyle"
|
||||
BasedOn="{StaticResource BorderCardStyle}"
|
||||
TargetType="Border">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Background" Value="{ThemeResource SystemControlBaseHighAcrylicElementBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="AcrylicBorderCardStyle"
|
||||
BasedOn="{StaticResource BorderCardStyle}"
|
||||
TargetType="Border">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Background" Value="{ThemeResource SystemControlAcrylicElementMediumHighBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GridCardStyle" TargetType="Grid">
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="AcrylicGridCardStyle"
|
||||
BasedOn="{StaticResource GridCardStyle}"
|
||||
TargetType="Grid">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Background" Value="{ThemeResource SystemControlAcrylicElementMediumHighBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="AcrylicBaseHighGridCardStyle"
|
||||
BasedOn="{StaticResource GridCardStyle}"
|
||||
TargetType="Grid">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Background" Value="{ThemeResource SystemControlBaseHighAcrylicElementBrush}"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -2,6 +2,53 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:shch="using:Snap.Hutao.Control.Helper">
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarErrorSeverityBackgroundBrush"
|
||||
FallbackColor="#FDE7E9"
|
||||
TintColor="#FDE7E9"
|
||||
TintOpacity="0.6"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarWarningSeverityBackgroundBrush"
|
||||
FallbackColor="#FFF4CE"
|
||||
TintColor="#FFF4CE"
|
||||
TintOpacity="0.6"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarSuccessSeverityBackgroundBrush"
|
||||
FallbackColor="#DFF6DD"
|
||||
TintColor="#DFF6DD"
|
||||
TintOpacity="0.6"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarInformationalSeverityBackgroundBrush"
|
||||
FallbackColor="#80F6F6F6"
|
||||
TintColor="#80F6F6F6"
|
||||
TintOpacity="0.6"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarErrorSeverityBackgroundBrush"
|
||||
FallbackColor="#442726"
|
||||
TintColor="#442726"
|
||||
TintOpacity="0.6"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarWarningSeverityBackgroundBrush"
|
||||
FallbackColor="#433519"
|
||||
TintColor="#433519"
|
||||
TintOpacity="0.6"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarSuccessSeverityBackgroundBrush"
|
||||
FallbackColor="#393D1B"
|
||||
TintColor="#393D1B"
|
||||
TintOpacity="0.6"/>
|
||||
<AcrylicBrush
|
||||
x:Key="InfoBarInformationalSeverityBackgroundBrush"
|
||||
FallbackColor="#34424d"
|
||||
TintColor="#34424d"
|
||||
TintOpacity="0.6"/>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<Thickness x:Key="InfoBarIconMargin">19,16,19,16</Thickness>
|
||||
<Thickness x:Key="InfoBarContentRootPadding">0,0,0,0</Thickness>
|
||||
<x:Double x:Key="InfoBarIconFontSize">20</x:Double>
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
|
||||
<StackPanel Spacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="StackPanelSpacing8Template">
|
||||
<StackPanel Spacing="8"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
|
||||
<cwcont:UniformGrid
|
||||
ColumnSpacing="2"
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
<x:Double x:Key="ContentDialogMinHeight">64</x:Double>
|
||||
<x:Double x:Key="LargeAppBarButtonWidth">100</x:Double>
|
||||
|
||||
<x:Double x:Key="AppBarThemeCompactActualHeight">50</x:Double>
|
||||
|
||||
<!-- ProgressBar -->
|
||||
<x:Double x:Key="LargeBackgroundProgressBarOpacity">0.2</x:Double>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -1,5 +1,385 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cw="using:CommunityToolkit.WinUI">
|
||||
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
|
||||
<Thickness x:Key="PivotHeaderItemMargin">16,0,0,0</Thickness>
|
||||
<Thickness x:Key="PivotItemMargin">0</Thickness>
|
||||
|
||||
<Style x:Key="CardPivotStyle" TargetType="Pivot">
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Background" Value="{ThemeResource PivotBackground}"/>
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<Grid/>
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Pivot">
|
||||
<Grid
|
||||
x:Name="RootElement"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
Background="{TemplateBinding Background}">
|
||||
<Grid.Resources>
|
||||
<Style x:Key="BaseContentControlStyle" TargetType="ContentControl">
|
||||
<Setter Property="FontFamily" Value="XamlAutoFontFamily"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ContentControl">
|
||||
<ContentPresenter
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
OpticalMarginAlignment="TrimSideBearings"/>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="TitleContentControlStyle"
|
||||
BasedOn="{StaticResource BaseContentControlStyle}"
|
||||
TargetType="ContentControl">
|
||||
<Setter Property="FontFamily" Value="{ThemeResource PivotTitleFontFamily}"/>
|
||||
<Setter Property="FontWeight" Value="{ThemeResource PivotTitleThemeFontWeight}"/>
|
||||
<Setter Property="FontSize" Value="{ThemeResource PivotTitleFontSize}"/>
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<ContentControl
|
||||
x:Name="TitleContentControl"
|
||||
Grid.Row="0"
|
||||
Margin="{StaticResource PivotPortraitThemePadding}"
|
||||
Content="{TemplateBinding Title}"
|
||||
ContentTemplate="{TemplateBinding TitleTemplate}"
|
||||
IsTabStop="False"
|
||||
Style="{StaticResource TitleContentControlStyle}"
|
||||
Visibility="Collapsed"/>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.Resources>
|
||||
<ControlTemplate x:Key="NextTemplate" TargetType="Button">
|
||||
<Border
|
||||
x:Name="Root"
|
||||
Background="{ThemeResource PivotNextButtonBackground}"
|
||||
BorderBrush="{ThemeResource PivotNextButtonBorderBrush}"
|
||||
BorderThickness="{ThemeResource PivotNavButtonBorderThemeThickness}">
|
||||
<FontIcon
|
||||
x:Name="Arrow"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource PivotNextButtonForeground}"
|
||||
Glyph=""
|
||||
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 AcrylicBaseHighGridCardStyle}">
|
||||
<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>
|
||||
@@ -42,7 +42,7 @@
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="{TemplateBinding Padding}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MaxWidth="{Binding Path=(shch:ScrollViewerHelper.LeftPanelMaxWidth), RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ScrollContentPresenter x:Name="ScrollContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}"/>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<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:cwc="using:CommunityToolkit.WinUI.Controls">
|
||||
<!-- Settings -->
|
||||
<x:Double x:Key="SettingsCardSpacing">3</x:Double>
|
||||
<x:Double x:Key="SettingsCardMinHeight">0</x:Double>
|
||||
@@ -6,6 +9,12 @@
|
||||
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
|
||||
|
||||
<x:Double x:Key="SettingsCardContentControlMinWidth">120</x:Double>
|
||||
<x:Double x:Key="SettingsCardContentControlMinWidth2">160</x:Double>
|
||||
|
||||
<x:Double x:Key="SettingsCardContentControlSpacing">10</x:Double>
|
||||
|
||||
<Thickness x:Key="SettingsCardAlignSettingsExpanderPadding">16,16,44,16</Thickness>
|
||||
<Thickness x:Key="SettingsExpanderItemHasIconPadding">16,8,16,8</Thickness>
|
||||
|
||||
<Style
|
||||
x:Key="SettingsSectionHeaderTextBlockStyle"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<x:String x:Key="DocumentLink_Home">https://hut.ao</x:String>
|
||||
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String>
|
||||
<x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String>
|
||||
<x:String x:Key="DocumentLink_Loopback">https://hut.ao/zh/advanced/FAQ.html#%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87%E7%BD%91%E7%BB%9C%E4%BB%A3%E7%90%86%E4%BD%BF%E7%94%A8%E8%83%A1%E6%A1%83%E5%B7%A5%E5%85%B7%E7%AE%B1</x:String>
|
||||
|
||||
<!-- Other -->
|
||||
<x:String x:Key="HolographicHat_GetToken_Release">https://github.com/HolographicHat/GetToken/releases/latest</x:String>
|
||||
@@ -27,8 +28,10 @@
|
||||
|
||||
<!-- EmotionIcon -->
|
||||
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon52">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon445">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.Http;
|
||||
using Snap.Hutao.Core.IO.Http.Proxy;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Service;
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.Globalization;
|
||||
|
||||
@@ -31,7 +34,7 @@ internal static class DependencyInjection
|
||||
.AddJsonOptions()
|
||||
.AddDatabase()
|
||||
.AddInjections()
|
||||
.AddHttpClients()
|
||||
.AddAllHttpClients()
|
||||
|
||||
// Discrete services
|
||||
.AddSingleton<IMessenger, WeakReferenceMessenger>()
|
||||
@@ -48,10 +51,10 @@ internal static class DependencyInjection
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void InitializeCulture(this IServiceProvider serviceProvider)
|
||||
{
|
||||
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
|
||||
appOptions.PreviousCulture = CultureInfo.CurrentCulture;
|
||||
CultureOptions cultureOptions = serviceProvider.GetRequiredService<CultureOptions>();
|
||||
cultureOptions.SystemCulture = CultureInfo.CurrentCulture;
|
||||
|
||||
CultureInfo cultureInfo = appOptions.CurrentCulture;
|
||||
CultureInfo cultureInfo = cultureOptions.CurrentCulture;
|
||||
|
||||
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
||||
|
||||
@@ -38,8 +38,8 @@ internal static class IocConfiguration
|
||||
|
||||
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
|
||||
{
|
||||
RuntimeOptions hutaoOptions = provider.GetRequiredService<RuntimeOptions>();
|
||||
string dbFile = System.IO.Path.Combine(hutaoOptions.DataFolder, "Userdata.db");
|
||||
RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
|
||||
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
|
||||
string sqlConnectionString = $"Data Source={dbFile}";
|
||||
|
||||
// Temporarily create a context
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.IO.Http.Proxy;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -14,12 +15,26 @@ internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
private const string ApplicationJson = "application/json";
|
||||
|
||||
/// <summary>
|
||||
/// 添加 <see cref="HttpClient"/>
|
||||
/// 此方法将会自动生成
|
||||
/// </summary>
|
||||
/// <param name="services">集合</param>
|
||||
/// <returns>可继续操作的集合</returns>
|
||||
public static IServiceCollection AddAllHttpClients(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.ConfigureHttpClientDefaults(clientBuilder =>
|
||||
{
|
||||
clientBuilder
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler())
|
||||
.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
|
||||
{
|
||||
HttpClientHandler clientHandler = (HttpClientHandler)handler;
|
||||
clientHandler.UseProxy = true;
|
||||
clientHandler.Proxy = provider.GetRequiredService<DynamicHttpProxy>();
|
||||
});
|
||||
})
|
||||
.AddHttpClients();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static partial IServiceCollection AddHttpClients(this IServiceCollection services);
|
||||
|
||||
/// <summary>
|
||||
@@ -29,10 +44,10 @@ internal static partial class IocHttpClientConfiguration
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void DefaultConfiguration(IServiceProvider serviceProvider, HttpClient client)
|
||||
{
|
||||
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent);
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(runtimeOptions.UserAgent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -89,6 +104,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
/// HoYoLAB web
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
[SuppressMessage("", "IDE0051")]
|
||||
private static void XRpc4Configuration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
|
||||
@@ -11,10 +11,18 @@ internal sealed class HutaoException : Exception
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
public HutaoException(string message, Exception? innerException)
|
||||
private HutaoException(string message, Exception? innerException)
|
||||
: base($"{message}\n{innerException?.Message}", innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public HutaoExceptionKind Kind { get; private set; }
|
||||
|
||||
public static void ThrowIf(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
throw new HutaoException(kind, message, innerException);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Service.Game.Package;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
@@ -35,6 +36,22 @@ internal static class ThrowHelper
|
||||
throw new GameFileOperationException(message, inner);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static InvalidDataException InvalidData(string message, Exception? inner = default)
|
||||
{
|
||||
throw new InvalidDataException(message, inner);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void InvalidDataIf([DoesNotReturnIf(true)] bool condition, string message, Exception? inner = default)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
throw new InvalidDataException(message, inner);
|
||||
}
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static InvalidOperationException InvalidOperation(string message, Exception? inner = default)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.NetworkManagement.WindowsFirewall;
|
||||
using Snap.Hutao.Win32.Security;
|
||||
using System.Runtime.InteropServices;
|
||||
using static Snap.Hutao.Win32.AdvApi32;
|
||||
using static Snap.Hutao.Win32.ApiMsWinNetIsolation;
|
||||
using static Snap.Hutao.Win32.Macros;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Http.Loopback;
|
||||
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed unsafe class LoopbackManager : ObservableObject
|
||||
{
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
private readonly string hutaoContainerStringSID = default!;
|
||||
private bool isLoopbackEnabled;
|
||||
|
||||
public LoopbackManager(IServiceProvider serviceProvider)
|
||||
{
|
||||
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
|
||||
|
||||
INET_FIREWALL_APP_CONTAINER* pContainers = default;
|
||||
try
|
||||
{
|
||||
WIN32_ERROR error = NetworkIsolationEnumAppContainers(NETISO_FLAG.NETISO_FLAG_MAX, out uint acCount, out pContainers);
|
||||
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error));
|
||||
for (uint i = 0; i < acCount; i++)
|
||||
{
|
||||
INET_FIREWALL_APP_CONTAINER* pContainer = pContainers + i;
|
||||
ReadOnlySpan<char> appContainerName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pContainer->appContainerName);
|
||||
if (appContainerName.Equals(runtimeOptions.FamilyName, StringComparison.Ordinal))
|
||||
{
|
||||
ConvertSidToStringSidW(pContainer->appContainerSid, out PWSTR stringSid);
|
||||
hutaoContainerStringSID = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid).ToString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// This function returns 1 rather than 0 specfied in the document.
|
||||
_ = NetworkIsolationFreeAppContainers(pContainers);
|
||||
}
|
||||
|
||||
WIN32_ERROR error2 = NetworkIsolationGetAppContainerConfig(out uint accCount, out SID_AND_ATTRIBUTES* pSids);
|
||||
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(error2));
|
||||
for (uint i = 0; i < accCount; i++)
|
||||
{
|
||||
ConvertSidToStringSidW((pSids + i)->Sid, out PWSTR stringSid);
|
||||
ReadOnlySpan<char> stringSidSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(stringSid);
|
||||
if (stringSidSpan.Equals(hutaoContainerStringSID, StringComparison.Ordinal))
|
||||
{
|
||||
IsLoopbackEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLoopbackEnabled { get => isLoopbackEnabled; private set => SetProperty(ref isLoopbackEnabled, value); }
|
||||
|
||||
public void EnableLoopback()
|
||||
{
|
||||
NetworkIsolationGetAppContainerConfig(out uint accCount, out SID_AND_ATTRIBUTES* pSids);
|
||||
List<SID_AND_ATTRIBUTES> sids = new((int)(accCount + 1));
|
||||
for (uint i = 0; i < accCount; i++)
|
||||
{
|
||||
sids.Add(*(pSids + i));
|
||||
}
|
||||
|
||||
ConvertStringSidToSidW(hutaoContainerStringSID, out PSID pSid);
|
||||
SID_AND_ATTRIBUTES sidAndAttributes = default;
|
||||
sidAndAttributes.Sid = pSid;
|
||||
sids.Add(sidAndAttributes);
|
||||
IsLoopbackEnabled = NetworkIsolationSetAppContainerConfig(CollectionsMarshal.AsSpan(sids)) is WIN32_ERROR.ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
110
src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Proxy/DynamicHttpProxy.cs
Normal file
110
src/Snap.Hutao/Snap.Hutao/Core/IO/Http/Proxy/DynamicHttpProxy.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Win32.Registry;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Http.Proxy;
|
||||
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed partial class DynamicHttpProxy : ObservableObject, IWebProxy, IDisposable
|
||||
{
|
||||
private const string ProxySettingPath = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections";
|
||||
|
||||
private static readonly Lazy<MethodInfo> LazyConstructSystemProxyMethod = new(GetConstructSystemProxyMethod);
|
||||
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly RegistryWatcher watcher;
|
||||
|
||||
private IWebProxy innerProxy = default!;
|
||||
|
||||
public DynamicHttpProxy(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.serviceProvider = serviceProvider;
|
||||
UpdateInnerProxy();
|
||||
|
||||
watcher = new(ProxySettingPath, OnSystemProxySettingsChanged);
|
||||
watcher.Start();
|
||||
}
|
||||
|
||||
public string CurrentProxyUri
|
||||
{
|
||||
get
|
||||
{
|
||||
Uri? proxyUri = GetProxy("https://hut.ao".ToUri());
|
||||
return proxyUri is null
|
||||
? SH.ViewPageFeedbackCurrentProxyNoProxyDescription
|
||||
: proxyUri.AbsoluteUri;
|
||||
}
|
||||
}
|
||||
|
||||
public IWebProxy InnerProxy
|
||||
{
|
||||
get => innerProxy;
|
||||
|
||||
[MemberNotNull(nameof(innerProxy))]
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(innerProxy, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
(innerProxy as IDisposable)?.Dispose();
|
||||
innerProxy = value;
|
||||
}
|
||||
}
|
||||
|
||||
public ICredentials? Credentials
|
||||
{
|
||||
get => InnerProxy.Credentials;
|
||||
set => InnerProxy.Credentials = value;
|
||||
}
|
||||
|
||||
public Uri? GetProxy(Uri destination)
|
||||
{
|
||||
return InnerProxy.GetProxy(destination);
|
||||
}
|
||||
|
||||
public bool IsBypassed(Uri host)
|
||||
{
|
||||
return InnerProxy.IsBypassed(host);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(innerProxy as IDisposable)?.Dispose();
|
||||
watcher.Dispose();
|
||||
}
|
||||
|
||||
public void OnSystemProxySettingsChanged()
|
||||
{
|
||||
UpdateInnerProxy();
|
||||
|
||||
// TaskContext can't be injected directly since there are some recursive dependencies.
|
||||
ITaskContext taskContext = serviceProvider.GetRequiredService<ITaskContext>();
|
||||
taskContext.BeginInvokeOnMainThread(() => OnPropertyChanged(nameof(CurrentProxyUri)));
|
||||
}
|
||||
|
||||
private static MethodInfo GetConstructSystemProxyMethod()
|
||||
{
|
||||
Type? systemProxyInfoType = typeof(System.Net.Http.SocketsHttpHandler).Assembly.GetType("System.Net.Http.SystemProxyInfo");
|
||||
ArgumentNullException.ThrowIfNull(systemProxyInfoType);
|
||||
|
||||
MethodInfo? constructSystemProxyMethod = systemProxyInfoType.GetMethod("ConstructSystemProxy", BindingFlags.Static | BindingFlags.Public);
|
||||
ArgumentNullException.ThrowIfNull(constructSystemProxyMethod);
|
||||
|
||||
return constructSystemProxyMethod;
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(innerProxy))]
|
||||
private void UpdateInnerProxy()
|
||||
{
|
||||
IWebProxy? proxy = LazyConstructSystemProxyMethod.Value.Invoke(default, default) as IWebProxy;
|
||||
ArgumentNullException.ThrowIfNull(proxy);
|
||||
|
||||
InnerProxy = proxy;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
|
||||
private readonly long contentLength;
|
||||
private readonly int bufferSize;
|
||||
private readonly SafeFileHandle destFileHandle;
|
||||
private readonly int maxDegreeOfParallelism;
|
||||
private readonly List<Shard> shards;
|
||||
|
||||
private HttpShardCopyWorker(HttpShardCopyWorkerOptions<TStatus> options)
|
||||
@@ -28,6 +29,7 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
|
||||
contentLength = options.ContentLength;
|
||||
bufferSize = options.BufferSize;
|
||||
destFileHandle = options.GetFileHandle();
|
||||
maxDegreeOfParallelism = options.MaxDegreeOfParallelism;
|
||||
shards = CalculateShards(contentLength);
|
||||
|
||||
static List<Shard> CalculateShards(long contentLength)
|
||||
@@ -56,7 +58,11 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
|
||||
public Task CopyAsync(IProgress<TStatus> progress, CancellationToken token = default)
|
||||
{
|
||||
ShardProgress shardProgress = new(progress, statusFactory, contentLength);
|
||||
return Parallel.ForEachAsync(shards, token, (shard, token) => CopyShardAsync(shard, shardProgress, token));
|
||||
ParallelOptions parallelOptions = new()
|
||||
{
|
||||
MaxDegreeOfParallelism = maxDegreeOfParallelism,
|
||||
};
|
||||
return Parallel.ForEachAsync(shards, parallelOptions, (shard, token) => CopyShardAsync(shard, shardProgress, token));
|
||||
|
||||
async ValueTask CopyShardAsync(Shard shard, IProgress<ShardStatus> progress, CancellationToken token)
|
||||
{
|
||||
|
||||
@@ -22,6 +22,8 @@ internal sealed class HttpShardCopyWorkerOptions<TStatus>
|
||||
|
||||
public int BufferSize { get; set; } = 80 * 1024;
|
||||
|
||||
public int MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount;
|
||||
|
||||
public SafeFileHandle GetFileHandle()
|
||||
{
|
||||
return File.OpenHandle(DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.RandomAccess | FileOptions.Asynchronous, ContentLength);
|
||||
|
||||
@@ -11,11 +11,14 @@ namespace Snap.Hutao.Core.IO.Ini;
|
||||
[HighQuality]
|
||||
internal static class IniSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// 反序列化
|
||||
/// </summary>
|
||||
/// <param name="fileStream">文件流</param>
|
||||
/// <returns>Ini 元素集合</returns>
|
||||
public static List<IniElement> DeserializeFromFile(string filePath)
|
||||
{
|
||||
using (FileStream readStream = File.OpenRead(filePath))
|
||||
{
|
||||
return Deserialize(readStream);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<IniElement> Deserialize(FileStream fileStream)
|
||||
{
|
||||
List<IniElement> results = [];
|
||||
@@ -50,11 +53,14 @@ internal static class IniSerializer
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 序列化
|
||||
/// </summary>
|
||||
/// <param name="fileStream">写入的流</param>
|
||||
/// <param name="elements">元素</param>
|
||||
public static void SerializeToFile(string filePath, IEnumerable<IniElement> elements)
|
||||
{
|
||||
using (FileStream writeStream = File.Create(filePath))
|
||||
{
|
||||
Serialize(writeStream, elements);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
|
||||
{
|
||||
using (StreamWriter writer = new(fileStream))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using CommunityToolkit.WinUI.Notifications;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core.LifeCycle.InterProcess;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Service.DailyNote;
|
||||
using Snap.Hutao.Service.Discord;
|
||||
@@ -24,24 +24,9 @@ namespace Snap.Hutao.Core.LifeCycle;
|
||||
[SuppressMessage("", "CA1001")]
|
||||
internal sealed partial class Activation : IActivation
|
||||
{
|
||||
/// <summary>
|
||||
/// 操作
|
||||
/// </summary>
|
||||
public const string Action = nameof(Action);
|
||||
|
||||
/// <summary>
|
||||
/// Uid
|
||||
/// </summary>
|
||||
public const string Uid = nameof(Uid);
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏启动参数
|
||||
/// </summary>
|
||||
public const string LaunchGame = nameof(LaunchGame);
|
||||
|
||||
/// <summary>
|
||||
/// 从剪贴板导入成就
|
||||
/// </summary>
|
||||
public const string ImportUIAFFromClipboard = nameof(ImportUIAFFromClipboard);
|
||||
|
||||
private const string CategoryAchievement = "ACHIEVEMENT";
|
||||
@@ -55,29 +40,20 @@ internal sealed partial class Activation : IActivation
|
||||
private readonly SemaphoreSlim activateSemaphore = new(1);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Activate(object? sender, AppActivationArguments args)
|
||||
public void Activate(HutaoActivationArguments args)
|
||||
{
|
||||
_ = sender;
|
||||
if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
|
||||
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
|
||||
{
|
||||
HandleActivationAsync(args, true).SafeForget();
|
||||
return;
|
||||
}
|
||||
|
||||
HandleActivationAsync(args).SafeForget();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void NonRedirectToActivate(object? sender, AppActivationArguments args)
|
||||
public void Initialize()
|
||||
{
|
||||
_ = sender;
|
||||
if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
|
||||
{
|
||||
HandleActivationAsync(args, false).SafeForget();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void InitializeWith(AppInstance appInstance)
|
||||
{
|
||||
appInstance.Activated += Activate;
|
||||
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget();
|
||||
ToastNotificationManagerCompat.OnActivated += NotificationActivate;
|
||||
}
|
||||
|
||||
@@ -95,44 +71,40 @@ internal sealed partial class Activation : IActivation
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask HandleActivationAsync(AppActivationArguments args, bool isRedirected)
|
||||
private async ValueTask HandleActivationAsync(HutaoActivationArguments args)
|
||||
{
|
||||
if (activateSemaphore.CurrentCount > 0)
|
||||
{
|
||||
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
await HandleActivationCoreAsync(args, isRedirected).ConfigureAwait(false);
|
||||
await HandleActivationCoreAsync(args).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask HandleActivationCoreAsync(AppActivationArguments args, bool isRedirected)
|
||||
private async ValueTask HandleActivationCoreAsync(HutaoActivationArguments args)
|
||||
{
|
||||
if (args.Kind == ExtendedActivationKind.Protocol)
|
||||
if (args.Kind is HutaoActivationKind.Protocol)
|
||||
{
|
||||
if (args.TryGetProtocolActivatedUri(out Uri? uri))
|
||||
{
|
||||
await HandleUrlActivationAsync(uri, isRedirected).ConfigureAwait(false);
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(args.ProtocolActivatedUri);
|
||||
await HandleUrlActivationAsync(args.ProtocolActivatedUri, args.IsRedirectTo).ConfigureAwait(false);
|
||||
}
|
||||
else if (args.Kind == ExtendedActivationKind.Launch)
|
||||
else if (args.Kind is HutaoActivationKind.Launch)
|
||||
{
|
||||
if (args.TryGetLaunchActivatedArgument(out string? arguments))
|
||||
ArgumentNullException.ThrowIfNull(args.LaunchActivatedArguments);
|
||||
switch (args.LaunchActivatedArguments)
|
||||
{
|
||||
switch (arguments)
|
||||
{
|
||||
case LaunchGame:
|
||||
{
|
||||
await HandleLaunchGameActionAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
case LaunchGame:
|
||||
{
|
||||
await HandleLaunchGameActionAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
default:
|
||||
{
|
||||
await HandleNormalLaunchActionAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,11 +162,11 @@ internal sealed partial class Activation : IActivation
|
||||
|
||||
serviceProvider
|
||||
.GetRequiredService<IDiscordService>()
|
||||
.SetNormalActivity()
|
||||
.SetNormalActivityAsync()
|
||||
.SafeForget();
|
||||
}
|
||||
|
||||
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirected)
|
||||
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirectTo)
|
||||
{
|
||||
UriBuilder builder = new(uri);
|
||||
|
||||
@@ -207,13 +179,13 @@ internal sealed partial class Activation : IActivation
|
||||
case CategoryAchievement:
|
||||
{
|
||||
await WaitMainWindowAsync().ConfigureAwait(false);
|
||||
await HandleAchievementActionAsync(action, parameter, isRedirected).ConfigureAwait(false);
|
||||
await HandleAchievementActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
case CategoryDailyNote:
|
||||
{
|
||||
await HandleDailyNoteActionAsync(action, parameter, isRedirected).ConfigureAwait(false);
|
||||
await HandleDailyNoteActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -225,10 +197,10 @@ internal sealed partial class Activation : IActivation
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirected)
|
||||
private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirectTo)
|
||||
{
|
||||
_ = parameter;
|
||||
_ = isRedirected;
|
||||
_ = isRedirectTo;
|
||||
switch (action)
|
||||
{
|
||||
case UrlActionImport:
|
||||
@@ -245,7 +217,7 @@ internal sealed partial class Activation : IActivation
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirected)
|
||||
private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirectTo)
|
||||
{
|
||||
_ = parameter;
|
||||
switch (action)
|
||||
@@ -264,7 +236,7 @@ internal sealed partial class Activation : IActivation
|
||||
}
|
||||
|
||||
// Check if it's redirected.
|
||||
if (!isRedirected)
|
||||
if (!isRedirectTo)
|
||||
{
|
||||
// It's a direct open process, should exit immediately.
|
||||
Process.GetCurrentProcess().Kill();
|
||||
|
||||
@@ -36,7 +36,7 @@ internal static class AppActivationArgumentsExtensions
|
||||
/// <param name="activatedEventArgs">应用程序激活参数</param>
|
||||
/// <param name="arguments">参数</param>
|
||||
/// <returns>是否存在参数</returns>
|
||||
public static bool TryGetLaunchActivatedArgument(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
|
||||
public static bool TryGetLaunchActivatedArguments(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
|
||||
{
|
||||
arguments = null;
|
||||
if (activatedEventArgs.Data is not ILaunchActivatedEventArgs launchArgs)
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Security;
|
||||
using Windows.Win32.System.Com;
|
||||
using static Windows.Win32.PInvoke;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.System.Com;
|
||||
using static Snap.Hutao.Win32.ConstValues;
|
||||
using static Snap.Hutao.Win32.Kernel32;
|
||||
using static Snap.Hutao.Win32.Ole32;
|
||||
|
||||
namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
@@ -18,7 +19,7 @@ internal static class AppInstanceExtension
|
||||
private static readonly WaitCallback RunActionWaitCallback = RunAction;
|
||||
|
||||
// Hold the reference here to prevent memory corruption.
|
||||
private static HANDLE redirectEventHandle = HANDLE.Null;
|
||||
private static HANDLE redirectEventHandle;
|
||||
|
||||
/// <summary>
|
||||
/// 同步非阻塞重定向
|
||||
@@ -27,19 +28,24 @@ internal static class AppInstanceExtension
|
||||
/// <param name="args">参数</param>
|
||||
public static unsafe void RedirectActivationTo(this AppInstance appInstance, AppActivationArguments args)
|
||||
{
|
||||
redirectEventHandle = CreateEvent(default(SECURITY_ATTRIBUTES*), true, false, null);
|
||||
|
||||
// use ThreadPool.UnsafeQueueUserWorkItem to cancel stacktrace
|
||||
// like ExecutionContext.SuppressFlow
|
||||
ThreadPool.UnsafeQueueUserWorkItem(RunActionWaitCallback, () =>
|
||||
try
|
||||
{
|
||||
appInstance.RedirectActivationToAsync(args).AsTask().Wait();
|
||||
SetEvent(redirectEventHandle);
|
||||
});
|
||||
redirectEventHandle = CreateEventW(default, true, false, default);
|
||||
|
||||
ReadOnlySpan<HANDLE> handles = new(ref redirectEventHandle);
|
||||
CoWaitForMultipleObjects((uint)CWMO_FLAGS.CWMO_DEFAULT, INFINITE, handles, out uint _);
|
||||
CloseHandle(redirectEventHandle);
|
||||
// use ThreadPool.UnsafeQueueUserWorkItem to cancel stacktrace
|
||||
// like ExecutionContext.SuppressFlow
|
||||
ThreadPool.UnsafeQueueUserWorkItem(RunActionWaitCallback, () =>
|
||||
{
|
||||
appInstance.RedirectActivationToAsync(args).AsTask().Wait();
|
||||
SetEvent(redirectEventHandle);
|
||||
});
|
||||
|
||||
CoWaitForMultipleObjects(CWMO_FLAGS.CWMO_DEFAULT, INFINITE, [redirectEventHandle], out uint _);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(redirectEventHandle);
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH007")]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Windows.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Snap.Hutao.Core.LifeCycle;
|
||||
@@ -19,6 +19,6 @@ internal static class CurrentWindowReferenceExtension
|
||||
{
|
||||
return reference.Window is IWindowOptionsSource optionsSource
|
||||
? optionsSource.WindowOptions.Hwnd
|
||||
: (HWND)WindowNative.GetWindowHandle(reference.Window);
|
||||
: WindowNative.GetWindowHandle(reference.Window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
|
||||
namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
/// <summary>
|
||||
@@ -10,24 +8,7 @@ namespace Snap.Hutao.Core.LifeCycle;
|
||||
/// </summary>
|
||||
internal interface IActivation
|
||||
{
|
||||
/// <summary>
|
||||
/// 响应激活事件
|
||||
/// 激活事件一般不会在UI线程上触发
|
||||
/// </summary>
|
||||
/// <param name="sender">发送方</param>
|
||||
/// <param name="args">激活参数</param>
|
||||
void Activate(object? sender, AppActivationArguments args);
|
||||
void Activate(HutaoActivationArguments args);
|
||||
|
||||
/// <summary>
|
||||
/// 使用当前 App 实例初始化激活
|
||||
/// </summary>
|
||||
/// <param name="appInstance">App 实例</param>
|
||||
void InitializeWith(AppInstance appInstance);
|
||||
|
||||
/// <summary>
|
||||
/// 无转发触发激活事件
|
||||
/// </summary>
|
||||
/// <param name="sender">发送方</param>
|
||||
/// <param name="args">激活参数</param>
|
||||
void NonRedirectToActivate(object? sender, AppActivationArguments args);
|
||||
void Initialize();
|
||||
}
|
||||
@@ -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);
|
||||
ThrowHelper.InvalidDataIf(XxHash64.HashToUInt64(content) != header->Checksum, "PipePacket Content Hash incorrect");
|
||||
return content;
|
||||
}
|
||||
|
||||
private unsafe void RunPacketSession(NamedPipeServerStream serverStream, CancellationToken token)
|
||||
{
|
||||
Span<byte> headerSpan = stackalloc byte[sizeof(PipePacketHeader)];
|
||||
bool sessionTerminated = false;
|
||||
while (serverStream.IsConnected && !sessionTerminated && !token.IsCancellationRequested)
|
||||
{
|
||||
serverStream.ReadExactly(headerSpan);
|
||||
fixed (byte* pHeader = headerSpan)
|
||||
{
|
||||
PipePacketHeader* header = (PipePacketHeader*)pHeader;
|
||||
|
||||
switch ((header->Type, header->Command, header->ContentType))
|
||||
{
|
||||
case (PipePacketType.Request, PipePacketCommand.RedirectActivation, PipePacketContentType.Json):
|
||||
ReadOnlySpan<byte> content = GetValidatedContent(serverStream, header);
|
||||
messageDispatcher.RedirectActivation(JsonSerializer.Deserialize<HutaoActivationArguments>(content));
|
||||
break;
|
||||
case (PipePacketType.Termination, _, _):
|
||||
serverStream.Disconnect();
|
||||
sessionTerminated = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Console;
|
||||
using static Windows.Win32.PInvoke;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.System.Console;
|
||||
using static Snap.Hutao.Win32.Kernel32;
|
||||
|
||||
namespace Snap.Hutao.Core.Logging;
|
||||
|
||||
@@ -26,7 +26,7 @@ internal sealed class ConsoleWindowLifeTime : IDisposable
|
||||
SetConsoleMode(inputHandle, mode);
|
||||
}
|
||||
|
||||
SetConsoleTitle("Snap Hutao Debug Console");
|
||||
SetConsoleTitleW("Snap Hutao Debug Console");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,113 +14,99 @@ namespace Snap.Hutao.Core;
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed class RuntimeOptions
|
||||
{
|
||||
private readonly bool isWebView2Supported;
|
||||
private readonly string webView2Version = SH.CoreWebView2HelperVersionUndetected;
|
||||
private readonly Lazy<(Version Version, string UserAgent)> lazyVersionAndUserAgent = new(() =>
|
||||
{
|
||||
Version version = Package.Current.Id.Version.ToVersion();
|
||||
return (version, $"Snap Hutao/{version}");
|
||||
});
|
||||
|
||||
private bool? isElevated;
|
||||
private readonly Lazy<string> lazyDataFolder = new(() =>
|
||||
{
|
||||
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(preferredPath))
|
||||
{
|
||||
Directory.CreateDirectory(preferredPath);
|
||||
return preferredPath;
|
||||
}
|
||||
|
||||
// Fallback to MyDocuments
|
||||
string myDocuments = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
|
||||
#if RELEASE
|
||||
// 将测试版与正式版的文件目录分离
|
||||
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
|
||||
#else
|
||||
// 使得迁移能正常生成
|
||||
string folderName = "Hutao";
|
||||
#endif
|
||||
string path = Path.GetFullPath(Path.Combine(myDocuments, folderName));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
});
|
||||
|
||||
private readonly Lazy<string> lazyDeviceId = new(() =>
|
||||
{
|
||||
string userName = Environment.UserName;
|
||||
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
|
||||
return Convert.ToMd5HexString($"{userName}{machineGuid}");
|
||||
});
|
||||
|
||||
private readonly Lazy<(string Version, bool Supported)> lazyWebViewEnvironment = new(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string version = CoreWebView2Environment.GetAvailableBrowserVersionString();
|
||||
return (version, true);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return (SH.CoreWebView2HelperVersionUndetected, false);
|
||||
}
|
||||
});
|
||||
|
||||
private readonly Lazy<bool> lazyElevated = new(() =>
|
||||
{
|
||||
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
|
||||
{
|
||||
WindowsPrincipal principal = new(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
});
|
||||
|
||||
private readonly Lazy<string> lazyLocalCache = new(() => ApplicationData.Current.LocalCacheFolder.Path);
|
||||
private readonly Lazy<string> lazyInstalledLocation = new(() => Package.Current.InstalledLocation.Path);
|
||||
private readonly Lazy<string> lazyFamilyName = new(() => Package.Current.Id.FamilyName);
|
||||
|
||||
public RuntimeOptions(ILogger<RuntimeOptions> logger)
|
||||
{
|
||||
AppLaunchTime = DateTimeOffset.UtcNow;
|
||||
|
||||
DataFolder = GetDataFolderPath();
|
||||
LocalCache = ApplicationData.Current.LocalCacheFolder.Path;
|
||||
InstalledLocation = Package.Current.InstalledLocation.Path;
|
||||
FamilyName = Package.Current.Id.FamilyName;
|
||||
|
||||
Version = Package.Current.Id.Version.ToVersion();
|
||||
UserAgent = $"Snap Hutao/{Version}";
|
||||
|
||||
DeviceId = GetUniqueUserId();
|
||||
DetectWebView2Environment(logger, out webView2Version, out isWebView2Supported);
|
||||
|
||||
static string GetDataFolderPath()
|
||||
{
|
||||
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(preferredPath))
|
||||
{
|
||||
Directory.CreateDirectory(preferredPath);
|
||||
return preferredPath;
|
||||
}
|
||||
|
||||
// Fallback to MyDocuments
|
||||
string myDocuments = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
|
||||
#if RELEASE
|
||||
// 将测试版与正式版的文件目录分离
|
||||
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
|
||||
#else
|
||||
// 使得迁移能正常生成
|
||||
string folderName = "Hutao";
|
||||
#endif
|
||||
string path = Path.GetFullPath(Path.Combine(myDocuments, folderName));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
static string GetUniqueUserId()
|
||||
{
|
||||
string userName = Environment.UserName;
|
||||
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
|
||||
return Convert.ToMd5HexString($"{userName}{machineGuid}");
|
||||
}
|
||||
|
||||
static void DetectWebView2Environment(ILogger<RuntimeOptions> logger, out string webView2Version, out bool isWebView2Supported)
|
||||
{
|
||||
try
|
||||
{
|
||||
webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString();
|
||||
isWebView2Supported = true;
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
webView2Version = SH.CoreWebView2HelperVersionUndetected;
|
||||
isWebView2Supported = false;
|
||||
logger.LogError(ex, "WebView2 Runtime not installed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Version Version { get; }
|
||||
public Version Version { get => lazyVersionAndUserAgent.Value.Version; }
|
||||
|
||||
public string UserAgent { get; }
|
||||
public string UserAgent { get => lazyVersionAndUserAgent.Value.UserAgent; }
|
||||
|
||||
public string InstalledLocation { get; }
|
||||
public string InstalledLocation { get => lazyInstalledLocation.Value; }
|
||||
|
||||
public string DataFolder { get; }
|
||||
public string DataFolder { get => lazyDataFolder.Value; }
|
||||
|
||||
public string LocalCache { get; }
|
||||
public string LocalCache { get => lazyLocalCache.Value; }
|
||||
|
||||
public string FamilyName { get; }
|
||||
public string FamilyName { get => lazyFamilyName.Value; }
|
||||
|
||||
public string DeviceId { get; }
|
||||
public string DeviceId { get => lazyDeviceId.Value; }
|
||||
|
||||
public string WebView2Version { get => webView2Version; }
|
||||
public string WebView2Version { get => lazyWebViewEnvironment.Value.Version; }
|
||||
|
||||
public bool IsWebView2Supported { get => isWebView2Supported; }
|
||||
public bool IsWebView2Supported { get => lazyWebViewEnvironment.Value.Supported; }
|
||||
|
||||
public bool IsElevated
|
||||
{
|
||||
get
|
||||
{
|
||||
return isElevated ??= GetElevated();
|
||||
|
||||
static bool GetElevated()
|
||||
{
|
||||
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
|
||||
{
|
||||
WindowsPrincipal principal = new(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool IsElevated { get => lazyElevated.Value; }
|
||||
|
||||
public DateTimeOffset AppLaunchTime { get; }
|
||||
}
|
||||
@@ -13,4 +13,9 @@ internal static class RuntimeOptionsExtension
|
||||
Directory.CreateDirectory(directory);
|
||||
return Path.Combine(directory, fileName);
|
||||
}
|
||||
|
||||
public static string GetDataFolderServerCacheFolder(this RuntimeOptions options)
|
||||
{
|
||||
return Path.Combine(options.DataFolder, "ServerCache");
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ internal static class SettingKeys
|
||||
public const string DataFolderPath = "DataFolderPath";
|
||||
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
|
||||
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
|
||||
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
|
||||
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled2";
|
||||
#endregion
|
||||
|
||||
#region Passport
|
||||
@@ -56,5 +56,11 @@ internal static class SettingKeys
|
||||
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
|
||||
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
|
||||
public const string OverrideUpdateVersionComparison = "OverrideUpdateVersionComparison";
|
||||
public const string OverridePackageConvertDirectoryPermissionsRequirement = "OverridePackageConvertDirectoryPermissionsRequirement";
|
||||
#endregion
|
||||
|
||||
#region Obsolete
|
||||
[Obsolete("重置调试控制台开关")]
|
||||
public const string IsAllocConsoleDebugModeEnabledLegacy1 = "IsAllocConsoleDebugModeEnabled";
|
||||
#endregion
|
||||
}
|
||||
@@ -39,6 +39,11 @@ internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
if (WScriptExists(DailyNoteRefreshScriptName, out string fullPath))
|
||||
{
|
||||
File.Delete(fullPath);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +68,7 @@ internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
|
||||
|
||||
public bool IsDailyNoteRefreshEnabled()
|
||||
{
|
||||
return WScriptExists(DailyNoteRefreshScriptName, out _);
|
||||
return TaskService.Instance.RootFolder.Tasks.Any(task => task.Name is DailyNoteRefreshTaskName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.System.Com;
|
||||
using Snap.Hutao.Win32.UI.Shell;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Storage;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Com;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using static Windows.Win32.PInvoke;
|
||||
using static Snap.Hutao.Win32.Macros;
|
||||
using static Snap.Hutao.Win32.Ole32;
|
||||
|
||||
namespace Snap.Hutao.Core.Shell;
|
||||
|
||||
@@ -34,31 +33,44 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT result = CoCreateInstance<ShellLink, IShellLinkW>(null, CLSCTX.CLSCTX_INPROC_SERVER, out IShellLinkW shellLink);
|
||||
Marshal.ThrowExceptionForHR(result);
|
||||
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath);
|
||||
}
|
||||
|
||||
shellLink.SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
|
||||
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
|
||||
shellLink.SetIconLocation(targetLogoPath, 0);
|
||||
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
IShellLinkDataList shellLinkDataList = (IShellLinkDataList)shellLink;
|
||||
shellLinkDataList.GetFlags(out uint flags);
|
||||
flags |= (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER;
|
||||
shellLinkDataList.SetFlags(flags);
|
||||
|
||||
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");
|
||||
|
||||
IPersistFile persistFile = (IPersistFile)shellLink;
|
||||
try
|
||||
// DO NOT revert if condition, COM interfaces need to be released properly
|
||||
HRESULT hr = CoCreateInstance(in ShellLink.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IShellLinkW.IID, out IShellLinkW* pShellLink);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
persistFile.Save(target, false);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
pShellLink->SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
|
||||
pShellLink->SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
|
||||
pShellLink->SetIconLocation(targetLogoPath, 0);
|
||||
|
||||
if (SUCCEEDED(pShellLink->QueryInterface(in IShellLinkDataList.IID, out IShellLinkDataList* pShellLinkDataList)))
|
||||
{
|
||||
pShellLinkDataList->GetFlags(out uint flags);
|
||||
pShellLinkDataList->SetFlags(flags | (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER);
|
||||
pShellLinkDataList->Release();
|
||||
}
|
||||
|
||||
if (SUCCEEDED(pShellLink->QueryInterface(in IPersistFile.IID, out IPersistFile* pPersistFile)))
|
||||
{
|
||||
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");
|
||||
|
||||
if (SUCCEEDED(pPersistFile->Save(target, false)))
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
|
||||
pPersistFile->Release();
|
||||
}
|
||||
|
||||
pShellLink->Release();
|
||||
}
|
||||
|
||||
return true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,12 @@ internal readonly struct Delay
|
||||
/// <summary>
|
||||
/// 随机延迟
|
||||
/// </summary>
|
||||
/// <param name="minMilliSeconds">最小,闭</param>
|
||||
/// <param name="maxMilliSeconds">最小,开</param>
|
||||
/// <param name="min">最小,闭</param>
|
||||
/// <param name="max">最小,开</param>
|
||||
/// <returns>任务</returns>
|
||||
public static ValueTask Random(int minMilliSeconds, int maxMilliSeconds)
|
||||
public static ValueTask RandomMilliSeconds(int min, int max)
|
||||
{
|
||||
return Task.Delay((int)(System.Random.Shared.NextDouble() * (maxMilliSeconds - minMilliSeconds)) + minMilliSeconds).AsValueTask();
|
||||
return Task.Delay((int)(System.Random.Shared.NextDouble() * (max - min)) + min).AsValueTask();
|
||||
}
|
||||
|
||||
public static ValueTask FromSeconds(int seconds)
|
||||
|
||||
18
src/Snap.Hutao/Snap.Hutao/Core/Threading/SpinWaitPolyfill.cs
Normal file
18
src/Snap.Hutao/Snap.Hutao/Core/Threading/SpinWaitPolyfill.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
internal delegate bool SpinWaitPredicate<T>(ref readonly T state);
|
||||
|
||||
internal static class SpinWaitPolyfill
|
||||
{
|
||||
public static unsafe void SpinUntil<T>(ref T state, delegate*<ref readonly T, bool> condition)
|
||||
{
|
||||
SpinWait spinner = default;
|
||||
while (!condition(ref state))
|
||||
{
|
||||
spinner.SpinOnce();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Windowing;
|
||||
using Snap.Hutao.Win32;
|
||||
using Windows.Graphics;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing.Backdrop;
|
||||
|
||||
internal interface IBackdropNeedEraseBackground;
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing.Backdrop;
|
||||
|
||||
internal sealed class TransparentBackdrop : SystemBackdrop, IDisposable, IBackdropNeedEraseBackground
|
||||
{
|
||||
private readonly object compositorLock = new();
|
||||
|
||||
private Color tintColor;
|
||||
private Windows.UI.Composition.CompositionColorBrush? brush;
|
||||
private Windows.UI.Composition.Compositor? compositor;
|
||||
|
||||
public TransparentBackdrop()
|
||||
: this(Colors.Transparent)
|
||||
{
|
||||
}
|
||||
|
||||
public TransparentBackdrop(Color tintColor)
|
||||
{
|
||||
this.tintColor = tintColor;
|
||||
}
|
||||
|
||||
internal Windows.UI.Composition.Compositor Compositor
|
||||
{
|
||||
get
|
||||
{
|
||||
if (compositor is null)
|
||||
{
|
||||
lock (compositorLock)
|
||||
{
|
||||
if (compositor is null)
|
||||
{
|
||||
DispatcherQueue.EnsureSystemDispatcherQueue();
|
||||
compositor = new Windows.UI.Composition.Compositor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return compositor;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
compositor?.Dispose();
|
||||
}
|
||||
|
||||
protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot)
|
||||
{
|
||||
brush ??= Compositor.CreateColorBrush(tintColor);
|
||||
connectedTarget.SystemBackdrop = brush;
|
||||
}
|
||||
|
||||
protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget)
|
||||
{
|
||||
disconnectedTarget.SystemBackdrop = null;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
[HighQuality]
|
||||
internal enum BackdropType
|
||||
{
|
||||
Transparent = -1,
|
||||
|
||||
/// <summary>
|
||||
/// 无
|
||||
/// </summary>
|
||||
|
||||
@@ -5,11 +5,11 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.UI.Input.KeyboardAndMouse;
|
||||
using System.Text;
|
||||
using Windows.System;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using static Windows.Win32.PInvoke;
|
||||
using static Snap.Hutao.Win32.User32;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing.HotKey;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32.UI.Input.KeyboardAndMouse;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using static Windows.Win32.PInvoke;
|
||||
using static Snap.Hutao.Win32.User32;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing.HotKey;
|
||||
|
||||
@@ -14,7 +14,7 @@ internal sealed partial class HotKeyController : IHotKeyController
|
||||
{
|
||||
private static readonly WaitCallback RunMouseClickRepeatForever = MouseClickRepeatForever;
|
||||
|
||||
private readonly object locker = new();
|
||||
private readonly object syncRoot = new();
|
||||
|
||||
private readonly HotKeyOptions hotKeyOptions;
|
||||
|
||||
@@ -40,7 +40,8 @@ internal sealed partial class HotKeyController : IHotKeyController
|
||||
|
||||
private static unsafe INPUT CreateInputForMouseEvent(MOUSE_EVENT_FLAGS flags)
|
||||
{
|
||||
INPUT input = new() { type = INPUT_TYPE.INPUT_MOUSE, };
|
||||
INPUT input = default;
|
||||
input.type = INPUT_TYPE.INPUT_MOUSE;
|
||||
input.Anonymous.mi.dwFlags = flags;
|
||||
return input;
|
||||
}
|
||||
@@ -75,7 +76,7 @@ internal sealed partial class HotKeyController : IHotKeyController
|
||||
|
||||
private void ToggleMouseClickRepeatForever()
|
||||
{
|
||||
lock (locker)
|
||||
lock (syncRoot)
|
||||
{
|
||||
if (hotKeyOptions.IsMouseClickRepeatForeverOn)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32.UI.Input.KeyboardAndMouse;
|
||||
using Windows.System;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing.HotKey;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
|
||||
@@ -9,13 +9,15 @@ using Microsoft.UI.Xaml.Media;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Service;
|
||||
using Snap.Hutao.Win32;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.Graphics.Dwm;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
using System.IO;
|
||||
using Windows.Graphics;
|
||||
using Windows.UI;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using static Windows.Win32.PInvoke;
|
||||
using static Snap.Hutao.Win32.DwmApi;
|
||||
using static Snap.Hutao.Win32.User32;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
@@ -53,10 +55,10 @@ internal sealed class WindowController
|
||||
|
||||
private void InitializeCore()
|
||||
{
|
||||
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
|
||||
window.AppWindow.Title = SH.FormatAppNameAndVersion(hutaoOptions.Version);
|
||||
window.AppWindow.SetIcon(Path.Combine(hutaoOptions.InstalledLocation, "Assets/Logo.ico"));
|
||||
window.AppWindow.Title = SH.FormatAppNameAndVersion(runtimeOptions.Version);
|
||||
window.AppWindow.SetIcon(Path.Combine(runtimeOptions.InstalledLocation, "Assets/Logo.ico"));
|
||||
ExtendsContentIntoTitleBar();
|
||||
|
||||
RecoverOrInitWindowSize();
|
||||
@@ -104,11 +106,11 @@ internal sealed class WindowController
|
||||
return;
|
||||
}
|
||||
|
||||
WINDOWPLACEMENT windowPlacement = Win32.StructMarshal.WINDOWPLACEMENT();
|
||||
WINDOWPLACEMENT windowPlacement = WINDOWPLACEMENT.Create();
|
||||
GetWindowPlacement(options.Hwnd, ref windowPlacement);
|
||||
|
||||
// prevent save value when we are maximized.
|
||||
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
|
||||
if (!windowPlacement.ShowCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
|
||||
{
|
||||
double scale = 1.0 / options.GetRasterizationScale();
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)window.AppWindow.GetRect().Scale(scale));
|
||||
@@ -157,6 +159,7 @@ internal sealed class WindowController
|
||||
{
|
||||
window.SystemBackdrop = backdropType switch
|
||||
{
|
||||
BackdropType.Transparent => new Backdrop.TransparentBackdrop(),
|
||||
BackdropType.MicaAlt => new MicaBackdrop() { Kind = MicaKind.BaseAlt },
|
||||
BackdropType.Mica => new MicaBackdrop() { Kind = MicaKind.Base },
|
||||
BackdropType.Acrylic => new DesktopAcrylicBackdrop(),
|
||||
@@ -193,7 +196,7 @@ internal sealed class WindowController
|
||||
private unsafe void UpdateImmersiveDarkMode(FrameworkElement titleBar, object discard)
|
||||
{
|
||||
BOOL isDarkMode = Control.Theme.ThemeHelper.IsDarkMode(titleBar.ActualTheme);
|
||||
DwmSetWindowAttribute(options.Hwnd, DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, &isDarkMode, unchecked((uint)sizeof(BOOL)));
|
||||
DwmSetWindowAttribute(options.Hwnd, DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, ref isDarkMode);
|
||||
}
|
||||
|
||||
private void UpdateDragRectangles()
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using WinRT.Interop;
|
||||
using static Snap.Hutao.Win32.User32;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
@@ -16,4 +20,12 @@ internal static class WindowExtension
|
||||
WindowController windowController = new(window, window.WindowOptions, serviceProvider);
|
||||
WindowControllers.Add(window, windowController);
|
||||
}
|
||||
|
||||
public static void SetLayeredWindow(this Window window)
|
||||
{
|
||||
HWND hwnd = (HWND)WindowNative.GetWindowHandle(window);
|
||||
nint style = GetWindowLongPtrW(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
|
||||
style |= (nint)WINDOW_EX_STYLE.WS_EX_LAYERED;
|
||||
SetWindowLongPtrW(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, style);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.Foundation;
|
||||
using WinRT.Interop;
|
||||
using static Windows.Win32.PInvoke;
|
||||
using static Snap.Hutao.Win32.User32;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
@@ -55,7 +55,7 @@ internal readonly struct WindowOptions
|
||||
/// <param name="persistSize">持久化尺寸</param>
|
||||
public WindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, bool persistSize = false)
|
||||
{
|
||||
Hwnd = (HWND)WindowNative.GetWindowHandle(window);
|
||||
Hwnd = WindowNative.GetWindowHandle(window);
|
||||
InputNonClientPointerSource = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);
|
||||
TitleBar = titleBar;
|
||||
InitSize = initSize;
|
||||
@@ -80,8 +80,8 @@ internal readonly struct WindowOptions
|
||||
{
|
||||
HWND fgHwnd = GetForegroundWindow();
|
||||
|
||||
uint threadIdHwnd = GetWindowThreadProcessId(Hwnd);
|
||||
uint threadIdFgHwnd = GetWindowThreadProcessId(fgHwnd);
|
||||
uint threadIdHwnd = GetWindowThreadProcessId(Hwnd, default);
|
||||
uint threadIdFgHwnd = GetWindowThreadProcessId(fgHwnd, default);
|
||||
|
||||
if (threadIdHwnd != threadIdFgHwnd)
|
||||
{
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing.Backdrop;
|
||||
using Snap.Hutao.Core.Windowing.HotKey;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using static Windows.Win32.PInvoke;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.UI.Shell;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
using static Snap.Hutao.Win32.ComCtl32;
|
||||
using static Snap.Hutao.Win32.ConstValues;
|
||||
using static Snap.Hutao.Win32.User32;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
@@ -25,8 +28,8 @@ internal sealed class WindowSubclass : IDisposable
|
||||
private readonly IHotKeyController hotKeyController;
|
||||
|
||||
// We have to explicitly hold a reference to SUBCLASSPROC
|
||||
private SUBCLASSPROC? windowProc;
|
||||
private SUBCLASSPROC? legacyDragBarProc;
|
||||
private SUBCLASSPROC windowProc = default!;
|
||||
private SUBCLASSPROC legacyDragBarProc = default!;
|
||||
|
||||
public WindowSubclass(Window window, in WindowOptions options, IServiceProvider serviceProvider)
|
||||
{
|
||||
@@ -56,7 +59,7 @@ internal sealed class WindowSubclass : IDisposable
|
||||
}
|
||||
|
||||
titleBarHooked = false;
|
||||
HWND hwndDragBar = FindWindowEx(options.Hwnd, default, "DRAG_BAR_WINDOW_CLASS", default);
|
||||
HWND hwndDragBar = FindWindowExW(options.Hwnd, default, "DRAG_BAR_WINDOW_CLASS", default);
|
||||
|
||||
if (hwndDragBar.IsNull)
|
||||
{
|
||||
@@ -75,12 +78,12 @@ internal sealed class WindowSubclass : IDisposable
|
||||
hotKeyController.UnregisterAll();
|
||||
|
||||
RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId);
|
||||
windowProc = null;
|
||||
windowProc = default!;
|
||||
|
||||
if (options.UseLegacyDragBarImplementation)
|
||||
{
|
||||
RemoveWindowSubclass(options.Hwnd, legacyDragBarProc, DragBarSubclassId);
|
||||
legacyDragBarProc = null;
|
||||
legacyDragBarProc = default!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ internal sealed class WindowSubclass : IDisposable
|
||||
{
|
||||
if (window is IMinMaxInfoHandler handler)
|
||||
{
|
||||
handler.HandleMinMaxInfo(ref *(MINMAXINFO*)lParam.Value, options.GetRasterizationScale());
|
||||
handler.HandleMinMaxInfo(ref *(MINMAXINFO*)lParam, options.GetRasterizationScale());
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -110,6 +113,16 @@ internal sealed class WindowSubclass : IDisposable
|
||||
hotKeyController.OnHotKeyPressed(*(HotKeyParameter*)&lParam);
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_ERASEBKGND:
|
||||
{
|
||||
if (window.SystemBackdrop is IBackdropNeedEraseBackground)
|
||||
{
|
||||
return (LRESULT)(int)BOOL.TRUE;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return DefSubclassProc(hwnd, uMsg, wParam, lParam);
|
||||
|
||||
@@ -27,18 +27,4 @@ internal static class StringExtension
|
||||
{
|
||||
return source.AsSpan().TrimEnd(value).ToString();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool EqualsAny(this string source, in ReadOnlySpan<string> values, StringComparison comparisonType)
|
||||
{
|
||||
foreach (ref readonly string value in values)
|
||||
{
|
||||
if (source.Equals(value, comparisonType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,14 @@
|
||||
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.System.Com;
|
||||
using Snap.Hutao.Win32.UI.Shell;
|
||||
using Snap.Hutao.Win32.UI.Shell.Common;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Com;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.Shell.Common;
|
||||
using static Windows.Win32.PInvoke;
|
||||
using static Snap.Hutao.Win32.Macros;
|
||||
using static Snap.Hutao.Win32.Ole32;
|
||||
using static Snap.Hutao.Win32.Shell32;
|
||||
|
||||
namespace Snap.Hutao.Factory.Picker;
|
||||
|
||||
@@ -21,32 +22,33 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
|
||||
|
||||
public unsafe ValueResult<bool, ValueFile> PickFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters)
|
||||
{
|
||||
CoCreateInstance<FileOpenDialog, IFileOpenDialog>(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileOpenDialog dialog).ThrowOnFailure();
|
||||
HRESULT hr = CoCreateInstance(in FileOpenDialog.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileDialog.IID, out IFileDialog* pFileDialog);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
FILEOPENDIALOGOPTIONS options =
|
||||
FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE |
|
||||
FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM |
|
||||
FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR;
|
||||
|
||||
dialog.SetOptions(options);
|
||||
SetDesktopAsStartupFolder(dialog);
|
||||
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
dialog.SetTitle(title);
|
||||
}
|
||||
pFileDialog->SetOptions(options);
|
||||
SetDesktopAsStartupFolder(pFileDialog);
|
||||
|
||||
if (!string.IsNullOrEmpty(defaultFileName))
|
||||
{
|
||||
dialog.SetFileName(defaultFileName);
|
||||
pFileDialog->SetFileName(defaultFileName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
pFileDialog->SetTitle(title);
|
||||
}
|
||||
|
||||
if (filters is { Length: > 0 })
|
||||
{
|
||||
SetFileTypes(dialog, filters);
|
||||
SetFileTypes(pFileDialog, filters);
|
||||
}
|
||||
|
||||
HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle());
|
||||
HRESULT res = pFileDialog->Show(currentWindowReference.GetWindowHandle());
|
||||
if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED))
|
||||
{
|
||||
return new(false, default);
|
||||
@@ -56,18 +58,18 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
|
||||
Marshal.ThrowExceptionForHR(res);
|
||||
}
|
||||
|
||||
dialog.GetResult(out IShellItem item);
|
||||
HRESULT t = pFileDialog->GetResult(out IShellItem* pShellItem);
|
||||
|
||||
PWSTR displayName = default;
|
||||
string file;
|
||||
try
|
||||
{
|
||||
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
|
||||
file = new((char*)displayName);
|
||||
pShellItem->GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
|
||||
file = new(displayName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem((nint)displayName.Value);
|
||||
CoTaskMemFree(displayName);
|
||||
}
|
||||
|
||||
return new(true, file);
|
||||
@@ -75,7 +77,8 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
|
||||
|
||||
public unsafe ValueResult<bool, ValueFile> SaveFile(string? title, string? defaultFileName, (string Name, string Type)[]? filters)
|
||||
{
|
||||
CoCreateInstance<FileSaveDialog, IFileSaveDialog>(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileSaveDialog dialog).ThrowOnFailure();
|
||||
HRESULT hr = CoCreateInstance(in FileSaveDialog.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileDialog.IID, out IFileDialog* pFileDialog);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
FILEOPENDIALOGOPTIONS options =
|
||||
FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE |
|
||||
@@ -83,25 +86,25 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
|
||||
FILEOPENDIALOGOPTIONS.FOS_STRICTFILETYPES |
|
||||
FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR;
|
||||
|
||||
dialog.SetOptions(options);
|
||||
SetDesktopAsStartupFolder(dialog);
|
||||
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
dialog.SetTitle(title);
|
||||
}
|
||||
pFileDialog->SetOptions(options);
|
||||
SetDesktopAsStartupFolder(pFileDialog);
|
||||
|
||||
if (!string.IsNullOrEmpty(defaultFileName))
|
||||
{
|
||||
dialog.SetFileName(defaultFileName);
|
||||
pFileDialog->SetFileName(defaultFileName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
pFileDialog->SetTitle(title);
|
||||
}
|
||||
|
||||
if (filters is { Length: > 0 })
|
||||
{
|
||||
SetFileTypes(dialog, filters);
|
||||
SetFileTypes(pFileDialog, filters);
|
||||
}
|
||||
|
||||
HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle());
|
||||
HRESULT res = pFileDialog->Show(currentWindowReference.GetWindowHandle());
|
||||
if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED))
|
||||
{
|
||||
return new(false, default);
|
||||
@@ -111,18 +114,18 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
|
||||
Marshal.ThrowExceptionForHR(res);
|
||||
}
|
||||
|
||||
dialog.GetResult(out IShellItem item);
|
||||
pFileDialog->GetResult(out IShellItem* pShellItem);
|
||||
|
||||
PWSTR displayName = default;
|
||||
string file;
|
||||
try
|
||||
{
|
||||
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
|
||||
file = new((char*)displayName);
|
||||
pShellItem->GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
|
||||
file = new(displayName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem((nint)displayName.Value);
|
||||
CoTaskMemFree(displayName);
|
||||
}
|
||||
|
||||
return new(true, file);
|
||||
@@ -130,7 +133,8 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
|
||||
|
||||
public unsafe ValueResult<bool, string> PickFolder(string? title)
|
||||
{
|
||||
CoCreateInstance<FileOpenDialog, IFileOpenDialog>(default, CLSCTX.CLSCTX_INPROC_SERVER, out IFileOpenDialog dialog).ThrowOnFailure();
|
||||
HRESULT hr = CoCreateInstance(in FileOpenDialog.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileDialog.IID, out IFileDialog* pFileDialog);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
FILEOPENDIALOGOPTIONS options =
|
||||
FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE |
|
||||
@@ -138,15 +142,15 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
|
||||
FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS |
|
||||
FILEOPENDIALOGOPTIONS.FOS_NOCHANGEDIR;
|
||||
|
||||
dialog.SetOptions(options);
|
||||
SetDesktopAsStartupFolder(dialog);
|
||||
pFileDialog->SetOptions(options);
|
||||
SetDesktopAsStartupFolder(pFileDialog);
|
||||
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
dialog.SetTitle(title);
|
||||
pFileDialog->SetTitle(title);
|
||||
}
|
||||
|
||||
HRESULT res = dialog.Show(currentWindowReference.GetWindowHandle());
|
||||
HRESULT res = pFileDialog->Show(currentWindowReference.GetWindowHandle());
|
||||
if (res == HRESULT_FROM_WIN32(WIN32_ERROR.ERROR_CANCELLED))
|
||||
{
|
||||
return new(false, default!);
|
||||
@@ -156,25 +160,24 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
|
||||
Marshal.ThrowExceptionForHR(res);
|
||||
}
|
||||
|
||||
dialog.GetResult(out IShellItem item);
|
||||
pFileDialog->GetResult(out IShellItem* pShellItem);
|
||||
|
||||
PWSTR displayName = default;
|
||||
string file;
|
||||
try
|
||||
{
|
||||
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
|
||||
pShellItem->GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out displayName);
|
||||
file = new((char*)displayName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem((nint)displayName.Value);
|
||||
CoTaskMemFree(displayName);
|
||||
}
|
||||
|
||||
return new(true, file);
|
||||
}
|
||||
|
||||
private static unsafe void SetFileTypes<TDialog>(TDialog dialog, (string Name, string Type)[] filters)
|
||||
where TDialog : IFileDialog
|
||||
private static unsafe void SetFileTypes(IFileDialog* pFileDialog, (string Name, string Type)[] filters)
|
||||
{
|
||||
List<nint> unmanagedStringPtrs = new(filters.Length * 2);
|
||||
List<COMDLG_FILTERSPEC> filterSpecs = new(filters.Length);
|
||||
@@ -190,10 +193,7 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
|
||||
filterSpecs.Add(spec);
|
||||
}
|
||||
|
||||
fixed (COMDLG_FILTERSPEC* ptr = CollectionsMarshal.AsSpan(filterSpecs))
|
||||
{
|
||||
dialog.SetFileTypes((uint)filterSpecs.Count, ptr);
|
||||
}
|
||||
pFileDialog->SetFileTypes(CollectionsMarshal.AsSpan(filterSpecs));
|
||||
|
||||
foreach (ref readonly nint ptr in CollectionsMarshal.AsSpan(unmanagedStringPtrs))
|
||||
{
|
||||
@@ -201,11 +201,11 @@ internal sealed partial class FileSystemPickerInteraction : IFileSystemPickerInt
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void SetDesktopAsStartupFolder<TDialog>(TDialog dialog)
|
||||
where TDialog : IFileDialog
|
||||
private static unsafe void SetDesktopAsStartupFolder(IFileDialog* pFileDialog)
|
||||
{
|
||||
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
SHCreateItemFromParsingName(desktopPath, default, typeof(IShellItem).GUID, out object shellItem).ThrowOnFailure();
|
||||
dialog.SetFolder((IShellItem)shellItem);
|
||||
HRESULT hr = SHCreateItemFromParsingName(desktopPath, default, in IShellItem.IID, out IShellItem* pShellItem);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
pFileDialog->SetFolder(pShellItem);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -32,9 +32,9 @@ internal sealed partial class GuideWindow : Window, IWindowOptionsSource, IMinMa
|
||||
|
||||
public unsafe void HandleMinMaxInfo(ref MINMAXINFO info, double scalingFactor)
|
||||
{
|
||||
info.ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, info.ptMinTrackSize.X);
|
||||
info.ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, info.ptMinTrackSize.Y);
|
||||
info.ptMaxTrackSize.X = (int)Math.Min(MaxWidth * scalingFactor, info.ptMaxTrackSize.X);
|
||||
info.ptMaxTrackSize.Y = (int)Math.Min(MaxHeight * scalingFactor, info.ptMaxTrackSize.Y);
|
||||
info.ptMinTrackSize.x = (int)Math.Max(MinWidth * scalingFactor, info.ptMinTrackSize.x);
|
||||
info.ptMinTrackSize.y = (int)Math.Max(MinHeight * scalingFactor, info.ptMinTrackSize.y);
|
||||
info.ptMaxTrackSize.x = (int)Math.Min(MaxWidth * scalingFactor, info.ptMaxTrackSize.x);
|
||||
info.ptMaxTrackSize.y = (int)Math.Min(MaxHeight * scalingFactor, info.ptMaxTrackSize.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Snap.Hutao/Snap.Hutao/IdentifyMonitorWindow.xaml
Normal file
19
src/Snap.Hutao/Snap.Hutao/IdentifyMonitorWindow.xaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<Window
|
||||
x:Class="Snap.Hutao.IdentifyMonitorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
mc:Ignorable="d">
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="3">
|
||||
<TextBlock Text="{shcm:ResourceString Name=WindowIdentifyMonitorHeader}"/>
|
||||
<TextBlock
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind Monitor}"
|
||||
TextAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
30
src/Snap.Hutao/Snap.Hutao/IdentifyMonitorWindow.xaml.cs
Normal file
30
src/Snap.Hutao/Snap.Hutao/IdentifyMonitorWindow.xaml.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Win32;
|
||||
using Windows.Graphics;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
internal sealed partial class IdentifyMonitorWindow : Window
|
||||
{
|
||||
public IdentifyMonitorWindow(DisplayArea displayArea, int index)
|
||||
{
|
||||
InitializeComponent();
|
||||
Monitor = $"{displayArea.DisplayId.Value:X8}:{index}";
|
||||
|
||||
OverlappedPresenter presenter = OverlappedPresenter.Create();
|
||||
presenter.SetBorderAndTitleBar(false, false);
|
||||
presenter.IsAlwaysOnTop = true;
|
||||
presenter.IsResizable = false;
|
||||
AppWindow.SetPresenter(presenter);
|
||||
|
||||
PointInt32 point = new(40, 32);
|
||||
SizeInt32 size = StructMarshal.SizeInt32(displayArea.WorkArea).Scale(0.1);
|
||||
AppWindow.MoveAndResize(StructMarshal.RectInt32(point, size), displayArea);
|
||||
}
|
||||
|
||||
public string Monitor { get; private set; }
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Snap.Hutao.ViewModel.Game;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -50,9 +50,9 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOpt
|
||||
/// <inheritdoc/>
|
||||
public unsafe void HandleMinMaxInfo(ref MINMAXINFO info, double scalingFactor)
|
||||
{
|
||||
info.ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, info.ptMinTrackSize.X);
|
||||
info.ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, info.ptMinTrackSize.Y);
|
||||
info.ptMaxTrackSize.X = (int)Math.Min(MaxWidth * scalingFactor, info.ptMaxTrackSize.X);
|
||||
info.ptMaxTrackSize.Y = (int)Math.Min(MaxHeight * scalingFactor, info.ptMaxTrackSize.Y);
|
||||
info.ptMinTrackSize.x = (int)Math.Max(MinWidth * scalingFactor, info.ptMinTrackSize.x);
|
||||
info.ptMinTrackSize.y = (int)Math.Max(MinHeight * scalingFactor, info.ptMinTrackSize.y);
|
||||
info.ptMaxTrackSize.x = (int)Math.Min(MaxWidth * scalingFactor, info.ptMaxTrackSize.x);
|
||||
info.ptMaxTrackSize.y = (int)Math.Min(MaxHeight * scalingFactor, info.ptMaxTrackSize.y);
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,18 @@
|
||||
x:Class="Snap.Hutao.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shv="using:Snap.Hutao.View"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<shv:MainView/>
|
||||
|
||||
<shv:TitleView
|
||||
x:Name="TitleBarView"
|
||||
Height="44"
|
||||
Margin="48,0,0,0"/>
|
||||
Margin="44,0,0,0"/>
|
||||
|
||||
<shv:MainView/>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -37,7 +37,7 @@ internal sealed partial class MainWindow : Window, IWindowOptionsSource, IMinMax
|
||||
/// <inheritdoc/>
|
||||
public unsafe void HandleMinMaxInfo(ref MINMAXINFO pInfo, double scalingFactor)
|
||||
{
|
||||
pInfo.ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo.ptMinTrackSize.X);
|
||||
pInfo.ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo.ptMinTrackSize.Y);
|
||||
pInfo.ptMinTrackSize.x = (int)Math.Max(MinWidth * scalingFactor, pInfo.ptMinTrackSize.x);
|
||||
pInfo.ptMinTrackSize.y = (int)Math.Max(MinHeight * scalingFactor, pInfo.ptMinTrackSize.y);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,12 @@ internal static class CollectionsNameValue
|
||||
return [.. Enum.GetValues<TEnum>().Select(x => new NameValue<TEnum>(x.ToString(), x))];
|
||||
}
|
||||
|
||||
public static List<NameValue<TEnum>> FromEnum<TEnum>(Func<TEnum, bool> codiction)
|
||||
where TEnum : struct, Enum
|
||||
{
|
||||
return [.. Enum.GetValues<TEnum>().Where(codiction).Select(x => new NameValue<TEnum>(x.ToString(), x))];
|
||||
}
|
||||
|
||||
public static List<NameValue<TSource>> From<TSource>(IEnumerable<TSource> sources, Func<TSource, string> nameSelector)
|
||||
{
|
||||
return [.. sources.Select(x => new NameValue<TSource>(nameSelector(x), x))];
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Snap.Hutao.Model.Entity;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Table("game_accounts")]
|
||||
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string, SchemeType>
|
||||
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string, SchemeType>, ICloneable<GameAccount>
|
||||
{
|
||||
/// <summary>
|
||||
/// 内部Id
|
||||
@@ -54,6 +54,17 @@ internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount,
|
||||
};
|
||||
}
|
||||
|
||||
public GameAccount Clone()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
AttachUid = AttachUid,
|
||||
Name = Name,
|
||||
MihoyoSDK = MihoyoSDK,
|
||||
Type = Type,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新绑定的Uid
|
||||
/// </summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user