mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
9 Commits
1.8.0
...
UnsafeCore
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c03ce3486 | ||
|
|
83e187ea9e | ||
|
|
d86232f413 | ||
|
|
4e6691ac51 | ||
|
|
84ad39b192 | ||
|
|
ce50fc41e0 | ||
|
|
1d71048f56 | ||
|
|
08cf823156 | ||
|
|
cca65635a6 |
27
.github/ISSUE_TEMPLATE/CHS-feature-request.yml
vendored
27
.github/ISSUE_TEMPLATE/CHS-feature-request.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: 功能请求
|
||||
description: 通过这个议题来向开发团队分享你的想法
|
||||
title: "[Feat]: 在这里填写一个合适的标题"
|
||||
labels: ["功能", "needs-triage", "priority:none"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
请按下方的要求填写完整的问题表单。
|
||||
|
||||
- type: textarea
|
||||
id: back
|
||||
attributes:
|
||||
label: 背景与动机
|
||||
description: 添加此功能的理由,如果你想要实现多个功能,请分别发起多个单独的议题
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: req
|
||||
attributes:
|
||||
label: 想要实现或优化的功能
|
||||
description: 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高
|
||||
validations:
|
||||
required: true
|
||||
27
.github/ISSUE_TEMPLATE/ENG-feature-request.yml
vendored
27
.github/ISSUE_TEMPLATE/ENG-feature-request.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Feature Request [English Form]
|
||||
description: Tell us about your thought
|
||||
title: "[Feat]: Place your title here"
|
||||
labels: ["功能", "needs-triage", "priority:none"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please fill the form below
|
||||
|
||||
- type: textarea
|
||||
id: back
|
||||
attributes:
|
||||
label: Background & Motivation
|
||||
description: Reason why this feature is needed. If multiple features is requested, please open multiple issues for each of them.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: req
|
||||
attributes:
|
||||
label: Detail of the Feature
|
||||
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted.
|
||||
validations:
|
||||
required: true
|
||||
79
.github/ISSUE_TEMPLATE/ENG-network-issue.yml
vendored
79
.github/ISSUE_TEMPLATE/ENG-network-issue.yml
vendored
@@ -1,79 +0,0 @@
|
||||
name: Network Issue [English Form]
|
||||
description: Submit this issue form when network issue affect your client experience
|
||||
title: "[Network]: Place your title here"
|
||||
labels: ["area-Network"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
- Masterain98
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Please use one sentence to briefly describe your issue as title above**
|
||||
**Please follow the instruction below to fill the form, so we can locate the issue quickly**
|
||||
|
||||
- type: textarea
|
||||
id: network-diagnosis-report
|
||||
attributes:
|
||||
label: Submit Your Network Diagnosis Report
|
||||
description: |
|
||||
STOP HERE!
|
||||
**Please run our network diagnosis tool before filling this form**
|
||||
**The diagnosis tool will generate a report and add it into a password-protected archive. Drag the `.zip` archive to the box below so it can be uploaded.**
|
||||
- Use the following link to download the Network Diagnosis Tool:
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/SH-Network-Diagnosis.exe)
|
||||
- [JIHu GitLab](https://jihulab.com/DGP-Studio/network-diagnosis-tool/-/jobs/11144011/artifacts/raw/SH-Network-Diagnosis.exe?inline=false)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: user-geo-location
|
||||
attributes:
|
||||
label: Your Geographical Location
|
||||
description: |
|
||||
Description accurate to country
|
||||
placeholder: USA
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: user-isp
|
||||
attributes:
|
||||
label: Your ISP Name
|
||||
description: |
|
||||
Name of your Internet service provider
|
||||
placeholder: AT&T
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: user-issue-category
|
||||
attributes:
|
||||
label: Issue Category
|
||||
description: Select an issue category
|
||||
options:
|
||||
- Cannot connect to server completely
|
||||
- Slow spped
|
||||
- Fetched wrong page or data
|
||||
- Image download error in the client
|
||||
- Image set pre-download error (client welcome wizard process)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Your Issue (cont.)
|
||||
description: If you selected `Other` in previous dropdown, please explain your issue in detail here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: One Last Step
|
||||
description: Check your issue form
|
||||
options:
|
||||
- label: I confirm I have attached the network diagnosis report archive in the issue
|
||||
required: true
|
||||
55
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
55
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: Publish Process
|
||||
description: FOR ADMIN USE ONLY. WILL CAUSE A BAN IF NO PERMISSION.
|
||||
title: "[Publish]: Version 1.9.98"
|
||||
labels: ["Publish"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: textarea
|
||||
id: main-body
|
||||
attributes:
|
||||
label: Publish Process
|
||||
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
|
||||
- [ ] 更新日志
|
||||
- [ ] 功能文档更新
|
||||
|
||||
## 发布版本
|
||||
|
||||
- [ ] 在 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
|
||||
```
|
||||
|
||||
- [ ] 通知用户
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: Final Check
|
||||
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
|
||||
65
.github/ISSUE_TEMPLATE/artifact-rating-rules.yml
vendored
Normal file
65
.github/ISSUE_TEMPLATE/artifact-rating-rules.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: 圣遗物评分细则建议
|
||||
description: 为圣遗物评分规则提供你的想法
|
||||
title: "[Artifact Rating] 请在这里填写角色名称"
|
||||
labels: area-AvatarInfo
|
||||
assignees: Lightczx
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
请按下方的要求填写完整的问题表单
|
||||
|
||||
- type: textarea
|
||||
id: your-suggested-rule
|
||||
attributes:
|
||||
label: 评分细则
|
||||
description: |
|
||||
请修改下方表格中的**角色名称**和**各属性权重**,并在表格后添加合适的说明
|
||||
你可以点击预览按钮(preview)来查看表格最终会显示出的内容
|
||||
value: |
|
||||
|项目|评分权重(0-100)|
|
||||
|-----|-----|
|
||||
|角色名称| 旅行者 |
|
||||
|生命值| 10 |
|
||||
|攻击力| 10 |
|
||||
|防御力| 10 |
|
||||
|暴击率| 10 |
|
||||
|暴击伤害| 10 |
|
||||
|元素精通| 10 |
|
||||
|充能效率| 10 |
|
||||
|治疗加成| 10 |
|
||||
|元素伤害| 10 |
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: no-duplicated-dropdown
|
||||
attributes:
|
||||
label: 我确认当前没有其它的该角色的圣遗物评分细则建议
|
||||
description: 如果有,你应该在已有的工单内回复以提出你的建议
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: title-filled-dropdown
|
||||
attributes:
|
||||
label: 我确认已设置合适的标题
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: all-filled-dropdown
|
||||
attributes:
|
||||
label: 我确认已完整填写表格
|
||||
options:
|
||||
- 否
|
||||
- 是
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,7 +1,7 @@
|
||||
name: BUG Report [English Form]
|
||||
description: Tell us what issue you get
|
||||
title: "[ENG][Bug]: Place your Issue Title Here"
|
||||
labels: ["BUG", "priority:none"]
|
||||
labels: ["BUG"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -50,8 +50,7 @@ body:
|
||||
label: Device ID
|
||||
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 issue is about program crash, please fill this so we can dump the log and locate the source easier
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 问题反馈
|
||||
description: 通过这个议题向开发团队反馈你发现的程序中的问题
|
||||
description: 告诉我们你的问题
|
||||
title: "[Bug]: 在这里填写一个合适的标题"
|
||||
labels: ["BUG", "priority:none"]
|
||||
labels: ["BUG"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: 检查清单
|
||||
description: |-
|
||||
请确保你已完整执行检查清单,否则你的议题可能会被忽略
|
||||
请确保你已完整执行检查清单,否则你的 Issue 可能会被忽略
|
||||
options:
|
||||
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
|
||||
required: true
|
||||
@@ -51,7 +51,6 @@ body:
|
||||
description: |
|
||||
在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID
|
||||
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
|
||||
如果你的程序已经无法启动,请下载并运行[此PowerShell脚本](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1),它将显示你的设备 ID
|
||||
validations:
|
||||
required: false
|
||||
|
||||
28
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
28
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Feature Request 功能请求
|
||||
description: Tell us about your thought 告诉我们你的想法
|
||||
title: "[Feat]: Place your title here 在这里填写一个合适的标题"
|
||||
labels: ["功能"]
|
||||
assignees:
|
||||
- Lightczx
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please fill the form below
|
||||
请按下方的要求填写完整的问题表单。
|
||||
|
||||
- type: textarea
|
||||
id: back
|
||||
attributes:
|
||||
label: Background & Motivation 背景与动机
|
||||
description: Reason why this feature is needed. If multiple features is requested, please open multiple issues for each of them. 添加此功能的理由,如果你想要实现多个功能,请分别发起多个单独的 Issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: req
|
||||
attributes:
|
||||
label: Detail of the Feature 想要实现或优化的功能
|
||||
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted. 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,5 +1,5 @@
|
||||
name: 网络问题
|
||||
description: 通过这个议题来反馈网络问题
|
||||
description: 当网络问题影响到你的程序使用时
|
||||
title: "[Network]: 在这里填写一个合适的标题"
|
||||
labels: ["area-Network"]
|
||||
assignees:
|
||||
@@ -19,10 +19,10 @@ body:
|
||||
description: |
|
||||
停下!
|
||||
**在填写下面的问题之前请先使用我们的网络诊断工具**
|
||||
**这个工具将会生成一份报告并加密压缩,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
|
||||
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
|
||||
- 你可以点击下面的链接以下载网络诊断工具:
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/SH-Network-Diagnosis.exe)
|
||||
- [极狐 GitLab](https://jihulab.com/DGP-Studio/network-diagnosis-tool/-/jobs/11144011/artifacts/raw/SH-Network-Diagnosis.exe?inline=false)
|
||||
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-hutao.exe)
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/network-diagnosis-hutao.exe)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -60,6 +60,7 @@ body:
|
||||
- 完全无法连接服务器
|
||||
- 连接速度慢
|
||||
- 获取到了不正确的页面或数据
|
||||
- 客户端提示 429 Error
|
||||
- 客户端图片下载错误
|
||||
- 客户端图片预下载错误
|
||||
- 其它
|
||||
@@ -73,12 +74,5 @@ body:
|
||||
description: 如果你在上一项中选择了`其它`或者你有更多信息需要提供,请在这里写下来
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
attributes:
|
||||
label: 最后一步
|
||||
description: 检查你提交的议题
|
||||
options:
|
||||
- label: 我已经在该议题中上传了包含网络诊断报告的加密压缩包
|
||||
required: true
|
||||
33
.github/workflows/PublishDistribution.yml
vendored
33
.github/workflows/PublishDistribution.yml
vendored
@@ -12,9 +12,40 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Download Assets
|
||||
- name: Download Release
|
||||
timeout-minutes: 5
|
||||
uses: robinraju/release-downloader@v1.7
|
||||
with:
|
||||
repository: "DGP-Studio/Snap.Hutao"
|
||||
latest: true
|
||||
fileName: "*.msix"
|
||||
out-file-path: ./release-download
|
||||
|
||||
# Upload to Drive
|
||||
- name: Upload Drive
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
RCCONF: ${{ secrets.RCCONF }}
|
||||
run: |
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
mkdir -p ~/.config/rclone/
|
||||
cat << EOF > ~/.config/rclone/rclone.conf
|
||||
$RCCONF
|
||||
EOF
|
||||
|
||||
rclone copy ./release-download/* dgpODCN:/releases/
|
||||
|
||||
# Purge Patch System Cache
|
||||
- name: Purge Patch
|
||||
env:
|
||||
PATCH_HOSTS: ${{ secrets.PATCH_HOSTS }}
|
||||
PURGE_TOKEN: ${{ secrets.PURGE_TOKEN }}
|
||||
PURGE_URL: ${{ secrets.PURGE_URL }}
|
||||
run: |
|
||||
curl -X PATCH PURGE_URL
|
||||
sudo echo "$PATCH_HOSTS" | sudo tee -a /etc/hosts
|
||||
curl --header "Authorization: token $PURGE_TOKEN" $PURGE_URL
|
||||
|
||||
26
.github/workflows/qodana_code_quality.yml
vendored
Normal file
26
.github/workflows/qodana_code_quality.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Qodana
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.yml'
|
||||
- '**.resx'
|
||||
|
||||
jobs:
|
||||
qodana:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 'Qodana Scan'
|
||||
uses: JetBrains/qodana-action@v2023.2
|
||||
with:
|
||||
pr-mode: false
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,7 +10,6 @@ src/Snap.Hutao/_ReSharper.Caches
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao/bin/
|
||||
src/Snap.Hutao/Snap.Hutao/obj/
|
||||
src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs
|
||||
src/Snap.Hutao/Snap.Hutao/Snap.Hutao_TemporaryKey.pfx
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao.Win32/bin/
|
||||
|
||||
@@ -55,7 +55,7 @@ steps:
|
||||
displayName: Install dotNet
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '8.x'
|
||||
version: '7.x'
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: NuGetToolInstaller@1
|
||||
@@ -86,7 +86,7 @@ steps:
|
||||
|
||||
- task: MagicChunks@2
|
||||
inputs:
|
||||
sourcePath: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\AppxManifest.xml'
|
||||
sourcePath: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\AppxManifest.xml'
|
||||
fileType: 'Xml'
|
||||
targetPathType: 'source'
|
||||
transformationType: 'json'
|
||||
@@ -107,7 +107,7 @@ steps:
|
||||
mkdir Assets
|
||||
|
||||
mkdir Resource
|
||||
workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64'
|
||||
workingDirectory: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64'
|
||||
|
||||
|
||||
- task: CopyFiles@2
|
||||
@@ -115,19 +115,19 @@ steps:
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Assets'
|
||||
Contents: '**'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\Assets'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\Assets'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy Resource Folder
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\Resource'
|
||||
Contents: '**'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64\Resource'
|
||||
TargetFolder: '$(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64\Resource'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: Build MSIX
|
||||
inputs:
|
||||
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net8.0-windows10.0.22621.0\win-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
|
||||
script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\makeappx.exe" pack /d $(Build.SourcesDirectory)\src\Snap.Hutao\Snap.Hutao\bin\x64\Release\net7.0-windows10.0.19041.0\win10-x64 /p $(Build.ArtifactStagingDirectory)/Snap.Hutao.Alpha-$(build_date).$(rev_number).msix'
|
||||
|
||||
- task: MsixSigning@1
|
||||
name: signMsix
|
||||
|
||||
33
qodana.yaml
Normal file
33
qodana.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
exclude:
|
||||
- name: Test
|
||||
paths:
|
||||
- Snap.Hutao.Test
|
||||
- Snap.Hutao.SourceGeneration
|
||||
- name: All
|
||||
paths:
|
||||
- Snap.Hutao.SourceGeneration
|
||||
- Snap.Hutao.Test
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-dotnet:2023.2-eap
|
||||
@@ -108,9 +108,7 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_diagnostic.SA1629.severity = none
|
||||
dotnet_diagnostic.SA1642.severity = none
|
||||
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
dotnet_diagnostic.IDE0060.severity = none
|
||||
dotnet_diagnostic.IDE0290.severity = none
|
||||
|
||||
# SA1208: System using directives should be placed before other using directives
|
||||
dotnet_diagnostic.SA1208.severity = none
|
||||
@@ -324,8 +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]
|
||||
#### 命名样式 ####
|
||||
|
||||
@@ -140,28 +140,8 @@ internal sealed class AttributeGenerator : IIncrementalGenerator
|
||||
public InjectionAttribute(InjectAs injectAs, Type interfaceType)
|
||||
{
|
||||
}
|
||||
|
||||
public object Key { get; set; }
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.Attributes.g.cs", coreDependencyInjectionAnnotations);
|
||||
|
||||
string resourceLocalization = """
|
||||
namespace Snap.Hutao.Resource.Localization;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Enum)]
|
||||
internal sealed class LocalizationAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
internal sealed class LocalizationKeyAttribute : Attribute
|
||||
{
|
||||
public LocalizationKeyAttribute(string key)
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Resource.Localization.Attributes.g.cs", resourceLocalization);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
@@ -69,8 +68,7 @@ internal sealed class DependencyPropertyGenerator : IIncrementalGenerator
|
||||
|
||||
string propertyName = (string)arguments[0].Value!;
|
||||
string propertyType = arguments[1].Value!.ToString();
|
||||
string defaultValue = arguments.ElementAtOrDefault(2).ToCSharpString() ?? "default";
|
||||
defaultValue = defaultValue == "null" ? "default" : defaultValue;
|
||||
string defaultValue = GetLiteralString(arguments.ElementAtOrDefault(2)) ?? "default";
|
||||
string propertyChangedCallback = arguments.ElementAtOrDefault(3) is { IsNull: false } arg3 ? $", {arg3.Value}" : string.Empty;
|
||||
|
||||
string code;
|
||||
@@ -127,4 +125,25 @@ internal sealed class DependencyPropertyGenerator : IIncrementalGenerator
|
||||
production.AddSource($"{normalizedClassName}.{propertyName}.g.cs", code);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetLiteralString(TypedConstant typedConstant)
|
||||
{
|
||||
if (typedConstant.IsNull)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (typedConstant.Value is bool boolValue)
|
||||
{
|
||||
return boolValue ? "true" : "false";
|
||||
}
|
||||
|
||||
string result = typedConstant.Value!.ToString();
|
||||
if (string.IsNullOrEmpty(result))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
@@ -11,18 +10,7 @@ namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class SaltConstantGenerator : IIncrementalGenerator
|
||||
{
|
||||
private static readonly HttpClient httpClient;
|
||||
private static readonly Lazy<Response<SaltLatest>> lazySaltInfo;
|
||||
|
||||
static SaltConstantGenerator()
|
||||
{
|
||||
httpClient = new();
|
||||
lazySaltInfo = new Lazy<Response<SaltLatest>>(() =>
|
||||
{
|
||||
string body = httpClient.GetStringAsync("https://internal.snapgenshin.cn/Archive/Salt/Latest").GetAwaiter().GetResult();
|
||||
return JsonParser.FromJson<Response<SaltLatest>>(body)!;
|
||||
});
|
||||
}
|
||||
private static readonly HttpClient httpClient = new();
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
@@ -31,7 +19,8 @@ internal sealed class SaltConstantGenerator : IIncrementalGenerator
|
||||
|
||||
private static void GenerateSaltContstants(IncrementalGeneratorPostInitializationContext context)
|
||||
{
|
||||
Response<SaltLatest> saltInfo = lazySaltInfo.Value;
|
||||
string body = httpClient.GetStringAsync("https://internal.snapgenshin.cn/Archive/Salt/Latest").GetAwaiter().GetResult();
|
||||
Response<SaltLatest> saltInfo = JsonParser.FromJson<Response<SaltLatest>>(body)!;
|
||||
string code = $$"""
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ internal sealed class HttpClientGenerator : IIncrementalGenerator
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
@@ -85,7 +86,7 @@ internal sealed class HttpClientGenerator : IIncrementalGenerator
|
||||
|
||||
private static void FillUpWithAddHttpClient(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
|
||||
{
|
||||
List<string> lines = [];
|
||||
List<string> lines = new();
|
||||
StringBuilder lineBuilder = new();
|
||||
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
|
||||
@@ -81,7 +81,7 @@ internal sealed class InjectionGenerator : IIncrementalGenerator
|
||||
|
||||
private static void FillUpWithAddServices(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
|
||||
{
|
||||
List<string> lines = [];
|
||||
List<string> lines = new();
|
||||
StringBuilder lineBuilder = new();
|
||||
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
@@ -92,29 +92,17 @@ internal sealed class InjectionGenerator : IIncrementalGenerator
|
||||
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
|
||||
|
||||
string injectAsName = arguments[0].ToCSharpString();
|
||||
|
||||
bool hasKey = injectionInfo.TryGetNamedArgumentValue("Key", out TypedConstant key);
|
||||
|
||||
switch (injectAsName, hasKey)
|
||||
switch (injectAsName)
|
||||
{
|
||||
case (InjectAsSingletonName, false):
|
||||
case InjectAsSingletonName:
|
||||
lineBuilder.Append(" services.AddSingleton<");
|
||||
break;
|
||||
case (InjectAsSingletonName, true):
|
||||
lineBuilder.Append(" services.AddKeyedSingleton<");
|
||||
break;
|
||||
case (InjectAsTransientName, false):
|
||||
case InjectAsTransientName:
|
||||
lineBuilder.Append(" services.AddTransient<");
|
||||
break;
|
||||
case (InjectAsTransientName, true):
|
||||
lineBuilder.Append(" services.AddKeyedTransient<");
|
||||
break;
|
||||
case (InjectAsScopedName, false):
|
||||
case InjectAsScopedName:
|
||||
lineBuilder.Append(" services.AddScoped<");
|
||||
break;
|
||||
case (InjectAsScopedName, true):
|
||||
lineBuilder.Append(" services.AddKeyedScoped<");
|
||||
break;
|
||||
default:
|
||||
production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, context.Context.Node.GetLocation(), injectAsName));
|
||||
break;
|
||||
@@ -125,14 +113,7 @@ internal sealed class InjectionGenerator : IIncrementalGenerator
|
||||
lineBuilder.Append($"{arguments[1].Value}, ");
|
||||
}
|
||||
|
||||
if (hasKey)
|
||||
{
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>({key.ToCSharpString()});");
|
||||
}
|
||||
else
|
||||
{
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>();");
|
||||
}
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>();");
|
||||
|
||||
lines.Add(lineBuilder.ToString());
|
||||
}
|
||||
|
||||
@@ -50,8 +50,6 @@ internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
namespace Snap.Hutao.Resource.Localization;
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(LocalizedEnumGenerator)}}", "1.0.0.0")]
|
||||
@@ -81,7 +79,7 @@ internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
}
|
||||
else
|
||||
{
|
||||
return SH.ResourceManager.GetString(key, CultureInfo.CurrentCulture);
|
||||
return SH.ResourceManager.GetString(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +102,7 @@ internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
return SH.ResourceManager.GetString(key, CultureInfo.CurrentCulture);
|
||||
return SH.ResourceManager.GetString(key);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
@@ -41,10 +41,10 @@ public static class JsonParser
|
||||
public static T? FromJson<T>(this string json)
|
||||
{
|
||||
// Initialize, if needed, the ThreadStatic variables
|
||||
propertyInfoCache ??= [];
|
||||
fieldInfoCache ??= [];
|
||||
stringBuilder ??= new();
|
||||
splitArrayPool ??= [];
|
||||
propertyInfoCache ??= new Dictionary<Type, Dictionary<string, PropertyInfo>>();
|
||||
fieldInfoCache ??= new Dictionary<Type, Dictionary<string, FieldInfo>>();
|
||||
stringBuilder ??= new StringBuilder();
|
||||
splitArrayPool ??= new Stack<List<string>>();
|
||||
|
||||
// Remove all whitespace not within strings to make parsing simpler
|
||||
stringBuilder.Length = 0;
|
||||
@@ -99,7 +99,7 @@ public static class JsonParser
|
||||
// Splits { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
|
||||
private static List<string> Split(string json)
|
||||
{
|
||||
List<string> splitArray = splitArrayPool!.Count > 0 ? splitArrayPool.Pop() : [];
|
||||
List<string> splitArray = splitArrayPool!.Count > 0 ? splitArrayPool.Pop() : new List<string>();
|
||||
splitArray.Clear();
|
||||
if (json.Length == 2)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
@@ -14,19 +13,4 @@ internal static class AttributeDataExtension
|
||||
{
|
||||
return data.NamedArguments.Any(a => a.Key == key && predicate((TValue)a.Value.Value!));
|
||||
}
|
||||
|
||||
public static bool TryGetNamedArgumentValue(this AttributeData data, string key, out TypedConstant value)
|
||||
{
|
||||
foreach (KeyValuePair<string, TypedConstant> pair in data.NamedArguments)
|
||||
{
|
||||
if (pair.Key == key)
|
||||
{
|
||||
value = pair.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ internal static class EnumerableExtension
|
||||
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
HashSet<TKey> set = [];
|
||||
HashSet<TKey> set = new();
|
||||
|
||||
do
|
||||
{
|
||||
|
||||
@@ -1,629 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.XPath;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Resx;
|
||||
|
||||
[Generator]
|
||||
public sealed class ResxGenerator : IIncrementalGenerator
|
||||
{
|
||||
private static readonly DiagnosticDescriptor InvalidResx = new("SH401", "Couldn't parse Resx file", "Couldn't parse Resx file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
private static readonly DiagnosticDescriptor InvalidPropertiesForNamespace = new("SH402", "Couldn't compute namespace", "Couldn't compute namespace for file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
private static readonly DiagnosticDescriptor InvalidPropertiesForResourceName = new("SH403", "Couldn't compute resource name", "Couldn't compute resource name for file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
private static readonly DiagnosticDescriptor InconsistentProperties = new("SH404", "Inconsistent properties", "Property '{0}' values for '{1}' are inconsistent", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<(string? AssemblyName, bool SupportNullableReferenceTypes)> compilationProvider = context.CompilationProvider
|
||||
.Select(static (compilation, cancellationToken) => (compilation.AssemblyName, SupportNullableReferenceTypes: compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute") is not null));
|
||||
|
||||
IncrementalValueProvider<ImmutableArray<AdditionalText>> resxProvider = context.AdditionalTextsProvider
|
||||
.Where(text => text.Path.EndsWith(".resx", StringComparison.OrdinalIgnoreCase))
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(
|
||||
source: context.AnalyzerConfigOptionsProvider.Combine(compilationProvider.Combine(resxProvider)),
|
||||
action: (ctx, source) => Execute(ctx, source.Left, source.Right.Left.AssemblyName, source.Right.Left.SupportNullableReferenceTypes, source.Right.Right));
|
||||
}
|
||||
|
||||
private static void Execute(SourceProductionContext context, AnalyzerConfigOptionsProvider options, string? assemblyName, bool supportNullableReferenceTypes, ImmutableArray<AdditionalText> files)
|
||||
{
|
||||
// Group additional file by resource kind ((a.resx, a.en.resx, a.en-us.resx), (b.resx, b.en-us.resx))
|
||||
IOrderedEnumerable<IGrouping<string, AdditionalText>> group = files
|
||||
.GroupBy(file => GetResourceName(file.Path), StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x.Key, StringComparer.Ordinal);
|
||||
List<IGrouping<string, AdditionalText>> resxGroups = [.. group];
|
||||
|
||||
foreach (IGrouping<string, AdditionalText>? resxGroup in resxGroups)
|
||||
{
|
||||
string? rootNamespaceConfiguration = GetMetadataValue(context, options, "RootNamespace", resxGroup);
|
||||
string? projectDirConfiguration = GetMetadataValue(context, options, "ProjectDir", resxGroup);
|
||||
string? namespaceConfiguration = GetMetadataValue(context, options, "Namespace", "DefaultResourcesNamespace", resxGroup);
|
||||
string? resourceNameConfiguration = GetMetadataValue(context, options, "ResourceName", globalName: null, resxGroup);
|
||||
string? classNameConfiguration = GetMetadataValue(context, options, "ClassName", globalName: null, resxGroup);
|
||||
|
||||
string rootNamespace = rootNamespaceConfiguration ?? assemblyName ?? "";
|
||||
string projectDir = projectDirConfiguration ?? assemblyName ?? "";
|
||||
string? defaultResourceName = ComputeResourceName(rootNamespace, projectDir, resxGroup.Key);
|
||||
string? defaultNamespace = ComputeNamespace(rootNamespace, projectDir, resxGroup.Key);
|
||||
|
||||
string? ns = namespaceConfiguration ?? defaultNamespace;
|
||||
string? resourceName = resourceNameConfiguration ?? defaultResourceName;
|
||||
string className = classNameConfiguration ?? ToCSharpNameIdentifier(Path.GetFileName(resxGroup.Key));
|
||||
|
||||
if (ns == null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForNamespace, location: null, resxGroup.First().Path));
|
||||
}
|
||||
|
||||
if (resourceName == null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForResourceName, location: null, resxGroup.First().Path));
|
||||
}
|
||||
|
||||
List<ResxEntry>? entries = LoadResourceFiles(context, resxGroup);
|
||||
|
||||
string content = $"""
|
||||
// Debug info:
|
||||
// key: {resxGroup.Key}
|
||||
// files: {string.Join(", ", resxGroup.Select(f => f.Path))}
|
||||
// RootNamespace (metadata): {rootNamespaceConfiguration}
|
||||
// ProjectDir (metadata): {projectDirConfiguration}
|
||||
// Namespace / DefaultResourcesNamespace (metadata): {namespaceConfiguration}
|
||||
// ResourceName (metadata): {resourceNameConfiguration}
|
||||
// ClassName (metadata): {classNameConfiguration}
|
||||
// AssemblyName: {assemblyName}
|
||||
// RootNamespace (computed): {rootNamespace}
|
||||
// ProjectDir (computed): {projectDir}
|
||||
// defaultNamespace: {defaultNamespace}
|
||||
// defaultResourceName: {defaultResourceName}
|
||||
// Namespace: {ns}
|
||||
// ResourceName: {resourceName}
|
||||
// ClassName: {className}
|
||||
""";
|
||||
|
||||
if (resourceName != null && entries != null)
|
||||
{
|
||||
content += GenerateCode(ns, className, resourceName, entries, supportNullableReferenceTypes);
|
||||
}
|
||||
|
||||
context.AddSource($"{Path.GetFileName(resxGroup.Key)}.resx.g.cs", SourceText.From(content, Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateCode(string? ns, string className, string resourceName, List<ResxEntry> entries, bool enableNullableAttributes)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#nullable enable");
|
||||
|
||||
if (ns != null)
|
||||
{
|
||||
sb.AppendLine($$"""
|
||||
|
||||
namespace {{ns}};
|
||||
|
||||
""");
|
||||
}
|
||||
|
||||
sb.AppendLine($$"""
|
||||
internal partial class {{className}}
|
||||
{
|
||||
private static global::System.Resources.ResourceManager? resourceMan;
|
||||
|
||||
public {{className}}()
|
||||
{
|
||||
}
|
||||
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (resourceMan is null)
|
||||
{
|
||||
resourceMan = new global::System.Resources.ResourceManager("{{resourceName}}", typeof({{className}}).Assembly);
|
||||
}
|
||||
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo? Culture { get; set; }
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static object? GetObject(global::System.Globalization.CultureInfo? culture, string name, object? defaultValue)
|
||||
{
|
||||
culture ??= Culture;
|
||||
object? obj = ResourceManager.GetObject(name, culture);
|
||||
if (obj == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static object? GetObject(global::System.Globalization.CultureInfo? culture, string name)
|
||||
{
|
||||
return GetObject(culture: culture, name: name, defaultValue: null);
|
||||
}
|
||||
|
||||
public static object? GetObject(string name)
|
||||
{
|
||||
return GetObject(culture: null, name: name, defaultValue: null);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static object? GetObject(string name, object? defaultValue)
|
||||
{
|
||||
return GetObject(culture: null, name: name, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
public static global::System.IO.Stream? GetStream(string name)
|
||||
{
|
||||
return GetStream(culture: null, name: name);
|
||||
}
|
||||
|
||||
public static global::System.IO.Stream? GetStream(global::System.Globalization.CultureInfo? culture, string name)
|
||||
{
|
||||
culture ??= Culture;
|
||||
return ResourceManager.GetStream(name, culture);
|
||||
}
|
||||
|
||||
public static string? GetString(global::System.Globalization.CultureInfo? culture, string name)
|
||||
{
|
||||
return GetString(culture: culture, name: name, args: null);
|
||||
}
|
||||
|
||||
public static string? GetString(global::System.Globalization.CultureInfo? culture, string name, params object?[]? args)
|
||||
{
|
||||
culture ??= Culture;
|
||||
string? str = ResourceManager.GetString(name, culture);
|
||||
if (str == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args != null)
|
||||
{
|
||||
return string.Format(culture, str, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
public static string? GetString(string name, params object?[]? args)
|
||||
{
|
||||
return GetString(culture: null, name: name, args: args);
|
||||
}
|
||||
|
||||
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetString(string name, string? defaultValue)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: null);
|
||||
}
|
||||
|
||||
public static string? GetString(string name)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: null, args: null);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(global::System.Globalization.CultureInfo? culture, string name, string? defaultValue)
|
||||
{
|
||||
return GetStringOrDefault(culture: culture, name: name, defaultValue: defaultValue, args: null);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(global::System.Globalization.CultureInfo? culture, string name, string? defaultValue, params object?[]? args)
|
||||
{
|
||||
culture ??= Culture;
|
||||
string? str = ResourceManager.GetString(name, culture);
|
||||
if (str == null)
|
||||
{
|
||||
if (defaultValue == null || args == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Format(culture, defaultValue, args);
|
||||
}
|
||||
}
|
||||
|
||||
if (args != null)
|
||||
{
|
||||
return string.Format(culture, str, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(string name, string? defaultValue, params object?[]? args)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: args);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(string name, string? defaultValue)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: null);
|
||||
}
|
||||
""");
|
||||
|
||||
foreach (ResxEntry? entry in entries.OrderBy(e => e.Name, StringComparer.Ordinal))
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.IsText)
|
||||
{
|
||||
XElement summary = new("summary", new XElement("para", $"Looks up a localized string for \"{entry.Name}\"."));
|
||||
if (!string.IsNullOrWhiteSpace(entry.Comment))
|
||||
{
|
||||
summary.Add(new XElement("para", entry.Comment));
|
||||
}
|
||||
|
||||
if (!entry.IsFileRef)
|
||||
{
|
||||
foreach((string? each, string locale) in entry.Values.Zip(entry.Locales,(x,y)=>(x,y)))
|
||||
{
|
||||
summary.Add(new XElement("para", $"{GetStringWithPadding(locale, 8)} Value: \"{each}\""));
|
||||
}
|
||||
}
|
||||
|
||||
string comment = summary.ToString().Replace("\r\n", "\r\n /// ", StringComparison.Ordinal);
|
||||
|
||||
sb.AppendLine($$"""
|
||||
/// {{comment}}
|
||||
public static string {{ToCSharpNameIdentifier(entry.Name!)}}
|
||||
{
|
||||
get => GetString("{{entry.Name}}")!;
|
||||
}
|
||||
|
||||
""");
|
||||
|
||||
if (entry.Values.FirstOrDefault() is string value)
|
||||
{
|
||||
int args = Regex.Matches(value, "\\{(?<num>[0-9]+)(\\:[^}]*)?\\}", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant)
|
||||
.Cast<Match>()
|
||||
.Select(m => int.Parse(m.Groups["num"].Value, CultureInfo.InvariantCulture))
|
||||
.Distinct()
|
||||
.DefaultIfEmpty(-1)
|
||||
.Max();
|
||||
|
||||
if (args >= 0)
|
||||
{
|
||||
string inParams = string.Join(", ", Enumerable.Range(0, args + 1).Select(arg => "object? arg" + arg.ToString(CultureInfo.InvariantCulture)));
|
||||
string callParams = string.Join(", ", Enumerable.Range(0, args + 1).Select(arg => "arg" + arg.ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
sb.AppendLine($$"""
|
||||
/// {{comment}}
|
||||
public static string Format{{ToCSharpNameIdentifier(entry.Name!)}}({{inParams}})
|
||||
{
|
||||
return GetString("{{entry.Name}}", {{callParams}})!;
|
||||
}
|
||||
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($$"""
|
||||
public static global::{{entry.FullTypeName}}? {{ToCSharpNameIdentifier(entry.Name!)}}
|
||||
{
|
||||
get => (global::{{entry.FullTypeName}}?)GetObject("{{entry.Name}}");
|
||||
}
|
||||
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine($$"""
|
||||
}
|
||||
|
||||
internal partial class {{className}}Names
|
||||
{
|
||||
""");
|
||||
|
||||
foreach (ResxEntry entry in entries)
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.AppendLine($$"""
|
||||
public const string {{ToCSharpNameIdentifier(entry.Name!)}} = "entry.Name";
|
||||
""");
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GetStringWithPadding(string source, int length)
|
||||
{
|
||||
if (source.Length >= length)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
return source + new string('_', length - source.Length);
|
||||
}
|
||||
|
||||
private static string? ComputeResourceName(string rootNamespace, string projectDir, string resourcePath)
|
||||
{
|
||||
string fullProjectDir = EnsureEndSeparator(Path.GetFullPath(projectDir));
|
||||
string fullResourcePath = Path.GetFullPath(resourcePath);
|
||||
|
||||
if (fullProjectDir == fullResourcePath)
|
||||
{
|
||||
return rootNamespace;
|
||||
}
|
||||
|
||||
if (fullResourcePath.StartsWith(fullProjectDir, StringComparison.Ordinal))
|
||||
{
|
||||
string relativePath = fullResourcePath.Substring(fullProjectDir.Length);
|
||||
return rootNamespace + '.' + relativePath.Replace('/', '.').Replace('\\', '.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ComputeNamespace(string rootNamespace, string projectDir, string resourcePath)
|
||||
{
|
||||
string fullProjectDir = EnsureEndSeparator(Path.GetFullPath(projectDir));
|
||||
string fullResourcePath = EnsureEndSeparator(Path.GetDirectoryName(Path.GetFullPath(resourcePath))!);
|
||||
|
||||
if (fullProjectDir == fullResourcePath)
|
||||
{
|
||||
return rootNamespace;
|
||||
}
|
||||
|
||||
if (fullResourcePath.StartsWith(fullProjectDir, StringComparison.Ordinal))
|
||||
{
|
||||
string relativePath = fullResourcePath.Substring(fullProjectDir.Length);
|
||||
return rootNamespace + '.' + relativePath.Replace('/', '.').Replace('\\', '.').TrimEnd('.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<ResxEntry>? LoadResourceFiles(SourceProductionContext context, IGrouping<string, AdditionalText> resxGroug)
|
||||
{
|
||||
List<ResxEntry> entries = [];
|
||||
foreach (AdditionalText? entry in resxGroug.OrderBy(file => file.Path, StringComparer.Ordinal))
|
||||
{
|
||||
SourceText? content = entry.GetText(context.CancellationToken);
|
||||
if (content is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
XDocument document = XDocument.Parse(content.ToString());
|
||||
foreach (XElement? element in document.XPathSelectElements("/root/data"))
|
||||
{
|
||||
string? name = element.Attribute("name")?.Value;
|
||||
string? type = element.Attribute("type")?.Value;
|
||||
string? comment = element.Attribute("comment")?.Value;
|
||||
string? value = element.Element("value")?.Value;
|
||||
|
||||
ResxEntry existingEntry = entries.Find(e => e.Name == name);
|
||||
if (existingEntry != null)
|
||||
{
|
||||
existingEntry.Comment ??= comment;
|
||||
existingEntry.Values.Add(value);
|
||||
existingEntry.Locales.Add(GetLocaleName(entry.Path));
|
||||
}
|
||||
else
|
||||
{
|
||||
entries.Add(new() { Name = name, Values = [value], Locales = [GetLocaleName(entry.Path)], Comment = comment, Type = type });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InvalidResx, location: null, entry.Path));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static string? GetMetadataValue(SourceProductionContext context, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, string name, IEnumerable<AdditionalText> additionalFiles)
|
||||
{
|
||||
return GetMetadataValue(context, analyzerConfigOptionsProvider, name, name, additionalFiles);
|
||||
}
|
||||
|
||||
private static string? GetMetadataValue(SourceProductionContext context, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, string name, string? globalName, IEnumerable<AdditionalText> additionalFiles)
|
||||
{
|
||||
string? result = null;
|
||||
foreach (AdditionalText file in additionalFiles)
|
||||
{
|
||||
if (analyzerConfigOptionsProvider.GetOptions(file).TryGetValue("build_metadata.AdditionalFiles." + name, out string? value))
|
||||
{
|
||||
if (result != null && value != result)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InconsistentProperties, location: null, name, file.Path));
|
||||
return null;
|
||||
}
|
||||
|
||||
result = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (globalName != null && analyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property." + globalName, out string? globalValue) && !string.IsNullOrEmpty(globalValue))
|
||||
{
|
||||
return globalValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ToCSharpNameIdentifier(string name)
|
||||
{
|
||||
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#identifiers
|
||||
// https://docs.microsoft.com/en-us/dotnet/api/system.globalization.unicodecategory?view=net-5.0
|
||||
StringBuilder sb = new();
|
||||
foreach (char c in name)
|
||||
{
|
||||
UnicodeCategory category = char.GetUnicodeCategory(c);
|
||||
switch (category)
|
||||
{
|
||||
case UnicodeCategory.UppercaseLetter:
|
||||
case UnicodeCategory.LowercaseLetter:
|
||||
case UnicodeCategory.TitlecaseLetter:
|
||||
case UnicodeCategory.ModifierLetter:
|
||||
case UnicodeCategory.OtherLetter:
|
||||
case UnicodeCategory.LetterNumber:
|
||||
sb.Append(c);
|
||||
break;
|
||||
|
||||
case UnicodeCategory.DecimalDigitNumber:
|
||||
case UnicodeCategory.ConnectorPunctuation:
|
||||
case UnicodeCategory.Format:
|
||||
if (sb.Length == 0)
|
||||
{
|
||||
sb.Append('_');
|
||||
}
|
||||
sb.Append(c);
|
||||
break;
|
||||
|
||||
default:
|
||||
sb.Append('_');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string EnsureEndSeparator(string path)
|
||||
{
|
||||
if (path[path.Length - 1] == Path.DirectorySeparatorChar)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return path + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
private static string GetResourceName(string path)
|
||||
{
|
||||
string pathWithoutExtension = Path.Combine(Path.GetDirectoryName(path)!, Path.GetFileNameWithoutExtension(path));
|
||||
int indexOf = pathWithoutExtension.LastIndexOf('.');
|
||||
if (indexOf < 0)
|
||||
{
|
||||
return pathWithoutExtension;
|
||||
}
|
||||
|
||||
return Regex.IsMatch(pathWithoutExtension.Substring(indexOf + 1), "^[a-zA-Z]{2}(-[a-zA-Z]{2,4})?$", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1))
|
||||
? pathWithoutExtension.Substring(0, indexOf)
|
||||
: pathWithoutExtension;
|
||||
}
|
||||
|
||||
private static string GetLocaleName(string path)
|
||||
{
|
||||
string fileName = Path.GetFileNameWithoutExtension(path);
|
||||
int indexOf = fileName.LastIndexOf('.');
|
||||
if (indexOf < 0)
|
||||
{
|
||||
return "Neutral";
|
||||
}
|
||||
|
||||
return fileName.Substring(indexOf + 1);
|
||||
}
|
||||
|
||||
private sealed class ResxEntry
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public List<string?> Values { get; set; } = default!;
|
||||
public List<string> Locales { get; set; } = default!;
|
||||
public string? Comment { get; set; }
|
||||
public string? Type { get; set; }
|
||||
|
||||
public bool IsText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Type == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Values.FirstOrDefault() is string value)
|
||||
{
|
||||
string[] parts = value.Split(';');
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
string type = parts[1];
|
||||
if (type.StartsWith("System.String,", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string? FullTypeName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsText)
|
||||
{
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (Values.FirstOrDefault() is string value)
|
||||
{
|
||||
string[] parts = value.Split(';');
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
string type = parts[1];
|
||||
return type.Split(',')[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsFileRef
|
||||
{
|
||||
get => Type != null && Type.StartsWith("System.Resources.ResXFileRef,", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Resx;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
|
||||
int previousIndex = 0;
|
||||
int index = str.IndexOf(oldValue, comparison);
|
||||
while (index is not -1)
|
||||
{
|
||||
sb.Append(str, previousIndex, index - previousIndex);
|
||||
sb.Append(newValue);
|
||||
index += oldValue.Length;
|
||||
|
||||
previousIndex = index;
|
||||
index = str.IndexOf(oldValue, index, comparison);
|
||||
}
|
||||
|
||||
sb.Append(str, previousIndex, str.Length - previousIndex);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
@@ -7,10 +7,17 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" Version="3.3.4" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
|
||||
|
||||
@@ -57,13 +57,23 @@ internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
|
||||
private static void CompilationStart(CompilationStartAnalysisContext context)
|
||||
{
|
||||
SyntaxKind[] types = [SyntaxKind.ClassDeclaration, SyntaxKind.InterfaceDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.EnumDeclaration,];
|
||||
SyntaxKind[] types =
|
||||
{
|
||||
SyntaxKind.ClassDeclaration,
|
||||
SyntaxKind.InterfaceDeclaration,
|
||||
SyntaxKind.StructDeclaration,
|
||||
SyntaxKind.EnumDeclaration,
|
||||
};
|
||||
context.RegisterSyntaxNodeAction(HandleTypeShouldBeInternal, types);
|
||||
context.RegisterSyntaxNodeAction(HandleMethodParameterShouldUseRefLikeKeyword, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleConstructorParameterShouldUseRefLikeKeyword, SyntaxKind.ConstructorDeclaration);
|
||||
|
||||
SyntaxKind[] expressions = [SyntaxKind.EqualsExpression, SyntaxKind.NotEqualsExpression,];
|
||||
SyntaxKind[] expressions =
|
||||
{
|
||||
SyntaxKind.EqualsExpression,
|
||||
SyntaxKind.NotEqualsExpression,
|
||||
};
|
||||
context.RegisterSyntaxNodeAction(HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching, expressions);
|
||||
context.RegisterSyntaxNodeAction(HandleIsPatternShouldUseRecursivePattern, SyntaxKind.IsPatternExpression);
|
||||
context.RegisterSyntaxNodeAction(HandleArgumentNullExceptionThrowIfNull, SyntaxKind.SuppressNullableWarningExpression);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace Snap.Hutao.Test.PlatformExtensions;
|
||||
namespace Snap.Hutao.Test;
|
||||
|
||||
[TestClass]
|
||||
public sealed class DependencyInjectionTest
|
||||
@@ -2,20 +2,11 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Snap.Hutao.Test.BaseClassLibrary;
|
||||
namespace Snap.Hutao.Test;
|
||||
|
||||
[TestClass]
|
||||
public class JsonSerializeTest
|
||||
{
|
||||
private TestContext? testContext;
|
||||
|
||||
public TestContext? TestContext { get => testContext; set => testContext = value; }
|
||||
|
||||
private readonly JsonSerializerOptions AlowStringNumberOptions = new()
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
private const string SmapleObjectJson = """
|
||||
{
|
||||
"A" :1
|
||||
@@ -53,29 +44,13 @@ public class JsonSerializeTest
|
||||
[TestMethod]
|
||||
public void NumberStringKeyCanSerializeAsKey()
|
||||
{
|
||||
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, AlowStringNumberOptions)!;
|
||||
Assert.AreEqual(sample[111], "12");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ByteArraySerializeAsBase64()
|
||||
{
|
||||
byte[] array =
|
||||
#if NET8_0_OR_GREATER
|
||||
[1, 2, 3, 4, 5];
|
||||
#else
|
||||
{ 1, 2, 3, 4, 5 };
|
||||
#endif
|
||||
ByteArraySample sample = new()
|
||||
JsonSerializerOptions options = new()
|
||||
{
|
||||
Array = array,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
string result = JsonSerializer.Serialize(sample);
|
||||
TestContext!.WriteLine($"ByteArray Serialize Result: {result}");
|
||||
Assert.AreEqual(result, """
|
||||
{"Array":"AQIDBAU="}
|
||||
""");
|
||||
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, options)!;
|
||||
Assert.AreEqual(sample[111], "12");
|
||||
}
|
||||
|
||||
private sealed class Sample
|
||||
@@ -89,9 +64,4 @@ public class JsonSerializeTest
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||
public int A { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ByteArraySample
|
||||
{
|
||||
public byte[]? Array { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,11 @@ public sealed class ForEachRuntimeBehaviorTest
|
||||
[TestMethod]
|
||||
public void ListOfStringCanEnumerateAsReadOnlySpanOfChar()
|
||||
{
|
||||
List<string> strings =
|
||||
#if NET8_0_OR_GREATER
|
||||
["a", "b", "c"];
|
||||
#else
|
||||
new() { "a", "b", "c" };
|
||||
#endif
|
||||
List<string> strings = new()
|
||||
{
|
||||
"a", "b", "c"
|
||||
};
|
||||
|
||||
int count = 0;
|
||||
foreach (ReadOnlySpan<char> chars in strings)
|
||||
{
|
||||
|
||||
@@ -8,13 +8,8 @@ public sealed class RangeRuntimeBehaviorTest
|
||||
[TestMethod]
|
||||
public void RangeTrimLastOne()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
int[] array = [1, 2, 3, 4];
|
||||
int[] test = [1, 2, 3];
|
||||
#else
|
||||
int[] array = { 1, 2, 3, 4 };
|
||||
int[] test = { 1, 2, 3 };
|
||||
#endif
|
||||
int[] result = array[..^1];
|
||||
Assert.AreEqual(3, result.Length);
|
||||
Assert.IsTrue(MemoryExtensions.SequenceEqual<int>(test, result));
|
||||
|
||||
@@ -6,35 +6,11 @@ public sealed class UnsafeRuntimeBehaviorTest
|
||||
[TestMethod]
|
||||
public unsafe void UInt32AllSetIs()
|
||||
{
|
||||
byte[] bytes =
|
||||
#if NET8_0_OR_GREATER
|
||||
[0xFF, 0xFF, 0xFF, 0xFF];
|
||||
#else
|
||||
{ 0xFF, 0xFF, 0xFF, 0xFF, };
|
||||
#endif
|
||||
byte[] bytes = { 0xFF, 0xFF, 0xFF, 0xFF, };
|
||||
|
||||
fixed (byte* pBytes = bytes)
|
||||
{
|
||||
Assert.AreEqual(uint.MaxValue, *(uint*)pBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public sealed class NewModifierRuntimeBehaviorTest
|
||||
{
|
||||
private interface IBase
|
||||
{
|
||||
int GetValue();
|
||||
}
|
||||
|
||||
private interface IBaseImpl : IBase
|
||||
{
|
||||
new int GetValue();
|
||||
}
|
||||
|
||||
private sealed class Impl : IBaseImpl
|
||||
{
|
||||
public int GetValue() => 1;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<Configurations>Debug;Release;Debug As Fake Elevated</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"$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"
|
||||
]
|
||||
}
|
||||
"useSafeHandles": false
|
||||
}
|
||||
@@ -28,12 +28,8 @@ WaitForSingleObject
|
||||
WriteProcessMemory
|
||||
|
||||
// OLE32
|
||||
CoCreateInstance
|
||||
CoWaitForMultipleObjects
|
||||
|
||||
// SHELL32
|
||||
SHCreateItemFromParsingName
|
||||
|
||||
// USER32
|
||||
AttachThreadInput
|
||||
FindWindowExW
|
||||
@@ -50,10 +46,6 @@ SetForegroundWindow
|
||||
UnregisterHotKey
|
||||
|
||||
// COM
|
||||
FileOpenDialog
|
||||
FileSaveDialog
|
||||
IFileOpenDialog
|
||||
IFileSaveDialog
|
||||
IPersistFile
|
||||
IShellLinkW
|
||||
ShellLink
|
||||
@@ -64,7 +56,6 @@ IMemoryBufferByteAccess
|
||||
|
||||
// Const value
|
||||
INFINITE
|
||||
MAX_PATH
|
||||
WM_GETMINMAXINFO
|
||||
WM_HOTKEY
|
||||
WM_NCRBUTTONDOWN
|
||||
@@ -72,8 +63,6 @@ WM_NCRBUTTONUP
|
||||
WM_NULL
|
||||
|
||||
// Type & Enum definition
|
||||
HRESULT_FROM_WIN32
|
||||
SLGP_FLAGS
|
||||
|
||||
// System.Threading
|
||||
LPTHREAD_START_ROUTINE
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
||||
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -19,6 +19,18 @@ internal static class StructMarshal
|
||||
/// <returns>新的实例</returns>
|
||||
public static unsafe WINDOWPLACEMENT WINDOWPLACEMENT()
|
||||
{
|
||||
return new() { length = unchecked((uint)sizeof(WINDOWPLACEMENT)) };
|
||||
return new() { length = SizeOf<WINDOWPLACEMENT>() };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取结构的大小
|
||||
/// </summary>
|
||||
/// <typeparam name="TStruct">结构类型</typeparam>
|
||||
/// <returns>结构的大小</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe uint SizeOf<TStruct>()
|
||||
where TStruct : unmanaged
|
||||
{
|
||||
return unchecked((uint)sizeof(TStruct));
|
||||
}
|
||||
}
|
||||
37
src/Snap.Hutao/Snap.Hutao.Win32/UnsafePInvoke.cs
Normal file
37
src/Snap.Hutao/Snap.Hutao.Win32/UnsafePInvoke.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.UI.Core;
|
||||
using Windows.Win32.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Win32;
|
||||
|
||||
internal static class UnsafePInvoke
|
||||
{
|
||||
private enum WINDOW_TYPE : uint
|
||||
{
|
||||
IMMERSIVE_BODY,
|
||||
IMMERSIVE_DOCK,
|
||||
IMMERSIVE_HOSTED,
|
||||
IMMERSIVE_TEST,
|
||||
IMMERSIVE_BODY_ACTIVE,
|
||||
IMMERSIVE_DOCK_ACTIVE,
|
||||
NOT_IMMERSIVE,
|
||||
}
|
||||
|
||||
[DllImport("Windows.UI.dll", CharSet = CharSet.None, EntryPoint = "#1500", ExactSpelling = false, SetLastError = true)]
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
||||
private static extern HRESULT PrivateCreateCoreWindow(WINDOW_TYPE WindowType, PWSTR pWindowTitle, int x, int y, uint uWidth, uint uHeight, uint dwAttributes, HWND hOwnerWindow, Guid riid, out nint ppv);
|
||||
|
||||
public static unsafe CoreWindow PrivateCreateCoreWindow(string title, HWND hOwnerWindow)
|
||||
{
|
||||
fixed(char* pTitle = title)
|
||||
{
|
||||
PrivateCreateCoreWindow(WINDOW_TYPE.NOT_IMMERSIVE, pTitle, 0, 0, 400, 400, 0, hOwnerWindow, typeof(ICoreWindow).GUID, out nint thisPtr);
|
||||
return CoreWindow.FromAbi(thisPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,4 +69,4 @@ internal class WinRTCustomMarshaler : ICustomMarshaler
|
||||
return Marshal.GetObjectForIUnknown(pNativeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Color.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Converter.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/CornerRadius.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/FlyoutStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/FontStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Glyph.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/InfoBarOverride.xaml"/>
|
||||
@@ -20,7 +19,6 @@
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/NumericValue.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/PageOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/SettingsStyle.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
|
||||
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
|
||||
|
||||
@@ -3,18 +3,12 @@
|
||||
|
||||
namespace Snap.Hutao.Control.Animation;
|
||||
|
||||
internal static class ControlAnimationConstants
|
||||
/// <summary>
|
||||
/// 动画时长
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal static class AnimationDurations
|
||||
{
|
||||
/// <summary>
|
||||
/// 1
|
||||
/// </summary>
|
||||
public const string One = "1";
|
||||
|
||||
/// <summary>
|
||||
/// 1.1
|
||||
/// </summary>
|
||||
public const string OnePointOne = "1.1";
|
||||
|
||||
/// <summary>
|
||||
/// 图片缩放动画
|
||||
/// </summary>
|
||||
@@ -19,10 +19,10 @@ internal sealed class ImageZoomInAnimation : ImplicitAnimation<string, Vector3>
|
||||
/// </summary>
|
||||
public ImageZoomInAnimation()
|
||||
{
|
||||
Duration = ControlAnimationConstants.ImageZoom;
|
||||
Duration = AnimationDurations.ImageZoom;
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
|
||||
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
|
||||
To = ControlAnimationConstants.OnePointOne;
|
||||
To = Core.StringLiterals.OnePointOne;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -19,10 +19,10 @@ internal sealed class ImageZoomOutAnimation : ImplicitAnimation<string, Vector3>
|
||||
/// </summary>
|
||||
public ImageZoomOutAnimation()
|
||||
{
|
||||
Duration = ControlAnimationConstants.ImageZoom;
|
||||
Duration = AnimationDurations.ImageZoom;
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
|
||||
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
|
||||
To = ControlAnimationConstants.One;
|
||||
To = Core.StringLiterals.One;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
/// <summary>
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
public AutoHeightBehavior()
|
||||
{
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Initialize()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateElement();
|
||||
}
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Height = AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
/// <summary>
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("TargetWidth", typeof(double), 1.0D)]
|
||||
[DependencyProperty("TargetHeight", typeof(double), 1.0D)]
|
||||
internal sealed partial class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private readonly SizeChangedEventHandler sizeChangedEventHandler;
|
||||
|
||||
public AutoWidthBehavior()
|
||||
{
|
||||
sizeChangedEventHandler = OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Initialize()
|
||||
{
|
||||
UpdateElement();
|
||||
AssociatedObject.SizeChanged += sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= sizeChangedEventHandler;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateElement();
|
||||
}
|
||||
|
||||
private void UpdateElement()
|
||||
{
|
||||
AssociatedObject.Width = AssociatedObject.Height * (TargetWidth / TargetHeight);
|
||||
}
|
||||
}
|
||||
@@ -35,11 +35,6 @@ internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIEle
|
||||
|
||||
private void TryExecuteCommand()
|
||||
{
|
||||
if (AssociatedObject is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (executed)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Labs.WinUI.MarqueeTextRns;
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
internal sealed class MarqueeTextBehavior : BehaviorBase<MarqueeText>
|
||||
{
|
||||
private readonly PointerEventHandler pointerEnteredEventHandler;
|
||||
private readonly PointerEventHandler pointerExitedEventHandler;
|
||||
|
||||
public MarqueeTextBehavior()
|
||||
{
|
||||
pointerEnteredEventHandler = OnPointerEntered;
|
||||
pointerExitedEventHandler = OnPointerExited;
|
||||
}
|
||||
|
||||
protected override bool Initialize()
|
||||
{
|
||||
AssociatedObject.PointerEntered += pointerEnteredEventHandler;
|
||||
AssociatedObject.PointerExited += pointerExitedEventHandler;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool Uninitialize()
|
||||
{
|
||||
AssociatedObject.PointerEntered -= pointerEnteredEventHandler;
|
||||
AssociatedObject.PointerExited -= pointerExitedEventHandler;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnPointerEntered(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
AssociatedObject.StartMarquee();
|
||||
}
|
||||
|
||||
private void OnPointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
AssociatedObject.StopMarquee();
|
||||
}
|
||||
}
|
||||
@@ -11,17 +11,12 @@ namespace Snap.Hutao.Control.Behavior;
|
||||
/// 打开附着的浮出控件操作
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class ShowAttachedFlyoutAction : DependencyObject, IAction
|
||||
internal sealed class OpenAttachedFlyoutAction : DependencyObject, IAction
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object? Execute(object sender, object parameter)
|
||||
public object Execute(object sender, object parameter)
|
||||
{
|
||||
if (sender is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement);
|
||||
return default;
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
@@ -13,7 +12,7 @@ internal sealed class SelectedItemInViewBehavior : BehaviorBase<ListViewBase>
|
||||
{
|
||||
if (AssociatedObject.SelectedItem is { } item)
|
||||
{
|
||||
AssociatedObject.SmoothScrollIntoViewWithItemAsync(item, ScrollItemPlacement.Center).SafeForget();
|
||||
AssociatedObject.ScrollIntoView(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Xaml.Interactivity;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
[DependencyProperty("Animation", typeof(AnimationSet))]
|
||||
[DependencyProperty("TargetObject", typeof(UIElement))]
|
||||
internal sealed partial class StartAnimationActionNoThrow : DependencyObject, IAction
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object Execute(object sender, object parameter)
|
||||
{
|
||||
if (Animation is not null)
|
||||
{
|
||||
if (TargetObject is not null)
|
||||
{
|
||||
Animation.Start(TargetObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Animation.Start(sender as UIElement);
|
||||
}
|
||||
}
|
||||
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,9 @@ internal sealed partial class SegmentedBar : ContentControl
|
||||
double offset = 0;
|
||||
foreach (ref readonly IColorSegment segment in CollectionsMarshal.AsSpan(list))
|
||||
{
|
||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||
collection.Add(new GradientStop() { Color = segment.Color, Offset = offset, });
|
||||
offset += segment.Value / total;
|
||||
collection.Add(new() { Color = segment.Color, Offset = offset, });
|
||||
collection.Add(new GradientStop() { Color = segment.Color, Offset = offset, });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
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
|
||||
{
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Helper;
|
||||
|
||||
[SuppressMessage("", "SH001")]
|
||||
[DependencyProperty("IsItemsEnabled", typeof(bool), true, nameof(OnIsItemsEnabledChanged), IsAttached = true, AttachedType = typeof(SettingsExpander))]
|
||||
public sealed partial class SettingsExpanderHelper
|
||||
{
|
||||
private static void OnIsItemsEnabledChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
foreach (object item in ((SettingsExpander)dp).Items)
|
||||
{
|
||||
if (item is Microsoft.UI.Xaml.Controls.Control control)
|
||||
{
|
||||
control.IsEnabled = (bool)e.NewValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
internal interface IScopedPageScopeReferenceTracker
|
||||
{
|
||||
IServiceScope CreateScope();
|
||||
}
|
||||
@@ -110,7 +110,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
|
||||
if (exception is HttpRequestException httpRequestException)
|
||||
{
|
||||
infoBarService.Error(httpRequestException, SH.FormatControlImageCompositionImageHttpRequest(uri));
|
||||
infoBarService.Error(httpRequestException, SH.ControlImageCompositionImageHttpRequest.Format(uri));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -196,7 +196,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
{
|
||||
await AnimationBuilder
|
||||
.Create()
|
||||
.Opacity(from: 0D, to: 1D, duration: ControlAnimationConstants.ImageFadeIn)
|
||||
.Opacity(from: 0D, to: 1D, duration: AnimationDurations.ImageFadeIn)
|
||||
.StartAsync(this, token)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
@@ -217,7 +217,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
|
||||
{
|
||||
await AnimationBuilder
|
||||
.Create()
|
||||
.Opacity(from: 1D, to: 0D, duration: ControlAnimationConstants.ImageFadeOut)
|
||||
.Opacity(from: 1D, to: 0D, duration: AnimationDurations.ImageFadeOut)
|
||||
.StartAsync(this, token)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using System.Numerics;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class DefaultItemCollectionTransitionProvider : ItemCollectionTransitionProvider
|
||||
{
|
||||
private const double DefaultAnimationDurationInMs = 300.0;
|
||||
|
||||
static DefaultItemCollectionTransitionProvider()
|
||||
{
|
||||
AnimationSlowdownFactor = 1.0;
|
||||
}
|
||||
|
||||
public static double AnimationSlowdownFactor { get; set; }
|
||||
|
||||
protected override bool ShouldAnimateCore(ItemCollectionTransition transition)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void StartTransitions(IList<ItemCollectionTransition> transitions)
|
||||
{
|
||||
List<ItemCollectionTransition> addTransitions = [];
|
||||
List<ItemCollectionTransition> removeTransitions = [];
|
||||
List<ItemCollectionTransition> moveTransitions = [];
|
||||
|
||||
foreach (ItemCollectionTransition transition in addTransitions)
|
||||
{
|
||||
switch (transition.Operation)
|
||||
{
|
||||
case ItemCollectionTransitionOperation.Add:
|
||||
addTransitions.Add(transition);
|
||||
break;
|
||||
case ItemCollectionTransitionOperation.Remove:
|
||||
removeTransitions.Add(transition);
|
||||
break;
|
||||
case ItemCollectionTransitionOperation.Move:
|
||||
moveTransitions.Add(transition);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
StartAddTransitions(addTransitions, removeTransitions.Count > 0, moveTransitions.Count > 0);
|
||||
StartRemoveTransitions(removeTransitions);
|
||||
StartMoveTransitions(moveTransitions, removeTransitions.Count > 0);
|
||||
}
|
||||
|
||||
private static void StartAddTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveTransitions, bool hasMoveTransitions)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
|
||||
ScalarKeyFrameAnimation fadeInAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
fadeInAnimation.InsertKeyFrame(0.0f, 0.0f);
|
||||
|
||||
if (hasMoveTransitions && hasRemoveTransitions)
|
||||
{
|
||||
fadeInAnimation.InsertKeyFrame(0.66f, 0.0f);
|
||||
}
|
||||
else if (hasMoveTransitions || hasRemoveTransitions)
|
||||
{
|
||||
fadeInAnimation.InsertKeyFrame(0.5f, 0.0f);
|
||||
}
|
||||
|
||||
fadeInAnimation.InsertKeyFrame(1.0f, 1.0f);
|
||||
fadeInAnimation.Duration = TimeSpan.FromMilliseconds(
|
||||
DefaultAnimationDurationInMs * ((hasRemoveTransitions ? 1 : 0) + (hasMoveTransitions ? 1 : 0) + 1) * AnimationSlowdownFactor);
|
||||
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
visual.StartAnimation("Opacity", fadeInAnimation);
|
||||
batch.End();
|
||||
batch.Completed += (_, _) => progress.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private static void StartRemoveTransitions(IList<ItemCollectionTransition> transitions)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
|
||||
ScalarKeyFrameAnimation fadeOutAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
fadeOutAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue");
|
||||
fadeOutAnimation.InsertKeyFrame(1.0f, 0.0f);
|
||||
fadeOutAnimation.Duration = TimeSpan.FromMilliseconds(DefaultAnimationDurationInMs * AnimationSlowdownFactor);
|
||||
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
visual.StartAnimation(nameof(Visual.Opacity), fadeOutAnimation);
|
||||
batch.End();
|
||||
batch.Completed += (_, _) =>
|
||||
{
|
||||
visual.Opacity = 1.0f;
|
||||
progress.Complete();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void StartMoveTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveAnimations)
|
||||
{
|
||||
foreach (ItemCollectionTransition transition in transitions)
|
||||
{
|
||||
ItemCollectionTransitionProgress progress = transition.Start();
|
||||
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
|
||||
Compositor compositor = visual.Compositor;
|
||||
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
|
||||
// Animate offset.
|
||||
if (transition.OldBounds.X != transition.NewBounds.X ||
|
||||
transition.OldBounds.Y != transition.NewBounds.Y)
|
||||
{
|
||||
AnimateOffset(visual, compositor, transition.OldBounds, transition.NewBounds, hasRemoveAnimations);
|
||||
}
|
||||
|
||||
batch.End();
|
||||
batch.Completed += (_, _) => progress.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AnimateOffset(Visual visual, Compositor compositor, Rect oldBounds, Rect newBounds, bool hasRemoveAnimations)
|
||||
{
|
||||
Vector2KeyFrameAnimation offsetAnimation = compositor.CreateVector2KeyFrameAnimation();
|
||||
|
||||
offsetAnimation.SetVector2Parameter("delta", new Vector2(
|
||||
(float)(oldBounds.X - newBounds.X),
|
||||
(float)(oldBounds.Y - newBounds.Y)));
|
||||
offsetAnimation.SetVector2Parameter("final", default);
|
||||
offsetAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue + delta");
|
||||
if (hasRemoveAnimations)
|
||||
{
|
||||
offsetAnimation.InsertExpressionKeyFrame(0.5f, "delta");
|
||||
}
|
||||
|
||||
offsetAnimation.InsertExpressionKeyFrame(1.0f, "final");
|
||||
offsetAnimation.Duration = TimeSpan.FromMilliseconds(
|
||||
DefaultAnimationDurationInMs * ((hasRemoveAnimations ? 1 : 0) + 1) * AnimationSlowdownFactor);
|
||||
|
||||
visual.StartAnimation("TransformMatrix._41_42", offsetAnimation);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
[DebuggerDisplay("Count = {Count}, Height = {Height}")]
|
||||
internal class UniformStaggeredColumnLayout : List<UniformStaggeredItem>
|
||||
{
|
||||
public double Height { get; private set; }
|
||||
|
||||
public new void Add(UniformStaggeredItem item)
|
||||
{
|
||||
Height = item.Top + item.Height;
|
||||
base.Add(item);
|
||||
}
|
||||
|
||||
public new void Clear()
|
||||
{
|
||||
Height = 0;
|
||||
base.Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class UniformStaggeredItem
|
||||
{
|
||||
public UniformStaggeredItem(int index)
|
||||
{
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public double Top { get; internal set; }
|
||||
|
||||
public double Height { get; internal set; }
|
||||
|
||||
public int Index { get; }
|
||||
|
||||
public UIElement? Element { get; internal set; }
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Collections.Specialized;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
[DependencyProperty("MinItemWidth", typeof(double), 0D, nameof(OnMinItemWidthChanged))]
|
||||
[DependencyProperty("MinColumnSpacing", typeof(double), 0D, nameof(OnSpacingChanged))]
|
||||
[DependencyProperty("MinRowSpacing", typeof(double), 0D, nameof(OnSpacingChanged))]
|
||||
internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
|
||||
{
|
||||
context.LayoutState = new UniformStaggeredLayoutState(context);
|
||||
base.InitializeForContextCore(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
|
||||
{
|
||||
context.LayoutState = null;
|
||||
base.UninitializeForContextCore(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
|
||||
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
state.RemoveFromIndex(args.NewStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
state.RemoveFromIndex(args.NewStartingIndex);
|
||||
state.RecycleElementAt(args.NewStartingIndex); // We must recycle the element to ensure that it gets the correct context
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
|
||||
int maxIndex = Math.Max(args.NewStartingIndex, args.OldStartingIndex);
|
||||
state.RemoveRange(minIndex, maxIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
state.RemoveFromIndex(args.OldStartingIndex);
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
state.Clear();
|
||||
break;
|
||||
}
|
||||
|
||||
base.OnItemsChangedCore(context, source, args);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
|
||||
{
|
||||
if (context.ItemCount == 0)
|
||||
{
|
||||
return new Size(availableSize.Width, 0);
|
||||
}
|
||||
|
||||
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
|
||||
{
|
||||
return new Size(availableSize.Width, 0.0f);
|
||||
}
|
||||
|
||||
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
|
||||
|
||||
double availableWidth = availableSize.Width;
|
||||
double availableHeight = availableSize.Height;
|
||||
|
||||
(int numberOfColumns, double columnWidth) = GetNumberOfColumnsAndWidth(availableWidth, MinItemWidth, MinColumnSpacing);
|
||||
|
||||
if (columnWidth != state.ColumnWidth)
|
||||
{
|
||||
// The items will need to be remeasured
|
||||
state.Clear();
|
||||
}
|
||||
|
||||
state.ColumnWidth = columnWidth;
|
||||
|
||||
// adjust for column spacing on all columns expect the first
|
||||
double totalWidth = state.ColumnWidth + ((numberOfColumns - 1) * (state.ColumnWidth + MinColumnSpacing));
|
||||
if (totalWidth > availableWidth)
|
||||
{
|
||||
numberOfColumns--;
|
||||
}
|
||||
else if (double.IsInfinity(availableWidth))
|
||||
{
|
||||
availableWidth = totalWidth;
|
||||
}
|
||||
|
||||
if (numberOfColumns != state.NumberOfColumns)
|
||||
{
|
||||
// The items will not need to be remeasured, but they will need to go into new columns
|
||||
state.ClearColumns();
|
||||
}
|
||||
|
||||
if (MinRowSpacing != state.RowSpacing)
|
||||
{
|
||||
// If the RowSpacing changes the height of the rows will be different.
|
||||
// The columns stores the height so we'll want to clear them out to
|
||||
// get the proper height
|
||||
state.ClearColumns();
|
||||
state.RowSpacing = MinRowSpacing;
|
||||
}
|
||||
|
||||
Span<double> columnHeights = new double[numberOfColumns];
|
||||
Span<int> itemsPerColumn = new int[numberOfColumns];
|
||||
HashSet<int> deadColumns = [];
|
||||
|
||||
for (int i = 0; i < context.ItemCount; i++)
|
||||
{
|
||||
int columnIndex = GetLowestColumnIndex(columnHeights);
|
||||
|
||||
bool measured = false;
|
||||
UniformStaggeredItem item = state.GetItemAt(i);
|
||||
if (item.Height == 0)
|
||||
{
|
||||
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
|
||||
// The first element must be force refreshed otherwise
|
||||
// it will use the old one realized
|
||||
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
|
||||
// Now we need to refresh the first element of each column
|
||||
ElementRealizationOptions options = i < numberOfColumns ? ElementRealizationOptions.ForceCreate : ElementRealizationOptions.None;
|
||||
|
||||
// Item has not been measured yet. Get the element and store the values
|
||||
UIElement element = context.GetOrCreateElementAt(i, options);
|
||||
element.Measure(new Size(state.ColumnWidth, availableHeight));
|
||||
item.Height = element.DesiredSize.Height;
|
||||
item.Element = element;
|
||||
measured = true;
|
||||
}
|
||||
|
||||
double spacing = itemsPerColumn[columnIndex] > 0 ? MinRowSpacing : 0;
|
||||
item.Top = columnHeights[columnIndex] + spacing;
|
||||
double bottom = item.Top + item.Height;
|
||||
columnHeights[columnIndex] = bottom;
|
||||
itemsPerColumn[columnIndex]++;
|
||||
state.AddItemToColumn(item, columnIndex);
|
||||
|
||||
if (bottom < context.RealizationRect.Top)
|
||||
{
|
||||
// The bottom of the element is above the realization area
|
||||
if (item.Element is not null)
|
||||
{
|
||||
context.RecycleElement(item.Element);
|
||||
item.Element = null;
|
||||
}
|
||||
}
|
||||
else if (item.Top > context.RealizationRect.Bottom)
|
||||
{
|
||||
// The top of the element is below the realization area
|
||||
if (item.Element is not null)
|
||||
{
|
||||
context.RecycleElement(item.Element);
|
||||
item.Element = null;
|
||||
}
|
||||
|
||||
deadColumns.Add(columnIndex);
|
||||
}
|
||||
else if (measured == false)
|
||||
{
|
||||
// We ALWAYS want to measure an item that will be in the bounds
|
||||
item.Element = context.GetOrCreateElementAt(i);
|
||||
item.Element.Measure(new Size(state.ColumnWidth, availableHeight));
|
||||
if (item.Height != item.Element.DesiredSize.Height)
|
||||
{
|
||||
// this item changed size; we need to recalculate layout for everything after this
|
||||
state.RemoveFromIndex(i + 1);
|
||||
item.Height = item.Element.DesiredSize.Height;
|
||||
columnHeights[columnIndex] = item.Top + item.Height;
|
||||
}
|
||||
}
|
||||
|
||||
if (deadColumns.Count == numberOfColumns)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
double desiredHeight = state.GetHeight();
|
||||
|
||||
return new Size(availableWidth, desiredHeight);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
|
||||
{
|
||||
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
|
||||
{
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
|
||||
|
||||
// Cycle through each column and arrange the items that are within the realization bounds
|
||||
for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
|
||||
{
|
||||
UniformStaggeredColumnLayout layout = state.GetColumnLayout(columnIndex);
|
||||
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
|
||||
for (int i = 0; i < layoutSpan.Length; i++)
|
||||
{
|
||||
ref readonly UniformStaggeredItem item = ref layoutSpan[i];
|
||||
|
||||
double bottom = item.Top + item.Height;
|
||||
if (bottom < context.RealizationRect.Top)
|
||||
{
|
||||
// element is above the realization bounds
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.Top <= context.RealizationRect.Bottom)
|
||||
{
|
||||
double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex);
|
||||
|
||||
Rect bounds = new(itemHorizontalOffset, item.Top, state.ColumnWidth, item.Height);
|
||||
UIElement element = context.GetOrCreateElementAt(item.Index);
|
||||
element.Arrange(bounds);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double minColumnSpacing)
|
||||
{
|
||||
// test if the width can fit in 2 items
|
||||
if ((2 * minItemWidth) + minColumnSpacing > availableWidth)
|
||||
{
|
||||
return (1, availableWidth);
|
||||
}
|
||||
|
||||
int columnCount = Math.Max(1, (int)((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing)));
|
||||
double columnWidthAddSpacing = (availableWidth + minColumnSpacing) / columnCount;
|
||||
return (columnCount, columnWidthAddSpacing - minColumnSpacing);
|
||||
}
|
||||
|
||||
private static int GetLowestColumnIndex(in ReadOnlySpan<double> columnHeights)
|
||||
{
|
||||
int columnIndex = 0;
|
||||
double height = columnHeights[0];
|
||||
for (int j = 1; j < columnHeights.Length; j++)
|
||||
{
|
||||
if (columnHeights[j] < height)
|
||||
{
|
||||
columnIndex = j;
|
||||
height = columnHeights[j];
|
||||
}
|
||||
}
|
||||
|
||||
return columnIndex;
|
||||
}
|
||||
|
||||
private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
|
||||
panel.InvalidateMeasure();
|
||||
}
|
||||
|
||||
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
|
||||
panel.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Snap.Hutao.Control.Layout;
|
||||
|
||||
internal sealed class UniformStaggeredLayoutState
|
||||
{
|
||||
private readonly List<UniformStaggeredItem> items = [];
|
||||
private readonly VirtualizingLayoutContext context;
|
||||
private readonly Dictionary<int, UniformStaggeredColumnLayout> columnLayout = [];
|
||||
private double lastAverageHeight;
|
||||
|
||||
public UniformStaggeredLayoutState(VirtualizingLayoutContext context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public double ColumnWidth { get; internal set; }
|
||||
|
||||
public int NumberOfColumns
|
||||
{
|
||||
get => columnLayout.Count;
|
||||
}
|
||||
|
||||
public double RowSpacing { get; internal set; }
|
||||
|
||||
internal void AddItemToColumn(UniformStaggeredItem item, int columnIndex)
|
||||
{
|
||||
if (!this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout))
|
||||
{
|
||||
columnLayout = [];
|
||||
this.columnLayout[columnIndex] = columnLayout;
|
||||
}
|
||||
|
||||
if (!columnLayout.Contains(item))
|
||||
{
|
||||
columnLayout.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "CA2201")]
|
||||
internal UniformStaggeredItem GetItemAt(int index)
|
||||
{
|
||||
if (index < 0)
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
if (index <= (items.Count - 1))
|
||||
{
|
||||
return items[index];
|
||||
}
|
||||
else
|
||||
{
|
||||
UniformStaggeredItem item = new(index);
|
||||
items.Add(item);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH007")]
|
||||
internal UniformStaggeredColumnLayout GetColumnLayout(int columnIndex)
|
||||
{
|
||||
this.columnLayout.TryGetValue(columnIndex, out UniformStaggeredColumnLayout? columnLayout);
|
||||
return columnLayout!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear everything that has been calculated.
|
||||
/// </summary>
|
||||
internal void Clear()
|
||||
{
|
||||
columnLayout.Clear();
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the layout columns so they will be recalculated.
|
||||
/// </summary>
|
||||
internal void ClearColumns()
|
||||
{
|
||||
columnLayout.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the estimated height of the layout.
|
||||
/// </summary>
|
||||
/// <returns>The estimated height of the layout.</returns>
|
||||
/// <remarks>
|
||||
/// If all of the items have been calculated then the actual height will be returned.
|
||||
/// If all of the items have not been calculated then an estimated height will be calculated based on the average height of the items.
|
||||
/// </remarks>
|
||||
internal double GetHeight()
|
||||
{
|
||||
double desiredHeight = columnLayout.Values.Max(c => c.Height);
|
||||
int itemCount = columnLayout.Values.Sum(c => c.Count);
|
||||
|
||||
if (itemCount == context.ItemCount)
|
||||
{
|
||||
return desiredHeight;
|
||||
}
|
||||
|
||||
double averageHeight = 0;
|
||||
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
|
||||
{
|
||||
averageHeight += layout.Height / layout.Count;
|
||||
}
|
||||
|
||||
averageHeight /= columnLayout.Count;
|
||||
double estimatedHeight = (averageHeight * context.ItemCount) / columnLayout.Count;
|
||||
if (estimatedHeight > desiredHeight)
|
||||
{
|
||||
desiredHeight = estimatedHeight;
|
||||
}
|
||||
|
||||
if (Math.Abs(desiredHeight - lastAverageHeight) < 5)
|
||||
{
|
||||
return lastAverageHeight;
|
||||
}
|
||||
|
||||
lastAverageHeight = desiredHeight;
|
||||
return desiredHeight;
|
||||
}
|
||||
|
||||
internal void RecycleElementAt(int index)
|
||||
{
|
||||
UIElement element = context.GetOrCreateElementAt(index);
|
||||
context.RecycleElement(element);
|
||||
}
|
||||
|
||||
internal void RemoveFromIndex(int index)
|
||||
{
|
||||
if (index >= items.Count)
|
||||
{
|
||||
// Item was added/removed but we haven't realized that far yet
|
||||
return;
|
||||
}
|
||||
|
||||
int numToRemove = items.Count - index;
|
||||
items.RemoveRange(index, numToRemove);
|
||||
|
||||
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
|
||||
{
|
||||
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
|
||||
for (int i = 0; i < layoutSpan.Length; i++)
|
||||
{
|
||||
if (layoutSpan[i].Index >= index)
|
||||
{
|
||||
numToRemove = layoutSpan.Length - i;
|
||||
layout.RemoveRange(i, numToRemove);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveRange(int startIndex, int endIndex)
|
||||
{
|
||||
for (int i = startIndex; i <= endIndex; i++)
|
||||
{
|
||||
if (i > items.Count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ref readonly UniformStaggeredItem item = ref CollectionsMarshal.AsSpan(items)[i];
|
||||
item.Height = 0;
|
||||
item.Top = 0;
|
||||
|
||||
// We must recycle all elements to ensure that it gets the correct context
|
||||
RecycleElementAt(i);
|
||||
}
|
||||
|
||||
foreach ((_, UniformStaggeredColumnLayout layout) in columnLayout)
|
||||
{
|
||||
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
|
||||
for (int i = 0; i < layoutSpan.Length; i++)
|
||||
{
|
||||
if ((startIndex <= layoutSpan[i].Index) && (layoutSpan[i].Index <= endIndex))
|
||||
{
|
||||
int numToRemove = layoutSpan.Length - i;
|
||||
layout.RemoveRange(i, numToRemove);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ 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));
|
||||
|
||||
[SuppressMessage("", "IDE0052")]
|
||||
private FrameworkElement? presenter;
|
||||
|
||||
public Loading()
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
mc:Ignorable="d">
|
||||
|
||||
<cwc:SegmentedItem
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentBulletedList}}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Tag="List"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
|
||||
<cwc:SegmentedItem
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentGridView}}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Tag="Grid"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
@@ -12,8 +11,6 @@ namespace Snap.Hutao.Control.Panel;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("Current", typeof(string), List)]
|
||||
[DependencyProperty("LocalSettingKeySuffixForCurrent", typeof(string))]
|
||||
[DependencyProperty("LocalSettingKeyExtraForCurrent", typeof(string), "")]
|
||||
internal sealed partial class PanelSelector : Segmented
|
||||
{
|
||||
public const string List = nameof(List);
|
||||
@@ -45,41 +42,21 @@ internal sealed partial class PanelSelector : Segmented
|
||||
selectedIndexChangedCallbackToken = RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged);
|
||||
}
|
||||
|
||||
private static void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp)
|
||||
private void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp)
|
||||
{
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
selector.Current = IndexTypeMap[(int)selector.GetValue(dp)];
|
||||
|
||||
if (!string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent))
|
||||
{
|
||||
LocalSetting.Set(GetSettingKey(selector), selector.Current);
|
||||
}
|
||||
Current = IndexTypeMap[(int)GetValue(dp)];
|
||||
}
|
||||
|
||||
private static void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
private void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
|
||||
if (string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string value = LocalSetting.Get(GetSettingKey(selector), selector.Current);
|
||||
selector.Current = value;
|
||||
|
||||
selector.SelectedItem = selector.Items.Cast<SegmentedItem>().Single(item => (string)item.Tag == selector.Current);
|
||||
selector.SelectedItem = selector.Items.Cast<SegmentedItem>().Single(item => (string)item.Tag == Current);
|
||||
}
|
||||
|
||||
private static void OnRootUnload(object sender, RoutedEventArgs e)
|
||||
private void OnRootUnload(object sender, RoutedEventArgs e)
|
||||
{
|
||||
PanelSelector selector = (PanelSelector)sender;
|
||||
selector.UnregisterPropertyChangedCallback(SelectedIndexProperty, selector.selectedIndexChangedCallbackToken);
|
||||
selector.Unloaded -= selector.unloadedEventHandler;
|
||||
}
|
||||
|
||||
private static string GetSettingKey(PanelSelector selector)
|
||||
{
|
||||
return $"Control.PanelSelector.{selector.LocalSettingKeySuffixForCurrent}{selector.LocalSettingKeyExtraForCurrent}";
|
||||
UnregisterPropertyChangedCallback(SelectedIndexProperty, selectedIndexChangedCallbackToken);
|
||||
Loaded -= loadedEventHandler;
|
||||
Unloaded -= unloadedEventHandler;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ namespace Snap.Hutao.Control;
|
||||
[SuppressMessage("", "CA1001")]
|
||||
internal class ScopedPage : Page
|
||||
{
|
||||
// Allow GC to Collect the IServiceScope
|
||||
private static readonly WeakReference<IServiceScope> PreviousScopeReference = new(default!);
|
||||
|
||||
private readonly RoutedEventHandler unloadEventHandler;
|
||||
private readonly CancellationTokenSource viewCancellationTokenSource = new();
|
||||
private readonly IServiceScope currentScope;
|
||||
@@ -28,7 +31,22 @@ internal class ScopedPage : Page
|
||||
{
|
||||
unloadEventHandler = OnUnloaded;
|
||||
Unloaded += unloadEventHandler;
|
||||
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
|
||||
currentScope = Ioc.Default.CreateScope();
|
||||
DisposePreviousScope();
|
||||
|
||||
// track current
|
||||
PreviousScopeReference.SetTarget(currentScope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放上个范围
|
||||
/// </summary>
|
||||
public static void DisposePreviousScope()
|
||||
{
|
||||
if (PreviousScopeReference.TryGetTarget(out IServiceScope? scope))
|
||||
{
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
/// <summary>
|
||||
/// By injecting into services, we take dvantage of the fact that
|
||||
/// IServiceProvider disposes all injected services when it is disposed.
|
||||
/// </summary>
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IScopedPageScopeReferenceTracker))]
|
||||
internal sealed partial class ScopedPageScopeReferenceTracker : IScopedPageScopeReferenceTracker, IDisposable
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
private readonly WeakReference<IServiceScope> previousScopeReference = new(default!);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposePreviousScope();
|
||||
}
|
||||
|
||||
public IServiceScope CreateScope()
|
||||
{
|
||||
IServiceScope currentScope = serviceProvider.CreateScope();
|
||||
|
||||
// In case previous one is not disposed.
|
||||
DisposePreviousScope();
|
||||
previousScopeReference.SetTarget(currentScope);
|
||||
return currentScope;
|
||||
}
|
||||
|
||||
private void DisposePreviousScope()
|
||||
{
|
||||
if (previousScopeReference.TryGetTarget(out IServiceScope? scope))
|
||||
{
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<CornerRadius x:Key="ControlCornerRadiusTop">4,4,0,0</CornerRadius>
|
||||
<CornerRadius x:Key="ControlCornerRadiusBottom">0,0,4,4</CornerRadius>
|
||||
<CornerRadius x:Key="ControlCornerRadiusTopRightAndBottomLeft">0,4,0,4</CornerRadius>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Style
|
||||
x:Key="WebViewerFlyoutPresenterStyle"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="FlyoutPresenterPadding0And2Style"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="0,2"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="FlyoutPresenterPadding6Style"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="6"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -3,18 +3,8 @@
|
||||
<x:String x:Key="FontIconContentSetting"></x:String>
|
||||
<x:String x:Key="FontIconContentRefresh"></x:String>
|
||||
<x:String x:Key="FontIconContentDelete"></x:String>
|
||||
<x:String x:Key="FontIconContentChevronRight"></x:String>
|
||||
<x:String x:Key="FontIconContentWarning"></x:String>
|
||||
<x:String x:Key="FontIconContentGame"></x:String>
|
||||
<x:String x:Key="FontIconContentOpenInNewWindow"></x:String>
|
||||
<x:String x:Key="FontIconContentFolder"></x:String>
|
||||
<x:String x:Key="FontIconContentCopy"></x:String>
|
||||
<x:String x:Key="FontIconContentBulletedList"></x:String>
|
||||
<x:String x:Key="FontIconContentCheckList"></x:String>
|
||||
<x:String x:Key="FontIconContentWebsite"></x:String>
|
||||
<x:String x:Key="FontIconContentHomeGroup"></x:String>
|
||||
<x:String x:Key="FontIconContentAsteriskBadge12"></x:String>
|
||||
<x:String x:Key="FontIconContentZipFolder"></x:String>
|
||||
<x:String x:Key="FontIconContentGridView"></x:String>
|
||||
<x:String x:Key="FontIconContentGiftboxOpen"></x:String>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -11,28 +11,13 @@
|
||||
<ItemsPanelTemplate x:Key="WrapPanelSpacing4Template">
|
||||
<cwcont:WrapPanel HorizontalSpacing="4" VerticalSpacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing0Template">
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelTemplate">
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing2Template">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
|
||||
<cwcont:UniformGrid
|
||||
ColumnSpacing="2"
|
||||
Columns="2"
|
||||
RowSpacing="2"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="UniformGridColumns5Spacing4Template">
|
||||
<cwcont:UniformGrid
|
||||
ColumnSpacing="4"
|
||||
Columns="5"
|
||||
RowSpacing="4"/>
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="UniformGridColumns5Spacing8Template">
|
||||
<cwcont:UniformGrid
|
||||
ColumnSpacing="8"
|
||||
Columns="5"
|
||||
RowSpacing="8"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
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">
|
||||
<Style x:Key="TwoPanelScrollViewerStyle" TargetType="ScrollViewer">
|
||||
<Setter Property="HorizontalScrollMode" Value="Auto"/>
|
||||
<Setter Property="VerticalScrollMode" Value="Auto"/>
|
||||
<Setter Property="IsHorizontalRailEnabled" Value="True"/>
|
||||
<Setter Property="IsVerticalRailEnabled" Value="True"/>
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="ZoomMode" Value="Disabled"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Top"/>
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Visible"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollViewer">
|
||||
<Border
|
||||
x:Name="Root"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid Background="{TemplateBinding Background}">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="{TemplateBinding Padding}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MaxWidth="{Binding Path=(shch:ScrollViewerHelper.LeftPanelMaxWidth), RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ScrollContentPresenter x:Name="ScrollContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}"/>
|
||||
<ContentPresenter Grid.Column="1" Content="{Binding Path=(shch:ScrollViewerHelper.RightPanel), RelativeSource={RelativeSource Mode=TemplatedParent}}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.RowSpan="2" Grid.ColumnSpan="2"/>
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
Padding="{ThemeResource ScrollViewerScrollBarMargin}"
|
||||
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}">
|
||||
<ScrollBar
|
||||
x:Name="VerticalScrollBar"
|
||||
HorizontalAlignment="Right"
|
||||
IsTabStop="False"
|
||||
Maximum="{TemplateBinding ScrollableHeight}"
|
||||
Orientation="Vertical"
|
||||
ViewportSize="{TemplateBinding ViewportHeight}"
|
||||
Value="{TemplateBinding VerticalOffset}"/>
|
||||
</Grid>
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Padding="{ThemeResource ScrollViewerScrollBarMargin}"
|
||||
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}">
|
||||
<ScrollBar
|
||||
x:Name="HorizontalScrollBar"
|
||||
IsTabStop="False"
|
||||
Maximum="{TemplateBinding ScrollableWidth}"
|
||||
Orientation="Horizontal"
|
||||
ViewportSize="{TemplateBinding ViewportWidth}"
|
||||
Value="{TemplateBinding HorizontalOffset}"/>
|
||||
</Grid>
|
||||
<Border
|
||||
x:Name="ScrollBarSeparator"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Background="{ThemeResource ScrollViewerScrollBarSeparatorBackground}"
|
||||
Opacity="0"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="ScrollingIndicatorStates">
|
||||
|
||||
<VisualStateGroup.Transitions>
|
||||
<VisualTransition From="MouseIndicator" To="NoIndicator">
|
||||
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualTransition>
|
||||
<VisualTransition From="MouseIndicatorFull" To="NoIndicator">
|
||||
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualTransition>
|
||||
<VisualTransition From="MouseIndicatorFull" To="MouseIndicator">
|
||||
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="{ThemeResource ScrollViewerSeparatorContractDelay}">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualTransition>
|
||||
<VisualTransition From="TouchIndicator" To="NoIndicator">
|
||||
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="0:0:0.5">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="0:0:0.5">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>None</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualTransition>
|
||||
</VisualStateGroup.Transitions>
|
||||
<VisualState x:Name="NoIndicator"/>
|
||||
<VisualState x:Name="TouchIndicator">
|
||||
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>TouchIndicator</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>TouchIndicator</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="MouseIndicator">
|
||||
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="MouseIndicatorFull">
|
||||
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="IndicatorMode">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<ScrollingIndicatorMode>MouseIndicator</ScrollingIndicatorMode>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="ScrollBarSeparatorStates">
|
||||
|
||||
<VisualStateGroup.Transitions>
|
||||
<VisualTransition From="ScrollBarSeparatorExpanded" To="ScrollBarSeparatorCollapsed">
|
||||
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
BeginTime="{ThemeResource ScrollViewerSeparatorContractBeginTime}"
|
||||
Storyboard.TargetName="ScrollBarSeparator"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="0"
|
||||
Duration="{ThemeResource ScrollViewerSeparatorContractDuration}"/>
|
||||
</Storyboard>
|
||||
</VisualTransition>
|
||||
</VisualStateGroup.Transitions>
|
||||
<VisualState x:Name="ScrollBarSeparatorCollapsed"/>
|
||||
<VisualState x:Name="ScrollBarSeparatorExpanded">
|
||||
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
BeginTime="{ThemeResource ScrollViewerSeparatorExpandBeginTime}"
|
||||
Storyboard.TargetName="ScrollBarSeparator"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="{ThemeResource ScrollViewerSeparatorExpandDuration}"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="ScrollBarSeparatorExpandedWithoutAnimation">
|
||||
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
BeginTime="{ThemeResource ScrollViewerSeparatorExpandBeginTime}"
|
||||
Storyboard.TargetName="ScrollBarSeparator"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="0"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
<VisualState x:Name="ScrollBarSeparatorCollapsedWithoutAnimation">
|
||||
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
BeginTime="{ThemeResource ScrollViewerSeparatorContractBeginTime}"
|
||||
Storyboard.TargetName="ScrollBarSeparator"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="0"
|
||||
Duration="0"/>
|
||||
</Storyboard>
|
||||
</VisualState>
|
||||
|
||||
</VisualStateGroup>
|
||||
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Border>
|
||||
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -4,9 +4,6 @@
|
||||
<x:Double x:Key="SettingsCardMinHeight">0</x:Double>
|
||||
<x:Double x:Key="SettingsCardWrapThreshold">0</x:Double>
|
||||
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
|
||||
|
||||
<x:Double x:Key="SettingsCardContentControlMinWidth">120</x:Double>
|
||||
|
||||
<Style
|
||||
x:Key="SettingsSectionHeaderTextBlockStyle"
|
||||
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
|
||||
@@ -19,17 +16,16 @@
|
||||
x:Key="SettingsContentComboBoxStyle"
|
||||
BasedOn="{StaticResource DefaultComboBoxStyle}"
|
||||
TargetType="ComboBox">
|
||||
<Setter Property="MinWidth" Value="{ThemeResource SettingsCardContentControlMinWidth}"/>
|
||||
<Setter Property="MinWidth" Value="120"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="SettingButtonStyle"
|
||||
BasedOn="{StaticResource DefaultButtonStyle}"
|
||||
TargetType="Button">
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardBorderBrush}"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
<Setter Property="Padding" Value="16,6,16,6"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="MinWidth" Value="{ThemeResource SettingsCardContentControlMinWidth}"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -14,10 +14,4 @@
|
||||
<TransitionCollection x:Key="ReorderThemeTransitions">
|
||||
<ReorderThemeTransition/>
|
||||
</TransitionCollection>
|
||||
<TransitionCollection x:Key="RepositionThemeTransitions">
|
||||
<RepositionThemeTransition/>
|
||||
</TransitionCollection>
|
||||
<TransitionCollection x:Key="NavigationThemeTransitions">
|
||||
<NavigationThemeTransition/>
|
||||
</TransitionCollection>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -15,7 +15,22 @@ internal abstract class ValueConverter<TFrom, TTo> : IValueConverter
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
#if DEBUG
|
||||
try
|
||||
{
|
||||
return Convert((TFrom)value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
|
||||
.LogError(ex, "值转换器异常");
|
||||
|
||||
throw;
|
||||
}
|
||||
#else
|
||||
return Convert((TFrom)value);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
@@ -25,16 +24,17 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
{
|
||||
private const string CacheFolderName = nameof(ImageCache);
|
||||
|
||||
private static readonly FrozenDictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
|
||||
// TODO: use FrozenDictionary
|
||||
private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new()
|
||||
{
|
||||
[0] = TimeSpan.FromSeconds(4),
|
||||
[1] = TimeSpan.FromSeconds(16),
|
||||
[2] = TimeSpan.FromSeconds(64),
|
||||
}.ToFrozenDictionary();
|
||||
};
|
||||
|
||||
private readonly ILogger logger;
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ILogger<ImageCache> logger;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
|
||||
@@ -62,7 +62,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// <inheritdoc/>
|
||||
public void Remove(Uri uriForCachedItem)
|
||||
{
|
||||
Remove(new ReadOnlySpan<Uri>(ref uriForCachedItem));
|
||||
Remove(new ReadOnlySpan<Uri>(uriForCachedItem));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -76,7 +76,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
string folder = GetCacheFolder();
|
||||
string[] files = Directory.GetFiles(folder);
|
||||
|
||||
List<string> filesToDelete = [];
|
||||
List<string> filesToDelete = new();
|
||||
foreach (ref readonly Uri uri in uriForCachedItems)
|
||||
{
|
||||
string filePath = Path.Combine(folder, GetCacheFileName(uri));
|
||||
@@ -125,7 +125,7 @@ internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// <inheritdoc/>
|
||||
public ValueFile GetFileFromCategoryAndName(string category, string fileName)
|
||||
{
|
||||
Uri dummyUri = Web.HutaoEndpoints.StaticRaw(category, fileName).ToUri();
|
||||
Uri dummyUri = Web.HutaoEndpoints.StaticFile(category, fileName).ToUri();
|
||||
return Path.Combine(GetCacheFolder(), GetCacheFileName(dummyUri));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ namespace Snap.Hutao.Core;
|
||||
internal sealed class CommandLineBuilder
|
||||
{
|
||||
private const char WhiteSpace = ' ';
|
||||
|
||||
private readonly Dictionary<string, string?> options = [];
|
||||
private readonly Dictionary<string, string?> options = new();
|
||||
|
||||
/// <summary>
|
||||
/// 当符合条件时添加参数
|
||||
|
||||
@@ -38,11 +38,7 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceProvider.IsDisposedSlow())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Troubeshooting why the serviceProvider will NRE
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
@@ -96,11 +92,7 @@ internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceProvider.IsDisposedSlow())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Troubeshooting why the serviceProvider will NRE
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
@@ -3,12 +3,6 @@
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 由于 AddHttpClient 不支持 KeyedService, 所以使用工厂模式
|
||||
/// </summary>
|
||||
/// <typeparam name="TClient">抽象类型</typeparam>
|
||||
/// <typeparam name="TClientCN">官服/米游社类型</typeparam>
|
||||
/// <typeparam name="TClientOS">国际/HoYoLAB类型</typeparam>
|
||||
internal abstract class OverseaSupportFactory<TClient, TClientCN, TClientOS> : IOverseaSupportFactory<TClient>
|
||||
where TClientCN : notnull, TClient
|
||||
where TClientOS : notnull, TClient
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Service;
|
||||
using System.Globalization;
|
||||
@@ -35,6 +36,7 @@ internal static class DependencyInjection
|
||||
|
||||
// Discrete services
|
||||
.AddSingleton<IMessenger, WeakReferenceMessenger>()
|
||||
|
||||
.BuildServiceProvider(true);
|
||||
|
||||
Ioc.Default.ConfigureServices(serviceProvider);
|
||||
@@ -54,9 +56,6 @@ internal static class DependencyInjection
|
||||
|
||||
CultureInfo.CurrentCulture = cultureInfo;
|
||||
CultureInfo.CurrentUICulture = cultureInfo;
|
||||
|
||||
ApplicationLanguages.PrimaryLanguageOverride = cultureInfo.Name;
|
||||
|
||||
SH.Culture = cultureInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 服务集合扩展
|
||||
/// </summary>
|
||||
internal static class EnumerableServiceExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 选择对应的服务
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">服务类型</typeparam>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="isOversea">是否为海外服/Hoyolab</param>
|
||||
/// <returns>对应的服务</returns>
|
||||
[Obsolete("该方法会导致不必要的服务实例化")]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static TService Pick<TService>(this IEnumerable<TService> services, bool isOversea)
|
||||
where TService : IOverseaSupport
|
||||
{
|
||||
return services.Single(s => s.IsOversea == isOversea);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 选择对应的服务
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">服务类型</typeparam>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="isOversea">是否为海外服/Hoyolab</param>
|
||||
/// <returns>对应的服务</returns>
|
||||
[Obsolete("该方法会导致不必要的服务实例化")]
|
||||
public static TService PickRequiredService<TService>(this IServiceProvider serviceProvider, bool isOversea)
|
||||
where TService : IOverseaSupport
|
||||
{
|
||||
return serviceProvider.GetRequiredService<IEnumerable<TService>>().Pick(isOversea);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥1的客户端使用此配置
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void XRpcConfiguration(HttpClient client)
|
||||
@@ -50,7 +50,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥2的客户端使用此配置
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void XRpc2Configuration(HttpClient client)
|
||||
@@ -64,11 +64,11 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥1的客户端使用此配置
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// HoYoLAB app
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
@@ -84,10 +84,11 @@ internal static partial class IocHttpClientConfiguration
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥2的客户端使用此配置
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// HoYoLAB web
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
[SuppressMessage("", "IDE0051")]
|
||||
private static void XRpc4Configuration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
|
||||
@@ -16,15 +16,4 @@ internal static class ServiceProviderExtension
|
||||
{
|
||||
return ActivatorUtilities.CreateInstance<T>(serviceProvider, parameters);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsDisposedSlow(this IServiceProvider? serviceProvider)
|
||||
{
|
||||
if (serviceProvider is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return serviceProvider.GetType().GetField("_disposed")?.GetValue(serviceProvider) is true;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ internal sealed class DatabaseCorruptedException : Exception
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="innerException">内部错误</param>
|
||||
public DatabaseCorruptedException(string message, Exception? innerException)
|
||||
: base(SH.FormatCoreExceptionServiceDatabaseCorruptedMessage($"{message}\n{innerException?.Message}"), innerException)
|
||||
: base(SH.CoreExceptionServiceDatabaseCorruptedMessage.Format($"{message}\n{innerException?.Message}"), innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ internal sealed partial class ExceptionRecorder
|
||||
app.DebugSettings.XamlResourceReferenceFailed += OnXamlResourceReferenceFailed;
|
||||
}
|
||||
|
||||
[SuppressMessage("", "CA2012")]
|
||||
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
ValueTask<string?> task = serviceProvider
|
||||
|
||||
@@ -16,7 +16,7 @@ internal sealed class RuntimeEnvironmentException : Exception
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="innerException">内部错误</param>
|
||||
public RuntimeEnvironmentException(string message, Exception? innerException)
|
||||
: base($"{message}\n{innerException?.Message}", innerException)
|
||||
: base($"{message}\n{innerException.Message}", innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ internal sealed class UserdataCorruptedException : Exception
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="innerException">内部错误</param>
|
||||
public UserdataCorruptedException(string message, Exception? innerException)
|
||||
: base(SH.FormatCoreExceptionServiceUserdataCorruptedMessage($"{message}\n{innerException?.Message}"), innerException)
|
||||
: base(SH.CoreExceptionServiceUserdataCorruptedMessage.Format($"{message}\n{innerException?.Message}"), innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,12 @@ using Windows.Storage.Streams;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.DataTransfer;
|
||||
|
||||
/// <summary>
|
||||
/// 剪贴板互操作
|
||||
/// </summary>
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Transient, typeof(IClipboardProvider))]
|
||||
internal sealed partial class ClipboardProvider : IClipboardProvider
|
||||
[Injection(InjectAs.Transient, typeof(IClipboardInterop))]
|
||||
internal sealed partial class ClipboardInterop : IClipboardInterop
|
||||
{
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ITaskContext taskContext;
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.IO.DataTransfer;
|
||||
/// <summary>
|
||||
/// 剪贴板互操作
|
||||
/// </summary>
|
||||
internal interface IClipboardProvider
|
||||
internal interface IClipboardInterop
|
||||
{
|
||||
/// <summary>
|
||||
/// 从剪贴板文本中反序列化
|
||||
@@ -18,7 +18,7 @@ internal static class IniSerializer
|
||||
/// <returns>Ini 元素集合</returns>
|
||||
public static List<IniElement> Deserialize(FileStream fileStream)
|
||||
{
|
||||
List<IniElement> results = [];
|
||||
List<IniElement> results = new();
|
||||
using (StreamReader reader = new(fileStream))
|
||||
{
|
||||
while (reader.ReadLine() is { } line)
|
||||
|
||||
104
src/Snap.Hutao/Snap.Hutao/Core/IO/PickerExtension.cs
Normal file
104
src/Snap.Hutao/Snap.Hutao/Core/IO/PickerExtension.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Pickers;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 选择器拓展
|
||||
/// </summary>
|
||||
internal static class PickerExtension
|
||||
{
|
||||
/// <inheritdoc cref="FileOpenPicker.PickSingleFileAsync"/>
|
||||
public static async ValueTask<ValueResult<bool, ValueFile>> TryPickSingleFileAsync(this FileOpenPicker picker)
|
||||
{
|
||||
StorageFile? file;
|
||||
Exception? exception = null;
|
||||
try
|
||||
{
|
||||
file = await picker.PickSingleFileAsync().AsTask().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
file = null;
|
||||
}
|
||||
|
||||
if (file is not null)
|
||||
{
|
||||
return new(true, file.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
InfoBarWaringPickerException(exception);
|
||||
return new(false, default!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="FileSavePicker.PickSaveFileAsync"/>
|
||||
public static async ValueTask<ValueResult<bool, ValueFile>> TryPickSaveFileAsync(this FileSavePicker picker)
|
||||
{
|
||||
StorageFile? file;
|
||||
Exception? exception = null;
|
||||
try
|
||||
{
|
||||
file = await picker.PickSaveFileAsync().AsTask().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
file = null;
|
||||
}
|
||||
|
||||
if (file is not null)
|
||||
{
|
||||
return new(true, file.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
InfoBarWaringPickerException(exception);
|
||||
return new(false, default!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="FolderPicker.PickSingleFolderAsync"/>
|
||||
public static async ValueTask<ValueResult<bool, string>> TryPickSingleFolderAsync(this FolderPicker picker)
|
||||
{
|
||||
StorageFolder? folder;
|
||||
Exception? exception = null;
|
||||
try
|
||||
{
|
||||
folder = await picker.PickSingleFolderAsync().AsTask().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
folder = null;
|
||||
}
|
||||
|
||||
if (folder is not null)
|
||||
{
|
||||
return new(true, folder.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
InfoBarWaringPickerException(exception);
|
||||
return new(false, default!);
|
||||
}
|
||||
}
|
||||
|
||||
private static void InfoBarWaringPickerException(Exception? exception)
|
||||
{
|
||||
if (exception is not null)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<IInfoBarService>()
|
||||
.Warning(
|
||||
SH.CoreIOPickerExtensionPickerExceptionInfoBarTitle,
|
||||
SH.CoreIOPickerExtensionPickerExceptionInfoBarMessage.Format(exception.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ namespace Snap.Hutao.Core.Json.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// 实现日期的转换
|
||||
/// 此转换器无法实现无损往返
|
||||
/// 必须在反序列化后调整 Offset
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
|
||||
@@ -20,10 +18,7 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
if (reader.GetString() is { } dataTimeString)
|
||||
{
|
||||
// By doing so, the DateTimeOffset parsed out will be a
|
||||
// no offset datetime, and need to be adjusted later
|
||||
DateTime dateTime = DateTime.ParseExact(dataTimeString, Format, CultureInfo.InvariantCulture);
|
||||
return new DateTimeOffset(dateTime, default);
|
||||
return DateTimeOffset.ParseExact(dataTimeString, Format, CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
return default;
|
||||
@@ -32,6 +27,6 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.DateTime.ToString(Format, CultureInfo.InvariantCulture));
|
||||
writer.WriteStringValue(value.ToString(Format, CultureInfo.CurrentCulture));
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,8 @@ internal sealed class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEn
|
||||
|
||||
private static IEnumerable<int> EnumerateNumbers(string source)
|
||||
{
|
||||
foreach (StringSegment id in new StringTokenizer(source, [Comma]))
|
||||
// TODO: Use Collection Literals
|
||||
foreach (StringSegment id in new StringTokenizer(source, new[] { Comma }))
|
||||
{
|
||||
yield return int.Parse(id.AsSpan(), CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ internal sealed partial class Activation : IActivation
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
serviceProvider.GetRequiredService<MainWindow>();
|
||||
currentWindowReference.Window = serviceProvider.GetRequiredService<MainWindow>();
|
||||
|
||||
serviceProvider
|
||||
.GetRequiredService<IMetadataService>()
|
||||
@@ -270,7 +270,7 @@ internal sealed partial class Activation : IActivation
|
||||
|
||||
if (currentWindowReference.Window is null)
|
||||
{
|
||||
serviceProvider.GetRequiredService<LaunchGameWindow>();
|
||||
currentWindowReference.Window = serviceProvider.GetRequiredService<LaunchGameWindow>();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -37,9 +37,10 @@ internal static class AppInstanceExtension
|
||||
SetEvent(redirectEventHandle);
|
||||
});
|
||||
|
||||
ReadOnlySpan<HANDLE> handles = new(ref redirectEventHandle);
|
||||
ReadOnlySpan<HANDLE> handles = new(redirectEventHandle);
|
||||
CoWaitForMultipleObjects((uint)CWMO_FLAGS.CWMO_DEFAULT, INFINITE, handles, out uint _);
|
||||
CloseHandle(redirectEventHandle);
|
||||
|
||||
// TODO: Release handle
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH007")]
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Windows.Win32.Foundation;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
internal static class CurrentWindowReferenceExtension
|
||||
{
|
||||
public static XamlRoot GetXamlRoot(this ICurrentWindowReference reference)
|
||||
{
|
||||
return reference.Window.Content.XamlRoot;
|
||||
}
|
||||
|
||||
public static HWND GetWindowHandle(this ICurrentWindowReference reference)
|
||||
{
|
||||
return reference.Window is IWindowOptionsSource optionsSource
|
||||
? optionsSource.WindowOptions.Hwnd
|
||||
: (HWND)WindowNative.GetWindowHandle(reference.Window);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,5 @@ namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
internal interface ICurrentWindowReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Only set in WindowController
|
||||
/// </summary>
|
||||
public Window Window { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
internal static class Random
|
||||
{
|
||||
public static string GetLowerHexString(int length)
|
||||
{
|
||||
return new(System.Random.Shared.GetItems("0123456789abcdef".AsSpan(), length));
|
||||
}
|
||||
|
||||
public static string GetUpperAndNumberString(int length)
|
||||
{
|
||||
return new(System.Random.Shared.GetItems("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".AsSpan(), length));
|
||||
}
|
||||
|
||||
public static string GetLowerAndNumberString(int length)
|
||||
{
|
||||
return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyz".AsSpan(), length));
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,4 @@ internal static class SettingKeys
|
||||
public const string IsHomeCardGachaStatisticsPresented = "IsHomeCardGachaStatisticsPresented";
|
||||
public const string IsHomeCardAchievementPresented = "IsHomeCardAchievementPresented";
|
||||
public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented";
|
||||
|
||||
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
|
||||
}
|
||||
@@ -2,14 +2,11 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
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;
|
||||
|
||||
namespace Snap.Hutao.Core.Shell;
|
||||
|
||||
@@ -40,17 +37,16 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT result = CoCreateInstance<ShellLink, IShellLinkW>(null, CLSCTX.CLSCTX_INPROC_SERVER, out IShellLinkW shellLink);
|
||||
Marshal.ThrowExceptionForHR(result);
|
||||
|
||||
IShellLinkW shellLink = (IShellLinkW)new ShellLink();
|
||||
shellLink.SetPath("powershell");
|
||||
shellLink.SetArguments($"""
|
||||
-Command "Start-Process shell:AppsFolder\{runtimeOptions.FamilyName}!App -verb runas"
|
||||
""");
|
||||
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_SHOWMINNOACTIVE);
|
||||
|
||||
shellLink.SetIconLocation(targetLogoPath, 0);
|
||||
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");
|
||||
string target = Path.Combine(desktop, $"{SH.AppNameAndVersion.Format(runtimeOptions.Version)}.lnk");
|
||||
|
||||
IPersistFile persistFile = (IPersistFile)shellLink;
|
||||
try
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user