Compare commits

..

1 Commits

Author SHA1 Message Date
qhy040404
5182fd8577 trigger gc after viewmodel dispose 2024-04-28 14:19:24 +08:00
1362 changed files with 19013 additions and 39164 deletions

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ body:
attributes:
label: Snap Hutao Version
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:
required: true
@@ -62,19 +62,20 @@ body:
description: Please select the most associated category of your issue
options:
- Installation and Environment
- Game Launcher
- Wish Export
- Achievement
- My Character
- Game Launcher
- Realtime Note
- Develop Plan
- Spiral Abyss
- Wiki
- MiHoYo Account Panel
- Daily Checkin Reward
- Hutao Passport/Hutao Cloud
- User Interface
- File Cache
- Wish Export
- Game Record
- Hutao Database
- User Interface
- Snap Hutao Cloud
- Snap Hutao Account
- Checkin
- Wiki
- Announcement
- Other
validations:

View File

@@ -1,7 +1,9 @@
name: Feature Request [English Form]
description: Tell us about your thought
title: "[Feat]: Place your title here"
labels: ["feature request", "needs-triage", "priority:none"]
labels: ["功能", "needs-triage", "priority:none"]
assignees:
- Lightczx
body:
- type: markdown
attributes:
@@ -20,6 +22,6 @@ body:
id: req
attributes:
label: Detail of the Feature
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted.
description: Descripbe the feaure in detail. The more detailed and convincing the desciprtion the more likyly feature will be accepted.
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:
- main
- develop
- 'feat/*'
paths-ignore:
- '.gitattributes'
- '.github/**'
@@ -45,8 +44,13 @@ jobs:
run: dotnet tool restore && dotnet cake
env:
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
if: success() && github.event_name != 'pull_request'
@@ -64,55 +68,12 @@ jobs:
> 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
> [!TIP]
> 普通用户请 [点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/) 下载最新的稳定版本
> 普通用户请[点击这里](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
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)** 到 **受信任的根证书颁发机构** 以安装测试版安装包
> 请注意,从 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

View File

@@ -10,7 +10,7 @@ jobs:
- uses: actions/stale@v9
with:
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-close: 3
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
[![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)
[![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)
![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)
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/Labs-Windows](https://github.com/CommunityToolkit/Labs-Windows)
* [CommunityToolkit/Windows](https://github.com/CommunityToolkit/Windows)
* [dahall/taskscheduler](https://github.com/dahall/taskscheduler)
* [dotnet/efcore](https://github.com/dotnet/efcore)
* [dotnet/runtime](https://github.com/dotnet/runtime)
* [DotNetAnalyzers/StyleCopAnalyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
* [quartznet/quartznet](https://github.com/quartznet/quartznet)
### 支撑项目 / 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.
| [![](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) |
| [![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/) | |
|:----------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|
| [![](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) |
- 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
- [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
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)

View File

@@ -11,18 +11,6 @@ var version = "version";
var repoDir = "repoDir";
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
{
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}");
}
@@ -96,19 +79,10 @@ else // Local
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")
.IsDependentOn("Build binary package")
.IsDependentOn("Copy files")
.IsDependentOn("Build MSIX")
.IsDependentOn("Sign");
.IsDependentOn("Build MSIX");
Task("NuGet Restore")
.Does(() =>
@@ -183,7 +157,6 @@ Task("Build binary package")
.Append("/p:AppxPackageSigningEnabled=false")
.Append("/p:AppxBundle=Never")
.Append("/p:AppxPackageOutput=" + outputPath)
.AppendIf("/p:AlphaConstants=IS_ALPHA_BUILD", !AppVeyor.IsRunningOnAppVeyor)
};
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");
}
var makeappxPath = System.IO.Path.Combine(winsdkBinPath, "makeappx.exe");
var p = StartProcess(
makeappxPath,
"makeappx.exe",
new ProcessSettings
{
Arguments = arguments
@@ -236,46 +206,7 @@ Task("Build MSIX")
);
if (p != 0)
{
throw new InvalidOperationException("Build MSIX 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;
throw new InvalidOperationException("Build failed with exit code " + p);
}
});

View File

@@ -1,5 +1,3 @@
files:
- source: /src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.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
csharp_style_prefer_primary_constructors = false:none
dotnet_diagnostic.SA1124.severity = none
[*.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,
};
private const string SampleObjectJson = """
private const string SmapleObjectJson = """
{
"A" :1
}
""";
private const string SampleEmptyStringObjectJson = """
private const string SmapleEmptyStringObjectJson = """
{
"A" : ""
}
""";
private const string SampleNumberKeyDictionaryJson = """
private const string SmapleNumberKeyDictionaryJson = """
{
"111" : "12",
"222" : "34"
@@ -35,7 +35,7 @@ public sealed class JsonSerializeTest
[TestMethod]
public void DelegatePropertyCanSerialize()
{
SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SampleObjectJson)!;
SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SmapleObjectJson)!;
Assert.AreEqual(sample.B, 1);
}
@@ -43,23 +43,14 @@ public sealed class JsonSerializeTest
[ExpectedException(typeof(JsonException))]
public void EmptyStringCannotSerializeAsNumber()
{
SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SampleEmptyStringObjectJson)!;
SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SmapleEmptyStringObjectJson)!;
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]
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");
}
@@ -101,11 +92,6 @@ public sealed class JsonSerializeTest
public int A { get; set; }
}
private sealed class SampleEmptyUriClass
{
public Uri A { get; set; } = default!;
}
private sealed class SampleByteArrayPropertyClass
{
public byte[]? Array { get; set; }

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

View File

@@ -1,7 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
namespace Snap.Hutao.Test.PlatformExtensions;
@@ -12,8 +11,6 @@ public sealed class DependencyInjectionTest
.AddSingleton<IService, ServiceA>()
.AddSingleton<IService, ServiceB>()
.AddScoped<IScopedService, ServiceA>()
.AddKeyedTransient<IKeyedService, KeyedServiceA>("A")
.AddKeyedTransient<IKeyedService, KeyedServiceB>("B")
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
.AddLogging(builder => builder.AddConsole())
.BuildServiceProvider();
@@ -53,15 +50,6 @@ public sealed class DependencyInjectionTest
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
{
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.InteropServices;
@@ -47,59 +46,7 @@ public sealed class UnsafeRuntimeBehaviorTest
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
{

View File

@@ -13,9 +13,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" 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="MSTest.TestAdapter" Version="3.4.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.4.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

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

View File

@@ -3,12 +3,12 @@
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Microsoft.Windows.AppNotifications;
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Shell;
using System.Diagnostics;
namespace Snap.Hutao;
@@ -39,7 +39,7 @@ public sealed partial class App : Application
""";
private readonly IServiceProvider serviceProvider;
private readonly IAppActivation activation;
private readonly IActivation activation;
private readonly ILogger<App> logger;
/// <summary>
@@ -50,33 +50,22 @@ public sealed partial class App : Application
{
// Load app resource
InitializeComponent();
activation = serviceProvider.GetRequiredService<IAppActivation>();
activation = serviceProvider.GetRequiredService<IActivation>();
logger = serviceProvider.GetRequiredService<ILogger<App>>();
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
this.serviceProvider = serviceProvider;
}
public new void Exit()
{
XamlApplicationLifetime.Exiting = true;
base.Exit();
}
/// <inheritdoc/>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
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();
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
{
logger.LogDebug("Application exiting on RedirectActivationTo");
Exit();
return;
}
@@ -84,13 +73,15 @@ public sealed partial class App : Application
logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow));
LogDiagnosticInformation();
// Manually invoke
// manually invoke
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();
}
}

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.
// 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>
/// 1

View File

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

View File

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

View File

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

View File

@@ -3,9 +3,9 @@
using CommunityToolkit.WinUI.Behaviors;
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>
/// 在元素加载完成后执行命令的行为

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml;
namespace Snap.Hutao.UI.Xaml;
namespace Snap.Hutao.Control;
/// <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,34 +7,41 @@ using Microsoft.UI.Xaml.Data;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using Windows.Foundation;
using Windows.Foundation.Collections;
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>
where T : class, IAdvancedCollectionViewItem
internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPropertyChanged, ISupportIncrementalLoading, IComparer<object>
where T : class
{
private readonly bool created;
private readonly List<T> view;
private readonly ObservableCollection<SortDescription> sortDescriptions;
private readonly Dictionary<string, PropertyInfo?> sortProperties;
private readonly bool liveShapingEnabled;
private readonly HashSet<string?> observedFilterProperties = [];
private IList<T> source;
private Predicate<T>? filter;
private int deferCounter;
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
public AdvancedCollectionView(IList<T> source)
public AdvancedCollectionView()
: this([])
{
}
public AdvancedCollectionView(IList<T> source, bool isLiveShaping = false)
{
liveShapingEnabled = isLiveShaping;
view = [];
sortDescriptions = [];
sortDescriptions.CollectionChanged += SortDescriptionsCollectionChanged;
sortProperties = [];
Source = source;
created = true;
}
public event EventHandler<object>? CurrentChanged;
@@ -45,55 +52,7 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
public event VectorChangedEventHandler<object>? VectorChanged;
public int Count
{
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
public IList<T> Source
{
get => source;
@@ -115,26 +74,106 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
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,
OnDetachAction = listener => sourceINCC.CollectionChanged -= listener.OnEvent,
// Call the actual collection changed event
OnEventAction = (source, changed, arg3) => SourceNotifyCollectionChangedCollectionChanged(source, arg3),
// The source doesn't exist anymore
OnDetachAction = (listener) =>
{
ArgumentNullException.ThrowIfNull(sourceWeakEventListener);
sourceNotifyCollectionChanged.CollectionChanged -= sourceWeakEventListener.OnEvent;
},
};
sourceINCC.CollectionChanged += sourceWeakEventListener.OnEvent;
sourceNotifyCollectionChanged.CollectionChanged += sourceWeakEventListener.OnEvent;
}
HandleSourceChanged();
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]
{
get => view[index];
@@ -188,12 +227,14 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
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)
@@ -206,10 +247,9 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
Remove(view[index]);
}
[SuppressMessage("", "SH007")]
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)
@@ -242,13 +282,46 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
return (source as ISupportIncrementalLoading)?.LoadMoreItemsAsync(count);
}
public void ObserveFilterProperty(string propertyName)
{
observedFilterProperties.Add(propertyName);
}
public void ClearObservedFilterProperties()
{
observedFilterProperties.Clear();
}
public IDisposable DeferRefresh()
{
return new NotificationDeferrer(this);
}
int IComparer<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)
{
object? cx, cy;
@@ -260,8 +333,10 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
}
else
{
cx = x?.GetPropertyValue(sd.PropertyName);
cy = y?.GetPropertyValue(sd.PropertyName);
PropertyInfo? pi = sortProperties[sd.PropertyName];
cx = pi?.GetValue(x);
cy = pi?.GetValue(y);
}
int cmp = sd.Comparer.Compare(cx, cy);
@@ -275,63 +350,77 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
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));
}
private void ItemOnPropertyChanged(object? item, PropertyChangedEventArgs e)
{
if (!liveShapingEnabled)
{
return;
}
ArgumentNullException.ThrowIfNull(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);
// Check if item is in view
if (oldIndex < 0)
if ((filterResult ?? true) && SortDescriptions.Any(sd => sd.PropertyName == e.PropertyName))
{
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);
}
}
view.RemoveAt(oldIndex);
int targetIndex = view.BinarySearch(typedItem, comparer: this);
if (targetIndex < 0)
else if (string.IsNullOrEmpty(e.PropertyName))
{
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);
HandleSourceChanged();
}
}
private void AttachPropertyChangedHandler(IEnumerable items)
{
if (items is null)
if (!liveShapingEnabled || items is null)
{
return;
}
@@ -347,7 +436,7 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
private void DetachPropertyChangedHandler(IEnumerable items)
{
if (items is null)
if (!liveShapingEnabled || items is null)
{
return;
}
@@ -363,7 +452,9 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
private void HandleSortChanged()
{
sortProperties.Clear();
view.Sort(this);
sortProperties.Clear();
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
}
@@ -384,18 +475,18 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
}
}
HashSet<T> viewSet = new(view);
HashSet<T> viewHash = new(view);
int viewIndex = 0;
for (int index = 0; index < source.Count; index++)
{
T item = source[index];
if (viewSet.Contains(item))
if (viewHash.Contains(item))
{
viewIndex++;
continue;
}
if (HandleSourceItemAdded(index, item, viewIndex))
if (HandleItemAdded(index, item, viewIndex))
{
viewIndex++;
}
@@ -404,114 +495,101 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
private void HandleSourceChanged()
{
sortProperties.Clear();
T? currentItem = CurrentItem;
view.Clear();
view.TrimExcess();
if (filter is null && sortDescriptions.Count <= 0)
foreach (T item in Source)
{
// Fast path
View.AddRange(Source);
}
else
{
foreach (T item in Source)
if (filter is not null && !filter(item))
{
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)
{
int targetIndex = view.BinarySearch(item, this);
if (targetIndex < 0)
{
targetIndex = ~targetIndex;
}
view.Insert(targetIndex, item);
}
else
{
view.Add(item);
}
view.Insert(targetIndex, item);
}
else
{
view.Add(item);
}
}
sortProperties.Clear();
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
MoveCurrentTo(currentItem);
}
private void SourceNotifyCollectionChangedCollectionChanged(NotifyCollectionChangedEventArgs e)
private void SourceNotifyCollectionChangedCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
ArgumentNullException.ThrowIfNull(e.NewItems);
AttachPropertyChangedHandler(e.NewItems);
if (deferCounter > 0)
if (deferCounter <= 0)
{
break;
}
if (e.NewItems?.Count is 1)
{
object? newItem = e.NewItems[0];
ArgumentNullException.ThrowIfNull(newItem);
HandleSourceItemAdded(e.NewStartingIndex, (T)newItem);
}
else
{
HandleSourceChanged();
if (e.NewItems?.Count == 1)
{
object? newItem = e.NewItems[0];
ArgumentNullException.ThrowIfNull(newItem);
HandleItemAdded(e.NewStartingIndex, (T)newItem);
}
else
{
HandleSourceChanged();
}
}
break;
case NotifyCollectionChangedAction.Remove:
ArgumentNullException.ThrowIfNull(e.OldItems);
DetachPropertyChangedHandler(e.OldItems);
if (deferCounter > 0)
if (deferCounter <= 0)
{
break;
}
if (e.OldItems?.Count == 1)
{
object? oldItem = e.OldItems[0];
ArgumentNullException.ThrowIfNull(oldItem);
HandleSourceItemRemoved(e.OldStartingIndex, (T)oldItem);
}
else
{
HandleSourceChanged();
if (e.OldItems?.Count == 1)
{
object? oldItem = e.OldItems[0];
ArgumentNullException.ThrowIfNull(oldItem);
HandleItemRemoved(e.OldStartingIndex, (T)oldItem);
}
else
{
HandleSourceChanged();
}
}
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Reset:
if (deferCounter > 0)
if (deferCounter <= 0)
{
break;
HandleSourceChanged();
}
HandleSourceChanged();
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))
{
return false;
}
int newViewIndex = newStartingIndex;
int newViewIndex = view.Count;
if (sortDescriptions.Count > 0)
{
sortProperties.Clear();
newViewIndex = view.BinarySearch(newItem, this);
if (newViewIndex < 0)
{
@@ -566,7 +644,7 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
return true;
}
private void HandleSourceItemRemoved(int oldStartingIndex, T oldItem)
private void HandleItemRemoved(int oldStartingIndex, T oldItem)
{
if (filter is not null && !filter(oldItem))
{
@@ -592,12 +670,6 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
if (itemIndex <= CurrentPosition)
{
CurrentPosition--;
// Removed item is last item
if (view.Count == itemIndex)
{
OnCurrentChanged();
}
}
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, itemIndex, item));
@@ -615,57 +687,52 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
private bool MoveCurrentToIndex(int i)
{
if (i < -1 || i >= view.Count)
{
return false;
}
if (i == CurrentPosition)
{
return false;
}
if (i < -1 || i >= view.Count)
{
// view is empty, i is 0, current pos is -1
OnPropertyChanged(nameof(CurrentItem));
return false;
}
OnCurrentChanging(out bool cancel);
if (cancel)
CurrentChangingEventArgs e = new();
OnCurrentChanging(e);
if (e.Cancel)
{
return false;
}
CurrentPosition = i;
OnCurrentChanged();
OnCurrentChanged(default!);
return true;
}
private void OnCurrentChanging(out bool cancel)
private void OnCurrentChanging(CurrentChangingEventArgs e)
{
if (!created || deferCounter > 0)
if (deferCounter > 0)
{
cancel = false;
return;
}
CurrentChangingEventArgs e = new();
CurrentChanging?.Invoke(this, e);
cancel = e.Cancel;
}
private void OnCurrentChanged()
private void OnCurrentChanged(object e)
{
if (!created || deferCounter > 0)
if (deferCounter > 0)
{
return;
}
OnCurrentChangedOverride();
CurrentChanged?.Invoke(this, default!);
CurrentChanged?.Invoke(this, e);
OnPropertyChanged(nameof(CurrentItem));
}
private void OnVectorChanged(IVectorChangedEventArgs e)
{
if (!created || deferCounter > 0)
if (deferCounter > 0)
{
return;
}

View File

@@ -2,16 +2,18 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
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
where T : class
{
bool CanFilter { get; }
bool CanSort { get; }
object? ICollectionView.CurrentItem
{
get => CurrentItem;
@@ -21,9 +23,9 @@ internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
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]
{
@@ -40,6 +42,8 @@ internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
void Add(T item);
void ClearObservedFilterProperties();
bool ICollection<object>.Contains(object item)
{
return Contains((T)item);
@@ -65,18 +69,7 @@ internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
int IList<object>.IndexOf(object item)
{
if (item is T dataItem1)
{
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!);
return IndexOf((T)item);
}
int IndexOf(T item);
@@ -93,7 +86,9 @@ internal interface IAdvancedCollectionView<T> : ICollectionView, IEnumerable
return MoveCurrentTo((T)item);
}
bool MoveCurrentTo(T? item);
bool MoveCurrentTo(T item);
void ObserveFilterProperty(string propertyName);
void Refresh();

View File

@@ -3,11 +3,11 @@
using Windows.Foundation.Collections;
namespace Snap.Hutao.UI.Xaml.Data;
namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
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;
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.Data;
namespace Snap.Hutao.UI.Xaml.Control;
namespace Snap.Hutao.Control.Collection.Selector;
[DependencyProperty("EnableMemberPath", typeof(string))]
internal sealed partial class ComboBox2 : ComboBox
{
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
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.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
{
/// <inheritdoc/>
public object? Convert(object value, Type targetType, object parameter, string language)
{
return Convert((TFrom)value);
}
/// <inheritdoc/>
public object? ConvertBack(object value, Type targetType, object parameter, string language)
{
return ConvertBack((TTo)value);
}
/// <summary>
/// 从源类型转换到目标类型
/// </summary>
/// <param name="from">源</param>
/// <returns>目标</returns>
public abstract TTo Convert(TFrom from);
/// <summary>
/// 从目标类型转换到源类型
/// 重写时请勿调用基类方法
/// </summary>
/// <param name="to">目标</param>
/// <returns>源</returns>
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.
// Licensed under the MIT license.
namespace Snap.Hutao.UI.Input;
namespace Snap.Hutao.Control.Extension;
internal static class CommandInvocation
{

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.UI.Xaml.Control;
namespace Snap.Hutao.Control.Extension;
/// <summary>
/// 对话框扩展
@@ -17,13 +17,13 @@ internal static class ContentDialogExtension
/// <param name="contentDialog">对话框</param>
/// <param name="taskContext">任务上下文</param>
/// <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();
// E_ASYNC_OPERATION_NOT_STARTED 0x80000019
// Only a single ContentDialog can be open at any time.
_ = contentDialog.ShowAsync();
return new ContentDialogScope(contentDialog, taskContext);
contentDialog.ShowAsync().AsTask().SafeForget();
return new ContentDialogHideToken(contentDialog, taskContext);
}
}

View File

@@ -3,9 +3,9 @@
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 ITaskContext taskContext;
@@ -13,7 +13,7 @@ internal struct ContentDialogScope : IDisposable, IAsyncDisposable
private bool disposing = false;
private bool disposed = false;
public ContentDialogScope(ContentDialog contentDialog, ITaskContext taskContext)
public ContentDialogHideToken(ContentDialog contentDialog, ITaskContext taskContext)
{
this.contentDialog = contentDialog;
this.taskContext = taskContext;

View File

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

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml;
namespace Snap.Hutao.UI.Xaml;
namespace Snap.Hutao.Control.Extension;
internal static class FrameworkElementExtension
{
@@ -39,6 +39,7 @@ internal static class FrameworkElementExtension
}
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,10 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using System.Diagnostics;
namespace Snap.Hutao.Web.WebView2;
namespace Snap.Hutao.Control.Extension;
/// <summary>
/// Bridge 拓展
@@ -38,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);
}

View File

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

View File

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

View File

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

View File

@@ -3,11 +3,10 @@
using Microsoft.UI.Xaml;
namespace Snap.Hutao.UI.Xaml;
namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[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))]
public sealed partial class UIElementHelper
{
@@ -22,10 +21,4 @@ public sealed partial class UIElementHelper
UIElement element = (UIElement)dp;
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.
// Licensed under the MIT license.
namespace Snap.Hutao.UI.Xaml.Control;
namespace Snap.Hutao.Control;
internal interface IScopedPageScopeReferenceTracker : IDisposable
{

View File

@@ -1,6 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.UI.Xaml;
namespace Snap.Hutao.Control;
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), 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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shuxci="using:Snap.Hutao.UI.Xaml.Control.Image"
xmlns:shuxm="using:Snap.Hutao.UI.Xaml.Markup">
<Style TargetType="shuxci:CachedImage">
xmlns:shci="using:Snap.Hutao.Control.Image">
<Style TargetType="shci:CachedImage">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="LazyLoadingThreshold" Value="256"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="shuxci:CachedImage">
<ControlTemplate TargetType="shci:CachedImage">
<Grid
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
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
Name="PlaceholderImage"
Margin="{TemplateBinding PlaceholderMargin}"

View File

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

View File

@@ -6,18 +6,23 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Animation;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Graphics;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.UI.Xaml.Media.Animation;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
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))]
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 SizeChangedEventHandler sizeChangedEventHandler;
private readonly TypedEventHandler<LoadedImageSurface, LoadedImageSourceLoadCompletedEventArgs> loadedImageSourceLoadCompletedEventHandler;
private TaskCompletionSource? surfaceLoadTaskCompletionSource;
private SpriteVisual? spriteVisual;
private bool isShow = true;
/// <summary>
/// 构造一个新的单色图像
/// </summary>
protected CompositionImage()
{
serviceProvider = this.ServiceProvider();
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 virtual void LoadImageSurfaceCompleted(LoadedImageSurface surface)
{
}
/// <summary>
/// 更新视觉对象
/// </summary>
/// <param name="spriteVisual">拼合视觉</param>
protected virtual void UpdateVisual(SpriteVisual spriteVisual)
{
spriteVisual.Size = ActualSize;
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (!string.IsNullOrEmpty(Source.OriginalString))
{
OnSourceChangedCore(Source);
}
}
private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
{
CompositionImage image = (CompositionImage)sender;
@@ -72,7 +87,9 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
// value is different from old one
if (inner != (arg.OldValue as Uri))
{
image.OnSourceChangedCore(inner);
image
.ApplyImageAsync(inner, token)
.SafeForget(logger, ex => OnApplyImageFailed(serviceProvider, inner, ex));
}
}
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)
{
Debugger.Break();
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
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)
{
await HideAsync(token).ConfigureAwait(true);
@@ -145,28 +154,21 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
await ShowAsync(token).ConfigureAwait(true);
}
}
else
{
Debugger.Break();
}
}
}
private async ValueTask<LoadedImageSurface> LoadImageSurfaceAsync(string file, CancellationToken token)
{
token.ThrowIfCancellationRequested();
TaskCompletionSource cancelTcs = new();
CancellationTokenRegistration registration = token.Register(() => cancelTcs.TrySetResult(), false);
surfaceLoadTaskCompletionSource = new();
LoadedImageSurface? surface = default;
try
{
surface = LoadedImageSurface.StartLoadFromUri(file.ToUri());
surface.LoadCompleted += OnLoadImageSurfaceLoadCompleted;
surface.LoadCompleted += loadedImageSourceLoadCompletedEventHandler;
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);
@@ -176,10 +178,8 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{
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;
if (EnableShowHideAnimation)
if (EnableLazyLoading)
{
await AnimationBuilder
.Create()
.Opacity(from: 0D, to: 1D, duration: Constants.ImageScaleFadeIn)
.Opacity(from: 0D, to: 1D, duration: ControlAnimationConstants.ImageScaleFadeIn)
.StartAsync(this, token)
.ConfigureAwait(true);
}
@@ -210,11 +210,11 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
{
isShow = false;
if (EnableShowHideAnimation)
if (EnableLazyLoading)
{
await AnimationBuilder
.Create()
.Opacity(from: 1D, to: 0D, duration: Constants.ImageScaleFadeOut)
.Opacity(from: 1D, to: 0D, duration: ControlAnimationConstants.ImageScaleFadeOut)
.StartAsync(this, token)
.ConfigureAwait(true);
}

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.UI.Xaml.Control.Image;
namespace Snap.Hutao.Control.Image;
/// <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.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.DataTransfer;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.UI.Xaml.Control.Theme;
using System.Diagnostics;
using Snap.Hutao.Win32;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Graphics.Imaging;
using Windows.Media.Casting;
using Windows.Storage.Streams;
using Windows.Foundation;
namespace Snap.Hutao.UI.Xaml.Control.Image;
namespace Snap.Hutao.Control.Image.Implementation;
[SuppressMessage("", "CA1001")]
[SuppressMessage("", "SH003")]
@@ -27,32 +20,30 @@ namespace Snap.Hutao.UI.Xaml.Control.Image;
[TemplateVisualState(Name = FailedState, GroupName = CommonGroup)]
[TemplatePart(Name = PartImage, 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("DecodePixelHeight", typeof(int), 0)]
[DependencyProperty("DecodePixelWidth", typeof(int), 0)]
[DependencyProperty("DecodePixelType", typeof(DecodePixelType), DecodePixelType.Physical)]
[DependencyProperty("IsCacheEnabled", typeof(bool), false)]
[DependencyProperty("EnableLazyLoading", typeof(bool), false, nameof(EnableLazyLoadingChanged))]
[DependencyProperty("LazyLoadingThreshold", typeof(double), default(double), nameof(LazyLoadingThresholdChanged))]
[DependencyProperty("PlaceholderSource", typeof(object), default(object))]
[DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("PlaceholderMargin", typeof(Thickness))]
[DependencyProperty("Source", typeof(object), default(object), nameof(OnSourceChanged))]
[DependencyProperty("ShowAsMonoChrome", typeof(bool), false)]
internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
[DependencyProperty("Source", typeof(object), default(object), nameof(SourceChanged))]
internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
{
private const string PartImage = "Image";
private const string PartPlaceholderImage = "PlaceholderImage";
private const string CommonGroup = "CommonStates";
private const string LoadingState = "Loading";
private const string LoadedState = "Loaded";
private const string UnloadedState = "Unloaded";
private const string FailedState = "Failed";
protected const string PartImage = "Image";
protected const string PartPlaceholderImage = "PlaceholderImage";
protected const string CommonGroup = "CommonStates";
protected const string LoadingState = "Loading";
protected const string LoadedState = "Loaded";
protected const string UnloadedState = "Unloaded";
protected const string FailedState = "Failed";
private CancellationTokenSource? tokenSource;
public CachedImage()
{
DefaultStyleKey = typeof(CachedImage);
ActualThemeChanged += OnActualThemeChanged;
}
private object? lazyLoadingSource;
private bool isInViewport;
public bool IsInitialized { get; private set; }
@@ -61,28 +52,26 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
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)
{
return image.GetAlphaMask();
}
return default!;
// By default we just use the built-in UWP image cache provided within the Image control.
return Task.FromResult<ImageSource?>(new BitmapImage(imageUri));
}
public CastingSource GetAsCastingSource()
protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
{
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)
{
return image.GetAsCastingSource();
}
VisualStateManager.GoToState(this, LoadedState, true);
}
return default!;
protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e)
{
VisualStateManager.GoToState(this, FailedState, true);
}
protected override void OnApplyTemplate()
@@ -91,10 +80,19 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
RemoveImageFailed(OnImageFailed);
Image = GetTemplateChild(PartImage);
PlaceholderImage = GetTemplateChild(PartPlaceholderImage);
IsInitialized = true;
SetSource(Source);
if (Source is null || !EnableLazyLoading || isInViewport)
{
lazyLoadingSource = null;
SetSource(Source);
}
else
{
lazyLoadingSource = Source;
}
AttachImageOpened(OnImageOpened);
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;
}
@@ -162,7 +187,15 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
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)
@@ -170,49 +203,11 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https");
}
private async Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
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)
private void AttachSource(ImageSource? source)
{
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
// We only need to call those methods if we fail in other cases before we get here.
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.Source = source;
@@ -226,15 +221,13 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
{
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);
}
}
private void AttachPlaceholderSource(BitmapImage? source, Uri? uri)
private void AttachPlaceholderSource(ImageSource? source)
{
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);
}
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, FailedState, true);
VisualStateManager.GoToState(this, LoadedState, true);
}
}
@@ -265,9 +256,10 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
}
tokenSource?.Cancel();
tokenSource = new CancellationTokenSource();
AttachSource(default, default);
AttachSource(null);
if (source is null)
{
@@ -276,6 +268,13 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
VisualStateManager.GoToState(this, LoadingState, true);
if (source as ImageSource is { } imageSource)
{
AttachSource(imageSource);
return;
}
if (source as Uri is not { } uri)
{
string? url = source as string ?? source.ToString();
@@ -320,13 +319,20 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
tokenSource?.Cancel();
tokenSource = new();
AttachPlaceholderSource(default, default);
AttachPlaceholderSource(null);
if (source is null)
{
return;
}
if (source as ImageSource is { } imageSource)
{
AttachPlaceholderSource(imageSource);
return;
}
if (source as Uri is not { } uri)
{
string? url = source as string ?? source.ToString();
@@ -348,13 +354,13 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
return;
}
Uri? actualUri = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
ImageSource? img = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
// Only attach our image if we still have a valid request.
AttachPlaceholderSource(new BitmapImage(), actualUri);
AttachPlaceholderSource(img);
}
}
catch (OperationCanceledException)
@@ -373,36 +379,99 @@ internal sealed partial class CachedImage : Microsoft.UI.Xaml.Controls.Control,
return;
}
Uri? actualUri = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
if (IsCacheEnabled)
{
// Only attach our image if we still have a valid request.
AttachSource(new BitmapImage(), actualUri);
}
}
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
[Command("CopyToClipboardCommand")]
private async Task CopyToClipboard()
{
if (Image is Microsoft.UI.Xaml.Controls.Image { Source: BitmapImage bitmap })
{
using (FileStream netStream = File.OpenRead(bitmap.UriSource.LocalPath))
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
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);
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);
}
AttachSource(bitmap);
}
}
}
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;
namespace Snap.Hutao.UI.Xaml.Control.Layout;
namespace Snap.Hutao.Control.Layout;
[DebuggerDisplay("Count = {Count}, Height = {Height}")]
internal class UniformStaggeredColumnLayout : List<UniformStaggeredItem>

View File

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

View File

@@ -7,7 +7,7 @@ using System.Collections.Specialized;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.UI.Xaml.Control.Layout;
namespace Snap.Hutao.Control.Layout;
[DependencyProperty("MinItemWidth", typeof(double), 0D, nameof(OnMinItemWidthChanged))]
[DependencyProperty("MinColumnSpacing", typeof(double), 0D, nameof(OnSpacingChanged))]
@@ -63,12 +63,12 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
/// <inheritdoc/>
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
if (context.ItemCount is 0)
if (context.ItemCount == 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);
}
@@ -188,7 +188,6 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
}
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
for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
@@ -205,13 +204,9 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
// Partial or fully in the view
if (item.Top <= context.RealizationRect.Bottom)
{
double itemHorizontalOffset = (state.ColumnWidth + MinColumnSpacing) * columnIndex;
double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex);
double width = columnIndex == virtualColumnCount - 1
? finalSize.Width - itemHorizontalOffset
: state.ColumnWidth;
Rect bounds = new(itemHorizontalOffset, item.Top, width, item.Height);
Rect bounds = new(itemHorizontalOffset, item.Top, state.ColumnWidth, item.Height);
UIElement element = context.GetOrCreateElementAt(item.Index);
element.Arrange(bounds);
}

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml.Controls;
using System.Runtime.InteropServices;
namespace Snap.Hutao.UI.Xaml.Control.Layout;
namespace Snap.Hutao.Control.Layout;
internal sealed class UniformStaggeredLayoutState
{
@@ -49,7 +49,7 @@ internal sealed class UniformStaggeredLayoutState
throw new IndexOutOfRangeException();
}
if (index <= items.Count - 1)
if (index <= (items.Count - 1))
{
return items[index];
}
@@ -83,7 +83,7 @@ internal sealed class UniformStaggeredLayoutState
}
averageHeight /= columnLayout.Count;
double estimatedHeight = averageHeight * context.ItemCount / columnLayout.Count;
double estimatedHeight = (averageHeight * context.ItemCount) / columnLayout.Count;
if (estimatedHeight > desiredHeight)
{
desiredHeight = estimatedHeight;
@@ -100,11 +100,7 @@ internal sealed class UniformStaggeredLayoutState
internal void Clear()
{
if (items.Count > 0)
{
RecycleElements();
}
RecycleElements();
ClearColumns();
ClearItems();
}
@@ -121,9 +117,12 @@ internal sealed class UniformStaggeredLayoutState
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);
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;
layout.RemoveRange(i, numToRemove);

View File

@@ -2,15 +2,12 @@
// Licensed under the MIT license.
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 = "LoadingOut", GroupName = "CommonStates")]
[TemplatePart(Name = "ContentGrid", Type = typeof(FrameworkElement))]
[TemplatePart(Name = "LoadingOutStoryboard", Type = typeof(Storyboard))]
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));
@@ -20,6 +17,7 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
public Loading()
{
DefaultStyleKey = typeof(Loading);
DefaultStyleResourceUri = "ms-appx:///Control/Loading.xaml".ToUri();
}
public bool IsLoading
@@ -31,11 +29,6 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (GetTemplateChild("LoadingOutStoryboard") is Storyboard storyboard)
{
storyboard.Completed -= UnloadPresenter;
storyboard.Completed += UnloadPresenter;
}
Update();
}
@@ -43,25 +36,13 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
private static void IsLoadingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Loading control = (Loading)d;
control.presenter ??= control.GetTemplateChild("ContentGrid") as FrameworkElement;
if ((bool)e.NewValue)
{
control.presenter ??= control.GetTemplateChild("ContentGrid") as FrameworkElement;
}
control.Update();
control?.Update();
}
private void Update()
{
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
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:clw="using:CommunityToolkit.Labs.WinUI"
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">
xmlns:shc="using:Snap.Hutao.Control">
<Style BasedOn="{StaticResource DefaultLoadingStyle}" TargetType="shuxc:Loading"/>
<Style BasedOn="{StaticResource DefaultLoadingStyle}" TargetType="shc:Loading"/>
<Style x:Key="DefaultLoadingStyle" TargetType="shuxc:Loading">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Style x:Key="DefaultLoadingStyle" TargetType="shc:Loading">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="shuxc:Loading">
<ControlTemplate TargetType="shc:Loading">
<Border
x:Name="RootGrid"
Background="{TemplateBinding Background}"
@@ -25,8 +23,11 @@
<ContentPresenter
x:Name="ContentGrid"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
x:Load="True"/>
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<ContentPresenter.RenderTransform>
<CompositeTransform/>
</ContentPresenter.RenderTransform>
</ContentPresenter>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="LoadingIn">
@@ -53,7 +54,7 @@
</Storyboard>
</VisualState>
<VisualState x:Name="LoadingOut">
<Storyboard x:Name="LoadingOutStoryboard">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1">
<EasingDoubleKeyFrame.EasingFunction>
@@ -81,62 +82,6 @@
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<Style
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>
</ResourceDictionary>

View File

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

View File

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

View File

@@ -4,21 +4,23 @@
using Microsoft.UI.Xaml.Markup;
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]
[MarkupExtensionReturnType(ReturnType = typeof(string))]
internal sealed class ResourceStringExtension : MarkupExtension
{
private string? name;
public string? Name { get => name; set => name = value is null ? null : string.Intern(value); }
public string? CultureName { get; set; }
/// <summary>
/// Gets or sets associated ID from resource strings.
/// </summary>
public string? Name { get; set; }
/// <inheritdoc/>
protected override object ProvideValue()
{
CultureInfo cultureInfo = CultureName is not null ? CultureInfo.GetCultureInfo(CultureName) : CultureInfo.CurrentCulture;
return SH.ResourceManager.GetString(Name ?? string.Empty, cultureInfo) ?? Name ?? string.Empty;
return SH.ResourceManager.GetString(Name ?? string.Empty, CultureInfo.CurrentCulture) ?? Name ?? string.Empty;
}
}

View File

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

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.UI.Xaml.Markup;
namespace Snap.Hutao.Control.Markup;
/// <summary>
/// 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.
// Licensed under the MIT license.
using Snap.Hutao.UI;
using Snap.Hutao.Win32.System.WinRT;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using WinRT;
namespace Snap.Hutao.Core.Graphics.Imaging;
namespace Snap.Hutao.Control.Media;
/// <summary>
/// 软件位图拓展
/// </summary>
[HighQuality]
internal static class SoftwareBitmapExtension
{
/// <summary>
/// 混合模式 正常
/// </summary>
/// <param name="softwareBitmap">软件位图</param>
/// <param name="tint">底色</param>
public static unsafe void NormalBlend(this SoftwareBitmap softwareBitmap, Bgra32 tint)
{
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))
{
@@ -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 Windows.Foundation;
namespace Snap.Hutao.UI.Xaml.Control.Panel;
namespace Snap.Hutao.Control.Panel;
[DependencyProperty("Spacing", typeof(double), default(double), nameof(OnSpacingChanged))]
internal partial class EqualPanel : Microsoft.UI.Xaml.Controls.Panel

View File

@@ -5,7 +5,7 @@ using Microsoft.UI.Xaml;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.UI.Xaml.Control.Panel;
namespace Snap.Hutao.Control.Panel;
[DependencyProperty("MinItemWidth", typeof(double))]
[DependencyProperty("Spacing", typeof(double))]

View File

@@ -1,21 +1,21 @@
<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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
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}"
mc:Ignorable="d">
<cwc:SegmentedItem
Icon="{shuxm:FontIcon Glyph={StaticResource FontIconContentBulletedList}}"
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentBulletedList}}"
Tag="List"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
<cwc:SegmentedItem
Icon="{shuxm:FontIcon Glyph={StaticResource FontIconContentGridView}}"
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentGridView}}"
Tag="Grid"
ToolTipService.ToolTip="{shuxm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
ToolTipService.ToolTip="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
</cwc:Segmented>

View File

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

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml;
using Windows.Foundation;
namespace Snap.Hutao.UI.Xaml.Control.Panel;
namespace Snap.Hutao.Control.Panel;
[DependencyProperty("MinItemWidth", typeof(double))]
[DependencyProperty("ColumnSpacing", typeof(double))]

View File

@@ -3,17 +3,17 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.ViewModel.Abstraction;
namespace Snap.Hutao.UI.Xaml.Control;
namespace Snap.Hutao.Control;
[HighQuality]
[SuppressMessage("", "CA1001")]
internal class ScopedPage : Page
{
private readonly RoutedEventHandler unloadEventHandler;
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope pageScope;
@@ -21,7 +21,8 @@ internal class ScopedPage : Page
protected ScopedPage()
{
Unloaded += OnUnloaded;
unloadEventHandler = OnUnloaded;
Unloaded += unloadEventHandler;
pageScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
}
@@ -35,11 +36,6 @@ internal class ScopedPage : Page
extra.NotifyNavigationCompleted();
}
public virtual void UnloadObjectOverride(DependencyObject unloadableObject)
{
XamlMarkupHelper.UnloadObject(unloadableObject);
}
/// <summary>
/// 初始化
/// 应当在 InitializeComponent() 前调用
@@ -50,14 +46,8 @@ internal class ScopedPage : Page
{
try
{
TViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>();
using (viewModel.DisposeLock.Enter())
{
viewModel.Resurrect();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
viewModel.DeferContentLoader = new DeferContentLoader(this);
}
IViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
}
catch (Exception ex)
@@ -95,7 +85,7 @@ internal class ScopedPage : Page
return;
}
Unloaded -= OnUnloaded;
Unloaded -= unloadEventHandler;
}
private void DisposeViewModel()
@@ -106,10 +96,11 @@ internal class ScopedPage : Page
viewCancellationTokenSource.Cancel();
IViewModel viewModel = (IViewModel)DataContext;
// Wait to ensure viewmodel operation is completed
using (viewModel.DisposeLock.Enter())
using (SemaphoreSlim locker = viewModel.DisposeLock)
{
viewModel.Uninitialize();
// Wait to ensure viewmodel operation is completed
locker.Wait();
viewModel.IsViewDisposed = true;
// Dispose the scope
pageScope.Dispose();

View File

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

View File

@@ -5,7 +5,7 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
namespace Snap.Hutao.UI.Xaml.Control;
namespace Snap.Hutao.Control;
[DependencyProperty("IsWidthRestricted", typeof(bool), true)]
[DependencyProperty("IsHeightRestricted", typeof(bool), true)]
@@ -20,20 +20,9 @@ internal sealed partial class SizeRestrictedContentControl : ContentControl
{
element.Measure(availableSize);
Size contentDesiredSize = element.DesiredSize;
Size contentActualOrDesiredSize = new(
Math.Clamp(element.ActualWidth, contentDesiredSize.Width, availableSize.Width),
Math.Clamp(element.ActualHeight, contentDesiredSize.Height, availableSize.Height));
if (minContentWidth > availableSize.Width)
{
minContentWidth = 0;
}
if (minContentHeight > availableSize.Height)
{
minContentHeight = 0;
}
Math.Max(element.ActualWidth, contentDesiredSize.Width),
Math.Max(element.ActualHeight, contentDesiredSize.Height));
if (IsWidthRestricted)
{

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