Compare commits

..

9 Commits

Author SHA1 Message Date
DismissedLight
7c03ce3486 1.7.11 hotfix package 2023-10-18 19:54:55 +08:00
DismissedLight
83e187ea9e fix launch args 2023-10-18 19:54:55 +08:00
Lightczx
d86232f413 fix #1028 2023-10-18 19:54:55 +08:00
Masterain
4e6691ac51 Update FUNDING.yml 2023-10-18 19:54:55 +08:00
Masterain
84ad39b192 Update FUNDING.yml 2023-10-18 19:54:55 +08:00
DismissedLight
ce50fc41e0 1.7.10 package 2023-10-17 22:14:50 +08:00
Masterain
1d71048f56 New Crowdin updates (#1019) 2023-10-17 22:14:50 +08:00
DismissedLight
08cf823156 auto select existed account when detecting 2023-10-17 22:14:50 +08:00
Lightczx
cca65635a6 investigate into CoreWindow 2023-10-17 14:52:38 +08:00
489 changed files with 14176 additions and 13696 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,61 +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
- [ ] 更新日志
- [ ] 功能文档更新
## 发布版本 [半自动]
- [ ] 在 GitHub 个人设置中更新 [Publish-Automate Beta PAT](https://github.com/settings/tokens?type=beta),有效期需小于预计发版需要天数
- [ ] 将更新的 PAT 更新至 Publish-Automate 库的 [Actions Secrets](https://github.com/DGP-Studio/Publish-Automate/settings/secrets/actions) 中
***
- [ ] 运行 [Auto Publish Action](https://github.com/DGP-Studio/Publish-Automate/actions/workflows/auto-publish.yml)
- [ ] 在 https://store.rg-adguard.net/ 下载新版本安装包
- [ ] Store URL: https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52
- [ ] 命名格式为 `Snap.Hutao x.x.x.msix`
- [ ] Merge 文档 PR
- [ ] 发布 Release
- [ ] 更新日志格式(以 1.6.2 版本为例)
```jsx
## Update log
https://hut.ao/en/statements/update-log.html#_1-6-2
## 更新日志
[此处从文档复制]
## What's Changed
**Full Changelog**: https://github.com/DGP-Studio/Snap.Hutao/compare/1.6.0...1.6.2
```
- [ ] 通知用户
- 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View 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
View File

@@ -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/

View File

@@ -1,5 +1,4 @@
![Banner2-large](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/742129f4-f903-4d16-bf1e-3cbfad873ee4)
![](res/HutaoRepoBanner2.png)
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新 功能相结合,它提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
@@ -48,4 +47,4 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
## 开发 / Development
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)
[![Star History Chart](https://api.star-history.com/svg?repos=DGP-Studio/Snap.Hutao&type=Date)](https://star-history.com/#DGP-Studio/Snap.Hutao&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=DGP-Studio/Snap.Hutao&type=Date)](https://star-history.com/#DGP-Studio/Snap.Hutao&Date)

View File

@@ -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
View 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

View File

@@ -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]
#### 命名样式 ####

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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()))

View File

@@ -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());
}

View File

@@ -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);
}
}
""");

View File

@@ -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)
{

View File

@@ -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;
}
}

View File

@@ -20,7 +20,7 @@ internal static class EnumerableExtension
if (enumerator.MoveNext())
{
HashSet<TKey> set = [];
HashSet<TKey> set = new();
do
{

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -7,13 +7,20 @@
<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.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
</ItemGroup>
</Project>

View File

@@ -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);

View File

@@ -1,51 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public class CollectionsMarshalTest
{
[TestMethod]
public void DictionaryMarshalGetValueRefOrNullRefIsNullRef()
{
#if NET8_0_OR_GREATER
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
Dictionary<string, string> dictionaryRefKeyRefValue = [];
#else
Dictionary<uint, string> dictionaryValueKeyRefValue = new();
Dictionary<uint, uint> dictionaryValueKeyValueValue = new();
Dictionary<string, uint> dictionaryRefKeyValueValue = new();
Dictionary<string, string> dictionaryRefKeyRefValue = new();
#endif
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyRefValue, 1U)));
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyValueValue, 1U)));
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyValueValue, "no such key")));
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryRefKeyRefValue, "no such key")));
}
[TestMethod]
public void DictionaryMarshalGetValueRefOrAddDefaultIsDefault()
{
#if NET8_0_OR_GREATER
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
Dictionary<string, string> dictionaryRefKeyRefValue = [];
#else
Dictionary<uint, string> dictionaryValueKeyRefValue = new();
Dictionary<uint, uint> dictionaryValueKeyValueValue = new();
Dictionary<string, uint> dictionaryRefKeyValueValue = new();
Dictionary<string, string> dictionaryRefKeyRefValue = new();
#endif
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyRefValue, 1U, out _) == default);
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyValueValue, 1U, out _) == default);
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyValueValue, "no such key", out _) == default);
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryRefKeyRefValue, "no such key", out _) == default);
}
}

View File

@@ -1,7 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using System;
namespace Snap.Hutao.Test.PlatformExtensions;
namespace Snap.Hutao.Test;
[TestClass]
public sealed class DependencyInjectionTest

View File

@@ -1,217 +0,0 @@
using System;
using System.Buffers.Binary;
using System.Globalization;
using System.Text;
namespace Snap.Hutao.Test.IncomingFeature;
[TestClass]
public sealed class GeniusInvokationDecoding
{
public TestContext? TestContext { get; set; }
/// <summary>
/// https://www.bilibili.com/video/av278125720
/// </summary>
[TestMethod]
public unsafe void GeniusInvokationShareCodeDecoding()
{
// 51 bytes obfuscated data
byte[] bytes = Convert.FromBase64String("BCHBwxQNAYERyVANCJGBynkOCZER2pgOCrFx8poQChGR9bYQDEGB9rkQDFKRD7oRDeEB");
// ---------------------------------------------
// | Data | Caesar Cipher Key |
// |----------|-------------------|
// | 50 Bytes | 1 Byte |
// ---------------------------------------------
// Data:
// 00000100 00100001 11000001 11000011 00010100
// 00001101 00000001 10000001 00010001 11001001
// 01010000 00001101 00001000 10010001 10000001
// 11001010 01111001 00001110 00001001 10010001
// 00010001 11011010 10011000 00001110 00001010
// 10110001 01110001 11110010 10011010 00010000
// 00001010 00010001 10010001 11110101 10110110
// 00010000 00001100 01000001 10000001 11110110
// 10111001 00010000 00001100 01010010 10010001
// 00001111 10111010 00010001 00001101 11100001
// ---------------------------------------------
// Caesar Cipher Key:
// 00000001
// ---------------------------------------------
fixed (byte* ptr = bytes)
{
// Reinterpret as 50 byte actual data and 1 deobfuscate key byte
EncryptedDataAndKey* data = (EncryptedDataAndKey*)ptr;
byte* dataPtr = data->Data;
// ----------------------------------------------------------
// | First | Second | Padding |
// |-----------|----------|---------|
// | 25 Bytes | 25 Bytes | 1 Byte |
// ----------------------------------------------------------
// We are doing two things here:
// 1. Retrieve actual data by subtracting key
// 2. Store data into two halves by alternating between them
// ----------------------------------------------------------
// What we will get after this step:
// ----------------------------------------------------------
// First:
// 00000011 11000000 00010011 00000000 00010000
// 01001111 00000111 10000000 01111000 00001000
// 00010000 10010111 00001001 01110000 10011001
// 00001001 10010000 10110101 00001011 10000000
// 10111000 00001011 10010000 10111001 00001100
// ----------------------------------------------------------
// Second:
// 00100000 11000010 00001100 10000000 11001000
// 00001100 10010000 11001001 00001101 10010000
// 11011001 00001101 10110000 11110001 00001111
// 00010000 11110100 00001111 01000000 11110101
// 00001111 01010001 00001110 00010000 11100000
// ----------------------------------------------------------
RearrangeBuffer rearranged = default;
byte* pFirst = rearranged.First;
byte* pSecond = rearranged.Second;
for (int i = 0; i < 50; i++)
{
// Determine which half are we going to insert
byte** ppTarget = i % 2 == 0 ? &pFirst : &pSecond;
// (actual data = data - key) and store it directly to the target half
**ppTarget = unchecked((byte)(dataPtr[i] - data->Key));
(*ppTarget)++;
}
// Prepare decoded data result storage
DecryptedData decoded = default;
ushort* pDecoded = decoded.Data;
// ----------------------------------------------------------
// | Data |
// |----------| x 17 = 51 Bytes
// | 3 Bytes |
// ----------------------------------------------------------
// Grouping each 3 bytes and read out as 2 ushort with
// 12 bits each (Big Endian)
// ----------------------------------------------------------
// 00000011 1100·0000 00010011|
// 00000000 0001·0000 01001111|
// 00000111 1000·0000 01111000|
// 00001000 0001·0000 10010111|
// 00001001 0111·0000 10011001|
// 00001001 1001·0000 10110101|
// 00001011 1000·0000 10111000|
// 00001011 1001·0000 10111001|
// 00001100 0010·0000 11000010|
// 00001100 1000·0000 11001000|
// 00001100 1001·0000 11001001|
// 00001101 1001·0000 11011001|
// 00001101 1011·0000 11110001|
// 00001111 0001·0000 11110100|
// 00001111 0100·0000 11110101|
// 00001111 0101·0001 00001110|
// 00010000 1110·0000 -padding|[padding32]
// ----------------------------------------------------------
// reinterpret as DecodeGroupingHelper for each 3 bytes
DecodeGroupingHelper* pGroup = (DecodeGroupingHelper*)&rearranged;
for (int i = 0; i < 17; i++)
{
(ushort first, ushort second) = pGroup->GetData();
*pDecoded = first;
*(pDecoded + 1) = second;
pDecoded += 2;
pGroup++;
}
// Now we get
// 60, 19, 1,
// 79,120,120,
// 129,151,151,
// 153,153,181,
// 184,184,185,
// 185,194,194,
// 200,200,201,
// 201,217,217,
// 219,241,241,
// 244,244,245,
// 245,270,270,
StringBuilder stringBuilder = new();
for (int i = 0; i < 33; i++)
{
stringBuilder
.AppendFormat(CultureInfo.InvariantCulture, "{0,3}", decoded.Data[i])
.Append(',');
if (i % 11 == 10)
{
stringBuilder.Append('\n');
}
}
TestContext?.WriteLine(stringBuilder.ToString(0, stringBuilder.Length - 1));
ushort[] resultArray = new ushort[33];
Span<ushort> result = new((ushort*)&decoded, 33);
result.CopyTo(resultArray);
ushort[] testKnownResult =
#if NET8_0_OR_GREATER
[
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
];
#else
{
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
};
#endif
CollectionAssert.AreEqual(resultArray, testKnownResult);
}
}
private struct EncryptedDataAndKey
{
public unsafe fixed byte Data[50];
public byte Key;
}
private struct RearrangeBuffer
{
public unsafe fixed byte First[25];
public unsafe fixed byte Second[25];
// Make it 51 bytes
// allow to be group as 17 DecodeGroupingHelper later
public byte padding;
// prevent accidently int32 cast access violation
public byte paddingTo32;
}
private struct DecodeGroupingHelper
{
public unsafe fixed byte Data[3];
public unsafe (ushort First, ushort Second) GetData()
{
fixed (byte* ptr = Data)
{
uint value = BinaryPrimitives.ReverseEndianness((*(uint*)ptr) & 0x00FFFFFF) >> 8; // keep low 24 bits only
return ((ushort)((value >> 12) & 0x0FFF), (ushort)(value & 0x0FFF));
}
}
}
private struct DecryptedData
{
public unsafe fixed ushort Data[33];
}
}

View File

@@ -2,18 +2,11 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Test.BaseClassLibrary;
namespace Snap.Hutao.Test;
[TestClass]
public class JsonSerializeTest
{
public TestContext? TestContext { get; set; }
private readonly JsonSerializerOptions AlowStringNumberOptions = new()
{
NumberHandling = JsonNumberHandling.AllowReadingFromString,
};
private const string SmapleObjectJson = """
{
"A" :1
@@ -51,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
@@ -87,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; }
}
}

View File

@@ -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)
{

View File

@@ -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));

View File

@@ -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;
}
}

View File

@@ -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">

View File

@@ -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
}

View File

@@ -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

View File

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

View File

@@ -1,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>
@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta">
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -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));
}
}

View 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);
}
}
}

View File

@@ -69,4 +69,4 @@ internal class WinRTCustomMarshaler : ICustomMarshaler
return Marshal.GetObjectForIUnknown(pNativeData);
}
}
}
}

View File

@@ -4,18 +4,12 @@
"add": {
"extensionToExtension": {
"add": {
".json": [
".txt"
]
".json": [ ".txt" ]
}
},
"pathSegment": {
"add": {
".*": [
".cs",
".resx",
".appxmanifest"
]
".*": [ ".cs", ".resx" ]
}
},
"fileSuffixToExtension": {
@@ -25,24 +19,12 @@
},
"fileToFile": {
"add": {
".filenesting.json": [
"App.xaml.cs"
],
"app.manifest": [
"App.xaml.cs"
],
"Package.appxmanifest": [
"App.xaml"
],
"Package.StoreAssociation.xml": [
"App.xaml"
],
".editorconfig": [
"Program.cs"
],
"GlobalUsing.cs": [
"Program.cs"
]
".filenesting.json": [ "App.xaml.cs" ],
"app.manifest": [ "App.xaml.cs" ],
"Package.appxmanifest": [ "App.xaml" ],
"Package.StoreAssociation.xml": [ "App.xaml" ],
".editorconfig": [ "Program.cs" ],
"GlobalUsing.cs": [ "Program.cs" ]
}
}
}

View File

@@ -10,10 +10,8 @@
<ResourceDictionary Source="ms-appx:///Control/Image/CachedImage.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Card.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Color.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/ComboBox.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Converter.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/CornerRadius.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/FlyoutStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/FontStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Glyph.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/InfoBarOverride.xaml"/>
@@ -21,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"/>

View File

@@ -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>

View File

@@ -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/>

View File

@@ -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/>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -35,11 +35,6 @@ internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIEle
private void TryExecuteCommand()
{
if (AssociatedObject is null)
{
return;
}
if (executed)
{
return;

View File

@@ -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();
}
}

View File

@@ -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!;
}
}

View File

@@ -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;

View File

@@ -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!;
}
}

View File

@@ -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, });
}
}
}

View File

@@ -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
{
}

View File

@@ -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;
}
}
}
}

View File

@@ -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();
}

View File

@@ -30,6 +30,7 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
private readonly IServiceProvider serviceProvider;
private readonly RoutedEventHandler unloadEventHandler;
private readonly SizeChangedEventHandler sizeChangedEventHandler;
private readonly TypedEventHandler<LoadedImageSurface, LoadedImageSourceLoadCompletedEventArgs> loadedImageSourceLoadCompletedEventHandler;
@@ -45,6 +46,9 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
serviceProvider = this.ServiceProvider();
this.DisableInteraction();
unloadEventHandler = OnUnload;
Unloaded += unloadEventHandler;
sizeChangedEventHandler = OnSizeChanged;
SizeChanged += sizeChangedEventHandler;
@@ -63,6 +67,10 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{
}
protected virtual void Unloading()
{
}
/// <summary>
/// 更新视觉对象
/// </summary>
@@ -102,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
{
@@ -188,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);
}
@@ -209,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);
}
@@ -232,4 +240,14 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
UpdateVisual(spriteVisual);
}
}
private void OnUnload(object sender, RoutedEventArgs e)
{
Unloading();
spriteVisual?.Dispose();
spriteVisual = null;
SizeChanged -= sizeChangedEventHandler;
Unloaded -= unloadEventHandler;
}
}

View File

@@ -45,6 +45,16 @@ internal sealed class MonoChrome : CompositionImage
return compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
}
protected override void Unloading()
{
ActualThemeChanged -= actualThemeChangedEventHandler;
backgroundBrush?.Dispose();
backgroundBrush = null;
base.Unloading();
}
private void OnActualThemeChanged(FrameworkElement sender, object args)
{
if (backgroundBrush is not null)

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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; }
}

View File

@@ -1,272 +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)
{
// Item has not been measured yet. Get the element and store the values
UIElement element = context.GetOrCreateElementAt(i);
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);
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(layout))
{
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();
}
}

View File

@@ -1,205 +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;
}
}
internal UniformStaggeredColumnLayout GetColumnLayout(int columnIndex)
{
return columnLayout[columnIndex];
}
/// <summary>
/// Clear everything that has been calculated.
/// </summary>
internal void Clear()
{
// 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
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Finally we need to refresh the whole layout when we reset
if (context.ItemCount > 0)
{
for (int i = 0; i < context.ItemCount; i++)
{
RecycleElementAt(i);
}
}
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;
}
}
}
}
}

View File

@@ -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()

View File

@@ -9,11 +9,11 @@
mc:Ignorable="d">
<cwc:SegmentedItem
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentBulletedList}}"
Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
Tag="List"
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
<cwc:SegmentedItem
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentGridView}}"
Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Tag="Grid"
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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();
}
}
}

View File

@@ -1,51 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
using Windows.Foundation;
namespace Snap.Hutao.Control;
[DependencyProperty("IsWidthRestricted", typeof(bool), true)]
[DependencyProperty("IsHeightRestricted", typeof(bool), true)]
internal sealed partial class SizeRestrictedContentControl : ContentControl
{
private double minContentWidth;
private double minContentHeight;
protected override Size MeasureOverride(Size availableSize)
{
if (Content is FrameworkElement element)
{
element.Measure(availableSize);
Size contentDesiredSize = element.DesiredSize;
Size contentActualOrDesiredSize = new(
Math.Max(element.ActualWidth, contentDesiredSize.Width),
Math.Max(element.ActualHeight, contentDesiredSize.Height));
if (IsWidthRestricted)
{
if (contentActualOrDesiredSize.Width > minContentWidth)
{
minContentWidth = contentActualOrDesiredSize.Width;
}
element.MinWidth = minContentWidth;
}
if (IsHeightRestricted)
{
if (contentActualOrDesiredSize.Height > minContentHeight)
{
minContentHeight = contentActualOrDesiredSize.Height;
}
element.MinHeight = minContentHeight;
}
}
return base.MeasureOverride(availableSize);
}
}

View File

@@ -1,11 +0,0 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style
x:Key="CommandBarComboBoxStyle"
BasedOn="{StaticResource DefaultComboBoxStyle}"
TargetType="ComboBox">
<Setter Property="Padding" Value="12,7,0,7"/>
<Setter Property="Height" Value="36"/>
</Style>
<!-- https://github.com/microsoft/microsoft-ui-xaml/issues/4811 -->
<x:Int32 x:Key="__DiscardPageOverride">0</x:Int32>
</ResourceDictionary>

View File

@@ -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>

View File

@@ -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>

View File

@@ -93,19 +93,4 @@
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<SolidColorBrush x:Key="TextControlTextForeground" Color="{ThemeResource TextFillColorPrimary}"/>
<Thickness x:Key="TextControlTextMargin">1,0</Thickness>
<Style
x:Key="TextControlTextBlockStyle"
BasedOn="{StaticResource BodyTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource TextControlTextForeground}"/>
<Setter Property="Margin" Value="{StaticResource TextControlTextMargin}"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalTextAlignment" Value="Center"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
</ResourceDictionary>

View File

@@ -3,18 +3,8 @@
<x:String x:Key="FontIconContentSetting">&#xE713;</x:String>
<x:String x:Key="FontIconContentRefresh">&#xE72C;</x:String>
<x:String x:Key="FontIconContentDelete">&#xE74D;</x:String>
<x:String x:Key="FontIconContentChevronRight">&#xE76C;</x:String>
<x:String x:Key="FontIconContentWarning">&#xE7BA;</x:String>
<x:String x:Key="FontIconContentGame">&#xE7FC;</x:String>
<x:String x:Key="FontIconContentOpenInNewWindow">&#xE8A7;</x:String>
<x:String x:Key="FontIconContentFolder">&#xE8B7;</x:String>
<x:String x:Key="FontIconContentCopy">&#xE8C8;</x:String>
<x:String x:Key="FontIconContentBulletedList">&#xE8FD;</x:String>
<x:String x:Key="FontIconContentCheckList">&#xE9D5;</x:String>
<x:String x:Key="FontIconContentWebsite">&#xEB41;</x:String>
<x:String x:Key="FontIconContentHomeGroup">&#xEC26;</x:String>
<x:String x:Key="FontIconContentAsteriskBadge12">&#xEDAD;</x:String>
<x:String x:Key="FontIconContentZipFolder">&#xF012;</x:String>
<x:String x:Key="FontIconContentGridView">&#xF0E2;</x:String>
<x:String x:Key="FontIconContentGiftboxOpen">&#xF133;</x:String>
</ResourceDictionary>
</ResourceDictionary>

View File

@@ -11,31 +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="StackPanelSpacing4Template">
<StackPanel Spacing="4"/>
</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -14,10 +14,4 @@
<TransitionCollection x:Key="ReorderThemeTransitions">
<ReorderThemeTransition/>
</TransitionCollection>
<TransitionCollection x:Key="RepositionThemeTransitions">
<RepositionThemeTransition/>
</TransitionCollection>
<TransitionCollection x:Key="NavigationThemeTransitions">
<NavigationThemeTransition/>
</TransitionCollection>
</ResourceDictionary>

View File

@@ -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/>

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
internal interface IPinnable<TData>
{
ref readonly TData GetPinnableReference();
}

View File

@@ -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));
}

View File

@@ -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>
/// 当符合条件时添加参数

View File

@@ -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>();

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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)
@@ -63,14 +63,12 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.CNVersion);
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
client.DefaultRequestHeaders.Add("x-rpc-device_name", string.Empty);
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0");
//client.DefaultRequestHeaders.Add("x-rpc-tool_verison", "v4.2.2-ys");
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2");
}
/// <summary>
/// 对于需要添加动态密钥1的客户端使用此配置
/// 对于需要添加动态密钥的客户端使用此配置
/// HoYoLAB app
/// </summary>
/// <param name="client">配置后的客户端</param>
@@ -86,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;

View File

@@ -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;
}
}

View File

@@ -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)
{
}
}

View File

@@ -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

View File

@@ -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)
{
}
}

View File

@@ -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)
{
}
}

View File

@@ -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;

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.IO.DataTransfer;
/// <summary>
/// 剪贴板互操作
/// </summary>
internal interface IClipboardProvider
internal interface IClipboardInterop
{
/// <summary>
/// 从剪贴板文本中反序列化

View File

@@ -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)

View 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));
}
}
}

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