Compare commits

..

1 Commits

Author SHA1 Message Date
qhy040404
a845dff6ee Revert "temporary fix qr login"
This reverts commit d4bd610fe2.
2024-04-14 13:55:16 +08:00
1426 changed files with 21350 additions and 42031 deletions

View File

@@ -19,7 +19,7 @@ body:
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答 - label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
required: true required: true
- label: 我知道[文档站](https://hut.ao/zh/menu.html)的导航栏中有**搜索功能**,且已经搜索过相关关键词 - label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
required: true required: true
- label: 我的问题不是[已完成](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90)的问题也不是一个别人已发布的**重复的**问题 - label: 我的问题不是[已完成](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90)的问题也不是一个别人已发布的**重复的**问题
@@ -40,7 +40,7 @@ body:
attributes: attributes:
label: Snap Hutao 版本 label: Snap Hutao 版本
description: 在应用标题,应用程序的反馈中心界面中可以找到 description: 在应用标题,应用程序的反馈中心界面中可以找到
placeholder: 1.9.9.0 placeholder: 1.4.15.0
validations: validations:
required: true required: true
@@ -62,19 +62,20 @@ body:
description: 请设置一个你认为合适的分类,这将帮助我们快速定位问题 description: 请设置一个你认为合适的分类,这将帮助我们快速定位问题
options: options:
- 安装和环境 - 安装和环境
- 游戏启动器
- 祈愿记录
- 成就管理 - 成就管理
- 我的角色 - 角色信息面板
- 游戏启动器
- 实时便笺 - 实时便笺
- 养成计算 - 养成计算
- 深境螺旋/胡桃数据库
- Wiki
- 米游社账号面板
- 每日签到奖励
- 胡桃通行证/胡桃云
- 用户界面
- 文件缓存 - 文件缓存
- 祈愿记录
- 玩家查询
- 胡桃数据库
- 用户界面
- 胡桃云
- 胡桃帐号
- 签到
- Wiki
- 公告 - 公告
- 其它 - 其它
validations: validations:

View File

@@ -1,7 +1,9 @@
name: 功能请求 name: 功能请求
description: 通过这个议题来向开发团队分享你的想法 description: 通过这个议题来向开发团队分享你的想法
title: "[Feat]: 在这里填写一个合适的标题" title: "[Feat]: 在这里填写一个合适的标题"
labels: ["feature request", "needs-triage", "priority:none"] labels: ["功能", "needs-triage", "priority:none"]
assignees:
- Lightczx
body: body:
- type: markdown - type: markdown
attributes: attributes:
@@ -22,4 +24,4 @@ body:
label: 想要实现或优化的功能 label: 想要实现或优化的功能
description: 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高 description: 详细的描述一下你想要的功能,描述的越具体,采纳的可能性越高
validations: validations:
required: true required: true

View File

@@ -40,7 +40,7 @@ body:
attributes: attributes:
label: Snap Hutao Version label: Snap Hutao Version
description: You can find the version in application's title bar description: You can find the version in application's title bar
placeholder: e.g. 1.9.9.0 placeholder: e.g. 1.4.15.0
validations: validations:
required: true required: true
@@ -62,19 +62,20 @@ body:
description: Please select the most associated category of your issue description: Please select the most associated category of your issue
options: options:
- Installation and Environment - Installation and Environment
- Game Launcher
- Wish Export
- Achievement - Achievement
- My Character - My Character
- Game Launcher
- Realtime Note - Realtime Note
- Develop Plan - Develop Plan
- Spiral Abyss
- Wiki
- MiHoYo Account Panel
- Daily Checkin Reward
- Hutao Passport/Hutao Cloud
- User Interface
- File Cache - File Cache
- Wish Export
- Game Record
- Hutao Database
- User Interface
- Snap Hutao Cloud
- Snap Hutao Account
- Checkin
- Wiki
- Announcement - Announcement
- Other - Other
validations: validations:

View File

@@ -1,7 +1,9 @@
name: Feature Request [English Form] name: Feature Request [English Form]
description: Tell us about your thought description: Tell us about your thought
title: "[Feat]: Place your title here" title: "[Feat]: Place your title here"
labels: ["feature request", "needs-triage", "priority:none"] labels: ["功能", "needs-triage", "priority:none"]
assignees:
- Lightczx
body: body:
- type: markdown - type: markdown
attributes: attributes:
@@ -20,6 +22,6 @@ body:
id: req id: req
attributes: attributes:
label: Detail of the Feature label: Detail of the Feature
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted. description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted.
validations: validations:
required: true required: true

View File

@@ -1,15 +0,0 @@
<!--- Hi, thanks for considering make a PR contribution to Snap Hutao, we appreciate your work. -->
<!--- Before you create this PR, please check our contribution guide (https://hut.ao/en/development/contribute.html) and fill out the following form and checklist -->
## Description
<!--- Describe your changes -->
## Related Issue
<!--- If there's an associated issue, please use [GitHub Keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests) to link it -->
<!-- e.g. fix #999, resolve #999, close #999 -->
## Checklist
- [ ] The target PR branch is `develop` branch

View File

@@ -5,7 +5,6 @@ on:
branches: branches:
- main - main
- develop - develop
- 'feat/*'
paths-ignore: paths-ignore:
- '.gitattributes' - '.gitattributes'
- '.github/**' - '.github/**'
@@ -45,8 +44,13 @@ jobs:
run: dotnet tool restore && dotnet cake run: dotnet tool restore && dotnet cake
env: env:
VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }} VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }}
CERTIFICATE: ${{ secrets.CERTIFICATE }}
PW: ${{ secrets.PW }} - name: Sign Msix
if: success() && github.event_name != 'pull_request'
shell: pwsh
run: |
[System.Convert]::FromBase64String("${{ secrets.CERTIFICATE }}") | Set-Content -AsByteStream temp.pfx
signtool.exe sign /debug /v /a /fd SHA256 /f temp.pfx /p ${{ secrets.PW }} ${{ github.workspace }}\src\output\Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
- name: Upload signed msix - name: Upload signed msix
if: success() && github.event_name != 'pull_request' if: success() && github.event_name != 'pull_request'
@@ -64,55 +68,12 @@ jobs:
> 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用** > 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
> [!TIP] > [!TIP]
> 普通用户请 [点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/) 下载最新的稳定版本 > 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
> [!IMPORTANT] > [!IMPORTANT]
> 请先安装 **[DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt)** 到 **受信任的根证书颁发机构** 以安装测试版安装包 > 请注意,从 Snap Hutao Alpha 2023.12.21.3 开始,我们将使用全新的 CI 证书,原有的 Snap.Hutao.CI.cer 将在几天后过期停止使用。
" >
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 以安装测试版安装包
echo $summary >> $Env:GITHUB_STEP_SUMMARY
fallback_build:
runs-on: windows-latest
needs: build
if: failure()
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0
- name: Cake
id: cake
shell: pwsh
run: dotnet tool restore && dotnet cake
env:
VERSION_API_TOKEN: ${{ secrets.VERSION_API_TOKEN }}
CERTIFICATE: ${{ secrets.CERTIFICATE }}
PW: ${{ secrets.PW }}
- name: Upload signed msix
if: success() && github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}
path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix
- name: Add summary
if: success() && github.event_name != 'pull_request'
shell: pwsh
run: |
$summary = "
> [!WARNING]
> 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
> [!TIP]
> 普通用户请 [点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/) 下载最新的稳定版本
> [!IMPORTANT]
> 请先安装 **[DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt)** 到 **受信任的根证书颁发机构** 以安装测试版安装包
" "
echo $summary >> $Env:GITHUB_STEP_SUMMARY echo $summary >> $Env:GITHUB_STEP_SUMMARY

View File

@@ -10,7 +10,7 @@ jobs:
- uses: actions/stale@v9 - uses: actions/stale@v9
with: with:
any-of-labels: 'needs-more-info,需要更多信息' any-of-labels: 'needs-more-info,需要更多信息'
stale-issue-message: 'This issue is stale because it has been open 7 days with no activity. Remove stale label or comment or this will be closed in 3 days.' stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 3 days.'
days-before-stale: 7 days-before-stale: 7
days-before-close: 3 days-before-close: 3
close-issue-reason: not_planned close-issue-reason: not_planned

View File

@@ -17,15 +17,7 @@ You can follow the instructions in the [Quick Start](https://hut.ao/en/quick-sta
## 本地化翻译 / Localization ## 本地化翻译 / Localization
[![zh-TW translation](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-TW&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27zh-TW%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) ![zh-TW translation](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-TW&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27zh-TW%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ![en translation](https://img.shields.io/badge/dynamic/json?color=blue&label=en&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27en%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ![id translation](https://img.shields.io/badge/dynamic/json?color=blue&label=id&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27id%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ![ja translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ja&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ja%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ![ko translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ko&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ko%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)![pt-PT translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pt-PT&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27pt-PT%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json) ![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ru%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)
[![en translation](https://img.shields.io/badge/dynamic/json?color=blue&label=en&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27en%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
[![fr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27fr%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
[![id translation](https://img.shields.io/badge/dynamic/json?color=blue&label=id&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27id%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
[![ja translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ja&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ja%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
[![ko translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ko&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ko%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
[![pt-PT translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pt-PT&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27pt-PT%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
[![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ru%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
[![vi translation](https://img.shields.io/badge/dynamic/json?color=blue&label=vi&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27vi%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。 Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。
@@ -54,13 +46,13 @@ Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translatio
* [CommunityToolkit/dotnet](https://github.com/CommunityToolkit/dotnet) * [CommunityToolkit/dotnet](https://github.com/CommunityToolkit/dotnet)
* [CommunityToolkit/Labs-Windows](https://github.com/CommunityToolkit/Labs-Windows) * [CommunityToolkit/Labs-Windows](https://github.com/CommunityToolkit/Labs-Windows)
* [CommunityToolkit/Windows](https://github.com/CommunityToolkit/Windows) * [CommunityToolkit/Windows](https://github.com/CommunityToolkit/Windows)
* [dahall/taskscheduler](https://github.com/dahall/taskscheduler)
* [dotnet/efcore](https://github.com/dotnet/efcore) * [dotnet/efcore](https://github.com/dotnet/efcore)
* [dotnet/runtime](https://github.com/dotnet/runtime) * [dotnet/runtime](https://github.com/dotnet/runtime)
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers) * [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation) * [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK) * [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml) * [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
* [quartznet/quartznet](https://github.com/quartznet/quartznet)
### 支撑项目 / Supporter Project ### 支撑项目 / Supporter Project
@@ -72,9 +64,9 @@ Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translatio
Snap Hutao is currently using sponsored software from the following service providers. Snap Hutao is currently using sponsored software from the following service providers.
| [![](https://www.netlify.com/v3/img/components/netlify-light.svg)](https://www.netlify.com/) | [![](https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg)](https://crowdin.com/) | [![](https://gitlab.cn/images/icons/logos/logo-121-75.svg)](https://gitlab.cn/) | | [![](https://www.netlify.com/v3/img/components/netlify-light.svg)](https://www.netlify.com/) | [![](https://support.crowdin.com/assets/logos/core-logo/svg/crowdin-core-logo-cDark.svg)](https://crowdin.com/) | [![](https://gitlab.cn/images/icons/logos/logo-121-75.svg)](https://gitlab.cn/) |
| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | |:----------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|
| [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/73ae8b90-f3c7-4033-b2b7-f4126331ce66)](https://about.signpath.io) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/49aed8ee-9f19-4a8a-998c-7b93ee286d65)](https://1password.com/) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/ad121220-d2d3-4f49-b215-b6d063dc229d)](https://www.digitalocean.com) | | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/73ae8b90-f3c7-4033-b2b7-f4126331ce66)](https://about.signpath.io) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/49aed8ee-9f19-4a8a-998c-7b93ee286d65)](https://1password.com/) | [![](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/ad121220-d2d3-4f49-b215-b6d063dc229d)](https://about.signpath.io) |
| [![ducalis](https://raw.githubusercontent.com/DGP-Studio/Snap.Hutao.Docs/main/docs/.vuepress/public/svg/ducalis.svg)](https://hi.ducalis.io/) | [![jetbrains](https://github.com/DGP-Studio/Snap.Hutao/assets/36357191/4105772a-728a-4a84-9c6e-d713a5698a20)](https://www.jetbrains.com/opensource/) | |
- Netlify provides document and home page hosting service for Snap Hutao - Netlify provides document and home page hosting service for Snap Hutao
@@ -88,10 +80,6 @@ Snap Hutao is currently using sponsored software from the following service prov
- DigitalOcean provides reliable cloud database for Snap Hutao database backup - DigitalOcean provides reliable cloud database for Snap Hutao database backup
- [Ducalis.io](https://hi.ducalis.io/) provides Snap Hutao project with a complete decision-making toolkit for project management
- Jetbrains provides powerful IDE for Snap Hutao infrastructure services coding
## 开发 / Development ## 开发 / Development
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg) ![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)

View File

@@ -11,18 +11,6 @@ var version = "version";
var repoDir = "repoDir"; var repoDir = "repoDir";
var outputPath = "outputPath"; var outputPath = "outputPath";
var pfxPath = "pfxPath";
var pw = "pw";
// Extension
static ProcessArgumentBuilder AppendIf(this ProcessArgumentBuilder builder, string text, bool condition)
{
return condition ? builder.Append(text) : builder;
}
// Properties
string solution string solution
{ {
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao.sln"); get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao.sln");
@@ -65,11 +53,6 @@ if (GitHubActions.IsRunningOnGitHubActions)
} }
); );
var certificateBase64 = HasEnvironmentVariable("CERTIFICATE") ? EnvironmentVariable("CERTIFICATE") : throw new Exception("Cannot find CERTIFICATE");
pw = HasEnvironmentVariable("PW") ? EnvironmentVariable("PW") : throw new Exception("Cannot find PW");
pfxPath = System.IO.Path.Combine(repoDir, "temp.pfx");
System.IO.File.WriteAllBytes(pfxPath, System.Convert.FromBase64String(certificateBase64));
Information($"Version: {version}"); Information($"Version: {version}");
} }
@@ -96,19 +79,10 @@ else // Local
Information($"Version: {version}"); Information($"Version: {version}");
} }
// Windows SDK
var registry = new WindowsRegistry();
var winsdkRegistry = registry.LocalMachine.OpenKey(@"SOFTWARE\Microsoft\Windows Kits\Installed Roots");
var winsdkVersion = winsdkRegistry.GetSubKeyNames().MaxBy(key => int.Parse(key.Split(".")[2]));
var winsdkPath = (string)winsdkRegistry.GetValue("KitsRoot10");
var winsdkBinPath = System.IO.Path.Combine(winsdkPath, "bin", winsdkVersion, "x64");
Information($"Windows SDK: {winsdkPath}");
Task("Build") Task("Build")
.IsDependentOn("Build binary package") .IsDependentOn("Build binary package")
.IsDependentOn("Copy files") .IsDependentOn("Copy files")
.IsDependentOn("Build MSIX") .IsDependentOn("Build MSIX");
.IsDependentOn("Sign");
Task("NuGet Restore") Task("NuGet Restore")
.Does(() => .Does(() =>
@@ -183,7 +157,6 @@ Task("Build binary package")
.Append("/p:AppxPackageSigningEnabled=false") .Append("/p:AppxPackageSigningEnabled=false")
.Append("/p:AppxBundle=Never") .Append("/p:AppxBundle=Never")
.Append("/p:AppxPackageOutput=" + outputPath) .Append("/p:AppxPackageOutput=" + outputPath)
.AppendIf("/p:AlphaConstants=IS_ALPHA_BUILD", !AppVeyor.IsRunningOnAppVeyor)
}; };
DotNetBuild(project, settings); DotNetBuild(project, settings);
@@ -224,11 +197,8 @@ Task("Build MSIX")
{ {
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Local-{version}.msix"); arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Local-{version}.msix");
} }
var makeappxPath = System.IO.Path.Combine(winsdkBinPath, "makeappx.exe");
var p = StartProcess( var p = StartProcess(
makeappxPath, "makeappx.exe",
new ProcessSettings new ProcessSettings
{ {
Arguments = arguments Arguments = arguments
@@ -236,46 +206,7 @@ Task("Build MSIX")
); );
if (p != 0) if (p != 0)
{ {
throw new InvalidOperationException("Build MSIX failed with exit code " + p); throw new InvalidOperationException("Build failed with exit code " + p);
}
});
Task("Sign")
.IsDependentOn("Build MSIX")
.Does(() =>
{
if (AppVeyor.IsRunningOnAppVeyor)
{
Information("Move to SignPath. Skip signing.");
return;
}
else if (GitHubActions.IsRunningOnGitHubActions)
{
if (GitHubActions.Environment.PullRequest.IsPullRequest)
{
Information("Is Pull Request. Skip signing.");
return;
}
var signPath = System.IO.Path.Combine(winsdkBinPath, "signtool.exe");
var arguments = $"sign /debug /v /a /fd SHA256 /f {pfxPath} /p {pw} {System.IO.Path.Combine(outputPath, $"Snap.Hutao.Alpha-{version}.msix")}";
var p = StartProcess(
signPath,
new ProcessSettings
{
Arguments = arguments
}
);
if (p != 0)
{
throw new InvalidOperationException("Sign failed with exit code " + p);
}
}
else
{
Information("Local configuration. Skip signing.");
return;
} }
}); });

View File

@@ -1,5 +1,3 @@
files: files:
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx - source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.%osx_locale%.resx translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.%osx_locale%.resx
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.resx
translation: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SHRegex.%osx_locale%.resx

View File

@@ -322,7 +322,6 @@ dotnet_diagnostic.CA2227.severity = suggestion
dotnet_diagnostic.CA2251.severity = suggestion dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = false:none csharp_style_prefer_primary_constructors = false:none
dotnet_diagnostic.SA1124.severity = none
[*.vb] [*.vb]
#### 命名样式 #### #### 命名样式 ####

View File

@@ -1,31 +0,0 @@
using System.Net.Http;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public sealed class HttpClientTest
{
[TestMethod]
public void RedirectionHeaderTest()
{
HttpClientHandler handler = new()
{
UseCookies = false,
AllowAutoRedirect = false,
};
using (handler)
{
using (HttpClient httpClient = new(handler))
{
using (HttpRequestMessage request = new(HttpMethod.Get, "https://api.snapgenshin.com/patch/hutao/download"))
{
using (HttpResponseMessage response = httpClient.Send(request))
{
_ = 1;
}
}
}
}
}
}

View File

@@ -13,19 +13,19 @@ public sealed class JsonSerializeTest
NumberHandling = JsonNumberHandling.AllowReadingFromString, NumberHandling = JsonNumberHandling.AllowReadingFromString,
}; };
private const string SampleObjectJson = """ private const string SmapleObjectJson = """
{ {
"A" :1 "A" :1
} }
"""; """;
private const string SampleEmptyStringObjectJson = """ private const string SmapleEmptyStringObjectJson = """
{ {
"A" : "" "A" : ""
} }
"""; """;
private const string SampleNumberKeyDictionaryJson = """ private const string SmapleNumberKeyDictionaryJson = """
{ {
"111" : "12", "111" : "12",
"222" : "34" "222" : "34"
@@ -35,7 +35,7 @@ public sealed class JsonSerializeTest
[TestMethod] [TestMethod]
public void DelegatePropertyCanSerialize() public void DelegatePropertyCanSerialize()
{ {
SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SampleObjectJson)!; SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SmapleObjectJson)!;
Assert.AreEqual(sample.B, 1); Assert.AreEqual(sample.B, 1);
} }
@@ -43,23 +43,14 @@ public sealed class JsonSerializeTest
[ExpectedException(typeof(JsonException))] [ExpectedException(typeof(JsonException))]
public void EmptyStringCannotSerializeAsNumber() public void EmptyStringCannotSerializeAsNumber()
{ {
SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SampleEmptyStringObjectJson)!; SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SmapleEmptyStringObjectJson)!;
Assert.AreEqual(sample.A, 0); Assert.AreEqual(sample.A, 0);
} }
[TestMethod]
public void EmptyStringCanSerializeAsUri()
{
SampleEmptyUriClass sample = JsonSerializer.Deserialize<SampleEmptyUriClass>(SampleEmptyStringObjectJson)!;
Uri.TryCreate("", UriKind.RelativeOrAbsolute, out Uri? value);
Console.WriteLine(value);
Assert.AreEqual(sample.A, value);
}
[TestMethod] [TestMethod]
public void NumberStringKeyCanSerializeAsKey() public void NumberStringKeyCanSerializeAsKey()
{ {
Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SampleNumberKeyDictionaryJson, AlowStringNumberOptions)!; Dictionary<int, string> sample = JsonSerializer.Deserialize<Dictionary<int, string>>(SmapleNumberKeyDictionaryJson, AlowStringNumberOptions)!;
Assert.AreEqual(sample[111], "12"); Assert.AreEqual(sample[111], "12");
} }
@@ -89,19 +80,6 @@ public sealed class JsonSerializeTest
Assert.AreEqual(result, """{"A":1,"B":2}"""); Assert.AreEqual(result, """{"A":1,"B":2}""");
} }
[TestMethod]
public void LowercaseStringCanDeserializeAsEnum()
{
string source = """
{
"Value": "a"
}
""";
SampleClassHoldEnum sample = JsonSerializer.Deserialize<SampleClassHoldEnum>(source)!;
Assert.AreEqual(sample.Value, SampleEnum.A);
}
private sealed class SampleDelegatePropertyClass private sealed class SampleDelegatePropertyClass
{ {
public int A { get => B; set => B = value; } public int A { get => B; set => B = value; }
@@ -114,11 +92,6 @@ public sealed class JsonSerializeTest
public int A { get; set; } public int A { get; set; }
} }
private sealed class SampleEmptyUriClass
{
public Uri A { get; set; } = default!;
}
private sealed class SampleByteArrayPropertyClass private sealed class SampleByteArrayPropertyClass
{ {
public byte[]? Array { get; set; } public byte[]? Array { get; set; }
@@ -131,18 +104,6 @@ public sealed class JsonSerializeTest
public int B { get; set; } public int B { get; set; }
} }
[JsonConverter(typeof(JsonStringEnumConverter))]
private enum SampleEnum
{
A,
B,
}
private sealed class SampleClassHoldEnum
{
public SampleEnum Value { get; set; }
}
[JsonDerivedType(typeof(SampleClassImplementedInterface))] [JsonDerivedType(typeof(SampleClassImplementedInterface))]
private interface ISampleInterface private interface ISampleInterface
{ {

View File

@@ -1,14 +0,0 @@
using System.Collections.Generic;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public sealed class ListTest
{
[TestMethod]
public void IndexOfNullIsNegativeOne()
{
List<object> list = [new()];
Assert.AreEqual(-1, list.IndexOf(default!));
}
}

View File

@@ -1,28 +0,0 @@
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public class UnsafeAccessorTest
{
[TestMethod]
public void UnsafeAccessorCanGetInterfaceProperty()
{
TestClass test = new();
int value = InternalGetInterfaceProperty(test);
Assert.AreEqual(3, value);
}
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_TestProperty")]
private static extern int InternalGetInterfaceProperty(ITestInterface instance);
internal interface ITestInterface
{
internal int TestProperty { get; }
}
internal sealed class TestClass : ITestInterface
{
public int TestProperty { get; } = 3;
}
}

View File

@@ -6,25 +6,14 @@ namespace Snap.Hutao.Test.IncomingFeature;
public class SpiralAbyssScheduleIdTest public class SpiralAbyssScheduleIdTest
{ {
private static readonly TimeSpan Utc8 = new(8, 0, 0); private static readonly TimeSpan Utc8 = new(8, 0, 0);
private static readonly DateTimeOffset AcrobaticsBattleIntroducedTime = new(2024, 7, 1, 4, 0, 0, Utc8);
[TestMethod] [TestMethod]
public void Test() public void Test()
{ {
Console.WriteLine($"当前第 {GetForDateTimeOffset(DateTimeOffset.Now)} 期"); Console.WriteLine($"当前第 {GetForDateTimeOffset(DateTimeOffset.Now)} 期");
// 2020-07-01 04:00:00 为第 1 期 DateTimeOffset dateTimeOffset = new(2020, 7, 1, 4, 0, 0, Utc8);
// 2024-06-16 04:00:00 为第 96 期 Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(dateTimeOffset)} 期");
// 2024-07-01 04:00:00 为第 97 期
// 2024-07-16 04:00:00 为第 98 期
// 2024-08-01 04:00:00 为第 99 期
Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(new(2020, 07, 01, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-06-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 06, 16, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-07-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 07, 01, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-07-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 07, 16, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-08-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 08, 01, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-08-16 04:00:00 为第 {GetForDateTimeOffset(new(2024, 08, 16, 4, 0, 0, Utc8))} 期");
Console.WriteLine($"2024-09-01 04:00:00 为第 {GetForDateTimeOffset(new(2024, 09, 01, 4, 0, 0, Utc8))} 期");
} }
public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset) public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset)
@@ -49,12 +38,6 @@ public class SpiralAbyssScheduleIdTest
periodNum--; periodNum--;
} }
if (dateTimeOffset >= AcrobaticsBattleIntroducedTime)
{
// 当超过 96 期时,每一个月一期
periodNum = (4 * 12 * 2) + ((periodNum - (4 * 12 * 2)) / 2);
}
return periodNum; return periodNum;
} }
} }

View File

@@ -1,51 +0,0 @@
using System;
using System.Text.Json;
namespace Snap.Hutao.Test.IncomingFeature;
[TestClass]
public class UnlockerIslandFunctionOffsetTest
{
private static readonly JsonSerializerOptions Options = new()
{
WriteIndented = true,
};
[TestMethod]
public void GenerateJson()
{
UnlockerIslandConfigurationWrapper wrapper = new()
{
Oversea = new()
{
FunctionOffsetFieldOfView = 0x00000000_01688E60,
FunctionOffsetTargetFrameRate = 0x00000000_018834D0,
FunctionOffsetFog = 0x00000000_00FB2AD0,
},
Chinese = new()
{
FunctionOffsetFieldOfView = 0x00000000_01684560,
FunctionOffsetTargetFrameRate = 0x00000000_0187EBD0,
FunctionOffsetFog = 0x00000000_00FAE1D0,
},
};
Console.WriteLine(JsonSerializer.Serialize(wrapper, Options));
}
private sealed class UnlockerIslandConfigurationWrapper
{
public required UnlockerIslandConfiguration Oversea { get; set; }
public required UnlockerIslandConfiguration Chinese { get; set; }
}
private sealed class UnlockerIslandConfiguration
{
public required uint FunctionOffsetFieldOfView { get; set; }
public required uint FunctionOffsetTargetFrameRate { get; set; }
public required uint FunctionOffsetFog { get; set; }
}
}

View File

@@ -1,7 +1,6 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Linq;
namespace Snap.Hutao.Test.PlatformExtensions; namespace Snap.Hutao.Test.PlatformExtensions;
@@ -12,8 +11,6 @@ public sealed class DependencyInjectionTest
.AddSingleton<IService, ServiceA>() .AddSingleton<IService, ServiceA>()
.AddSingleton<IService, ServiceB>() .AddSingleton<IService, ServiceB>()
.AddScoped<IScopedService, ServiceA>() .AddScoped<IScopedService, ServiceA>()
.AddKeyedTransient<IKeyedService, KeyedServiceA>("A")
.AddKeyedTransient<IKeyedService, KeyedServiceB>("B")
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>)) .AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
.AddLogging(builder => builder.AddConsole()) .AddLogging(builder => builder.AddConsole())
.BuildServiceProvider(); .BuildServiceProvider();
@@ -53,15 +50,6 @@ public sealed class DependencyInjectionTest
Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService))); Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService)));
} }
[TestMethod]
public void KeyedServicesCanBeResolvedAsEnumerable()
{
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("A"));
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("B"));
Assert.AreEqual(0, services.GetServices<IKeyedService>().Count());
}
private interface IService private interface IService
{ {
Guid Id { get; } Guid Id { get; }
@@ -107,14 +95,4 @@ public sealed class DependencyInjectionTest
{ {
} }
} }
private interface IKeyedService;
private sealed class KeyedServiceA : IKeyedService
{
}
private sealed class KeyedServiceB : IKeyedService
{
}
} }

View File

@@ -1,45 +0,0 @@
using System.Drawing;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Snap.Hutao.Test.RuntimeBehavior;
[TestClass]
public sealed class HttpClientBehaviorTest
{
private const int MessageNotYetSent = 0;
[TestMethod]
public async Task RetrySendHttpRequestMessage()
{
using (HttpClient httpClient = new())
{
HttpRequestMessage requestMessage = new(HttpMethod.Post, "https://jsonplaceholder.typicode.com/posts");
JsonContent content = JsonContent.Create(new Point(12, 34));
requestMessage.Content = content;
using (requestMessage)
{
await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
}
Interlocked.Exchange(ref GetPrivateSendStatus(requestMessage), MessageNotYetSent);
Volatile.Write(ref GetPrivateDisposed(content), false);
await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
}
}
// private int _sendStatus
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_sendStatus")]
private static extern ref int GetPrivateSendStatus(HttpRequestMessage message);
// private bool _disposed
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
private static extern ref bool GetPrivateDisposed(HttpRequestMessage message);
// private bool _disposed
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
private static extern ref bool GetPrivateDisposed(HttpContent content);
}

View File

@@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -47,59 +46,7 @@ public sealed class UnsafeRuntimeBehaviorTest
Assert.AreEqual(1212, testStruct.Value4); Assert.AreEqual(1212, testStruct.Value4);
} }
[TestMethod]
public unsafe void UnsafeUtf8StringReference()
{
void* ptr = Unsafe.AsPointer(ref MemoryMarshal.GetReference("test"u8));
GC.Collect(GC.MaxGeneration);
ReadOnlySpan<byte> bytes = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr);
Console.WriteLine(System.Text.Encoding.UTF8.GetString(bytes));
}
[TestMethod]
public unsafe void UnsafeSizeInt32ToRectInt32Test()
{
RectInt32 rectInt32 = ToRectInt32(new(100, 200));
Assert.AreEqual(rectInt32.X, 0);
Assert.AreEqual(rectInt32.Y, 0);
Assert.AreEqual(rectInt32.Width, 100);
Assert.AreEqual(rectInt32.Height, 200);
unsafe RectInt32 ToRectInt32(SizeInt32 sizeInt32)
{
byte* pBytes = stackalloc byte[sizeof(RectInt32)];
*(SizeInt32*)(pBytes + 8) = sizeInt32;
return *(RectInt32*)pBytes;
}
}
private struct RectInt32
{
public int X;
public int Y;
public int Width;
public int Height;
public RectInt32(int x, int y, int width, int height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
}
private struct SizeInt32
{
public int Width;
public int Height;
public SizeInt32(int width, int height)
{
Width = width;
Height = height;
}
}
private readonly struct TestStruct private readonly struct TestStruct
{ {

View File

@@ -13,9 +13,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.5.0" /> <PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.5.0" /> <PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
<PackageReference Include="coverlet.collector" Version="6.0.2"> <PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -6,39 +6,30 @@
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<XamlControlsResources/> <XamlControlsResources/>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsCard/SettingsCard.xaml"/> <ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.TokenizingTextBox/TokenizingTextBox.xaml"/>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.Labs.WinUI.TokenView/TokenItem/TokenItem.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Loading.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Elevation.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Image/CachedImage.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/ItemIcon.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/Card.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Loading.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/Color.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/StandardView.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/ComboBox.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/AutoSuggestBox/AutoSuggestTokenBox.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/Converter.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/CardBlock.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/CornerRadius.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/CardProgressBar.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/FlyoutStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/HorizontalCard.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/FontStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Card/VerticalCard.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/Glyph.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Image/CachedImage.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/InfoBarOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/TextBlock/RateDeltaTextBlock.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/ItemsPanelTemplate.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Card.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/NumericValue.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Color.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/PageOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/ComboBox.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Converter.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/CornerRadius.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/SegmentedOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/FlyoutStyle.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/SettingsStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/FontStyle.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/Thickness.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Glyph.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/InfoBarOverride.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/ItemsPanelTemplate.xaml"/> <ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/NumericValue.xaml"/> <ResourceDictionary Source="ms-appx:///View/Card/Primitive/CardProgressBar.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/PageOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/PivotOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/ScrollViewer.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/SegmentedOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/SettingsStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Thickness.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/TransitionCollection.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/Uri.xaml"/>
<ResourceDictionary Source="ms-appx:///UI/Xaml/Control/Theme/WindowOverride.xaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<Style <Style
@@ -52,15 +43,15 @@
x:Name="NoneSelectionListViewItemStyle" x:Name="NoneSelectionListViewItemStyle"
BasedOn="{StaticResource DefaultListViewItemStyle}" BasedOn="{StaticResource DefaultListViewItemStyle}"
TargetType="ListViewItem"> TargetType="ListViewItem">
<Setter Property="Margin" Value="0,4,0,0"/>
<Setter Property="Padding" Value="0"/> <Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0,4,0,0"/>
</Style> </Style>
<Style <Style
x:Name="NoneSelectionGridViewItemStyle" x:Name="NoneSelectionGridViewItemStyle"
BasedOn="{StaticResource DefaultGridViewItemStyle}" BasedOn="{StaticResource DefaultGridViewItemStyle}"
TargetType="GridViewItem"> TargetType="GridViewItem">
<Setter Property="Margin" Value="0,0,2,4"/>
<Setter Property="Padding" Value="0"/> <Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0,0,2,4"/>
</Style> </Style>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>

View File

@@ -3,13 +3,15 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle; using Microsoft.Windows.AppLifecycle;
using Microsoft.Windows.AppNotifications;
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle; using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.LifeCycle.InterProcess; using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Shell;
using System.Diagnostics; using System.Diagnostics;
using System.Text;
using static Snap.Hutao.Core.Logging.ConsoleVirtualTerminalSequences;
namespace Snap.Hutao; namespace Snap.Hutao;
@@ -39,39 +41,33 @@ public sealed partial class App : Application
"""; """;
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
private readonly IAppActivation activation; private readonly IActivation activation;
private readonly ILogger<App> logger; private readonly ILogger<App> logger;
/// <summary>
/// Initializes the singleton application object.
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public App(IServiceProvider serviceProvider) public App(IServiceProvider serviceProvider)
{ {
// Load app resource // Load app resource
InitializeComponent(); InitializeComponent();
activation = serviceProvider.GetRequiredService<IAppActivation>(); activation = serviceProvider.GetRequiredService<IActivation>();
logger = serviceProvider.GetRequiredService<ILogger<App>>(); logger = serviceProvider.GetRequiredService<ILogger<App>>();
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this); serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
this.serviceProvider = serviceProvider; this.serviceProvider = serviceProvider;
} }
public new void Exit() /// <inheritdoc/>
{
XamlApplicationLifetime.Exiting = true;
base.Exit();
}
protected override void OnLaunched(LaunchActivatedEventArgs args) protected override void OnLaunched(LaunchActivatedEventArgs args)
{ {
try try
{ {
// Important: You must call AppNotificationManager::Default().Register
// before calling AppInstance.GetCurrent.GetActivatedEventArgs.
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
AppNotificationManager.Default.Register();
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs)) if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
{ {
logger.LogDebug("Application exiting on RedirectActivationTo");
Exit(); Exit();
return; return;
} }
@@ -79,13 +75,15 @@ public sealed partial class App : Application
logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow)); logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow));
LogDiagnosticInformation(); LogDiagnosticInformation();
// Manually invoke // manually invoke
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs)); activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
activation.PostInitialization(); activation.Initialize();
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
} }
catch (Exception ex) catch
{ {
logger.LogError(ex, "Application failed in App.OnLaunched"); // AppInstance.GetCurrent() calls failed
Process.GetCurrentProcess().Kill(); Process.GetCurrentProcess().Kill();
} }
} }

View File

@@ -0,0 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao;
/// <summary>
/// 应用程序资源提供器
/// </summary>
[Injection(InjectAs.Transient, typeof(IAppResourceProvider))]
internal sealed class AppResourceProvider : IAppResourceProvider
{
private readonly App app;
/// <summary>
/// 构造一个新的应用程序资源提供器
/// </summary>
/// <param name="app">应用</param>
public AppResourceProvider(App app)
{
this.app = app;
}
/// <inheritdoc/>
public T GetResource<T>(string name)
{
return (T)app.Resources[name];
}
}

View File

@@ -1,9 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
namespace Snap.Hutao.UI.Xaml.Media.Animation; namespace Snap.Hutao.Control.Animation;
internal static class Constants internal static class ControlAnimationConstants
{ {
/// <summary> /// <summary>
/// 1 /// 1

View File

@@ -6,7 +6,7 @@ using CommunityToolkit.WinUI.Animations;
using Microsoft.UI.Composition; using Microsoft.UI.Composition;
using System.Numerics; using System.Numerics;
namespace Snap.Hutao.UI.Xaml.Media.Animation; namespace Snap.Hutao.Control.Animation;
/// <summary> /// <summary>
/// 图片放大动画 /// 图片放大动画
@@ -19,10 +19,10 @@ internal sealed class ImageZoomInAnimation : ImplicitAnimation<string, Vector3>
/// </summary> /// </summary>
public ImageZoomInAnimation() public ImageZoomInAnimation()
{ {
Duration = Constants.ImageZoom; Duration = ControlAnimationConstants.ImageZoom;
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut; EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle; EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
To = Constants.OnePointOne; To = ControlAnimationConstants.OnePointOne;
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -6,7 +6,7 @@ using CommunityToolkit.WinUI.Animations;
using Microsoft.UI.Composition; using Microsoft.UI.Composition;
using System.Numerics; using System.Numerics;
namespace Snap.Hutao.UI.Xaml.Media.Animation; namespace Snap.Hutao.Control.Animation;
/// <summary> /// <summary>
/// 图片缩小动画 /// 图片缩小动画
@@ -19,10 +19,10 @@ internal sealed class ImageZoomOutAnimation : ImplicitAnimation<string, Vector3>
/// </summary> /// </summary>
public ImageZoomOutAnimation() public ImageZoomOutAnimation()
{ {
Duration = Constants.ImageZoom; Duration = ControlAnimationConstants.ImageZoom;
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut; EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle; EasingType = CommunityToolkit.WinUI.Animations.EasingType.Circle;
To = Constants.One; To = ControlAnimationConstants.One;
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -0,0 +1,102 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Snap.Hutao.Control.Extension;
using System.Collections;
namespace Snap.Hutao.Control.AutoSuggestBox;
[DependencyProperty("FilterCommand", typeof(ICommand))]
[DependencyProperty("FilterCommandParameter", typeof(object))]
[DependencyProperty("AvailableTokens", typeof(IReadOnlyDictionary<string, SearchToken>))]
internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox
{
public AutoSuggestTokenBox()
{
DefaultStyleKey = typeof(TokenizingTextBox);
TextChanged += OnFilterSuggestionRequested;
QuerySubmitted += OnQuerySubmitted;
TokenItemAdding += OnTokenItemAdding;
TokenItemAdded += OnTokenItemCollectionChanged;
TokenItemRemoved += OnTokenItemCollectionChanged;
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (this.FindDescendant("SuggestionsPopup") is Popup { Child: Border { Child: ListView listView } border })
{
IAppResourceProvider appResourceProvider = this.ServiceProvider().GetRequiredService<IAppResourceProvider>();
listView.Background = null;
listView.Margin = appResourceProvider.GetResource<Thickness>("AutoSuggestListPadding");
border.Background = appResourceProvider.GetResource<Microsoft.UI.Xaml.Media.Brush>("AutoSuggestBoxSuggestionsListBackground");
CornerRadius overlayCornerRadius = appResourceProvider.GetResource<CornerRadius>("OverlayCornerRadius");
CornerRadiusFilterConverter cornerRadiusFilterConverter = new() { Filter = CornerRadiusFilterKind.Bottom };
border.CornerRadius = (CornerRadius)cornerRadiusFilterConverter.Convert(overlayCornerRadius, typeof(CornerRadius), default, default);
}
}
private void OnFilterSuggestionRequested(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if (string.IsNullOrWhiteSpace(Text))
{
sender.ItemsSource = AvailableTokens
.OrderBy(kvp => kvp.Value.Kind)
.Select(kvp => kvp.Value);
}
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
{
sender.ItemsSource = AvailableTokens
.Where(kvp => kvp.Value.Value.Contains(Text, StringComparison.OrdinalIgnoreCase))
.OrderBy(kvp => kvp.Value.Kind)
.ThenBy(kvp => kvp.Value.Order)
.Select(kvp => kvp.Value)
.DefaultIfEmpty(SearchToken.NotFound);
}
}
private void OnQuerySubmitted(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
if (args.ChosenSuggestion is not null)
{
return;
}
CommandInvocation.TryExecute(FilterCommand, FilterCommandParameter);
}
private void OnTokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
{
if (string.IsNullOrWhiteSpace(args.TokenText))
{
return;
}
if (AvailableTokens.GetValueOrDefault(args.TokenText) is { } token)
{
args.Item = token;
}
else
{
args.Cancel = true;
}
}
private void OnTokenItemCollectionChanged(TokenizingTextBox sender, object args)
{
if (args is SearchToken { Kind: SearchTokenKind.None } token)
{
((IList)sender.ItemsSource).Remove(token);
}
FilterCommand.TryExecute(FilterCommandParameter);
}
}

View File

@@ -3,7 +3,7 @@
using Windows.UI; using Windows.UI;
namespace Snap.Hutao.UI.Xaml.Control.AutoSuggestBox; namespace Snap.Hutao.Control.AutoSuggestBox;
internal sealed class SearchToken internal sealed class SearchToken
{ {

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
namespace Snap.Hutao.UI.Xaml.Control.AutoSuggestBox; namespace Snap.Hutao.Control.AutoSuggestBox;
internal enum SearchTokenKind internal enum SearchTokenKind
{ {

View File

@@ -3,9 +3,9 @@
using CommunityToolkit.WinUI.Behaviors; using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.UI.Input; using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.UI.Xaml.Behavior; namespace Snap.Hutao.Control.Behavior;
/// <summary> /// <summary>
/// 在元素加载完成后执行命令的行为 /// 在元素加载完成后执行命令的行为

View File

@@ -0,0 +1,46 @@
// 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

@@ -3,18 +3,22 @@
using CommunityToolkit.WinUI.Behaviors; using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.UI.Input; using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.UI.Xaml.Behavior; namespace Snap.Hutao.Control.Behavior;
[SuppressMessage("", "CA1001")]
[DependencyProperty("Period", typeof(TimeSpan))] [DependencyProperty("Period", typeof(TimeSpan))]
[DependencyProperty("Command", typeof(ICommand))] [DependencyProperty("Command", typeof(ICommand))]
[DependencyProperty("CommandParameter", typeof(object))] [DependencyProperty("CommandParameter", typeof(object))]
internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavior : BehaviorBase<FrameworkElement> internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavior : BehaviorBase<FrameworkElement>, IDisposable
{ {
private CancellationTokenSource acutalThemeChangedCts = new(); private TaskCompletionSource acutalThemeChangedTaskCompletionSource = new();
private CancellationTokenSource periodicTimerStopCts = new(); private CancellationTokenSource periodicTimerCancellationTokenSource = new();
public void Dispose()
{
periodicTimerCancellationTokenSource.Dispose();
}
protected override bool Initialize() protected override bool Initialize()
{ {
@@ -22,25 +26,22 @@ internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavio
return true; return true;
} }
protected override bool Uninitialize()
{
periodicTimerStopCts.Cancel();
periodicTimerStopCts.Dispose();
AssociatedObject.ActualThemeChanged -= OnActualThemeChanged;
acutalThemeChangedCts.Dispose();
return true;
}
protected override void OnAssociatedObjectLoaded() protected override void OnAssociatedObjectLoaded()
{ {
RunCoreAsync().SafeForget(); RunCoreAsync().SafeForget();
} }
protected override bool Uninitialize()
{
periodicTimerCancellationTokenSource.Cancel();
AssociatedObject.ActualThemeChanged -= OnActualThemeChanged;
return true;
}
private void OnActualThemeChanged(FrameworkElement sender, object args) private void OnActualThemeChanged(FrameworkElement sender, object args)
{ {
acutalThemeChangedCts.Cancel(); acutalThemeChangedTaskCompletionSource.TrySetResult();
periodicTimerCancellationTokenSource.Cancel();
} }
private void TryExecuteCommand() private void TryExecuteCommand()
@@ -64,7 +65,6 @@ internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavio
break; break;
} }
// TODO: Reconsider approach to get the ServiceProvider
ITaskContext taskContext = Ioc.Default.GetRequiredService<ITaskContext>(); ITaskContext taskContext = Ioc.Default.GetRequiredService<ITaskContext>();
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
TryExecuteCommand(); TryExecuteCommand();
@@ -72,23 +72,15 @@ internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavio
await taskContext.SwitchToBackgroundAsync(); await taskContext.SwitchToBackgroundAsync();
try try
{ {
using (CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(periodicTimerStopCts.Token, periodicTimerStopCts.Token)) Task nextTickTask = timer.WaitForNextTickAsync(periodicTimerCancellationTokenSource.Token).AsTask();
{ await Task.WhenAny(nextTickTask, acutalThemeChangedTaskCompletionSource.Task).ConfigureAwait(false);
await timer.WaitForNextTickAsync(linkedCts.Token).ConfigureAwait(false);
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
if (periodicTimerStopCts.IsCancellationRequested)
{
break;
}
} }
acutalThemeChangedCts.Dispose(); acutalThemeChangedTaskCompletionSource = new();
acutalThemeChangedCts = new(); periodicTimerCancellationTokenSource = new();
periodicTimerStopCts.Dispose();
periodicTimerStopCts = new();
} }
while (true); while (true);
} }

View File

@@ -5,7 +5,7 @@ using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Behaviors; using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.UI.Xaml.Behavior; namespace Snap.Hutao.Control.Behavior;
internal sealed class SelectedItemInViewBehavior : BehaviorBase<ListViewBase> internal sealed class SelectedItemInViewBehavior : BehaviorBase<ListViewBase>
{ {

View File

@@ -5,8 +5,11 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.Xaml.Interactivity; using Microsoft.Xaml.Interactivity;
namespace Snap.Hutao.UI.Xaml.Behavior.Action; namespace Snap.Hutao.Control.Behavior;
/// <summary>
/// 打开附着的浮出控件操作
/// </summary>
[HighQuality] [HighQuality]
internal sealed class ShowAttachedFlyoutAction : DependencyObject, IAction internal sealed class ShowAttachedFlyoutAction : DependencyObject, IAction
{ {

View File

@@ -5,7 +5,7 @@ using CommunityToolkit.WinUI.Animations;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.Xaml.Interactivity; using Microsoft.Xaml.Interactivity;
namespace Snap.Hutao.UI.Xaml.Behavior.Action; namespace Snap.Hutao.Control.Behavior;
[DependencyProperty("Animation", typeof(AnimationSet))] [DependencyProperty("Animation", typeof(AnimationSet))]
[DependencyProperty("TargetObject", typeof(UIElement))] [DependencyProperty("TargetObject", typeof(UIElement))]

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
namespace Snap.Hutao.UI.Xaml; namespace Snap.Hutao.Control;
/// <summary> /// <summary>
/// 绑定探针 /// 绑定探针

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.UI;
namespace Snap.Hutao.Control.Brush;
internal sealed class ColorSegment : IColorSegment
{
public ColorSegment(Color color, double value)
{
Color = color;
Value = value;
}
public Color Color { get; set; }
public double Value { get; set; }
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Brush;
internal sealed class ColorSegmentCollection : List<IColorSegment>
{
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.UI;
namespace Snap.Hutao.Control.Brush;
internal interface IColorSegment
{
Color Color { get; }
double Value { get; set; }
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Shapes;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Brush;
[DependencyProperty("Source", typeof(ColorSegmentCollection), default!, nameof(OnSourceChanged))]
internal sealed partial class SegmentedBar : ContentControl
{
private readonly LinearGradientBrush brush = new() { StartPoint = new(0, 0), EndPoint = new(1, 0), };
public SegmentedBar()
{
HorizontalContentAlignment = HorizontalAlignment.Stretch;
VerticalContentAlignment = VerticalAlignment.Stretch;
Content = new Rectangle()
{
Fill = brush,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
};
}
private static void OnSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
UpdateLinearGradientBrush((SegmentedBar)obj);
}
private static void UpdateLinearGradientBrush(SegmentedBar segmentedBar)
{
GradientStopCollection collection = segmentedBar.brush.GradientStops;
collection.Clear();
ColorSegmentCollection segmentCollection = segmentedBar.Source;
double total = segmentCollection.Sum(seg => seg.Value);
if (total is 0D)
{
return;
}
double offset = 0;
foreach (ref readonly IColorSegment segment in CollectionsMarshal.AsSpan(segmentCollection))
{
collection.Add(new() { Color = segment.Color, Offset = offset, });
offset += segment.Value / total;
collection.Add(new() { Color = segment.Color, Offset = offset, });
}
}
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal class ButtonBaseBuilder<TButton> : IButtonBaseBuilder<TButton>
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase, new()
{
public TButton Button { get; } = new();
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal static class ButtonBaseBuilderExtension
{
public static TBuilder SetContent<TBuilder, TButton>(this TBuilder builder, object? content)
where TBuilder : IButtonBaseBuilder<TButton>
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
{
builder.Configure(builder => builder.Button.Content = content);
return builder;
}
public static TBuilder SetCommand<TBuilder, TButton>(this TBuilder builder, ICommand command)
where TBuilder : IButtonBaseBuilder<TButton>
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
{
builder.Configure(builder => builder.Button.Command = command);
return builder;
}
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal sealed class ButtonBuilder : ButtonBaseBuilder<Button>;

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal static class ButtonBuilderExtension
{
public static ButtonBuilder SetContent(this ButtonBuilder builder, object? content)
{
return builder.SetContent<ButtonBuilder, Button>(content);
}
public static ButtonBuilder SetCommand(this ButtonBuilder builder, ICommand command)
{
return builder.SetCommand<ButtonBuilder, Button>(command);
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal interface IButtonBaseBuilder<TButton> : IBuilder
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
{
TButton Button { get; }
}

View File

@@ -7,33 +7,41 @@ using Microsoft.UI.Xaml.Data;
using System.Collections; using System.Collections;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Windows.Foundation; using Windows.Foundation;
using Windows.Foundation.Collections; using Windows.Foundation.Collections;
using NotifyCollectionChangedAction = System.Collections.Specialized.NotifyCollectionChangedAction; using NotifyCollectionChangedAction = System.Collections.Specialized.NotifyCollectionChangedAction;
namespace Snap.Hutao.UI.Xaml.Data; namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPropertyChanged, ISupportIncrementalLoading, IComparer<T> internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPropertyChanged, ISupportIncrementalLoading, IComparer<object>
where T : class, IAdvancedCollectionViewItem where T : class
{ {
private readonly bool created;
private readonly List<T> view; private readonly List<T> view;
private readonly ObservableCollection<SortDescription> sortDescriptions; private readonly ObservableCollection<SortDescription> sortDescriptions;
private readonly Dictionary<string, PropertyInfo?> sortProperties;
private readonly bool liveShapingEnabled;
private readonly HashSet<string?> observedFilterProperties = [];
private IList<T> source; private IList<T> source;
private Predicate<T>? filter; private Predicate<T>? filter;
private int deferCounter; private int deferCounter;
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener; private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
public AdvancedCollectionView(IList<T> source) public AdvancedCollectionView()
: this([])
{ {
}
public AdvancedCollectionView(IList<T> source, bool isLiveShaping = false)
{
liveShapingEnabled = isLiveShaping;
view = []; view = [];
sortDescriptions = []; sortDescriptions = [];
sortDescriptions.CollectionChanged += SortDescriptionsCollectionChanged; sortDescriptions.CollectionChanged += SortDescriptionsCollectionChanged;
sortProperties = [];
Source = source; Source = source;
created = true;
} }
public event EventHandler<object>? CurrentChanged; public event EventHandler<object>? CurrentChanged;
@@ -44,55 +52,7 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
public event VectorChangedEventHandler<object>? VectorChanged; public event VectorChangedEventHandler<object>? VectorChanged;
public int Count public IList<T> Source
{
get => view.Count;
}
[Obsolete("IsReadOnly is not supported")]
public bool IsReadOnly { get => source is null; }
public IObservableVector<object> CollectionGroups
{
get => default!;
}
public T? CurrentItem
{
get => CurrentPosition > -1 && CurrentPosition < view.Count ? view[CurrentPosition] : default;
set => MoveCurrentTo(value);
}
public int CurrentPosition { get; private set; }
public bool HasMoreItems { get => source is ISupportIncrementalLoading { HasMoreItems: true }; }
public bool IsCurrentAfterLast { get => CurrentPosition >= view.Count; }
public bool IsCurrentBeforeFirst { get => CurrentPosition < 0; }
public Predicate<T>? Filter
{
get => filter;
set
{
if (filter == value)
{
return;
}
filter = value;
HandleFilterChanged();
}
}
public ObservableCollection<SortDescription> SortDescriptions { get => sortDescriptions; }
public IList<T> SourceCollection { get => source; }
public List<T> View { get => view; }
private IList<T> Source
{ {
get => source; get => source;
@@ -114,26 +74,106 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
sourceWeakEventListener?.Detach(); sourceWeakEventListener?.Detach();
if (source is INotifyCollectionChanged sourceINCC) if (source is INotifyCollectionChanged sourceNotifyCollectionChanged)
{ {
sourceWeakEventListener = new(this) sourceWeakEventListener = new WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>(this)
{ {
OnEventAction = OnSourceNotifyCollectionCollectionChanged, // Call the actual collection changed event
OnDetachAction = listener => sourceINCC.CollectionChanged -= listener.OnEvent, OnEventAction = (source, changed, arg3) => SourceNotifyCollectionChangedCollectionChanged(source, arg3),
// The source doesn't exist anymore
OnDetachAction = (listener) =>
{
ArgumentNullException.ThrowIfNull(sourceWeakEventListener);
sourceNotifyCollectionChanged.CollectionChanged -= sourceWeakEventListener.OnEvent;
},
}; };
sourceINCC.CollectionChanged += sourceWeakEventListener.OnEvent; sourceNotifyCollectionChanged.CollectionChanged += sourceWeakEventListener.OnEvent;
} }
HandleSourceChanged(); HandleSourceChanged();
OnPropertyChanged(); OnPropertyChanged();
static void OnSourceNotifyCollectionCollectionChanged(AdvancedCollectionView<T> target, object? source, NotifyCollectionChangedEventArgs args)
{
target.SourceNotifyCollectionChangedCollectionChanged(args);
}
} }
} }
public int Count
{
get => view.Count;
}
public bool IsReadOnly
{
get => source is null || source.IsReadOnly;
}
public IObservableVector<object> CollectionGroups
{
get => default!;
}
public T? CurrentItem
{
get => CurrentPosition > -1 && CurrentPosition < view.Count ? view[CurrentPosition] : default;
set => MoveCurrentTo(value);
}
public int CurrentPosition { get; private set; }
public bool HasMoreItems
{
get => source is ISupportIncrementalLoading { HasMoreItems: true };
}
public bool IsCurrentAfterLast
{
get => CurrentPosition >= view.Count;
}
public bool IsCurrentBeforeFirst
{
get => CurrentPosition < 0;
}
public bool CanFilter
{
get => true;
}
public Predicate<T>? Filter
{
get => filter;
set
{
if (filter == value)
{
return;
}
filter = value;
HandleFilterChanged();
}
}
public bool CanSort
{
get => true;
}
public IList<SortDescription> SortDescriptions
{
get => sortDescriptions;
}
public IEnumerable<T> SourceCollection
{
get => source;
}
public IReadOnlyList<T> View
{
get => view;
}
public T this[int index] public T this[int index]
{ {
get => view[index]; get => view[index];
@@ -187,12 +227,14 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
public bool Remove(T item) public bool Remove(T item)
{ {
return source.Remove(item); source.Remove(item);
return true;
} }
public int IndexOf(T item) [SuppressMessage("", "SH007")]
public int IndexOf(T? item)
{ {
return view.IndexOf(item); return view.IndexOf(item!);
} }
public void Insert(int index, T item) public void Insert(int index, T item)
@@ -205,10 +247,9 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
Remove(view[index]); Remove(view[index]);
} }
[SuppressMessage("", "SH007")]
public bool MoveCurrentTo(T? item) public bool MoveCurrentTo(T? item)
{ {
return (item is not null && item.Equals(CurrentItem)) || MoveCurrentToIndex(IndexOf(item!)); return (item is not null && item.Equals(CurrentItem)) || MoveCurrentToIndex(IndexOf(item));
} }
public bool MoveCurrentToPosition(int index) public bool MoveCurrentToPosition(int index)
@@ -241,13 +282,46 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
return (source as ISupportIncrementalLoading)?.LoadMoreItemsAsync(count); return (source as ISupportIncrementalLoading)?.LoadMoreItemsAsync(count);
} }
public void ObserveFilterProperty(string propertyName)
{
observedFilterProperties.Add(propertyName);
}
public void ClearObservedFilterProperties()
{
observedFilterProperties.Clear();
}
public IDisposable DeferRefresh() public IDisposable DeferRefresh()
{ {
return new NotificationDeferrer(this); return new NotificationDeferrer(this);
} }
int IComparer<T>.Compare(T? x, T? y) int IComparer<object>.Compare(object? x, object? y)
{ {
if (sortProperties.Count <= 0)
{
Type listType = source.GetType();
Type? type;
if (listType.IsGenericType)
{
type = listType.GetGenericArguments()[0];
}
else
{
type = x?.GetType();
}
foreach (SortDescription sd in sortDescriptions)
{
if (!string.IsNullOrEmpty(sd.PropertyName))
{
sortProperties[sd.PropertyName] = type?.GetProperty(sd.PropertyName);
}
}
}
foreach (SortDescription sd in sortDescriptions) foreach (SortDescription sd in sortDescriptions)
{ {
object? cx, cy; object? cx, cy;
@@ -259,8 +333,10 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
} }
else else
{ {
cx = x?.GetPropertyValue(sd.PropertyName); PropertyInfo? pi = sortProperties[sd.PropertyName];
cy = y?.GetPropertyValue(sd.PropertyName);
cx = pi?.GetValue(x);
cy = pi?.GetValue(y);
} }
int cmp = sd.Comparer.Compare(cx, cy); int cmp = sd.Comparer.Compare(cx, cy);
@@ -274,63 +350,77 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
return 0; return 0;
} }
protected virtual void OnCurrentChangedOverride() internal void OnPropertyChanged([CallerMemberName] string propertyName = default!)
{ {
}
private void OnPropertyChanged([CallerMemberName] string propertyName = default!)
{
if (!created)
{
return;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
private void ItemOnPropertyChanged(object? item, PropertyChangedEventArgs e) private void ItemOnPropertyChanged(object? item, PropertyChangedEventArgs e)
{ {
if (!liveShapingEnabled)
{
return;
}
ArgumentNullException.ThrowIfNull(item); ArgumentNullException.ThrowIfNull(item);
T typedItem = (T)item; T typedItem = (T)item;
if (!(filter?.Invoke(typedItem) ?? true) || !SortDescriptions.Any(sd => sd.PropertyName == e.PropertyName)) bool? filterResult = filter?.Invoke(typedItem);
if (filterResult.HasValue && observedFilterProperties.Contains(e.PropertyName))
{ {
return; int viewIndex = view.IndexOf(typedItem);
if (viewIndex != -1 && !filterResult.Value)
{
RemoveFromView(viewIndex, typedItem);
}
else if (viewIndex == -1 && filterResult.Value)
{
int index = source.IndexOf(typedItem);
HandleItemAdded(index, typedItem);
}
} }
int oldIndex = view.IndexOf(typedItem); if ((filterResult ?? true) && SortDescriptions.Any(sd => sd.PropertyName == e.PropertyName))
// Check if item is in view
if (oldIndex < 0)
{ {
return; int oldIndex = view.IndexOf(typedItem);
// Check if item is in view:
if (oldIndex < 0)
{
return;
}
view.RemoveAt(oldIndex);
int targetIndex = view.BinarySearch(typedItem, this);
if (targetIndex < 0)
{
targetIndex = ~targetIndex;
}
// Only trigger expensive UI updates if the index really changed:
if (targetIndex != oldIndex)
{
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, oldIndex, typedItem));
view.Insert(targetIndex, typedItem);
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, targetIndex, typedItem));
}
else
{
view.Insert(targetIndex, typedItem);
}
} }
else if (string.IsNullOrEmpty(e.PropertyName))
view.RemoveAt(oldIndex);
int targetIndex = view.BinarySearch(typedItem, comparer: this);
if (targetIndex < 0)
{ {
targetIndex = ~targetIndex; HandleSourceChanged();
}
// Only trigger expensive UI updates if the index really changed
if (targetIndex != oldIndex)
{
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, oldIndex, typedItem));
view.Insert(targetIndex, typedItem);
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, targetIndex, typedItem));
}
else
{
view.Insert(targetIndex, typedItem);
} }
} }
private void AttachPropertyChangedHandler(IEnumerable items) private void AttachPropertyChangedHandler(IEnumerable items)
{ {
if (items is null) if (!liveShapingEnabled || items is null)
{ {
return; return;
} }
@@ -346,7 +436,7 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
private void DetachPropertyChangedHandler(IEnumerable items) private void DetachPropertyChangedHandler(IEnumerable items)
{ {
if (items is null) if (!liveShapingEnabled || items is null)
{ {
return; return;
} }
@@ -362,7 +452,9 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
private void HandleSortChanged() private void HandleSortChanged()
{ {
sortProperties.Clear();
view.Sort(this); view.Sort(this);
sortProperties.Clear();
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset)); OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
} }
@@ -383,18 +475,18 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
} }
} }
HashSet<T> viewSet = new(view); HashSet<T> viewHash = new(view);
int viewIndex = 0; int viewIndex = 0;
for (int index = 0; index < source.Count; index++) for (int index = 0; index < source.Count; index++)
{ {
T item = source[index]; T item = source[index];
if (viewSet.Contains(item)) if (viewHash.Contains(item))
{ {
viewIndex++; viewIndex++;
continue; continue;
} }
if (HandleSourceItemAdded(index, item, viewIndex)) if (HandleItemAdded(index, item, viewIndex))
{ {
viewIndex++; viewIndex++;
} }
@@ -403,114 +495,101 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
private void HandleSourceChanged() private void HandleSourceChanged()
{ {
sortProperties.Clear();
T? currentItem = CurrentItem; T? currentItem = CurrentItem;
view.Clear(); view.Clear();
view.TrimExcess(); foreach (T item in Source)
if (filter is null && sortDescriptions.Count <= 0)
{ {
// Fast path if (filter is not null && !filter(item))
View.AddRange(Source);
}
else
{
foreach (T item in Source)
{ {
if (filter is not null && !filter(item)) continue;
}
if (sortDescriptions.Count > 0)
{
int targetIndex = view.BinarySearch(item, this);
if (targetIndex < 0)
{ {
continue; targetIndex = ~targetIndex;
} }
if (sortDescriptions.Count > 0) view.Insert(targetIndex, item);
{ }
int targetIndex = view.BinarySearch(item, this); else
if (targetIndex < 0) {
{ view.Add(item);
targetIndex = ~targetIndex;
}
view.Insert(targetIndex, item);
}
else
{
view.Add(item);
}
} }
} }
sortProperties.Clear();
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset)); OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
MoveCurrentTo(currentItem); MoveCurrentTo(currentItem);
} }
private void SourceNotifyCollectionChangedCollectionChanged(NotifyCollectionChangedEventArgs e) private void SourceNotifyCollectionChangedCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{ {
switch (e.Action) switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
ArgumentNullException.ThrowIfNull(e.NewItems); ArgumentNullException.ThrowIfNull(e.NewItems);
AttachPropertyChangedHandler(e.NewItems); AttachPropertyChangedHandler(e.NewItems);
if (deferCounter > 0) if (deferCounter <= 0)
{ {
break; if (e.NewItems?.Count == 1)
} {
object? newItem = e.NewItems[0];
if (e.NewItems?.Count is 1) ArgumentNullException.ThrowIfNull(newItem);
{ HandleItemAdded(e.NewStartingIndex, (T)newItem);
object? newItem = e.NewItems[0]; }
ArgumentNullException.ThrowIfNull(newItem); else
HandleSourceItemAdded(e.NewStartingIndex, (T)newItem); {
} HandleSourceChanged();
else }
{
HandleSourceChanged();
} }
break; break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
ArgumentNullException.ThrowIfNull(e.OldItems); ArgumentNullException.ThrowIfNull(e.OldItems);
DetachPropertyChangedHandler(e.OldItems); DetachPropertyChangedHandler(e.OldItems);
if (deferCounter > 0) if (deferCounter <= 0)
{ {
break; if (e.OldItems?.Count == 1)
} {
object? oldItem = e.OldItems[0];
if (e.OldItems?.Count == 1) ArgumentNullException.ThrowIfNull(oldItem);
{ HandleItemRemoved(e.OldStartingIndex, (T)oldItem);
object? oldItem = e.OldItems[0]; }
ArgumentNullException.ThrowIfNull(oldItem); else
HandleSourceItemRemoved(e.OldStartingIndex, (T)oldItem); {
} HandleSourceChanged();
else }
{
HandleSourceChanged();
} }
break; break;
case NotifyCollectionChangedAction.Move: case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Reset:
if (deferCounter > 0) if (deferCounter <= 0)
{ {
break; HandleSourceChanged();
} }
HandleSourceChanged();
break; break;
} }
} }
private bool HandleSourceItemAdded(int newStartingIndex, T newItem, int? viewIndex = null) private bool HandleItemAdded(int newStartingIndex, T newItem, int? viewIndex = null)
{ {
if (filter is not null && !filter(newItem)) if (filter is not null && !filter(newItem))
{ {
return false; return false;
} }
int newViewIndex = newStartingIndex; int newViewIndex = view.Count;
if (sortDescriptions.Count > 0) if (sortDescriptions.Count > 0)
{ {
sortProperties.Clear();
newViewIndex = view.BinarySearch(newItem, this); newViewIndex = view.BinarySearch(newItem, this);
if (newViewIndex < 0) if (newViewIndex < 0)
{ {
@@ -565,7 +644,7 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
return true; return true;
} }
private void HandleSourceItemRemoved(int oldStartingIndex, T oldItem) private void HandleItemRemoved(int oldStartingIndex, T oldItem)
{ {
if (filter is not null && !filter(oldItem)) if (filter is not null && !filter(oldItem))
{ {
@@ -591,12 +670,6 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
if (itemIndex <= CurrentPosition) if (itemIndex <= CurrentPosition)
{ {
CurrentPosition--; CurrentPosition--;
// Removed item is last item
if (view.Count == itemIndex)
{
OnCurrentChanged();
}
} }
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, itemIndex, item)); OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, itemIndex, item));
@@ -614,57 +687,52 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
private bool MoveCurrentToIndex(int i) private bool MoveCurrentToIndex(int i)
{ {
if (i < -1 || i >= view.Count)
{
return false;
}
if (i == CurrentPosition) if (i == CurrentPosition)
{ {
return false; return false;
} }
if (i < -1 || i >= view.Count) CurrentChangingEventArgs e = new();
{ OnCurrentChanging(e);
// view is empty, i is 0, current pos is -1 if (e.Cancel)
OnPropertyChanged(nameof(CurrentItem));
return false;
}
OnCurrentChanging(out bool cancel);
if (cancel)
{ {
return false; return false;
} }
CurrentPosition = i; CurrentPosition = i;
OnCurrentChanged(); OnCurrentChanged(default!);
return true; return true;
} }
private void OnCurrentChanging(out bool cancel) private void OnCurrentChanging(CurrentChangingEventArgs e)
{ {
if (!created || deferCounter > 0) if (deferCounter > 0)
{ {
cancel = false;
return; return;
} }
CurrentChangingEventArgs e = new();
CurrentChanging?.Invoke(this, e); CurrentChanging?.Invoke(this, e);
cancel = e.Cancel;
} }
private void OnCurrentChanged() private void OnCurrentChanged(object e)
{ {
if (!created || deferCounter > 0) if (deferCounter > 0)
{ {
return; return;
} }
OnCurrentChangedOverride(); CurrentChanged?.Invoke(this, e);
CurrentChanged?.Invoke(this, default!);
OnPropertyChanged(nameof(CurrentItem)); OnPropertyChanged(nameof(CurrentItem));
} }
private void OnVectorChanged(IVectorChangedEventArgs e) private void OnVectorChanged(IVectorChangedEventArgs e)
{ {
if (!created || deferCounter > 0) if (deferCounter > 0)
{ {
return; return;
} }

View File

@@ -2,16 +2,18 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections; using CommunityToolkit.WinUI.Collections;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using System.Collections; using System.Collections;
using System.Collections.ObjectModel;
namespace Snap.Hutao.UI.Xaml.Data; namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
where T : class where T : class
{ {
bool CanFilter { get; }
bool CanSort { get; }
object? ICollectionView.CurrentItem object? ICollectionView.CurrentItem
{ {
get => CurrentItem; get => CurrentItem;
@@ -21,9 +23,9 @@ internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
Predicate<T>? Filter { get; set; } Predicate<T>? Filter { get; set; }
ObservableCollection<SortDescription> SortDescriptions { get; } IList<SortDescription> SortDescriptions { get; }
IList<T> SourceCollection { get; } IEnumerable<T> SourceCollection { get; }
object IList<object>.this[int index] object IList<object>.this[int index]
{ {
@@ -40,6 +42,8 @@ internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
void Add(T item); void Add(T item);
void ClearObservedFilterProperties();
bool ICollection<object>.Contains(object item) bool ICollection<object>.Contains(object item)
{ {
return Contains((T)item); return Contains((T)item);
@@ -65,18 +69,7 @@ internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
int IList<object>.IndexOf(object item) int IList<object>.IndexOf(object item)
{ {
if (item is T dataItem1) return IndexOf((T)item);
{
return IndexOf(dataItem1);
}
// WinUI somehow pass in a FrameworkElement with DataContext as actual item
if (item is FrameworkElement { DataContext: T dataItem2 })
{
return IndexOf(dataItem2);
}
return IndexOf(default!);
} }
int IndexOf(T item); int IndexOf(T item);
@@ -93,7 +86,9 @@ internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
return MoveCurrentTo((T)item); return MoveCurrentTo((T)item);
} }
bool MoveCurrentTo(T? item); bool MoveCurrentTo(T item);
void ObserveFilterProperty(string propertyName);
void Refresh(); void Refresh();

View File

@@ -3,11 +3,11 @@
using Windows.Foundation.Collections; using Windows.Foundation.Collections;
namespace Snap.Hutao.UI.Xaml.Data; namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
internal sealed class VectorChangedEventArgs : IVectorChangedEventArgs internal sealed class VectorChangedEventArgs : IVectorChangedEventArgs
{ {
public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = default!) public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = null!)
{ {
CollectionChange = cc; CollectionChange = cc;
Index = (uint)index; Index = (uint)index;

View File

@@ -0,0 +1,39 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation.Collections;
namespace Snap.Hutao.Control.Collection.Alternating;
[Obsolete("Use SettingsCard instead")]
[DependencyProperty("ItemAlternateBackground", typeof(Microsoft.UI.Xaml.Media.Brush))]
internal sealed partial class AlternatingItemsControl : ItemsControl
{
private readonly VectorChangedEventHandler<object> itemsVectorChangedEventHandler;
public AlternatingItemsControl()
{
itemsVectorChangedEventHandler = OnItemsVectorChanged;
Items.VectorChanged += itemsVectorChangedEventHandler;
}
private void OnItemsVectorChanged(IObservableVector<object> items, IVectorChangedEventArgs args)
{
if (args.CollectionChange is CollectionChange.Reset)
{
int index = (int)args.Index;
for (int i = index; i < items.Count; i++)
{
if (items[i] is IAlternatingItem item)
{
item.Background = i % 2 is 0 ? default : ItemAlternateBackground;
}
else
{
break;
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Collection.Alternating;
[Obsolete("Use SettingsCard instead")]
internal interface IAlternatingItem
{
public Microsoft.UI.Xaml.Media.Brush? Background { get; set; }
}

View File

@@ -5,18 +5,19 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control.Collection.Selector;
[DependencyProperty("EnableMemberPath", typeof(string))] [DependencyProperty("EnableMemberPath", typeof(string))]
internal sealed partial class ComboBox2 : ComboBox internal sealed partial class ComboBox2 : ComboBox
{ {
protected override void PrepareContainerForItemOverride(DependencyObject element, object item) protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{ {
base.PrepareContainerForItemOverride(element, item);
if (element is ComboBoxItem comboBoxItem) if (element is ComboBoxItem comboBoxItem)
{ {
comboBoxItem.SetBinding(IsEnabledProperty, new Binding() { Path = new(EnableMemberPath) }); Binding binding = new() { Path = new(EnableMemberPath) };
comboBoxItem.SetBinding(IsEnabledProperty, binding);
} }
base.PrepareContainerForItemOverride(element, item);
} }
} }

View File

@@ -3,26 +3,43 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.UI.Xaml.Data.Converter; namespace Snap.Hutao.Control;
/// <summary>
/// 依赖对象转换器
/// </summary>
/// <typeparam name="TFrom">源类型</typeparam>
/// <typeparam name="TTo">目标类型</typeparam>
internal abstract class DependencyValueConverter<TFrom, TTo> : DependencyObject, IValueConverter internal abstract class DependencyValueConverter<TFrom, TTo> : DependencyObject, IValueConverter
{ {
/// <inheritdoc/>
public object? Convert(object value, Type targetType, object parameter, string language) public object? Convert(object value, Type targetType, object parameter, string language)
{ {
return Convert((TFrom)value); return Convert((TFrom)value);
} }
/// <inheritdoc/>
public object? ConvertBack(object value, Type targetType, object parameter, string language) public object? ConvertBack(object value, Type targetType, object parameter, string language)
{ {
return ConvertBack((TTo)value); return ConvertBack((TTo)value);
} }
/// <summary>
/// 从源类型转换到目标类型
/// </summary>
/// <param name="from">源</param>
/// <returns>目标</returns>
public abstract TTo Convert(TFrom from); public abstract TTo Convert(TFrom from);
/// <summary>
/// 从目标类型转换到源类型
/// 重写时请勿调用基类方法
/// </summary>
/// <param name="to">目标</param>
/// <returns>源</returns>
public virtual TFrom ConvertBack(TTo to) public virtual TFrom ConvertBack(TTo to)
{ {
throw HutaoException.NotSupported(); throw Must.NeverHappen();
} }
} }

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
namespace Snap.Hutao.UI.Input; namespace Snap.Hutao.Control.Extension;
internal static class CommandInvocation internal static class CommandInvocation
{ {

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control.Extension;
/// <summary> /// <summary>
/// 对话框扩展 /// 对话框扩展
@@ -17,13 +17,13 @@ internal static class ContentDialogExtension
/// <param name="contentDialog">对话框</param> /// <param name="contentDialog">对话框</param>
/// <param name="taskContext">任务上下文</param> /// <param name="taskContext">任务上下文</param>
/// <returns>用于恢复用户交互</returns> /// <returns>用于恢复用户交互</returns>
public static async ValueTask<ContentDialogScope> BlockAsync(this ContentDialog contentDialog, ITaskContext taskContext) public static async ValueTask<ContentDialogHideToken> BlockAsync(this ContentDialog contentDialog, ITaskContext taskContext)
{ {
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
// E_ASYNC_OPERATION_NOT_STARTED 0x80000019 // E_ASYNC_OPERATION_NOT_STARTED 0x80000019
// Only a single ContentDialog can be open at any time. // Only a single ContentDialog can be open at any time.
_ = contentDialog.ShowAsync(); contentDialog.ShowAsync().AsTask().SafeForget();
return new ContentDialogScope(contentDialog, taskContext); return new ContentDialogHideToken(contentDialog, taskContext);
} }
} }

View File

@@ -3,9 +3,9 @@
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control.Extension;
internal struct ContentDialogScope : IDisposable, IAsyncDisposable internal struct ContentDialogHideToken : IDisposable, IAsyncDisposable
{ {
private readonly ContentDialog contentDialog; private readonly ContentDialog contentDialog;
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
@@ -13,7 +13,7 @@ internal struct ContentDialogScope : IDisposable, IAsyncDisposable
private bool disposing = false; private bool disposing = false;
private bool disposed = false; private bool disposed = false;
public ContentDialogScope(ContentDialog contentDialog, ITaskContext taskContext) public ContentDialogHideToken(ContentDialog contentDialog, ITaskContext taskContext)
{ {
this.contentDialog = contentDialog; this.contentDialog = contentDialog;
this.taskContext = taskContext; this.taskContext = taskContext;

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Snap.Hutao.UI.Xaml; namespace Snap.Hutao.Control.Extension;
internal static class DependencyObjectExtension internal static class DependencyObjectExtension
{ {

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
namespace Snap.Hutao.UI.Xaml; namespace Snap.Hutao.Control.Extension;
internal static class FrameworkElementExtension internal static class FrameworkElementExtension
{ {
@@ -28,20 +28,4 @@ internal static class FrameworkElementExtension
frameworkElement.IsRightTapEnabled = false; frameworkElement.IsRightTapEnabled = false;
frameworkElement.IsTabStop = false; frameworkElement.IsTabStop = false;
} }
public static void InitializeDataContext<TDataContext>(this FrameworkElement frameworkElement, IServiceProvider? serviceProvider = default)
where TDataContext : class
{
IServiceProvider service = serviceProvider ?? Ioc.Default;
try
{
frameworkElement.DataContext = service.GetRequiredService<TDataContext>();
}
catch (Exception ex)
{
ILogger? logger = service.GetRequiredService(typeof(ILogger<>).MakeGenericType([frameworkElement.GetType()])) as ILogger;
logger?.LogError(ex, "Failed to initialize DataContext");
throw;
}
}
} }

View File

@@ -1,11 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using System.Diagnostics; using System.Diagnostics;
namespace Snap.Hutao.Web.WebView2; namespace Snap.Hutao.Control.Extension;
/// <summary>
/// Bridge 拓展
/// </summary>
[HighQuality]
internal static class WebView2Extension internal static class WebView2Extension
{ {
[Conditional("RELEASE")] [Conditional("RELEASE")]
@@ -34,7 +39,7 @@ internal static class WebView2Extension
} }
} }
public static bool IsDisposed(this Microsoft.UI.Xaml.Controls.WebView2 webView2) public static bool IsDisposed(this WebView2 webView2)
{ {
return WinRTExtension.IsDisposed(webView2); return WinRTExtension.IsDisposed(webView2);
} }

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
namespace Snap.Hutao.UI.Xaml; namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")] [SuppressMessage("", "SH001")]
[DependencyProperty("SquareLength", typeof(double), 0D, nameof(OnSquareLengthChanged), IsAttached = true, AttachedType = typeof(FrameworkElement))] [DependencyProperty("SquareLength", typeof(double), 0D, nameof(OnSquareLengthChanged), IsAttached = true, AttachedType = typeof(FrameworkElement))]

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")] [SuppressMessage("", "SH001")]
[DependencyProperty("IsTextSelectionEnabled", typeof(bool), false, IsAttached = true, AttachedType = typeof(InfoBar))] [DependencyProperty("IsTextSelectionEnabled", typeof(bool), false, IsAttached = true, AttachedType = typeof(InfoBar))]

View File

@@ -5,7 +5,7 @@ using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")] [SuppressMessage("", "SH001")]
[DependencyProperty("PaneCornerRadius", typeof(CornerRadius), default, nameof(OnPaneCornerRadiusChanged), IsAttached = true, AttachedType = typeof(NavigationView))] [DependencyProperty("PaneCornerRadius", typeof(CornerRadius), default, nameof(OnPaneCornerRadiusChanged), IsAttached = true, AttachedType = typeof(NavigationView))]
@@ -18,27 +18,22 @@ public sealed partial class NavigationViewHelper
if (navigationView.IsLoaded) if (navigationView.IsLoaded)
{ {
SetLoadedNavigationViewPaneCornerRadius(navigationView, newValue); SetNavigationViewPaneCornerRadius(navigationView, newValue);
return; return;
} }
navigationView.Loaded += SetNavigationViewPaneCornerRadius; navigationView.Loaded += (s, e) =>
{
NavigationView loadedNavigationView = (NavigationView)s;
SetNavigationViewPaneCornerRadius(loadedNavigationView, newValue);
};
} }
private static void SetNavigationViewPaneCornerRadius(object sender, RoutedEventArgs args) private static void SetNavigationViewPaneCornerRadius(NavigationView navigationView, CornerRadius value)
{
NavigationView navigationView = (NavigationView)sender;
CornerRadius value = GetPaneCornerRadius(navigationView);
SetLoadedNavigationViewPaneCornerRadius(navigationView, value);
navigationView.Loaded -= SetNavigationViewPaneCornerRadius;
}
private static void SetLoadedNavigationViewPaneCornerRadius(NavigationView navigationView, CornerRadius value)
{ {
if (navigationView.FindDescendant("RootSplitView") is SplitView splitView) if (navigationView.FindDescendant("RootSplitView") is SplitView splitView)
{ {
splitView.CornerRadius = value; splitView.CornerRadius = value;
} }
} }
} }

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")] [SuppressMessage("", "SH001")]
[DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))] [DependencyProperty("RightPanel", typeof(UIElement), IsAttached = true, AttachedType = typeof(ScrollViewer))]

View File

@@ -4,7 +4,7 @@
using CommunityToolkit.WinUI.Controls; using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")] [SuppressMessage("", "SH001")]
[DependencyProperty("IsItemsEnabled", typeof(bool), true, nameof(OnIsItemsEnabledChanged), IsAttached = true, AttachedType = typeof(SettingsExpander))] [DependencyProperty("IsItemsEnabled", typeof(bool), true, nameof(OnIsItemsEnabledChanged), IsAttached = true, AttachedType = typeof(SettingsExpander))]

View File

@@ -3,11 +3,10 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
namespace Snap.Hutao.UI.Xaml; namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")] [SuppressMessage("", "SH001")]
[DependencyProperty("VisibilityObject", typeof(object), null, nameof(OnVisibilityObjectChanged), IsAttached = true, AttachedType = typeof(UIElement))] [DependencyProperty("VisibilityObject", typeof(object), null, nameof(OnVisibilityObjectChanged), IsAttached = true, AttachedType = typeof(UIElement))]
[DependencyProperty("VisibilityBoolean", typeof(bool), null, nameof(OnVisibilityBooleanChanged), IsAttached = true, AttachedType = typeof(UIElement))]
[DependencyProperty("OpacityObject", typeof(object), null, nameof(OnOpacityObjectChanged), IsAttached = true, AttachedType = typeof(UIElement))] [DependencyProperty("OpacityObject", typeof(object), null, nameof(OnOpacityObjectChanged), IsAttached = true, AttachedType = typeof(UIElement))]
public sealed partial class UIElementHelper public sealed partial class UIElementHelper
{ {
@@ -22,10 +21,4 @@ public sealed partial class UIElementHelper
UIElement element = (UIElement)dp; UIElement element = (UIElement)dp;
element.Opacity = e.NewValue is null ? 0D : 1D; element.Opacity = e.NewValue is null ? 0D : 1D;
} }
private static void OnVisibilityBooleanChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)dp;
element.Visibility = e.NewValue is true ? Visibility.Visible : Visibility.Collapsed;
}
} }

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control;
internal interface IScopedPageScopeReferenceTracker : IDisposable internal interface IScopedPageScopeReferenceTracker : IDisposable
{ {

View File

@@ -1,6 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
namespace Snap.Hutao.UI.Xaml; namespace Snap.Hutao.Control;
internal interface IXamlElementAccessor; internal interface IXamlElementAccessor;

View File

@@ -0,0 +1,50 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.ExceptionService;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Image;
/// <summary>
/// 缓存图像
/// </summary>
[HighQuality]
internal sealed class CachedImage : Implementation.ImageEx
{
/// <summary>
/// 构造一个新的缓存图像
/// </summary>
public CachedImage()
{
DefaultStyleKey = typeof(CachedImage);
DefaultStyleResourceUri = "ms-appx:///Control/Image/CachedImage.xaml".ToUri();
IsCacheEnabled = true;
EnableLazyLoading = false;
}
/// <inheritdoc/>
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
try
{
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), HutaoExceptionKind.ImageCacheInvalidUri, SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.
}
catch (COMException)
{
// The image is corrupted, remove it.
imageCache.Remove(imageUri);
return default;
}
}
}

View File

@@ -1,27 +1,20 @@
<ResourceDictionary <ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shuxci="using:Snap.Hutao.UI.Xaml.Control.Image" xmlns:shci="using:Snap.Hutao.Control.Image">
xmlns:shuxm="using:Snap.Hutao.UI.Xaml.Markup"> <Style TargetType="shci:CachedImage">
<Style TargetType="shuxci:CachedImage">
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}"/> <Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}"/>
<Setter Property="IsTabStop" Value="False"/> <Setter Property="IsTabStop" Value="False"/>
<Setter Property="LazyLoadingThreshold" Value="256"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="shuxci:CachedImage"> <ControlTemplate TargetType="shci:CachedImage">
<Grid <Grid
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem IsEnabled="False" Text="{TemplateBinding SourceName}"/>
<MenuFlyoutItem IsEnabled="False" Text="{TemplateBinding CachedName}"/>
<MenuFlyoutItem Command="{Binding CopyToClipboardCommand, RelativeSource={RelativeSource TemplatedParent}}" Text="{shuxm:ResourceString Name=UIXamlControlCachedImageCopyImage}"/>
</MenuFlyout>
</Grid.ContextFlyout>
<Image <Image
Name="PlaceholderImage" Name="PlaceholderImage"
Margin="{TemplateBinding PlaceholderMargin}" Margin="{TemplateBinding PlaceholderMargin}"

View File

@@ -3,10 +3,9 @@
using Microsoft.Graphics.Canvas.Effects; using Microsoft.Graphics.Canvas.Effects;
using Microsoft.UI.Composition; using Microsoft.UI.Composition;
using Snap.Hutao.UI.Xaml.Control.Image;
using System.Numerics; using System.Numerics;
namespace Snap.Hutao.UI.Composition; namespace Snap.Hutao.Control.Image;
/// <summary> /// <summary>
/// 合成扩展 /// 合成扩展

View File

@@ -6,18 +6,23 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting; using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Animation;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Caching; using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Graphics;
using Snap.Hutao.Service.Notification; using Snap.Hutao.Service.Notification;
using Snap.Hutao.UI.Xaml.Media.Animation;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.UI.Xaml.Control.Image; namespace Snap.Hutao.Control.Image;
[DependencyProperty("EnableShowHideAnimation", typeof(bool), true)] /// <summary>
/// 合成图像控件
/// 为其他图像类控件提供基类
/// </summary>
[HighQuality]
[DependencyProperty("EnableLazyLoading", typeof(bool), true, nameof(OnSourceChanged))]
[DependencyProperty("Source", typeof(Uri), default!, nameof(OnSourceChanged))] [DependencyProperty("Source", typeof(Uri), default!, nameof(OnSourceChanged))]
internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Control internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{ {
@@ -25,38 +30,48 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
private readonly SizeChangedEventHandler sizeChangedEventHandler;
private readonly TypedEventHandler<LoadedImageSurface, LoadedImageSourceLoadCompletedEventArgs> loadedImageSourceLoadCompletedEventHandler;
private TaskCompletionSource? surfaceLoadTaskCompletionSource; private TaskCompletionSource? surfaceLoadTaskCompletionSource;
private SpriteVisual? spriteVisual; private SpriteVisual? spriteVisual;
private bool isShow = true; private bool isShow = true;
/// <summary>
/// 构造一个新的单色图像
/// </summary>
protected CompositionImage() protected CompositionImage()
{ {
serviceProvider = this.ServiceProvider(); serviceProvider = this.ServiceProvider();
this.DisableInteraction(); this.DisableInteraction();
SizeChanged += OnSizeChanged; sizeChangedEventHandler = OnSizeChanged;
SizeChanged += sizeChangedEventHandler;
loadedImageSourceLoadCompletedEventHandler = OnLoadImageSurfaceLoadCompleted;
} }
/// <summary>
/// 合成组合视觉
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="imageSurface">图像表面</param>
/// <returns>拼合视觉</returns>
protected abstract SpriteVisual CompositeSpriteVisual(Compositor compositor, LoadedImageSurface imageSurface); protected abstract SpriteVisual CompositeSpriteVisual(Compositor compositor, LoadedImageSurface imageSurface);
protected virtual void LoadImageSurfaceCompleted(LoadedImageSurface surface) protected virtual void LoadImageSurfaceCompleted(LoadedImageSurface surface)
{ {
} }
/// <summary>
/// 更新视觉对象
/// </summary>
/// <param name="spriteVisual">拼合视觉</param>
protected virtual void UpdateVisual(SpriteVisual spriteVisual) protected virtual void UpdateVisual(SpriteVisual spriteVisual)
{ {
spriteVisual.Size = ActualSize; spriteVisual.Size = ActualSize;
} }
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (!string.IsNullOrEmpty(Source.OriginalString))
{
OnSourceChangedCore(Source);
}
}
private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg) private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
{ {
CompositionImage image = (CompositionImage)sender; CompositionImage image = (CompositionImage)sender;
@@ -72,7 +87,9 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
// value is different from old one // value is different from old one
if (inner != (arg.OldValue as Uri)) if (inner != (arg.OldValue as Uri))
{ {
image.OnSourceChangedCore(inner); image
.ApplyImageAsync(inner, token)
.SafeForget(logger, ex => OnApplyImageFailed(serviceProvider, inner, ex));
} }
} }
else else
@@ -84,7 +101,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
private static void OnApplyImageFailed(IServiceProvider serviceProvider, Uri? uri, Exception exception) private static void OnApplyImageFailed(IServiceProvider serviceProvider, Uri? uri, Exception exception)
{ {
Debugger.Break();
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>(); IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
if (exception is HttpRequestException httpRequestException) if (exception is HttpRequestException httpRequestException)
@@ -101,13 +117,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
} }
} }
private void OnSourceChangedCore(Uri? uri)
{
CancellationToken token = loadingTokenSource.Register();
ILogger<CompositionImage> logger = serviceProvider.GetRequiredService<ILogger<CompositionImage>>();
ApplyImageAsync(uri, token).SafeForget(logger, ex => OnApplyImageFailed(serviceProvider, uri, ex));
}
private async ValueTask ApplyImageAsync(Uri? uri, CancellationToken token) private async ValueTask ApplyImageAsync(Uri? uri, CancellationToken token)
{ {
await HideAsync(token).ConfigureAwait(true); await HideAsync(token).ConfigureAwait(true);
@@ -145,28 +154,21 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
await ShowAsync(token).ConfigureAwait(true); await ShowAsync(token).ConfigureAwait(true);
} }
} }
else
{
Debugger.Break();
}
} }
} }
private async ValueTask<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token) private async ValueTask<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
{ {
token.ThrowIfCancellationRequested();
TaskCompletionSource cancelTcs = new();
CancellationTokenRegistration registration = token.Register(() => cancelTcs.TrySetResult(), false);
surfaceLoadTaskCompletionSource = new(); surfaceLoadTaskCompletionSource = new();
LoadedImageSurface? surface = default; LoadedImageSurface? surface = default;
try try
{ {
surface = LoadedImageSurface.StartLoadFromUri(file.ToUri()); surface = LoadedImageSurface.StartLoadFromUri(file.ToUri());
surface.LoadCompleted += OnLoadImageSurfaceLoadCompleted; surface.LoadCompleted += loadedImageSourceLoadCompletedEventHandler;
if (surface.DecodedPhysicalSize.Size() <= 0D) if (surface.DecodedPhysicalSize.Size() <= 0D)
{ {
await Task.WhenAny(surfaceLoadTaskCompletionSource.Task, cancelTcs.Task).ConfigureAwait(true); await Task.WhenAny(surfaceLoadTaskCompletionSource.Task, Task.Delay(5000, token)).ConfigureAwait(true);
await Task.Delay(50, token).ConfigureAwait(true);
} }
LoadImageSurfaceCompleted(surface); LoadImageSurfaceCompleted(surface);
@@ -176,10 +178,8 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{ {
if (surface is not null) if (surface is not null)
{ {
surface.LoadCompleted -= OnLoadImageSurfaceLoadCompleted; surface.LoadCompleted -= loadedImageSourceLoadCompletedEventHandler;
} }
registration.Dispose();
} }
} }
@@ -189,11 +189,11 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{ {
isShow = true; isShow = true;
if (EnableShowHideAnimation) if (EnableLazyLoading)
{ {
await AnimationBuilder await AnimationBuilder
.Create() .Create()
.Opacity(from: 0D, to: 1D, duration: Constants.ImageScaleFadeIn) .Opacity(from: 0D, to: 1D, duration: ControlAnimationConstants.ImageScaleFadeIn)
.StartAsync(this, token) .StartAsync(this, token)
.ConfigureAwait(true); .ConfigureAwait(true);
} }
@@ -210,11 +210,11 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{ {
isShow = false; isShow = false;
if (EnableShowHideAnimation) if (EnableLazyLoading)
{ {
await AnimationBuilder await AnimationBuilder
.Create() .Create()
.Opacity(from: 1D, to: 0D, duration: Constants.ImageScaleFadeOut) .Opacity(from: 1D, to: 0D, duration: ControlAnimationConstants.ImageScaleFadeOut)
.StartAsync(this, token) .StartAsync(this, token)
.ConfigureAwait(true); .ConfigureAwait(true);
} }

View File

@@ -5,9 +5,8 @@ using Microsoft.UI;
using Microsoft.UI.Composition; using Microsoft.UI.Composition;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Snap.Hutao.UI.Composition;
namespace Snap.Hutao.UI.Xaml.Control.Image; namespace Snap.Hutao.Control.Image;
/// <summary> /// <summary>
/// 渐变图像 /// 渐变图像

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
namespace Snap.Hutao.UI.Xaml.Control.Image; namespace Snap.Hutao.Control.Image;
/// <summary> /// <summary>
/// 渐变方向 /// 渐变方向

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
namespace Snap.Hutao.UI.Xaml.Control.Image; namespace Snap.Hutao.Control.Image;
/// <summary> /// <summary>
/// 渐变锚点 /// 渐变锚点

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Windows.Media.Casting;
namespace Snap.Hutao.Control.Image.Implementation;
[DependencyProperty("NineGrid", typeof(Thickness))]
internal partial class ImageEx : ImageExBase
{
public ImageEx()
: base()
{
}
public override CompositionBrush GetAlphaMask()
{
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)
{
return image.GetAlphaMask();
}
return default!;
}
public CastingSource GetAsCastingSource()
{
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)
{
return image.GetAsCastingSource();
}
return default!;
}
}

View File

@@ -6,18 +6,11 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching; using Snap.Hutao.Win32;
using Snap.Hutao.Core.DataTransfer;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.UI.Xaml.Control.Theme;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Media.Casting;
using Windows.Storage.Streams;
namespace Snap.Hutao.UI.Xaml.Control.Image; namespace Snap.Hutao.Control.Image.Implementation;
[SuppressMessage("", "CA1001")] [SuppressMessage("", "CA1001")]
[SuppressMessage("", "SH003")] [SuppressMessage("", "SH003")]
@@ -27,32 +20,30 @@ namespace Snap.Hutao.UI.Xaml.Control.Image;
[TemplateVisualState(Name = FailedState, GroupName = CommonGroup)] [TemplateVisualState(Name = FailedState, GroupName = CommonGroup)]
[TemplatePart(Name = PartImage, Type = typeof(object))] [TemplatePart(Name = PartImage, Type = typeof(object))]
[TemplatePart(Name = PartPlaceholderImage, Type = typeof(object))] [TemplatePart(Name = PartPlaceholderImage, Type = typeof(object))]
[DependencyProperty("SourceName", typeof(string), "Unknown")]
[DependencyProperty("CachedName", typeof(string), "Unknown")]
[DependencyProperty("NineGrid", typeof(Thickness))]
[DependencyProperty("Stretch", typeof(Stretch), Stretch.Uniform)] [DependencyProperty("Stretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("DecodePixelHeight", typeof(int), 0)]
[DependencyProperty("DecodePixelWidth", typeof(int), 0)]
[DependencyProperty("DecodePixelType", typeof(DecodePixelType), DecodePixelType.Physical)]
[DependencyProperty("IsCacheEnabled", typeof(bool), false)]
[DependencyProperty("EnableLazyLoading", typeof(bool), false, nameof(EnableLazyLoadingChanged))]
[DependencyProperty("LazyLoadingThreshold", typeof(double), default(double), nameof(LazyLoadingThresholdChanged))]
[DependencyProperty("PlaceholderSource", typeof(object), default(object))] [DependencyProperty("PlaceholderSource", typeof(object), default(object))]
[DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)] [DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("PlaceholderMargin", typeof(Thickness))] [DependencyProperty("PlaceholderMargin", typeof(Thickness))]
[DependencyProperty("Source", typeof(object), default(object), nameof(OnSourceChanged))] [DependencyProperty("Source", typeof(object), default(object), nameof(SourceChanged))]
[DependencyProperty("ShowAsMonoChrome", typeof(bool), false)] internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
{ {
private const string PartImage = "Image"; protected const string PartImage = "Image";
private const string PartPlaceholderImage = "PlaceholderImage"; protected const string PartPlaceholderImage = "PlaceholderImage";
private const string CommonGroup = "CommonStates"; protected const string CommonGroup = "CommonStates";
private const string LoadingState = "Loading"; protected const string LoadingState = "Loading";
private const string LoadedState = "Loaded"; protected const string LoadedState = "Loaded";
private const string UnloadedState = "Unloaded"; protected const string UnloadedState = "Unloaded";
private const string FailedState = "Failed"; protected const string FailedState = "Failed";
private CancellationTokenSource? tokenSource; private CancellationTokenSource? tokenSource;
private object? lazyLoadingSource;
public CachedImage() private bool isInViewport;
{
DefaultStyleKey = typeof(CachedImage);
ActualThemeChanged += OnActualThemeChanged;
}
public bool IsInitialized { get; private set; } public bool IsInitialized { get; private set; }
@@ -61,28 +52,26 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
get => true; get => true;
} }
private object? Image { get; set; } protected object? Image { get; private set; }
private object? PlaceholderImage { get; set; } protected object? PlaceholderImage { get; private set; }
public CompositionBrush GetAlphaMask() public abstract CompositionBrush GetAlphaMask();
protected virtual Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{ {
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image) // By default we just use the built-in UWP image cache provided within the Image control.
{ return Task.FromResult<ImageSource?>(new BitmapImage(imageUri));
return image.GetAlphaMask();
}
return default!;
} }
public CastingSource GetAsCastingSource() protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
{ {
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image) VisualStateManager.GoToState(this, LoadedState, true);
{ }
return image.GetAsCastingSource();
}
return default!; protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e)
{
VisualStateManager.GoToState(this, FailedState, true);
} }
protected override void OnApplyTemplate() protected override void OnApplyTemplate()
@@ -91,10 +80,19 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
RemoveImageFailed(OnImageFailed); RemoveImageFailed(OnImageFailed);
Image = GetTemplateChild(PartImage); Image = GetTemplateChild(PartImage);
PlaceholderImage = GetTemplateChild(PartPlaceholderImage);
IsInitialized = true; IsInitialized = true;
SetSource(Source); if (Source is null || !EnableLazyLoading || isInViewport)
{
lazyLoadingSource = null;
SetSource(Source);
}
else
{
lazyLoadingSource = Source;
}
AttachImageOpened(OnImageOpened); AttachImageOpened(OnImageOpened);
AttachImageFailed(OnImageFailed); AttachImageFailed(OnImageFailed);
@@ -150,9 +148,36 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
} }
} }
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) private static void EnableLazyLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ {
if (d is not CachedImage control) if (d is not ImageExBase control)
{
return;
}
bool value = (bool)e.NewValue;
if (value)
{
control.LayoutUpdated += control.OnImageExBaseLayoutUpdated;
control.InvalidateLazyLoading();
}
else
{
control.LayoutUpdated -= control.OnImageExBaseLayoutUpdated;
}
}
private static void LazyLoadingThresholdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageExBase { EnableLazyLoading: true } control)
{
control.InvalidateLazyLoading();
}
}
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ImageExBase control)
{ {
return; return;
} }
@@ -162,7 +187,15 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
return; return;
} }
control.SetSource(e.NewValue); if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
{
control.lazyLoadingSource = null;
control.SetSource(e.NewValue);
}
else
{
control.lazyLoadingSource = e.NewValue;
}
} }
private static bool IsHttpUri(Uri uri) private static bool IsHttpUri(Uri uri)
@@ -170,49 +203,11 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https"); return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https");
} }
private async Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token) private void AttachSource(ImageSource? source)
{
SourceName = Path.GetFileName(imageUri.ToString());
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
try
{
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
ElementTheme theme = ShowAsMonoChrome ? ThemeHelper.ApplicationToElement(ThemeHelper.ElementToApplication(ActualTheme)) : ElementTheme.Default;
string file = await imageCache.GetFileFromCacheAsync(imageUri, theme).ConfigureAwait(true); // BitmapImage need to be created by main thread.
CachedName = Path.GetFileName(file);
return file.ToUri();
}
catch (COMException)
{
// The image is corrupted, remove it.
imageCache.Remove(imageUri);
return default;
}
catch (Exception ex)
{
Debug.WriteLine(ex);
return default;
}
}
private void OnActualThemeChanged(FrameworkElement sender, object args)
{
SetSource(Source);
}
private void OnImageOpened(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(this, LoadedState, true);
}
private void OnImageFailed(object sender, ExceptionRoutedEventArgs e)
{
VisualStateManager.GoToState(this, FailedState, true);
}
private void AttachSource(BitmapImage? source, Uri? uri)
{ {
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
// We only need to call those methods if we fail in other cases before we get here.
if (Image is Microsoft.UI.Xaml.Controls.Image image) if (Image is Microsoft.UI.Xaml.Controls.Image image)
{ {
image.Source = source; image.Source = source;
@@ -226,15 +221,13 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
{ {
VisualStateManager.GoToState(this, UnloadedState, true); VisualStateManager.GoToState(this, UnloadedState, true);
} }
else else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
{ {
// https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-animations-and-media#optimize-image-resources
source.UriSource = uri;
VisualStateManager.GoToState(this, LoadedState, true); VisualStateManager.GoToState(this, LoadedState, true);
} }
} }
private void AttachPlaceholderSource(BitmapImage? source, Uri? uri) private void AttachPlaceholderSource(ImageSource? source)
{ {
if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image) if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image)
{ {
@@ -249,11 +242,9 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
{ {
VisualStateManager.GoToState(this, UnloadedState, true); VisualStateManager.GoToState(this, UnloadedState, true);
} }
else else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
{ {
// https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-animations-and-media#optimize-image-resources VisualStateManager.GoToState(this, LoadedState, true);
source.UriSource = uri;
VisualStateManager.GoToState(this, FailedState, true);
} }
} }
@@ -265,9 +256,10 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
} }
tokenSource?.Cancel(); tokenSource?.Cancel();
tokenSource = new CancellationTokenSource(); tokenSource = new CancellationTokenSource();
AttachSource(default, default); AttachSource(null);
if (source is null) if (source is null)
{ {
@@ -276,6 +268,13 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
VisualStateManager.GoToState(this, LoadingState, true); VisualStateManager.GoToState(this, LoadingState, true);
if (source as ImageSource is { } imageSource)
{
AttachSource(imageSource);
return;
}
if (source as Uri is not { } uri) if (source as Uri is not { } uri)
{ {
string? url = source as string ?? source.ToString(); string? url = source as string ?? source.ToString();
@@ -320,13 +319,20 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
tokenSource?.Cancel(); tokenSource?.Cancel();
tokenSource = new(); tokenSource = new();
AttachPlaceholderSource(default, default); AttachPlaceholderSource(null);
if (source is null) if (source is null)
{ {
return; return;
} }
if (source as ImageSource is { } imageSource)
{
AttachPlaceholderSource(imageSource);
return;
}
if (source as Uri is not { } uri) if (source as Uri is not { } uri)
{ {
string? url = source as string ?? source.ToString(); string? url = source as string ?? source.ToString();
@@ -348,13 +354,13 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
return; return;
} }
Uri? actualUri = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true); ImageSource? img = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource); ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested) if (!tokenSource.IsCancellationRequested)
{ {
// Only attach our image if we still have a valid request. // Only attach our image if we still have a valid request.
AttachPlaceholderSource(new BitmapImage(), actualUri); AttachPlaceholderSource(img);
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -373,36 +379,99 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
return; return;
} }
Uri? actualUri = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true); if (IsCacheEnabled)
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{ {
// Only attach our image if we still have a valid request. ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
AttachSource(new BitmapImage(), actualUri);
}
}
[Command("CopyToClipboardCommand")] ArgumentNullException.ThrowIfNull(tokenSource);
private async Task CopyToClipboard() if (!tokenSource.IsCancellationRequested)
{
if (Image is Microsoft.UI.Xaml.Controls.Image { Source: BitmapImage bitmap })
{
using (FileStream netStream = File.OpenRead(bitmap.UriSource.LocalPath))
{ {
using (IRandomAccessStream fxStream = netStream.AsRandomAccessStream()) // Only attach our image if we still have a valid request.
AttachSource(img);
}
}
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
{
string source = imageUri.OriginalString;
const string base64Head = "base64,";
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
if (index >= 0)
{
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
BitmapImage bitmap = new();
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{ {
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(fxStream); AttachSource(bitmap);
SoftwareBitmap softwareBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
using (InMemoryRandomAccessStream memory = new())
{
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, memory);
encoder.SetSoftwareBitmap(softwareBitmap);
await encoder.FlushAsync();
await Ioc.Default.GetRequiredService<IClipboardProvider>().SetBitmapAsync(memory).ConfigureAwait(false);
}
} }
} }
} }
else
{
AttachSource(new BitmapImage(imageUri)
{
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
});
}
}
private void OnImageExBaseLayoutUpdated(object? sender, object e)
{
InvalidateLazyLoading();
}
private void InvalidateLazyLoading()
{
if (!IsLoaded)
{
isInViewport = false;
return;
}
// Find the first ascendant ScrollViewer, if not found, use the root element.
FrameworkElement? hostElement = default;
IEnumerable<FrameworkElement> ascendants = this.FindAscendants().OfType<FrameworkElement>();
foreach (FrameworkElement ascendant in ascendants)
{
hostElement = ascendant;
if (hostElement is Microsoft.UI.Xaml.Controls.ScrollViewer)
{
break;
}
}
if (hostElement is null)
{
isInViewport = false;
return;
}
Rect controlRect = TransformToVisual(hostElement).TransformBounds(StructMarshal.Rect(ActualSize));
double lazyLoadingThreshold = LazyLoadingThreshold;
// Left/Top 1 Threshold, Right/Bottom 2 Threshold
Rect hostRect = new(
0 - lazyLoadingThreshold,
0 - lazyLoadingThreshold,
hostElement.ActualWidth + (2 * lazyLoadingThreshold),
hostElement.ActualHeight + (2 * lazyLoadingThreshold));
if (controlRect.IntersectsWith(hostRect))
{
isInViewport = true;
if (lazyLoadingSource is not null)
{
object source = lazyLoadingSource;
lazyLoadingSource = null;
SetSource(source);
}
}
else
{
isInViewport = false;
}
} }
} }

View File

@@ -0,0 +1,67 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Theme;
using Windows.Foundation;
namespace Snap.Hutao.Control.Image;
/// <summary>
/// 支持单色的图像
/// </summary>
[HighQuality]
internal sealed class MonoChrome : CompositionImage
{
private readonly TypedEventHandler<FrameworkElement, object> actualThemeChangedEventHandler;
private CompositionColorBrush? backgroundBrush;
/// <summary>
/// 构造一个新的单色图像
/// </summary>
public MonoChrome()
{
actualThemeChangedEventHandler = OnActualThemeChanged;
ActualThemeChanged += actualThemeChangedEventHandler;
}
/// <inheritdoc/>
protected override SpriteVisual CompositeSpriteVisual(Compositor compositor, LoadedImageSurface imageSurface)
{
CompositionColorBrush blackLayerBrush = compositor.CreateColorBrush(Colors.Black);
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.Uniform, vRatio: 0f);
CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBrush, imageSurfaceBrush, BlendEffectMode.Overlay);
CompositionEffectBrush opacityBrush = compositor.CompositeLuminanceToAlphaEffectBrush(overlayBrush);
backgroundBrush = compositor.CreateColorBrush();
SetBackgroundColor(backgroundBrush);
CompositionEffectBrush alphaMaskEffectBrush = compositor.CompositeAlphaMaskEffectBrush(backgroundBrush, opacityBrush);
return compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
}
private void OnActualThemeChanged(FrameworkElement sender, object args)
{
if (backgroundBrush is not null)
{
SetBackgroundColor(backgroundBrush);
}
}
private void SetBackgroundColor(CompositionColorBrush backgroundBrush)
{
ApplicationTheme theme = ThemeHelper.ElementToApplication(ActualTheme);
backgroundBrush.Color = theme switch
{
ApplicationTheme.Light => Colors.Black,
ApplicationTheme.Dark => Colors.White,
_ => Colors.Transparent,
};
}
}

View File

@@ -3,7 +3,7 @@
using System.Diagnostics; using System.Diagnostics;
namespace Snap.Hutao.UI.Xaml.Control.Layout; namespace Snap.Hutao.Control.Layout;
[DebuggerDisplay("Count = {Count}, Height = {Height}")] [DebuggerDisplay("Count = {Count}, Height = {Height}")]
internal class UniformStaggeredColumnLayout : List<UniformStaggeredItem> internal class UniformStaggeredColumnLayout : List<UniformStaggeredItem>

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
namespace Snap.Hutao.UI.Xaml.Control.Layout; namespace Snap.Hutao.Control.Layout;
internal sealed class UniformStaggeredItem internal sealed class UniformStaggeredItem
{ {

View File

@@ -4,10 +4,11 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Windows.Foundation; using Windows.Foundation;
namespace Snap.Hutao.UI.Xaml.Control.Layout; namespace Snap.Hutao.Control.Layout;
[DependencyProperty("MinItemWidth", typeof(double), 0D, nameof(OnMinItemWidthChanged))] [DependencyProperty("MinItemWidth", typeof(double), 0D, nameof(OnMinItemWidthChanged))]
[DependencyProperty("MinColumnSpacing", typeof(double), 0D, nameof(OnSpacingChanged))] [DependencyProperty("MinColumnSpacing", typeof(double), 0D, nameof(OnSpacingChanged))]
@@ -63,12 +64,12 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
/// <inheritdoc/> /// <inheritdoc/>
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{ {
if (context.ItemCount is 0) if (context.ItemCount == 0)
{ {
return new Size(availableSize.Width, 0); return new Size(availableSize.Width, 0);
} }
if ((context.RealizationRect.Width is 0) && (context.RealizationRect.Height is 0)) if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
{ {
return new Size(availableSize.Width, 0.0f); return new Size(availableSize.Width, 0.0f);
} }
@@ -188,7 +189,6 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
} }
UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState; UniformStaggeredLayoutState state = (UniformStaggeredLayoutState)context.LayoutState;
int virtualColumnCount = (int)(finalSize.Width / state.ColumnWidth);
// Cycle through each column and arrange the items that are within the realization bounds // Cycle through each column and arrange the items that are within the realization bounds
for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++) for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
@@ -205,13 +205,9 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
// Partial or fully in the view // Partial or fully in the view
if (item.Top <= context.RealizationRect.Bottom) if (item.Top <= context.RealizationRect.Bottom)
{ {
double itemHorizontalOffset = (state.ColumnWidth + MinColumnSpacing) * columnIndex; double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex);
double width = columnIndex == virtualColumnCount - 1 Rect bounds = new(itemHorizontalOffset, item.Top, state.ColumnWidth, item.Height);
? finalSize.Width - itemHorizontalOffset
: state.ColumnWidth;
Rect bounds = new(itemHorizontalOffset, item.Top, width, item.Height);
UIElement element = context.GetOrCreateElementAt(item.Index); UIElement element = context.GetOrCreateElementAt(item.Index);
element.Arrange(bounds); element.Arrange(bounds);
} }

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Snap.Hutao.UI.Xaml.Control.Layout; namespace Snap.Hutao.Control.Layout;
internal sealed class UniformStaggeredLayoutState internal sealed class UniformStaggeredLayoutState
{ {
@@ -49,7 +49,7 @@ internal sealed class UniformStaggeredLayoutState
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();
} }
if (index <= items.Count - 1) if (index <= (items.Count - 1))
{ {
return items[index]; return items[index];
} }
@@ -83,7 +83,7 @@ internal sealed class UniformStaggeredLayoutState
} }
averageHeight /= columnLayout.Count; averageHeight /= columnLayout.Count;
double estimatedHeight = averageHeight * context.ItemCount / columnLayout.Count; double estimatedHeight = (averageHeight * context.ItemCount) / columnLayout.Count;
if (estimatedHeight > desiredHeight) if (estimatedHeight > desiredHeight)
{ {
desiredHeight = estimatedHeight; desiredHeight = estimatedHeight;
@@ -100,11 +100,7 @@ internal sealed class UniformStaggeredLayoutState
internal void Clear() internal void Clear()
{ {
if (items.Count > 0) RecycleElements();
{
RecycleElements();
}
ClearColumns(); ClearColumns();
ClearItems(); ClearItems();
} }
@@ -121,9 +117,12 @@ internal sealed class UniformStaggeredLayoutState
internal void RecycleElements() internal void RecycleElements()
{ {
for (int i = 0; i < context.ItemCount; i++) if (context.ItemCount > 0)
{ {
RecycleElementAt(i); for (int i = 0; i < items.Count; i++)
{
RecycleElementAt(i);
}
} }
} }
@@ -180,7 +179,7 @@ internal sealed class UniformStaggeredLayoutState
Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout); Span<UniformStaggeredItem> layoutSpan = CollectionsMarshal.AsSpan(layout);
for (int i = 0; i < layoutSpan.Length; i++) for (int i = 0; i < layoutSpan.Length; i++)
{ {
if (startIndex <= layoutSpan[i].Index && layoutSpan[i].Index <= endIndex) if ((startIndex <= layoutSpan[i].Index) && (layoutSpan[i].Index <= endIndex))
{ {
int numToRemove = layoutSpan.Length - i; int numToRemove = layoutSpan.Length - i;
layout.RemoveRange(i, numToRemove); layout.RemoveRange(i, numToRemove);

View File

@@ -2,15 +2,12 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Media.Animation;
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control;
[TemplateVisualState(Name = "LoadingIn", GroupName = "CommonStates")] [TemplateVisualState(Name = "LoadingIn", GroupName = "CommonStates")]
[TemplateVisualState(Name = "LoadingOut", GroupName = "CommonStates")] [TemplateVisualState(Name = "LoadingOut", GroupName = "CommonStates")]
[TemplatePart(Name = "ContentGrid", Type = typeof(FrameworkElement))] [TemplatePart(Name = "ContentGrid", Type = typeof(FrameworkElement))]
[TemplatePart(Name = "LoadingOutStoryboard", Type = typeof(Storyboard))]
internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl 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)); public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(Loading), new PropertyMetadata(default(bool), IsLoadingPropertyChanged));
@@ -20,6 +17,7 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
public Loading() public Loading()
{ {
DefaultStyleKey = typeof(Loading); DefaultStyleKey = typeof(Loading);
DefaultStyleResourceUri = "ms-appx:///Control/Loading.xaml".ToUri();
} }
public bool IsLoading public bool IsLoading
@@ -31,11 +29,6 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
protected override void OnApplyTemplate() protected override void OnApplyTemplate()
{ {
base.OnApplyTemplate(); base.OnApplyTemplate();
if (GetTemplateChild("LoadingOutStoryboard") is Storyboard storyboard)
{
storyboard.Completed -= UnloadPresenter;
storyboard.Completed += UnloadPresenter;
}
Update(); Update();
} }
@@ -43,25 +36,13 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
private static void IsLoadingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) private static void IsLoadingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ {
Loading control = (Loading)d; Loading control = (Loading)d;
control.presenter ??= control.GetTemplateChild("ContentGrid") as FrameworkElement;
if ((bool)e.NewValue) control?.Update();
{
control.presenter ??= control.GetTemplateChild("ContentGrid") as FrameworkElement;
}
control.Update();
} }
private void Update() private void Update()
{ {
VisualStateManager.GoToState(this, IsLoading ? "LoadingIn" : "LoadingOut", true); VisualStateManager.GoToState(this, IsLoading ? "LoadingIn" : "LoadingOut", true);
} }
private void UnloadPresenter(object? sender, object? args)
{
if (presenter is not null)
{
XamlMarkupHelper.UnloadObject(presenter);
}
}
} }

View File

@@ -1,20 +1,18 @@
<ResourceDictionary <ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:clw="using:CommunityToolkit.Labs.WinUI" xmlns:shc="using:Snap.Hutao.Control">
xmlns:cw="using:CommunityToolkit.WinUI"
xmlns:shuxc="using:Snap.Hutao.UI.Xaml.Control"
xmlns:shuxci="using:Snap.Hutao.UI.Xaml.Control.Image"
xmlns:shuxm="using:Snap.Hutao.UI.Xaml.Markup">
<Style BasedOn="{StaticResource DefaultLoadingStyle}" TargetType="shuxc:Loading"/> <Style BasedOn="{StaticResource DefaultLoadingStyle}" TargetType="shc:Loading"/>
<Style x:Key="DefaultLoadingStyle" TargetType="shuxc:Loading"> <Style x:Key="DefaultLoadingStyle" TargetType="shc:Loading">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/> <Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="shuxc:Loading"> <ControlTemplate TargetType="shc:Loading">
<Border <Border
x:Name="RootGrid" x:Name="RootGrid"
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
@@ -25,8 +23,11 @@
<ContentPresenter <ContentPresenter
x:Name="ContentGrid" x:Name="ContentGrid"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
x:Load="True"/> <ContentPresenter.RenderTransform>
<CompositeTransform/>
</ContentPresenter.RenderTransform>
</ContentPresenter>
<VisualStateManager.VisualStateGroups> <VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates"> <VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="LoadingIn"> <VisualState x:Name="LoadingIn">
@@ -53,7 +54,7 @@
</Storyboard> </Storyboard>
</VisualState> </VisualState>
<VisualState x:Name="LoadingOut"> <VisualState x:Name="LoadingOut">
<Storyboard x:Name="LoadingOutStoryboard"> <Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Opacity"> <DoubleAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1"> <EasingDoubleKeyFrame KeyTime="0:0:0" Value="1">
<EasingDoubleKeyFrame.EasingFunction> <EasingDoubleKeyFrame.EasingFunction>
@@ -81,62 +82,6 @@
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>
</Setter> </Setter>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style> </Style>
<Style </ResourceDictionary>
x:Key="DefaultLoadingViewStyle"
BasedOn="{StaticResource DefaultLoadingStyle}"
TargetType="shuxc:Loading">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid>
<clw:Shimmer
cw:FrameworkElementExtensions.AncestorType="shuxc:Loading"
CornerRadius="{Binding (cw:FrameworkElementExtensions.Ancestor).CornerRadius, RelativeSource={RelativeSource Self}}"
IsActive="{Binding (cw:FrameworkElementExtensions.Ancestor).IsLoading, RelativeSource={RelativeSource Self}, Mode=OneWay}"
Duration="0:0:1"/>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<shuxci:CachedImage
Width="120"
Height="120"
Source="{StaticResource UI_EmotionIcon272}"/>
<TextBlock
Margin="0,16,0,0"
HorizontalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{shuxm:ResourceString Name=ViewControlLoadingText}"/>
</StackPanel>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="IsHitTestVisible" Value="False"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
</Style>
<Style
x:Key="DefaultLoadingCardStyle"
BasedOn="{StaticResource DefaultLoadingStyle}"
TargetType="shuxc:Loading">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<clw:Shimmer
cw:FrameworkElementExtensions.AncestorType="shuxc:Loading"
CornerRadius="0"
IsActive="{Binding (cw:FrameworkElementExtensions.Ancestor).IsLoading, RelativeSource={RelativeSource Self}, Mode=OneWay}"
Duration="0:0:1"/>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="Height" Value="{ThemeResource HomeAdaptiveCardHeight}"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="IsHitTestVisible" Value="False"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
</Style>
</ResourceDictionary>

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup; using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.UI.Xaml.Markup; namespace Snap.Hutao.Control.Markup;
/// <summary> /// <summary>
/// Custom <see cref="Markup"/> which can provide <see cref="BitmapIcon"/> values. /// Custom <see cref="Markup"/> which can provide <see cref="BitmapIcon"/> values.

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup; using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.UI.Xaml.Markup; namespace Snap.Hutao.Control.Markup;
/// <summary> /// <summary>
/// Custom <see cref="MarkupExtension"/> which can provide <see cref="FontIcon"/> values. /// Custom <see cref="MarkupExtension"/> which can provide <see cref="FontIcon"/> values.

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml.Markup; using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.UI.Xaml.Markup; namespace Snap.Hutao.Control.Markup;
[MarkupExtensionReturnType(ReturnType = typeof(int))] [MarkupExtensionReturnType(ReturnType = typeof(int))]
internal sealed class Int32Extension : MarkupExtension internal sealed class Int32Extension : MarkupExtension

View File

@@ -4,21 +4,23 @@
using Microsoft.UI.Xaml.Markup; using Microsoft.UI.Xaml.Markup;
using System.Globalization; using System.Globalization;
namespace Snap.Hutao.UI.Xaml.Markup; namespace Snap.Hutao.Control.Markup;
/// <summary>
/// Xaml extension to return a <see cref="string"/> value from resource file associated with a resource key
/// </summary>
[HighQuality] [HighQuality]
[MarkupExtensionReturnType(ReturnType = typeof(string))] [MarkupExtensionReturnType(ReturnType = typeof(string))]
internal sealed class ResourceStringExtension : MarkupExtension internal sealed class ResourceStringExtension : MarkupExtension
{ {
private string? name; /// <summary>
/// Gets or sets associated ID from resource strings.
public string? Name { get => name; set => name = value is null ? null : string.Intern(value); } /// </summary>
public string? Name { get; set; }
public string? CultureName { get; set; }
/// <inheritdoc/>
protected override object ProvideValue() protected override object ProvideValue()
{ {
CultureInfo cultureInfo = CultureName is not null ? CultureInfo.GetCultureInfo(CultureName) : CultureInfo.CurrentCulture; return SH.ResourceManager.GetString(Name ?? string.Empty, CultureInfo.CurrentCulture) ?? Name ?? string.Empty;
return SH.ResourceManager.GetString(Name ?? string.Empty, cultureInfo) ?? Name ?? string.Empty;
} }
} }

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml.Markup; using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.UI.Xaml.Markup; namespace Snap.Hutao.Control.Markup;
[MarkupExtensionReturnType(ReturnType = typeof(uint))] [MarkupExtensionReturnType(ReturnType = typeof(uint))]
internal sealed class UInt32Extension : MarkupExtension internal sealed class UInt32Extension : MarkupExtension

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Markup; using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.UI.Xaml.Markup; namespace Snap.Hutao.Control.Markup;
/// <summary> /// <summary>
/// Xaml 服务提供器扩展 /// Xaml 服务提供器扩展

View File

@@ -0,0 +1,64 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using Windows.UI;
namespace Snap.Hutao.Control.Media;
/// <summary>
/// BGRA 结构
/// </summary>
[HighQuality]
internal struct Bgra32
{
/// <summary>
/// B
/// </summary>
public byte B;
/// <summary>
/// G
/// </summary>
public byte G;
/// <summary>
/// R
/// </summary>
public byte R;
/// <summary>
/// A
/// </summary>
public byte A;
public Bgra32(byte b, byte g, byte r, byte a)
{
B = b;
G = g;
R = r;
A = a;
}
public readonly double Luminance { get => ((0.299 * R) + (0.587 * G) + (0.114 * B)) / 255; }
/// <summary>
/// 从 Color 转换
/// </summary>
/// <param name="color">颜色</param>
/// <returns>新的 BGRA8 结构</returns>
public static unsafe implicit operator Bgra32(Color color)
{
Unsafe.SkipInit(out Bgra32 bgra8);
*(uint*)&bgra8 = BinaryPrimitives.ReverseEndianness(*(uint*)&color);
return bgra8;
}
public static unsafe implicit operator Color(Bgra32 bgra8)
{
Unsafe.SkipInit(out Color color);
*(uint*)&color = BinaryPrimitives.ReverseEndianness(*(uint*)&bgra8);
return color;
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// Some part of this file came from:
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
namespace Snap.Hutao.Control.Media;
/// <summary>
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
/// </summary>
internal struct Hsla32
{
/// <summary>
/// The Hue in 0..360 range.
/// </summary>
public double H;
/// <summary>
/// The Saturation in 0..1 range.
/// </summary>
public double S;
/// <summary>
/// The Lightness in 0..1 range.
/// </summary>
public double L;
/// <summary>
/// The Alpha/opacity in 0..1 range.
/// </summary>
public double A;
}

View File

@@ -0,0 +1,185 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// Some part of this file came from:
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
using System.Buffers.Binary;
using Windows.UI;
namespace Snap.Hutao.Control.Media;
/// <summary>
/// RGBA 颜色
/// </summary>
[HighQuality]
internal struct Rgba32
{
/// <summary>
/// R
/// </summary>
public byte R;
/// <summary>
/// G
/// </summary>
public byte G;
/// <summary>
/// B
/// </summary>
public byte B;
/// <summary>
/// A
/// </summary>
public byte A;
/// <summary>
/// 构造一个新的 RGBA8 颜色
/// </summary>
/// <param name="hex">色值字符串</param>
public Rgba32(string hex)
: this(hex.Length == 6 ? Convert.ToUInt32($"{hex}FF", 16) : Convert.ToUInt32(hex, 16))
{
}
/// <summary>
/// 使用 RGBA 代码初始化新的结构
/// </summary>
/// <param name="xrgbaCode">RGBA 代码</param>
public unsafe Rgba32(uint xrgbaCode)
{
// uint layout: 0xRRGGBBAA is AABBGGRR
// AABBGGRR -> RRGGBBAA
fixed (Rgba32* pSelf = &this)
{
*(uint*)pSelf = BinaryPrimitives.ReverseEndianness(xrgbaCode);
}
}
private Rgba32(byte r, byte g, byte b, byte a)
{
R = r;
G = g;
B = b;
A = a;
}
public static unsafe implicit operator Color(Rgba32 hexColor)
{
// Goal : Rgba32:RRGGBBAA(0xAABBGGRR) -> Color: AARRGGBB(0xBBGGRRAA)
// Step1: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:AA000000(0x000000AA)
uint a = ((*(uint*)&hexColor) >> 24) & 0x000000FF;
// Step2: Rgba32:RRGGBBAA(0xAABBGGRR) -> UInt32:00RRGGBB(0xRRGGBB00)
uint rgb = ((*(uint*)&hexColor) << 8) & 0xFFFFFF00;
// Step2: UInt32:00RRGGBB(0xRRGGBB00) + UInt32:AA000000(0x000000AA) -> UInt32:AARRGGBB(0xRRGGBBAA)
uint rgba = rgb + a;
return *(Color*)&rgba;
}
/// <summary>
/// 从 HSL 颜色转换
/// </summary>
/// <param name="hsl">HSL 颜色</param>
/// <returns>RGBA8颜色</returns>
public static Rgba32 FromHsl(Hsla32 hsl)
{
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
double h1 = hsl.H / 60;
double x = chroma * (1 - Math.Abs((h1 % 2) - 1));
double m = hsl.L - (0.5 * chroma);
double r1, g1, b1;
if (h1 < 1)
{
r1 = chroma;
g1 = x;
b1 = 0;
}
else if (h1 < 2)
{
r1 = x;
g1 = chroma;
b1 = 0;
}
else if (h1 < 3)
{
r1 = 0;
g1 = chroma;
b1 = x;
}
else if (h1 < 4)
{
r1 = 0;
g1 = x;
b1 = chroma;
}
else if (h1 < 5)
{
r1 = x;
g1 = 0;
b1 = chroma;
}
else
{
r1 = chroma;
g1 = 0;
b1 = x;
}
byte r = (byte)(255 * (r1 + m));
byte g = (byte)(255 * (g1 + m));
byte b = (byte)(255 * (b1 + m));
byte a = (byte)(255 * hsl.A);
return new(r, g, b, a);
}
/// <summary>
/// 转换到 HSL 颜色
/// </summary>
/// <returns>HSL 颜色</returns>
public readonly Hsla32 ToHsl()
{
const double toDouble = 1.0 / 255;
double r = toDouble * R;
double g = toDouble * G;
double b = toDouble * B;
double max = Math.Max(Math.Max(r, g), b);
double min = Math.Min(Math.Min(r, g), b);
double chroma = max - min;
double h1;
if (chroma == 0)
{
h1 = 0;
}
else if (max == r)
{
// The % operator doesn't do proper modulo on negative
// numbers, so we'll add 6 before using it
h1 = (((g - b) / chroma) + 6) % 6;
}
else if (max == g)
{
h1 = 2 + ((b - r) / chroma);
}
else
{
h1 = 4 + ((r - g) / chroma);
}
double lightness = 0.5 * (max + min);
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
Hsla32 ret;
ret.H = 60 * h1;
ret.S = saturation;
ret.L = lightness;
ret.A = toDouble * A;
return ret;
}
}

View File

@@ -1,16 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.UI;
using Snap.Hutao.Win32.System.WinRT; using Snap.Hutao.Win32.System.WinRT;
using Windows.Foundation; using Windows.Foundation;
using Windows.Graphics.Imaging; using Windows.Graphics.Imaging;
using WinRT; using WinRT;
namespace Snap.Hutao.Core.Graphics.Imaging; namespace Snap.Hutao.Control.Media;
/// <summary>
/// 软件位图拓展
/// </summary>
[HighQuality]
internal static class SoftwareBitmapExtension internal static class SoftwareBitmapExtension
{ {
/// <summary>
/// 混合模式 正常
/// </summary>
/// <param name="softwareBitmap">软件位图</param>
/// <param name="tint">底色</param>
public static unsafe void NormalBlend(this SoftwareBitmap softwareBitmap, Bgra32 tint) public static unsafe void NormalBlend(this SoftwareBitmap softwareBitmap, Bgra32 tint)
{ {
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.ReadWrite)) using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.ReadWrite))
@@ -31,7 +39,7 @@ internal static class SoftwareBitmapExtension
} }
} }
public static unsafe Bgra32 GetBgra32AccentColor(this SoftwareBitmap softwareBitmap) public static unsafe Bgra32 GetAccentColor(this SoftwareBitmap softwareBitmap)
{ {
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read)) using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
{ {
@@ -51,25 +59,4 @@ internal static class SoftwareBitmapExtension
} }
} }
} }
public static unsafe Rgba32 GetRgba32AccentColor(this SoftwareBitmap softwareBitmap)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
{
using (IMemoryBufferReference reference = buffer.CreateReference())
{
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
double b = 0, g = 0, r = 0, a = 0;
foreach (ref readonly Bgra32 pixel in bytes)
{
b += pixel.B;
g += pixel.G;
r += pixel.R;
a += pixel.A;
}
return new((byte)(r / bytes.Length), (byte)(g / bytes.Length), (byte)(b / bytes.Length), (byte)(a / bytes.Length));
}
}
}
} }

View File

@@ -6,7 +6,7 @@ using System.Data;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Windows.Foundation; using Windows.Foundation;
namespace Snap.Hutao.UI.Xaml.Control.Panel; namespace Snap.Hutao.Control.Panel;
[DependencyProperty("Spacing", typeof(double), default(double), nameof(OnSpacingChanged))] [DependencyProperty("Spacing", typeof(double), default(double), nameof(OnSpacingChanged))]
internal partial class EqualPanel : Microsoft.UI.Xaml.Controls.Panel internal partial class EqualPanel : Microsoft.UI.Xaml.Controls.Panel

View File

@@ -2,10 +2,9 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using System.Runtime.InteropServices;
using Windows.Foundation; using Windows.Foundation;
namespace Snap.Hutao.UI.Xaml.Control.Panel; namespace Snap.Hutao.Control.Panel;
[DependencyProperty("MinItemWidth", typeof(double))] [DependencyProperty("MinItemWidth", typeof(double))]
[DependencyProperty("Spacing", typeof(double))] [DependencyProperty("Spacing", typeof(double))]
@@ -19,14 +18,13 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList(); foreach (UIElement child in Children)
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
{ {
// ScrollViewer will always return an Infinity Size, we should use ActualWidth for this situation. // ScrollViewer will always return an Infinity Size, we should use ActualWidth for this situation.
double availableWidth = double.IsInfinity(availableSize.Width) ? ActualWidth : availableSize.Width; double availableWidth = double.IsInfinity(availableSize.Width) ? ActualWidth : availableSize.Width;
double childAvailableWidth = (availableWidth + Spacing) / visibleChildren.Count; double childAvailableWidth = (availableWidth + Spacing) / Children.Count;
double childMaxAvailableWidth = Math.Max(MinItemWidth, childAvailableWidth); double childMaxAvailableWidth = Math.Max(MinItemWidth, childAvailableWidth);
visibleChild.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight)); child.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
} }
return base.MeasureOverride(availableSize); return base.MeasureOverride(availableSize);
@@ -34,14 +32,14 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList(); int itemCount = Children.Count;
double availableItemWidth = (finalSize.Width - (Spacing * (visibleChildren.Count - 1))) / visibleChildren.Count; double availableItemWidth = (finalSize.Width - (Spacing * (itemCount - 1))) / itemCount;
double actualItemWidth = Math.Max(MinItemWidth, availableItemWidth); double actualItemWidth = Math.Max(MinItemWidth, availableItemWidth);
double offset = 0; double offset = 0;
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren)) foreach (UIElement child in Children)
{ {
visibleChild.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height)); child.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
offset += actualItemWidth + Spacing; offset += actualItemWidth + Spacing;
} }
@@ -51,8 +49,7 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
private static void OnLoaded(object sender, RoutedEventArgs e) private static void OnLoaded(object sender, RoutedEventArgs e)
{ {
HorizontalEqualPanel panel = (HorizontalEqualPanel)sender; HorizontalEqualPanel panel = (HorizontalEqualPanel)sender;
int vivibleChildrenCount = panel.Children.Count(child => child.Visibility is Visibility.Visible); panel.MinWidth = (panel.MinItemWidth * panel.Children.Count) + (panel.Spacing * (panel.Children.Count - 1));
panel.MinWidth = (panel.MinItemWidth * vivibleChildrenCount) + (panel.Spacing * (vivibleChildrenCount - 1));
} }
private static void OnSizeChanged(object sender, SizeChangedEventArgs e) private static void OnSizeChanged(object sender, SizeChangedEventArgs e)

View File

@@ -1,21 +1,21 @@
<cwc:Segmented <cwc:Segmented
x:Class="Snap.Hutao.UI.Xaml.Control.LayoutSwitch" x:Class="Snap.Hutao.Control.Panel.PanelSelector"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwc="using:CommunityToolkit.WinUI.Controls" xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shuxm="using:Snap.Hutao.UI.Xaml.Markup" xmlns:shcm="using:Snap.Hutao.Control.Markup"
Style="{StaticResource DefaultSegmentedStyle}" Style="{StaticResource DefaultSegmentedStyle}"
mc:Ignorable="d"> mc:Ignorable="d">
<cwc:SegmentedItem <cwc:SegmentedItem
Icon="{shuxm:FontIcon Glyph={StaticResource FontIconContentBulletedList}}" Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentBulletedList}}"
Tag="List" Tag="List"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/> ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
<cwc:SegmentedItem <cwc:SegmentedItem
Icon="{shuxm:FontIcon Glyph={StaticResource FontIconContentGridView}}" Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentGridView}}"
Tag="Grid" Tag="Grid"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/> ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
</cwc:Segmented> </cwc:Segmented>

View File

@@ -6,12 +6,16 @@ using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Setting; using Snap.Hutao.Core.Setting;
using System.Collections.Frozen; using System.Collections.Frozen;
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control.Panel;
/// <summary>
/// 面板选择器
/// </summary>
[HighQuality]
[DependencyProperty("Current", typeof(string), List)] [DependencyProperty("Current", typeof(string), List)]
[DependencyProperty("LocalSettingKeySuffixForCurrent", typeof(string))] [DependencyProperty("LocalSettingKeySuffixForCurrent", typeof(string))]
[DependencyProperty("LocalSettingKeyExtraForCurrent", typeof(string), "")] [DependencyProperty("LocalSettingKeyExtraForCurrent", typeof(string), "")]
internal sealed partial class LayoutSwitch : Segmented internal sealed partial class PanelSelector : Segmented
{ {
public const string List = nameof(List); public const string List = nameof(List);
public const string Grid = nameof(Grid); public const string Grid = nameof(Grid);
@@ -22,15 +26,21 @@ internal sealed partial class LayoutSwitch : Segmented
KeyValuePair.Create(1, Grid), KeyValuePair.Create(1, Grid),
]); ]);
private readonly RoutedEventHandler loadedEventHandler = OnRootLoaded; private readonly RoutedEventHandler loadedEventHandler;
private readonly RoutedEventHandler unloadedEventHandler = OnRootUnload; private readonly RoutedEventHandler unloadedEventHandler;
private readonly long selectedIndexChangedCallbackToken; private readonly long selectedIndexChangedCallbackToken;
public LayoutSwitch() /// <summary>
/// 构造一个新的面板选择器
/// </summary>
public PanelSelector()
{ {
InitializeComponent(); InitializeComponent();
loadedEventHandler = OnRootLoaded;
Loaded += loadedEventHandler; Loaded += loadedEventHandler;
unloadedEventHandler = OnRootUnload;
Unloaded += unloadedEventHandler; Unloaded += unloadedEventHandler;
selectedIndexChangedCallbackToken = RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged); selectedIndexChangedCallbackToken = RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged);
@@ -38,7 +48,7 @@ internal sealed partial class LayoutSwitch : Segmented
private static void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp) private static void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp)
{ {
LayoutSwitch selector = (LayoutSwitch)sender; PanelSelector selector = (PanelSelector)sender;
selector.Current = IndexTypeMap[(int)selector.GetValue(dp)]; selector.Current = IndexTypeMap[(int)selector.GetValue(dp)];
if (!string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent)) if (!string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent))
@@ -49,7 +59,7 @@ internal sealed partial class LayoutSwitch : Segmented
private static void OnRootLoaded(object sender, RoutedEventArgs e) private static void OnRootLoaded(object sender, RoutedEventArgs e)
{ {
LayoutSwitch selector = (LayoutSwitch)sender; PanelSelector selector = (PanelSelector)sender;
if (string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent)) if (string.IsNullOrEmpty(selector.LocalSettingKeySuffixForCurrent))
{ {
@@ -64,12 +74,12 @@ internal sealed partial class LayoutSwitch : Segmented
private static void OnRootUnload(object sender, RoutedEventArgs e) private static void OnRootUnload(object sender, RoutedEventArgs e)
{ {
LayoutSwitch selector = (LayoutSwitch)sender; PanelSelector selector = (PanelSelector)sender;
selector.UnregisterPropertyChangedCallback(SelectedIndexProperty, selector.selectedIndexChangedCallbackToken); selector.UnregisterPropertyChangedCallback(SelectedIndexProperty, selector.selectedIndexChangedCallbackToken);
selector.Unloaded -= selector.unloadedEventHandler; selector.Unloaded -= selector.unloadedEventHandler;
} }
private static string GetSettingKey(LayoutSwitch selector) private static string GetSettingKey(PanelSelector selector)
{ {
return $"Control.PanelSelector.{selector.LocalSettingKeySuffixForCurrent}{selector.LocalSettingKeyExtraForCurrent}"; return $"Control.PanelSelector.{selector.LocalSettingKeySuffixForCurrent}{selector.LocalSettingKeyExtraForCurrent}";
} }

View File

@@ -2,9 +2,11 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Windows.Foundation; using Windows.Foundation;
namespace Snap.Hutao.UI.Xaml.Control.Panel; namespace Snap.Hutao.Control.Panel;
[DependencyProperty("MinItemWidth", typeof(double))] [DependencyProperty("MinItemWidth", typeof(double))]
[DependencyProperty("ColumnSpacing", typeof(double))] [DependencyProperty("ColumnSpacing", typeof(double))]

View File

@@ -3,26 +3,27 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Navigation;
using Snap.Hutao.ViewModel.Abstraction; using Snap.Hutao.ViewModel.Abstraction;
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control;
[HighQuality] [HighQuality]
[SuppressMessage("", "CA1001")] [SuppressMessage("", "CA1001")]
internal class ScopedPage : Page internal class ScopedPage : Page
{ {
private readonly RoutedEventHandler unloadEventHandler;
private readonly CancellationTokenSource viewCancellationTokenSource = new(); private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope pageScope; private readonly IServiceScope currentScope;
private bool inFrame = true; private bool inFrame = true;
protected ScopedPage() protected ScopedPage()
{ {
Unloaded += OnUnloaded; unloadEventHandler = OnUnloaded;
pageScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope(); Unloaded += unloadEventHandler;
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
} }
public async ValueTask NotifyRecipientAsync(INavigationData extra) public async ValueTask NotifyRecipientAsync(INavigationData extra)
@@ -35,11 +36,6 @@ internal class ScopedPage : Page
extra.NotifyNavigationCompleted(); extra.NotifyNavigationCompleted();
} }
public virtual void UnloadObjectOverride(DependencyObject unloadableObject)
{
XamlMarkupHelper.UnloadObject(unloadableObject);
}
/// <summary> /// <summary>
/// 初始化 /// 初始化
/// 应当在 InitializeComponent() 前调用 /// 应当在 InitializeComponent() 前调用
@@ -48,23 +44,9 @@ internal class ScopedPage : Page
protected void InitializeWith<TViewModel>() protected void InitializeWith<TViewModel>()
where TViewModel : class, IViewModel where TViewModel : class, IViewModel
{ {
try IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
{ viewModel.CancellationToken = viewCancellationTokenSource.Token;
TViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>(); DataContext = viewModel;
using (viewModel.DisposeLock.Enter())
{
viewModel.Resurrect();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
viewModel.DeferContentLoader = new DeferContentLoader(this);
}
DataContext = viewModel;
}
catch (Exception ex)
{
pageScope.ServiceProvider.GetRequiredService<ILogger<ScopedPage>>().LogError(ex, "Failed to initialize view model.");
throw;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -95,7 +77,7 @@ internal class ScopedPage : Page
return; return;
} }
Unloaded -= OnUnloaded; Unloaded -= unloadEventHandler;
} }
private void DisposeViewModel() private void DisposeViewModel()
@@ -106,14 +88,14 @@ internal class ScopedPage : Page
viewCancellationTokenSource.Cancel(); viewCancellationTokenSource.Cancel();
IViewModel viewModel = (IViewModel)DataContext; IViewModel viewModel = (IViewModel)DataContext;
// Wait to ensure viewmodel operation is completed using (SemaphoreSlim locker = viewModel.DisposeLock)
using (viewModel.DisposeLock.Enter())
{ {
viewModel.Uninitialize(); // Wait to ensure viewmodel operation is completed
locker.Wait();
viewModel.IsViewDisposed = true;
// Dispose the scope // Dispose the scope
pageScope.Dispose(); currentScope.Dispose();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
} }
} }
} }

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
namespace Snap.Hutao.UI.Xaml.Control; namespace Snap.Hutao.Control;
/// <summary> /// <summary>
/// By injecting into services, we take dvantage of the fact that /// By injecting into services, we take dvantage of the fact that
@@ -22,6 +22,7 @@ internal sealed partial class ScopedPageScopeReferenceTracker : IScopedPageScope
public IServiceScope CreateScope() public IServiceScope CreateScope()
{ {
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
IServiceScope currentScope = serviceProvider.CreateScope(); IServiceScope currentScope = serviceProvider.CreateScope();
// In case previous one is not disposed. // In case previous one is not disposed.

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