Compare commits

..

2 Commits

Author SHA1 Message Date
qhy040404
3b86783493 migrate to TokenizingTextBox 2024-02-23 18:08:30 +08:00
qhy040404
e3adc2e595 finish suggestion methods 2024-02-23 14:10:09 +08:00
710 changed files with 6750 additions and 18740 deletions

View File

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

@@ -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,15 +0,0 @@
<!--- Hi, thanks for considering make a PR contribution to Snap Hutao, we appreciate your work. -->
<!--- Before you create this PR, please fill 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

@@ -1,36 +1,41 @@
![HutaoRepoBanner3-en](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/7289da68-59cf-409b-bd85-4b5a01d0c091)
![HutaoRepoBanner2-20231222](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/2d178de1-95bc-44a1-a95e-20c5f11a8628)
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新功能相结合,提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新 功能相结合,提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed for modern Windows platform to improve the gaming experience for desktop players. By combining existing official resources with new features designed by the development team, it provides a complete and useful set of tools without the need to rely on mobile devices. Snap Hutao does not take any destructive modification to the game client to ensure the security of the toolkit.
## 安装 / Installation
## 下载使用 / Download
![](https://ci.appveyor.com/api/projects/status/n4s40t9llru4si9y?svg=true) [![GitHub Release](https://img.shields.io/github/release/DGP-Studio/Snap.Hutao?style=flat)](https://github.com/DGP-Studio/Snap.Hutao/releases/latest) [![Github All Releases](https://img.shields.io/github/downloads/DGP-Studio/Snap.Hutao/total.svg?style=flat)]()
---
你可以按照[快速开始](https://hut.ao/zh/quick-start.html)文档中提供的流程安装并设置 Snap Hutao
#### 使用安装器安装 / Install with Snap.Hutao.Depolyment Installer
You can follow the instructions in the [Quick Start](https://hut.ao/en/quick-start.html) document to install and set up Snap Hutao.
Snap.Hutao.Depolyment 是一个由 DGP-Studio 重新包装的 Windows 应用安装器,适用于缺少专业计算机知识的一般用户,可以在安装时同时解决缺少必要系统环境的问题。
## 本地化翻译 / Localization
Snap.Hutao.Depolyment is a Windows application installer repackaged by DGP-Studio for the users who lacks computer knowledge and can solve the problem of missing necessary system environment at the same time as the installation.
[![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) [![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)
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao.Deployment/releases/latest)
Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao.Deployment/-/releases)
Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translation platform where you can submit translated text for languages you are familiar with. We are grateful to every community member who has contributed to Snap Hutao and welcome more friends to participate in this project.
#### 使用 MSIX 包安装 / Install with MSIX Package
## 社区 / Community
直接使用 Snap Hutao MSIX 安装包,使用 Windows 内置的 App Installer 即可安装。如在安装中出现问题,请查阅我们的[常见问题](https://hut.ao/zh/advanced/FAQ.html)文档
[![Discord](https://img.shields.io/discord/952488447753465916?color=5865f2&label=%20Discord)](https://discord.gg/CcH5XtDtvR) [![QQ](https://img.shields.io/badge/QQ-EB1923?logo=tencent-qq&logoColor=white&label=567908135)](https://qm.qq.com/q/WJKykrY9W)
Install with Snap Hutao MSIX package, can be installed with Windows built-in App Installer. If you faced any issue, please check our [FAQ](https://hut.ao/en/advanced/FAQ.html) document.
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao/releases/latest)
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao/-/releases)
## 贡献 / Contribute
* [向我们提交 PR / Make Pull Requests](https://hut.ao/development/contribute.html)
* [为我们更新文档 / Enhance our Document](https://github.com/DGP-Studio/Snap.Hutao.Docs)
* [向我们提交 PR / Make Pull Requests](https://github.com/DGP-Studio/Snap.Hutao/pulls)
* [在 Crowdin 上进行本地化 / Translate Project on Crowdin](https://translate.hut.ao/)
* [为我们更新文档 / Enhance our Document ](https://github.com/DGP-Studio/Snap.Hutao.Docs)
## 特别感谢 / Special Thanks

View File

@@ -4,8 +4,8 @@
| Version | Supported |
| ------- | ------------------ |
| >=1.9.0 | :white_check_mark: |
| <1.9.0 | :x: |
| >=1.6.0 | :white_check_mark: |
| <1.6.0 | :x: |
## Reporting a Vulnerability

View File

@@ -69,15 +69,6 @@ else if (AppVeyor.IsRunningOnAppVeyor)
})[..^2];
Information($"Version: {version}");
}
else // Local
{
repoDir = System.Environment.CurrentDirectory;
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
version = System.DateTime.Now.ToString("yyyy.M.d.") + ((int)((System.DateTime.Now - System.DateTime.Today).TotalSeconds / 86400 * 65535)).ToString();
Information($"Version: {version}");
}
Task("Build")
.IsDependentOn("Build binary package")
@@ -121,17 +112,6 @@ Task("Generate AppxManifest")
Information("Using Release configuration");
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US\"");
}
else
{
Information("Using Local configuration.");
content = content
.Replace("Snap Hutao", "Snap Hutao Local")
.Replace("胡桃", "胡桃 Local")
.Replace("DGP Studio", "DGP Studio CI");
content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"E8B6E2B3-D2A0-4435-A81D-2A16AAF405C7\"");
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"E=admin@dgp-studio.cn, CN=DGP Studio CI, OU=CI, O=DGP-Studio, L=San Jose, S=CA, C=US\"");
content = System.Text.RegularExpressions.Regex.Replace(content, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\"");
}
System.IO.File.WriteAllText(manifest, content);
@@ -193,10 +173,6 @@ Task("Build MSIX")
{
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao-{version}.msix");
}
else
{
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Local-{version}.msix");
}
var p = StartProcess(
"makeappx.exe",
new ProcessSettings

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -110,6 +110,7 @@ dotnet_diagnostic.SA1642.severity = none
dotnet_diagnostic.IDE0005.severity = warning
dotnet_diagnostic.IDE0060.severity = none
dotnet_diagnostic.IDE0290.severity = none
# SA1208: System using directives should be placed before other using directives
dotnet_diagnostic.SA1208.severity = none
@@ -320,8 +321,7 @@ dotnet_diagnostic.CA2227.severity = suggestion
# CA2251: 使用 “string.Equals”
dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = false:none
csharp_style_prefer_primary_constructors = true:suggestion
[*.vb]
#### 命名样式 ####

View File

@@ -1,5 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace Snap.Hutao.Test.PlatformExtensions;
@@ -12,7 +11,6 @@ public sealed class DependencyInjectionTest
.AddSingleton<IService, ServiceB>()
.AddScoped<IScopedService, ServiceA>()
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
.AddLogging(builder => builder.AddConsole())
.BuildServiceProvider();
[TestMethod]
@@ -43,13 +41,6 @@ public sealed class DependencyInjectionTest
}
}
[TestMethod]
public void LoggerWithInterfaceTypeCanBeResolved()
{
Assert.IsNotNull(services.GetService<ILogger<IScopedService>>());
Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService)));
}
private interface IService
{
Guid Id { get; }

View File

@@ -12,11 +12,10 @@
<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.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">
<PackageReference Include="MSTest.TestAdapter" Version="3.2.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -6,7 +6,6 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources/>
<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"/>
@@ -23,13 +22,11 @@
<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

View File

@@ -7,7 +7,6 @@ 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;
@@ -22,17 +21,17 @@ namespace Snap.Hutao;
[SuppressMessage("", "SH001")]
public sealed partial class App : Application
{
private const string ConsoleBanner = $"""
private const string ConsoleBanner = """
----------------------------------------------------------------
_____ _ _ _
/ ____| | | | | | |
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
_____ _ _ _
/ ____| | | | | | |
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
____) || | | || (_| || |_) |_ | | | || |_| || |_| (_| || (_) |
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
| |
|_|
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
| |
|_|
Snap.Hutao is a open source software developed by DGP Studio.
Copyright (C) 2022 - 2024 DGP Studio, All Rights Reserved.
----------------------------------------------------------------
@@ -48,8 +47,6 @@ public sealed partial class App : Application
/// <param name="serviceProvider">服务提供器</param>
public App(IServiceProvider serviceProvider)
{
// DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown;
// Load app resource
InitializeComponent();
activation = serviceProvider.GetRequiredService<IActivation>();
@@ -72,7 +69,7 @@ public sealed partial class App : Application
return;
}
logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow));
logger.LogInformation(ConsoleBanner);
LogDiagnosticInformation();
// manually invoke
@@ -92,8 +89,8 @@ public sealed partial class App : Application
{
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
logger.LogColorizedInformation(("FamilyName: {Name}", ConsoleColor.Blue), (runtimeOptions.FamilyName, ConsoleColor.Cyan));
logger.LogColorizedInformation(("Version: {Version}", ConsoleColor.Blue), (runtimeOptions.Version, ConsoleColor.Cyan));
logger.LogColorizedInformation(("LocalCache: {Path}", ConsoleColor.Blue), (runtimeOptions.LocalCache, ConsoleColor.Cyan));
logger.LogInformation("FamilyName: {name}", runtimeOptions.FamilyName);
logger.LogInformation("Version: {version}", runtimeOptions.Version);
logger.LogInformation("LocalCache: {folder}", runtimeOptions.LocalCache);
}
}

View File

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

@@ -1,38 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.UI;
namespace Snap.Hutao.Control.AutoSuggestBox;
internal sealed class SearchToken
{
public static readonly SearchToken NotFound = new(SearchTokenKind.None, SH.ControlAutoSuggestBoxNotFoundValue, 0);
public SearchToken(SearchTokenKind kind, string value, int order, Uri? iconUri = null, Uri? sideIconUri = null, Color? quality = null)
{
Value = value;
Kind = kind;
IconUri = iconUri;
SideIconUri = sideIconUri;
Quality = quality;
Order = order;
}
public SearchTokenKind Kind { get; }
public string Value { get; set; } = default!;
public Uri? IconUri { get; }
public Uri? SideIconUri { get; }
public Color? Quality { get; }
public int Order { get; }
public override string ToString()
{
return Value;
}
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.AutoSuggestBox;
internal enum SearchTokenKind
{
None,
ItemQuality,
WeaponType,
FightProperty,
ElementName,
AssociationType,
BodyType,
Avatar,
Weapon,
}

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.Control.Behavior;
@@ -46,6 +45,10 @@ internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIEle
return;
}
executed = Command.TryExecute(CommandParameter);
if (Command is not null && Command.CanExecute(CommandParameter))
{
Command.Execute(CommandParameter);
executed = true;
}
}
}

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.Control.Behavior;
@@ -33,7 +32,6 @@ internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavio
protected override bool Uninitialize()
{
periodicTimerCancellationTokenSource.Cancel();
AssociatedObject.ActualThemeChanged -= OnActualThemeChanged;
return true;
}
@@ -51,7 +49,10 @@ internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavio
return;
}
Command.TryExecute(CommandParameter);
if (Command is not null && Command.CanExecute(CommandParameter))
{
Command.Execute(CommandParameter);
}
}
private async ValueTask RunCoreAsync()

View File

@@ -13,4 +13,6 @@ namespace Snap.Hutao.Control;
/// </summary>
[HighQuality]
[DependencyProperty("DataContext", typeof(object))]
internal sealed partial class BindingProxy : DependencyObject;
internal sealed partial class BindingProxy : DependencyObject
{
}

View File

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

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

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

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

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

@@ -30,7 +30,7 @@ internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, IN
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
public AdvancedCollectionView()
: this([])
: this(new List<T>(0))
{
}

View File

@@ -6,7 +6,6 @@ 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
{

View File

@@ -3,7 +3,6 @@
namespace Snap.Hutao.Control.Collection.Alternating;
[Obsolete("Use SettingsCard instead")]
internal interface IAlternatingItem
{
public Microsoft.UI.Xaml.Media.Brush? Background { get; set; }

View File

@@ -1,18 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Extension;
internal static class CommandInvocation
{
public static bool TryExecute(this ICommand? command, object? parameter = null)
{
if (command is not null && command.CanExecute(parameter))
{
command.Execute(parameter);
return true;
}
return false;
}
}

View File

@@ -2,13 +2,11 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Control.Extension;
internal static class DependencyObjectExtension
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IServiceProvider ServiceProvider(this DependencyObject obj)
{
return Ioc.Default;

View File

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

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("VisibilityObject", typeof(object), null, nameof(OnVisibilityObjectChanged), IsAttached = true, AttachedType = typeof(UIElement))]
[DependencyProperty("OpacityObject", typeof(object), null, nameof(OnOpacityObjectChanged), IsAttached = true, AttachedType = typeof(UIElement))]
public sealed partial class UIElementHelper
{
private static void OnVisibilityObjectChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)dp;
element.Visibility = e.NewValue is null ? Visibility.Collapsed : Visibility.Visible;
}
private static void OnOpacityObjectChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)dp;
element.Opacity = e.NewValue is null ? 0D : 1D;
}
}

View File

@@ -3,9 +3,7 @@
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;
@@ -21,9 +19,6 @@ internal sealed class CachedImage : Implementation.ImageEx
/// </summary>
public CachedImage()
{
DefaultStyleKey = typeof(CachedImage);
DefaultStyleResourceUri = "ms-appx:///Control/Image/CachedImage.xaml".ToUri();
IsCacheEnabled = true;
EnableLazyLoading = false;
}
@@ -31,11 +26,12 @@ internal sealed class CachedImage : Implementation.ImageEx
/// <inheritdoc/>
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
// We can only use Ioc to retrieve IImageCache, no IServiceProvider is available.
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
try
{
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
Verify.Operation(!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.

View File

@@ -168,7 +168,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
if (surface.DecodedPhysicalSize.Size() <= 0D)
{
await Task.WhenAny(surfaceLoadTaskCompletionSource.Task, Task.Delay(5000, token)).ConfigureAwait(true);
await Task.Delay(50, token).ConfigureAwait(true);
}
LoadImageSurfaceCompleted(surface);

View File

@@ -7,14 +7,21 @@ using Windows.Media.Casting;
namespace Snap.Hutao.Control.Image.Implementation;
[DependencyProperty("NineGrid", typeof(Thickness))]
internal partial class ImageEx : ImageExBase
internal class ImageEx : ImageExBase
{
private static readonly DependencyProperty NineGridProperty = DependencyProperty.Register(nameof(NineGrid), typeof(Thickness), typeof(ImageEx), new PropertyMetadata(default(Thickness)));
public ImageEx()
: base()
{
}
public Thickness NineGrid
{
get => (Thickness)GetValue(NineGridProperty);
set => SetValue(NineGridProperty, value);
}
public override CompositionBrush GetAlphaMask()
{
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)

View File

@@ -6,7 +6,6 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Win32;
using System.IO;
using Windows.Foundation;
@@ -159,6 +158,7 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
if (value)
{
control.LayoutUpdated += control.OnImageExBaseLayoutUpdated;
control.InvalidateLazyLoading();
}
else
@@ -169,7 +169,7 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
private static void LazyLoadingThresholdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageExBase { EnableLazyLoading: true } control)
if (d is ImageExBase control && control.EnableLazyLoading)
{
control.InvalidateLazyLoading();
}
@@ -229,6 +229,9 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
private void AttachPlaceholderSource(ImageSource? source)
{
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
// We only need to call those methods if we fail in other cases before we get here.
if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image)
{
image.Source = source;
@@ -237,15 +240,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
{
brush.ImageSource = source;
}
if (source is null)
{
VisualStateManager.GoToState(this, UnloadedState, true);
}
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
{
VisualStateManager.GoToState(this, LoadedState, true);
}
}
private async void SetSource(object? source)
@@ -317,7 +311,8 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
}
tokenSource?.Cancel();
tokenSource = new();
tokenSource = new CancellationTokenSource();
AttachPlaceholderSource(null);
@@ -448,10 +443,9 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return;
}
Rect controlRect = TransformToVisual(hostElement).TransformBounds(StructMarshal.Rect(ActualSize));
Rect controlRect = TransformToVisual(hostElement)
.TransformBounds(new Rect(0, 0, ActualWidth, ActualHeight));
double lazyLoadingThreshold = LazyLoadingThreshold;
// Left/Top 1 Threshold, Right/Bottom 2 Threshold
Rect hostRect = new(
0 - lazyLoadingThreshold,
0 - lazyLoadingThreshold,

View File

@@ -0,0 +1,151 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using System.Numerics;
using Windows.Foundation;
namespace Snap.Hutao.Control.Layout;
internal sealed class DefaultItemCollectionTransitionProvider : ItemCollectionTransitionProvider
{
private const double DefaultAnimationDurationInMs = 300.0;
static DefaultItemCollectionTransitionProvider()
{
AnimationSlowdownFactor = 1.0;
}
public static double AnimationSlowdownFactor { get; set; }
protected override bool ShouldAnimateCore(ItemCollectionTransition transition)
{
return true;
}
protected override void StartTransitions(IList<ItemCollectionTransition> transitions)
{
List<ItemCollectionTransition> addTransitions = [];
List<ItemCollectionTransition> removeTransitions = [];
List<ItemCollectionTransition> moveTransitions = [];
foreach (ItemCollectionTransition transition in addTransitions)
{
switch (transition.Operation)
{
case ItemCollectionTransitionOperation.Add:
addTransitions.Add(transition);
break;
case ItemCollectionTransitionOperation.Remove:
removeTransitions.Add(transition);
break;
case ItemCollectionTransitionOperation.Move:
moveTransitions.Add(transition);
break;
}
}
StartAddTransitions(addTransitions, removeTransitions.Count > 0, moveTransitions.Count > 0);
StartRemoveTransitions(removeTransitions);
StartMoveTransitions(moveTransitions, removeTransitions.Count > 0);
}
private static void StartAddTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveTransitions, bool hasMoveTransitions)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation fadeInAnimation = compositor.CreateScalarKeyFrameAnimation();
fadeInAnimation.InsertKeyFrame(0.0f, 0.0f);
if (hasMoveTransitions && hasRemoveTransitions)
{
fadeInAnimation.InsertKeyFrame(0.66f, 0.0f);
}
else if (hasMoveTransitions || hasRemoveTransitions)
{
fadeInAnimation.InsertKeyFrame(0.5f, 0.0f);
}
fadeInAnimation.InsertKeyFrame(1.0f, 1.0f);
fadeInAnimation.Duration = TimeSpan.FromMilliseconds(
DefaultAnimationDurationInMs * ((hasRemoveTransitions ? 1 : 0) + (hasMoveTransitions ? 1 : 0) + 1) * AnimationSlowdownFactor);
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
visual.StartAnimation("Opacity", fadeInAnimation);
batch.End();
batch.Completed += (_, _) => progress.Complete();
}
}
private static void StartRemoveTransitions(IList<ItemCollectionTransition> transitions)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation fadeOutAnimation = compositor.CreateScalarKeyFrameAnimation();
fadeOutAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue");
fadeOutAnimation.InsertKeyFrame(1.0f, 0.0f);
fadeOutAnimation.Duration = TimeSpan.FromMilliseconds(DefaultAnimationDurationInMs * AnimationSlowdownFactor);
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
visual.StartAnimation(nameof(Visual.Opacity), fadeOutAnimation);
batch.End();
batch.Completed += (_, _) =>
{
visual.Opacity = 1.0f;
progress.Complete();
};
}
}
private static void StartMoveTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveAnimations)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
// Animate offset.
if (transition.OldBounds.X != transition.NewBounds.X ||
transition.OldBounds.Y != transition.NewBounds.Y)
{
AnimateOffset(visual, compositor, transition.OldBounds, transition.NewBounds, hasRemoveAnimations);
}
batch.End();
batch.Completed += (_, _) => progress.Complete();
}
}
private static void AnimateOffset(Visual visual, Compositor compositor, Rect oldBounds, Rect newBounds, bool hasRemoveAnimations)
{
Vector2KeyFrameAnimation offsetAnimation = compositor.CreateVector2KeyFrameAnimation();
offsetAnimation.SetVector2Parameter("delta", new Vector2(
(float)(oldBounds.X - newBounds.X),
(float)(oldBounds.Y - newBounds.Y)));
offsetAnimation.SetVector2Parameter("final", default);
offsetAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue + delta");
if (hasRemoveAnimations)
{
offsetAnimation.InsertExpressionKeyFrame(0.5f, "delta");
}
offsetAnimation.InsertExpressionKeyFrame(1.0f, "final");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(
DefaultAnimationDurationInMs * ((hasRemoveAnimations ? 1 : 0) + 1) * AnimationSlowdownFactor);
visual.StartAnimation("TransformMatrix._41_42", offsetAnimation);
}
}

View File

@@ -18,12 +18,14 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = new UniformStaggeredLayoutState(context);
base.InitializeForContextCore(context);
}
/// <inheritdoc/>
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = null;
base.UninitializeForContextCore(context);
}
/// <inheritdoc/>
@@ -80,10 +82,16 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
(int numberOfColumns, double columnWidth) = GetNumberOfColumnsAndWidth(availableWidth, MinItemWidth, MinColumnSpacing);
if (columnWidth != state.ColumnWidth)
{
// The items will need to be remeasured
state.Clear();
}
state.ColumnWidth = columnWidth;
double totalWidth = ((state.ColumnWidth + MinColumnSpacing) * numberOfColumns) - MinColumnSpacing;
// adjust for column spacing on all columns expect the first
double totalWidth = state.ColumnWidth + ((numberOfColumns - 1) * (state.ColumnWidth + MinColumnSpacing));
if (totalWidth > availableWidth)
{
numberOfColumns--;
@@ -95,6 +103,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
if (numberOfColumns != state.NumberOfColumns)
{
// The items will not need to be remeasured, but they will need to go into new columns
state.ClearColumns();
}
@@ -161,7 +170,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
item.Element.Measure(new Size(state.ColumnWidth, availableHeight));
if (item.Height != item.Element.DesiredSize.Height)
{
// this item changed size; we need to recalculate layout for everything after this item
// this item changed size; we need to recalculate layout for everything after this
state.RemoveFromIndex(i + 1);
item.Height = item.Element.DesiredSize.Height;
columnHeights[columnIndex] = item.Top + item.Height;
@@ -192,16 +201,16 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
// Cycle through each column and arrange the items that are within the realization bounds
for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
{
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(state.GetColumnLayout(columnIndex)))
UniformStaggeredColumnLayout layout = state.GetColumnLayout(columnIndex);
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(layout))
{
double bottom = item.Top + item.Height;
if (bottom < context.RealizationRect.Top)
{
// Element is above the realization bounds
// element is above the realization bounds
continue;
}
// Partial or fully in the view
if (item.Top <= context.RealizationRect.Bottom)
{
double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex);
@@ -220,22 +229,21 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
return finalSize;
}
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double columnSpacing)
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double minColumnSpacing)
{
// test if the width can fit in 2 items
if ((2 * minItemWidth) + columnSpacing > availableWidth)
if ((2 * minItemWidth) + minColumnSpacing > availableWidth)
{
return (1, availableWidth);
}
int columnCount = Math.Max(1, (int)((availableWidth + columnSpacing) / (minItemWidth + columnSpacing)));
double columnWidthWithSpacing = (availableWidth + columnSpacing) / columnCount;
return (columnCount, columnWidthWithSpacing - columnSpacing);
int columnCount = Math.Max(1, (int)((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing)));
double columnWidthAddSpacing = (availableWidth + minColumnSpacing) / columnCount;
return (columnCount, columnWidthAddSpacing - minColumnSpacing);
}
private static int GetLowestColumnIndex(in ReadOnlySpan<double> columnHeights)
{
// We want to find the leftest column with the lowest height
int columnIndex = 0;
double height = columnHeights[0];
for (int j = 1; j < columnHeights.Length; j++)
@@ -252,11 +260,13 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((UniformStaggeredLayout)d).InvalidateMeasure();
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
panel.InvalidateMeasure();
}
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((UniformStaggeredLayout)d).InvalidateMeasure();
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
panel.InvalidateMeasure();
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Runtime.InteropServices;
@@ -66,6 +67,46 @@ internal sealed class UniformStaggeredLayoutState
return columnLayout[columnIndex];
}
/// <summary>
/// Clear everything that has been calculated.
/// </summary>
internal void Clear()
{
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
// The first element must be force refreshed otherwise
// it will use the old one realized
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Now we need to refresh the first element of each column
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Finally we need to refresh the whole layout when we reset
if (context.ItemCount > 0)
{
for (int i = 0; i < context.ItemCount; i++)
{
RecycleElementAt(i);
}
}
columnLayout.Clear();
items.Clear();
}
/// <summary>
/// Clear the layout columns so they will be recalculated.
/// </summary>
internal void ClearColumns()
{
columnLayout.Clear();
}
/// <summary>
/// Gets the estimated height of the layout.
/// </summary>
/// <returns>The estimated height of the layout.</returns>
/// <remarks>
/// If all of the items have been calculated then the actual height will be returned.
/// If all of the items have not been calculated then an estimated height will be calculated based on the average height of the items.
/// </remarks>
internal double GetHeight()
{
double desiredHeight = columnLayout.Values.Max(c => c.Height);
@@ -98,37 +139,10 @@ internal sealed class UniformStaggeredLayoutState
return desiredHeight;
}
internal void Clear()
{
RecycleElements();
ClearColumns();
ClearItems();
}
internal void ClearColumns()
{
columnLayout.Clear();
}
internal void ClearItems()
{
items.Clear();
}
internal void RecycleElements()
{
if (context.ItemCount > 0)
{
for (int i = 0; i < items.Count; i++)
{
RecycleElementAt(i);
}
}
}
internal void RecycleElementAt(int index)
{
context.RecycleElement(context.GetOrCreateElementAt(index));
UIElement element = context.GetOrCreateElementAt(index);
context.RecycleElement(element);
}
internal void RemoveFromIndex(int index)
@@ -161,7 +175,7 @@ internal sealed class UniformStaggeredLayoutState
{
for (int i = startIndex; i <= endIndex; i++)
{
if (i >= items.Count)
if (i > items.Count)
{
break;
}
@@ -170,7 +184,7 @@ internal sealed class UniformStaggeredLayoutState
item.Height = 0;
item.Top = 0;
// We must recycle all removed elements to ensure that it gets the correct context
// We must recycle all elements to ensure that it gets the correct context
RecycleElementAt(i);
}

View File

@@ -17,7 +17,7 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
public Loading()
{
DefaultStyleKey = typeof(Loading);
DefaultStyleResourceUri = "ms-appx:///Control/Loading.xaml".ToUri();
DefaultStyleResourceUri = new("ms-appx:///Control/Loading.xaml");
}
public bool IsLoading

View File

@@ -12,6 +12,7 @@ internal sealed class UInt32Extension : MarkupExtension
protected override object ProvideValue()
{
return XamlBindingHelper.ConvertValue(typeof(uint), Value);
_ = uint.TryParse(Value, out uint result);
return result;
}
}

View File

@@ -39,6 +39,24 @@ internal static class SoftwareBitmapExtension
}
}
public static unsafe double Luminance(this SoftwareBitmap softwareBitmap)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
{
using (IMemoryBufferReference reference = buffer.CreateReference())
{
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
double sum = 0;
foreach (ref readonly Bgra32 pixel in bytes)
{
sum += pixel.Luminance;
}
return sum / bytes.Length;
}
}
}
public static unsafe Bgra32 GetAccentColor(this SoftwareBitmap softwareBitmap)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))

View File

@@ -1,89 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Data;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;
[DependencyProperty("Spacing", typeof(double), default(double), nameof(OnSpacingChanged))]
internal partial class EqualPanel : Microsoft.UI.Xaml.Controls.Panel
{
private double maxItemWidth;
private double maxItemHeight;
private int visibleItemsCount;
public EqualPanel()
{
RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnHorizontalAlignmentChanged);
}
protected override Size MeasureOverride(Size availableSize)
{
maxItemWidth = 0;
maxItemHeight = 0;
List<UIElement> elements = [.. Children.Where(element => element.Visibility == Visibility.Visible)];
visibleItemsCount = elements.Count;
foreach (ref readonly UIElement child in CollectionsMarshal.AsSpan(elements))
{
child.Measure(availableSize);
maxItemWidth = Math.Max(maxItemWidth, child.DesiredSize.Width);
maxItemHeight = Math.Max(maxItemHeight, child.DesiredSize.Height);
}
if (visibleItemsCount > 0)
{
// Return equal widths based on the widest item
// In very specific edge cases the AvailableWidth might be infinite resulting in a crash.
if (HorizontalAlignment is not HorizontalAlignment.Stretch || double.IsInfinity(availableSize.Width))
{
return new Size((maxItemWidth * visibleItemsCount) + (Spacing * (visibleItemsCount - 1)), maxItemHeight);
}
else
{
// Equal columns based on the available width, adjust for spacing
double totalWidth = availableSize.Width - (Spacing * (visibleItemsCount - 1));
maxItemWidth = totalWidth / visibleItemsCount;
return new Size(availableSize.Width, maxItemHeight);
}
}
else
{
return new Size(0, 0);
}
}
protected override Size ArrangeOverride(Size finalSize)
{
double x = 0;
// Check if there's more (little) width available - if so, set max item width to the maximum possible as we have an almost perfect height.
if (finalSize.Width > (visibleItemsCount * maxItemWidth) + (Spacing * (visibleItemsCount - 1)))
{
maxItemWidth = (finalSize.Width - (Spacing * (visibleItemsCount - 1))) / visibleItemsCount;
}
IEnumerable<UIElement> elements = Children.Where(static e => e.Visibility == Visibility.Visible);
foreach (UIElement child in elements)
{
child.Arrange(new Rect(x, 0, maxItemWidth, maxItemHeight));
x += maxItemWidth + Spacing;
}
return finalSize;
}
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as EqualPanel)?.InvalidateMeasure();
}
private static void OnHorizontalAlignmentChanged(DependencyObject d, DependencyProperty dp)
{
(d as EqualPanel)?.InvalidateMeasure();
}
}

View File

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

View File

@@ -6,7 +6,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
Style="{StaticResource DefaultSegmentedStyle}"
mc:Ignorable="d">
<cwc:SegmentedItem

View File

@@ -4,7 +4,6 @@
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Setting;
using System.Collections.Frozen;
namespace Snap.Hutao.Control.Panel;
@@ -20,11 +19,11 @@ internal sealed partial class PanelSelector : Segmented
public const string List = nameof(List);
public const string Grid = nameof(Grid);
private static readonly FrozenDictionary<int, string> IndexTypeMap = FrozenDictionary.ToFrozenDictionary(
[
KeyValuePair.Create(0, List),
KeyValuePair.Create(1, Grid),
]);
private static readonly Dictionary<int, string> IndexTypeMap = new()
{
[0] = List,
[1] = Grid,
};
private readonly RoutedEventHandler loadedEventHandler;
private readonly RoutedEventHandler unloadedEventHandler;

View File

@@ -1,53 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Windows.Foundation;
using CommunityToolkit.WinUI.Controls;
namespace Snap.Hutao.Control.Panel;
[DependencyProperty("MinItemWidth", typeof(double))]
[DependencyProperty("ColumnSpacing", typeof(double))]
[DependencyProperty("RowSpacing", typeof(double))]
internal sealed partial class UniformPanel : Microsoft.UI.Xaml.Controls.Panel
internal sealed partial class UniformPanel : UniformGrid
{
private int columns;
protected override Size MeasureOverride(Size availableSize)
public UniformPanel()
{
columns = (int)((availableSize.Width + ColumnSpacing) / (MinItemWidth + ColumnSpacing));
double availableItemWidth = ((availableSize.Width + ColumnSpacing) / columns) - ColumnSpacing;
double maxDesiredHeight = 0;
foreach (UIElement child in Children)
{
child.Measure(new Size(availableItemWidth, availableSize.Height));
maxDesiredHeight = Math.Max(maxDesiredHeight, child.DesiredSize.Height);
}
int desiredRows = (int)Math.Ceiling(Children.Count / (double)columns);
double desiredHeight = ((maxDesiredHeight + RowSpacing) * desiredRows) - RowSpacing;
return new Size(availableSize.Width, desiredHeight);
Columns = 1;
SizeChanged += OnSizeChanged;
}
protected override Size ArrangeOverride(Size finalSize)
private void OnSizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e)
{
double itemWidth = ((finalSize.Width + ColumnSpacing) / columns) - ColumnSpacing;
for (int index = 0; index < Children.Count; index++)
{
UIElement child = Children[index];
int row = index / columns;
int column = index % columns;
double x = column * (itemWidth + ColumnSpacing);
double y = row * (child.DesiredSize.Height + RowSpacing);
child.Arrange(new Rect(x, y, itemWidth, child.DesiredSize.Height));
}
return finalSize;
Columns = (int)((e.NewSize.Width + ColumnSpacing) / (MinItemWidth + ColumnSpacing));
}
}

View File

@@ -15,7 +15,7 @@ internal class ScopedPage : Page
{
private readonly RoutedEventHandler unloadEventHandler;
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope pageScope;
private readonly IServiceScope currentScope;
private bool inFrame = true;
@@ -23,7 +23,7 @@ internal class ScopedPage : Page
{
unloadEventHandler = OnUnloaded;
Unloaded += unloadEventHandler;
pageScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
}
public async ValueTask NotifyRecipientAsync(INavigationData extra)
@@ -41,20 +41,14 @@ internal class ScopedPage : Page
/// 应当在 InitializeComponent() 前调用
/// </summary>
/// <typeparam name="TViewModel">视图模型类型</typeparam>
protected void InitializeWith<TViewModel>()
protected TViewModel InitializeWith<TViewModel>()
where TViewModel : class, IViewModel
{
try
{
IViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
}
catch (Exception ex)
{
pageScope.ServiceProvider.GetRequiredService<ILogger<ScopedPage>>().LogError(ex, "Failed to initialize view model.");
throw;
}
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
return (TViewModel)viewModel;
}
/// <inheritdoc/>
@@ -80,11 +74,7 @@ internal class ScopedPage : Page
DisposeViewModel();
}
if (this.IsDisposed())
{
return;
}
DataContext = null;
Unloaded -= unloadEventHandler;
}
@@ -103,8 +93,7 @@ internal class ScopedPage : Page
viewModel.IsViewDisposed = true;
// Dispose the scope
pageScope.Dispose();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
currentScope.Dispose();
}
}
}

View File

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

@@ -45,7 +45,15 @@ internal sealed partial class DescriptionTextBlock : ContentControl
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
try
{
UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
}
catch (Exception ex)
{
_ = ex;
}
}
private static void OnTextStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

View File

@@ -14,7 +14,6 @@ using Windows.UI;
namespace Snap.Hutao.Control.Text;
// TODO: change the parsing to syntax tree
[DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))]
[DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))]
internal sealed partial class HtmlDescriptionTextBlock : ContentControl

View File

@@ -4,19 +4,21 @@
xmlns:cwm="using:CommunityToolkit.WinUI.Media">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<x:Double x:Key="CompatShadowThemeOpacity">0.14</x:Double>
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
BlurRadius="8"
Opacity="0.14"
Offset="0,4,0"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<x:Double x:Key="CompatShadowThemeOpacity">0.28</x:Double>
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
BlurRadius="8"
Opacity="0.28"
Offset="0,4,0"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
BlurRadius="8"
Opacity="{ThemeResource CompatShadowThemeOpacity}"
Offset="0,4,0"/>
<Style x:Key="BorderCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>

View File

@@ -8,9 +8,6 @@
<ItemsPanelTemplate x:Key="WrapPanelSpacing0Template">
<cwcont:WrapPanel/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="WrapPanelSpacing2Template">
<cwcont:WrapPanel HorizontalSpacing="2" VerticalSpacing="2"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="WrapPanelSpacing4Template">
<cwcont:WrapPanel HorizontalSpacing="4" VerticalSpacing="4"/>
</ItemsPanelTemplate>
@@ -20,9 +17,6 @@
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing2Template">
<StackPanel Orientation="Horizontal" Spacing="2"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing4Template">
<StackPanel Orientation="Horizontal" Spacing="4"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
<StackPanel Spacing="4"/>
</ItemsPanelTemplate>

View File

@@ -11,6 +11,4 @@ internal static class KnownColors
public static readonly Color Orange = StructMarshal.Color(0xFFBC6932);
public static readonly Color Purple = StructMarshal.Color(0xFFA156E0);
public static readonly Color Blue = StructMarshal.Color(0xFF5180CB);
public static readonly Color Green = StructMarshal.Color(0xFF2A8F72);
public static readonly Color White = StructMarshal.Color(0xFF72778B);
}

View File

@@ -1,115 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cw="using:CommunityToolkit.WinUI"
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
xmlns:shcp="using:Snap.Hutao.Control.Panel"
xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/SegmentedItem/SegmentedItem.xaml"/>
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<StaticResource x:Key="SegmentedBackground" ResourceKey="ControlAltFillColorSecondaryBrush"/>
<StaticResource x:Key="SegmentedBorderBrush" ResourceKey="ControlStrokeColorDefaultBrush"/>
<Thickness x:Key="SegmentedBorderThickness">1</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="SegmentedBackground" ResourceKey="ControlAltFillColorSecondaryBrush"/>
<StaticResource x:Key="SegmentedBorderBrush" ResourceKey="ControlStrokeColorDefaultBrush"/>
<Thickness x:Key="SegmentedBorderThickness">1</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<StaticResource x:Key="SegmentedBackground" ResourceKey="SystemColorButtonFaceColor"/>
<StaticResource x:Key="SegmentedBorderBrush" ResourceKey="SystemColorHighlightColorBrush"/>
<Thickness x:Key="SegmentedBorderThickness">1</Thickness>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<x:Double x:Key="SegmentedItemSpacing">1</x:Double>
<x:Double x:Key="ButtonItemSpacing">2</x:Double>
<Style BasedOn="{StaticResource DefaultSegmentedStyle}" TargetType="cwc:Segmented"/>
<Style x:Key="DefaultSegmentedStyle" TargetType="cwc:Segmented">
<Style.Setters>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="Background" Value="{ThemeResource SegmentedBackground}"/>
<Setter Property="BorderBrush" Value="{ThemeResource SegmentedBorderBrush}"/>
<Setter Property="BorderThickness" Value="{ThemeResource SegmentedBorderThickness}"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SelectionMode" Value="Single"/>
<Setter Property="IsItemClickEnabled" Value="False"/>
<win:Setter Property="SingleSelectionFollowsFocus" Value="False"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="TabNavigation" Value="Once"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<shcp:EqualPanel
HorizontalAlignment="{Binding (cw:FrameworkElementExtensions.Ancestor).HorizontalAlignment, RelativeSource={RelativeSource Self}}"
cw:FrameworkElementExtensions.AncestorType="cwc:Segmented"
Spacing="{ThemeResource SegmentedItemSpacing}"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="cwc:Segmented">
<Grid>
<Border
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"/>
<ItemsPresenter Margin="{TemplateBinding Padding}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
<Style
x:Key="PivotSegmentedStyle"
BasedOn="{StaticResource DefaultSegmentedStyle}"
TargetType="cwc:Segmented">
<Style.Setters>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="ItemContainerStyle" Value="{StaticResource PivotSegmentedItemStyle}"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="{ThemeResource SegmentedItemSpacing}"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
<Style
x:Key="ButtonSegmentedStyle"
BasedOn="{StaticResource DefaultSegmentedStyle}"
TargetType="cwc:Segmented">
<Style.Setters>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="ItemContainerStyle" Value="{StaticResource ButtonSegmentedItemStyle}"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="{ThemeResource ButtonItemSpacing}"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ResourceDictionary>

View File

@@ -1,25 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32;
using Windows.UI;
namespace Snap.Hutao.Control.Theme;
internal static class SystemColors
{
public static Color BaseLowColor(bool isDarkMode)
{
return isDarkMode ? StructMarshal.Color(0x33FFFFFF) : StructMarshal.Color(0x33000000);
}
public static Color BaseMediumLowColor(bool isDarkMode)
{
return isDarkMode ? StructMarshal.Color(0x66FFFFFF) : StructMarshal.Color(0x66000000);
}
public static Color BaseHighColor(bool isDarkMode)
{
return isDarkMode ? StructMarshal.Color(0xFFFFFFFF) : StructMarshal.Color(0xFF000000);
}
}

View File

@@ -17,13 +17,8 @@
<x:String x:Key="UI_Icon_Intee_Explore_1">https://api.snapgenshin.com/static/raw/Bg/UI_Icon_Intee_Explore_1.png</x:String>
<x:String x:Key="UI_ImgSign_ItemIcon">https://api.snapgenshin.com/static/raw/Bg/UI_ImgSign_ItemIcon.png</x:String>
<x:String x:Key="UI_ItemIcon_None">https://api.snapgenshin.com/static/raw/Bg/UI_ItemIcon_None.png</x:String>
<!-- Mark -->
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Mark/UI_MarkTower.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Bg/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Bg/UI_MarkTower.png</x:String>
<!-- ItemIcon -->
<x:String x:Key="UI_ItemIcon_201">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_201.png</x:String>
@@ -35,7 +30,6 @@
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
<x:String x:Key="UI_EmotionIcon52">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon89">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon89.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>

View File

@@ -0,0 +1,18 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
/// <summary>
/// 可克隆
/// </summary>
/// <typeparam name="TSelf">自身类型</typeparam>
[HighQuality]
internal interface ICloneable<out TSelf>
{
/// <summary>
/// 克隆
/// </summary>
/// <returns>新的克隆</returns>
TSelf Clone();
}

View File

@@ -1,19 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.ViewModel.Guide;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Core.Caching;
@@ -29,22 +25,19 @@ namespace Snap.Hutao.Core.Caching;
internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string CacheFolderName = nameof(ImageCache);
private const string CacheFailedDownloadTasksName = $"{nameof(ImageCache)}.FailedDownloadTasks";
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = FrozenDictionary.ToFrozenDictionary(
[
KeyValuePair.Create(0, TimeSpan.FromSeconds(4)),
KeyValuePair.Create(1, TimeSpan.FromSeconds(16)),
KeyValuePair.Create(2, TimeSpan.FromSeconds(64)),
]);
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = new Dictionary<int, TimeSpan>()
{
[0] = TimeSpan.FromSeconds(4),
[1] = TimeSpan.FromSeconds(16),
[2] = TimeSpan.FromSeconds(64),
}.ToFrozenDictionary();
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly IHttpClientFactory httpClientFactory;
private readonly IServiceProvider serviceProvider;
private readonly ILogger<ImageCache> logger;
private readonly IMemoryCache memoryCache;
private string? baseFolder;
private string? cacheFolder;
@@ -111,12 +104,12 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
{
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
{
logger.LogColorizedInformation("Begin to download file from '{Uri}' to '{File}'", (uri, ConsoleColor.Cyan), (filePath, ConsoleColor.Cyan));
logger.LogDebug("Begin downloading image file from '{Uri}' to '{File}'", uri, filePath);
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
}
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
{
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", (uri, ConsoleColor.Cyan));
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", uri);
await task.ConfigureAwait(false);
}
@@ -139,7 +132,10 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
private static string GetCacheFileName(Uri uri)
{
return Hash.SHA1HexString(uri.ToString());
string url = uri.ToString();
byte[] chars = Encoding.UTF8.GetBytes(url);
byte[] hash = SHA1.HashData(chars);
return System.Convert.ToHexString(hash);
}
private static bool IsFileInvalid(string file, bool treatNullFileAsInvalid = true)
@@ -176,76 +172,40 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
while (retryCount < 3)
{
HttpRequestMessageBuilder requestMessageBuilder = httpRequestMessageBuilderFactory
.Create()
.SetRequestUri(uri)
// These headers are only available for our own api
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase))
.Get();
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
if (message.RequestMessage is { RequestUri: { } target } && target != uri)
{
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
{
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
}
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
}
if (responseMessage.IsSuccessStatusCode)
if (message.IsSuccessStatusCode)
{
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
if (responseMessage.Content.Headers.ContentType?.MediaType is "application/json")
using (FileStream fileStream = File.Create(baseFile))
{
#if DEBUG
DebugTrack(uri);
#endif
string raw = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
logger.LogColorizedCritical("Failed to download '{Uri}' with unexpected body '{Raw}'", (uri, ConsoleColor.Red), (raw, ConsoleColor.DarkYellow));
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
}
}
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
switch (message.StatusCode)
{
case HttpStatusCode.TooManyRequests:
{
using (FileStream fileStream = File.Create(baseFile))
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
retryCount++;
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;
}
}
switch (responseMessage.StatusCode)
{
case HttpStatusCode.TooManyRequests:
{
retryCount++;
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;
}
default:
#if DEBUG
DebugTrack(uri);
#endif
logger.LogColorizedCritical("Failed to download '{Uri}' with status code '{StatusCode}'", (uri, ConsoleColor.Red), (responseMessage.StatusCode, ConsoleColor.DarkYellow));
return;
}
default:
return;
}
}
}
}
}
#if DEBUG
internal partial class ImageCache
{
private void DebugTrack(Uri uri)
{
HashSet<string>? set = memoryCache.GetOrCreate(CacheFailedDownloadTasksName, entry => entry.Value ??= new HashSet<string>()) as HashSet<string>;
set?.Add(uri.ToString());
}
}
#endif
}

View File

@@ -1,32 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Collection;
internal sealed class TwoEnumerbleEnumerator<TFirst, TSecond> : IDisposable
{
private readonly IEnumerator<TFirst> firstEnumerator;
private readonly IEnumerator<TSecond> secondEnumerator;
public TwoEnumerbleEnumerator(IEnumerable<TFirst> firstEnumerable, IEnumerable<TSecond> secondEnumerable)
{
firstEnumerator = firstEnumerable.GetEnumerator();
secondEnumerator = secondEnumerable.GetEnumerator();
}
public (TFirst First, TSecond Second) Current { get => (firstEnumerator.Current, secondEnumerator.Current); }
public bool MoveNext(ref bool moveFirst, ref bool moveSecond)
{
moveFirst = moveFirst && firstEnumerator.MoveNext();
moveSecond = moveSecond && secondEnumerator.MoveNext();
return moveFirst || moveSecond;
}
public void Dispose()
{
firstEnumerator.Dispose();
secondEnumerator.Dispose();
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Core;
/// <summary>
/// 支持Md5转换
/// </summary>
[HighQuality]
internal static class Convert
{
/// <summary>
/// 获取字符串的MD5计算结果
/// </summary>
/// <param name="source">源字符串</param>
/// <returns>计算的结果</returns>
public static string ToMd5HexString(string source)
{
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
return System.Convert.ToHexString(hash);
}
}

View File

@@ -13,6 +13,13 @@ namespace Snap.Hutao.Core.Database;
[HighQuality]
internal static class DbSetExtension
{
/// <summary>
/// 添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int AddAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -20,13 +27,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
/// <summary>
/// 异步添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Add(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
/// <summary>
/// 添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
@@ -34,13 +55,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities, CancellationToken token = default)
/// <summary>
/// 异步添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
dbSet.AddRange(entities);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
/// <summary>
/// 移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -48,13 +83,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
/// <summary>
/// 异步移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Remove(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
/// <summary>
/// 更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -62,11 +111,18 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
/// <summary>
/// 异步更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Update(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -80,11 +136,11 @@ internal static class DbSetExtension
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet, CancellationToken token = default)
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class
{
DbContext dbContext = dbSet.Context();
int count = await dbContext.SaveChangesAsync(token).ConfigureAwait(false);
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
dbContext.ChangeTracker.Clear();
return count;
}

View File

@@ -38,7 +38,7 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
return;
}
if (serviceProvider.IsDisposed())
if (serviceProvider.IsDisposedSlow())
{
return;
}
@@ -96,7 +96,7 @@ internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
return;
}
if (serviceProvider.IsDisposed())
if (serviceProvider.IsDisposedSlow())
{
return;
}

View File

@@ -24,13 +24,7 @@ internal static class DependencyInjection
ServiceProvider serviceProvider = new ServiceCollection()
// Microsoft extension
.AddLogging(builder =>
{
builder
.SetMinimumLevel(LogLevel.Trace)
.AddDebug()
.AddConsoleWindow();
})
.AddLogging(builder => builder.AddDebug().AddConsoleWindow())
.AddMemoryCache()
// Hutao extensions

View File

@@ -33,17 +33,17 @@ internal static class IocConfiguration
return services
.AddTransient(typeof(Database.ScopedDbCurrent<,>))
.AddTransient(typeof(Database.ScopedDbCurrent<,,>))
.AddDbContextPool<AppDbContext>(AddDbContextCore);
.AddDbContext<AppDbContext>(AddDbContextCore);
}
private static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
{
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
// Temporarily create a context
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString))
using (AppDbContext context = AppDbContext.Create(sqlConnectionString))
{
if (context.Database.GetPendingMigrations().Any())
{

View File

@@ -25,7 +25,6 @@ internal static partial class IocHttpClientConfiguration
.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
{
HttpClientHandler clientHandler = (HttpClientHandler)handler;
clientHandler.AllowAutoRedirect = true;
clientHandler.UseProxy = true;
clientHandler.Proxy = provider.GetRequiredService<DynamicHttpProxy>();
});

View File

@@ -18,22 +18,13 @@ internal static class ServiceProviderExtension
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDisposed(this IServiceProvider? serviceProvider)
public static bool IsDisposedSlow(this IServiceProvider? serviceProvider)
{
if (serviceProvider is null)
{
return true;
}
if (serviceProvider is ServiceProvider serviceProviderImpl)
{
return GetPrivateDisposed(serviceProviderImpl);
}
return serviceProvider.GetType().GetField("_disposed")?.GetValue(serviceProvider) is true;
}
// private bool _disposed;
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_disposed")]
private static extern ref bool GetPrivateDisposed(ServiceProvider serviceProvider);
}

View File

@@ -1,7 +1,5 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Logging;
namespace Snap.Hutao.Core.Diagnostics;
internal readonly struct MeasureExecutionToken : IDisposable
@@ -19,6 +17,6 @@ internal readonly struct MeasureExecutionToken : IDisposable
public void Dispose()
{
logger.LogColorizedDebug(("{Caller} toke {Time} ms", ConsoleColor.Gray), (callerName, ConsoleColor.Yellow), (stopwatch.GetElapsedTime().TotalMilliseconds, ConsoleColor.DarkGreen));
logger.LogDebug("{Caller} toke {Time} ms", callerName, stopwatch.GetElapsedTime().TotalMilliseconds);
}
}

View File

@@ -23,16 +23,8 @@ internal sealed partial class ExceptionRecorder
public void Record(Application app)
{
app.UnhandledException += OnAppUnhandledException;
app.DebugSettings.FailFastOnErrors = false;
app.DebugSettings.IsBindingTracingEnabled = true;
app.DebugSettings.BindingFailed += OnXamlBindingFailed;
app.DebugSettings.IsXamlResourceReferenceTracingEnabled = true;
app.DebugSettings.XamlResourceReferenceFailed += OnXamlResourceReferenceFailed;
app.DebugSettings.LayoutCycleTracingLevel = LayoutCycleTracingLevel.High;
app.DebugSettings.LayoutCycleDebugBreakLevel = LayoutCycleDebugBreakLevel.High;
}
[SuppressMessage("", "CA2012")]

View File

@@ -5,67 +5,36 @@ namespace Snap.Hutao.Core.ExceptionService;
internal sealed class HutaoException : Exception
{
public HutaoException(string message, Exception? innerException)
public HutaoException(HutaoExceptionKind kind, string message, Exception? innerException)
: this(message, innerException)
{
Kind = kind;
}
private HutaoException(string message, Exception? innerException)
: base($"{message}\n{innerException?.Message}", innerException)
{
}
public HutaoExceptionKind Kind { get; private set; }
[DoesNotReturn]
public static HutaoException Throw(string message, Exception? innerException = default)
public static HutaoException Throw(HutaoExceptionKind kind, string message, Exception? innerException = default)
{
throw new HutaoException(message, innerException);
throw new HutaoException(kind, message, innerException);
}
public static void ThrowIf([DoesNotReturnIf(true)] bool condition, string message, Exception? innerException = default)
public static void ThrowIf(bool condition, HutaoExceptionKind kind, string message, Exception? innerException = default)
{
if (condition)
{
throw new HutaoException(message, innerException);
throw new HutaoException(kind, message, innerException);
}
}
public static void ThrowIfNot([DoesNotReturnIf(false)] bool condition, string message, Exception? innerException = default)
{
if (!condition)
{
throw new HutaoException(message, innerException);
}
}
[DoesNotReturn]
public static HutaoException GachaStatisticsInvalidItemId(uint id, Exception? innerException = default)
{
throw new HutaoException(SH.FormatServiceGachaStatisticsFactoryItemIdInvalid(id), innerException);
}
[DoesNotReturn]
public static HutaoException UserdataCorrupted(string message, Exception? innerException = default)
{
throw new HutaoException(message, innerException);
}
[DoesNotReturn]
public static InvalidCastException InvalidCast<TFrom, TTo>(string name, Exception? innerException = default)
public static HutaoException ServiceTypeCastFailed<TFrom, TTo>(string name, Exception? innerException = default)
{
string message = $"This instance of '{typeof(TFrom).FullName}' '{name}' doesn't implement '{typeof(TTo).FullName}'";
throw new InvalidCastException(message, innerException);
}
[DoesNotReturn]
public static InvalidOperationException InvalidOperation(string message, Exception? innerException = default)
{
throw new InvalidOperationException(message, innerException);
}
[DoesNotReturn]
public static NotSupportedException NotSupported(string? message = default, Exception? innerException = default)
{
throw new NotSupportedException(message, innerException);
}
[DoesNotReturn]
public static OperationCanceledException OperationCanceled(string message, Exception? innerException = default)
{
throw new OperationCanceledException(message, innerException);
throw new HutaoException(HutaoExceptionKind.ServiceTypeCastFailed, message, innerException);
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
internal enum HutaoExceptionKind
{
None,
ServiceTypeCastFailed,
FileSystemCreateFileInsufficientPermissions,
PrivateNamedPipeContentHashIncorrect,
}

View File

@@ -1,12 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.System.Com;
using Snap.Hutao.Win32.UI.Shell;
using System.IO;
using static Snap.Hutao.Win32.Macros;
using static Snap.Hutao.Win32.Ole32;
using static Snap.Hutao.Win32.Shell32;
namespace Snap.Hutao.Core.IO;
@@ -44,57 +39,4 @@ internal static class FileOperation
File.Move(sourceFileName, destFileName, false);
return true;
}
public static unsafe bool UnsafeMove(string sourceFileName, string destFileName)
{
bool result = false;
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(sourceFileName, default, in IShellItem.IID, out IShellItem* pSourceShellItem)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(destFileName, default, in IShellItem.IID, out IShellItem* pDestShellItem)))
{
pFileOperation->MoveItem(pSourceShellItem, pDestShellItem, default, default);
if (SUCCEEDED(pFileOperation->PerformOperations()))
{
result = true;
}
IUnknownMarshal.Release(pDestShellItem);
}
IUnknownMarshal.Release(pSourceShellItem);
}
IUnknownMarshal.Release(pFileOperation);
}
return result;
}
public static unsafe bool UnsafeDelete(string path)
{
bool result = false;
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(path, default, in IShellItem.IID, out IShellItem* pShellItem)))
{
pFileOperation->DeleteItem(pShellItem, default);
if (SUCCEEDED(pFileOperation->PerformOperations()))
{
result = true;
}
IUnknownMarshal.Release(pShellItem);
}
IUnknownMarshal.Release(pFileOperation);
}
return result;
}
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Core.IO.Hashing;
internal static class Hash
{
public static unsafe string SHA1HexString(string input)
{
return HashCore(Convert.ToHexString, SHA1.HashData, Encoding.UTF8.GetBytes, input);
}
public static unsafe string MD5HexString(string input)
{
return HashCore(Convert.ToHexString, System.Security.Cryptography.MD5.HashData, Encoding.UTF8.GetBytes, input);
}
private static unsafe TResult HashCore<TInput, TResult>(Func<byte[], TResult> resultConverter, Func<byte[], byte[]> hashMethod, Func<TInput, byte[]> bytesConverter, TInput input)
{
return resultConverter(hashMethod(bytesConverter(input)));
}
}

View File

@@ -34,6 +34,6 @@ internal static class MD5
public static async ValueTask<string> HashAsync(Stream stream, CancellationToken token = default)
{
byte[] bytes = await System.Security.Cryptography.MD5.HashDataAsync(stream, token).ConfigureAwait(false);
return Convert.ToHexString(bytes);
return System.Convert.ToHexString(bytes);
}
}

View File

@@ -18,6 +18,6 @@ internal static class SHA256
public static async ValueTask<string> HashAsync(Stream stream, CancellationToken token = default)
{
byte[] bytes = await System.Security.Cryptography.SHA256.HashDataAsync(stream, token).ConfigureAwait(false);
return Convert.ToHexString(bytes);
return System.Convert.ToHexString(bytes);
}
}

View File

@@ -3,7 +3,6 @@
using Microsoft.Win32.SafeHandles;
using Snap.Hutao.Core.Diagnostics;
using System.Buffers;
using System.IO;
using System.Net.Http;
@@ -78,36 +77,34 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
using (HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent())
Memory<byte> buffer = new byte[bufferSize];
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
{
Memory<byte> buffer = memoryOwner.Memory;
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
int totalBytesRead = 0;
int bytesReadAfterPreviousReport = 0;
do
{
int totalBytesRead = 0;
int bytesReadAfterPreviousReport = 0;
do
int bytesRead = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
if (bytesRead <= 0)
{
int bytesRead = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
if (bytesRead <= 0)
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
break;
}
await RandomAccess.WriteAsync(destFileHandle, buffer[..bytesRead], shard.StartOffset + totalBytesRead, token).ConfigureAwait(false);
totalBytesRead += bytesRead;
bytesReadAfterPreviousReport += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
stopwatch = ValueStopwatch.StartNew();
}
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
break;
}
await RandomAccess.WriteAsync(destFileHandle, buffer[..bytesRead], shard.StartOffset + totalBytesRead, token).ConfigureAwait(false);
totalBytesRead += bytesRead;
bytesReadAfterPreviousReport += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
stopwatch = ValueStopwatch.StartNew();
}
while (true);
}
while (true);
}
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Diagnostics;
using System.Buffers;
using System.IO;
namespace Snap.Hutao.Core.IO;
@@ -52,30 +51,26 @@ internal class StreamCopyWorker<TStatus>
long totalBytesRead = 0;
int bytesRead;
Memory<byte> buffer = new byte[bufferSize];
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(bufferSize))
do
{
Memory<byte> buffer = memoryOwner.Memory;
do
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
if (bytesRead == 0)
{
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
if (bytesRead is 0)
{
progress.Report(statusFactory(totalBytesRead));
break;
}
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
totalBytesRead += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000)
{
progress.Report(statusFactory(totalBytesRead));
stopwatch = ValueStopwatch.StartNew();
}
progress.Report(statusFactory(totalBytesRead));
break;
}
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
totalBytesRead += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000)
{
progress.Report(statusFactory(totalBytesRead));
stopwatch = ValueStopwatch.StartNew();
}
while (bytesRead > 0);
}
while (bytesRead > 0);
}
}

View File

@@ -1,40 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO;
internal sealed class StreamReaderWriter : IDisposable
{
private readonly StreamReader reader;
private readonly StreamWriter writer;
public StreamReaderWriter(StreamReader reader, StreamWriter writer)
{
this.reader = reader;
this.writer = writer;
}
public StreamReader Reader { get => reader; }
public StreamWriter Writer { get => writer; }
/// <inheritdoc cref="StreamReader.ReadLineAsync(CancellationToken)"/>
public ValueTask<string?> ReadLineAsync(CancellationToken token)
{
return reader.ReadLineAsync(token);
}
/// <inheritdoc cref="StreamWriter.WriteAsync(string?)"/>
public Task WriteAsync(string value)
{
return writer.WriteAsync(value);
}
public void Dispose()
{
writer.Dispose();
reader.Dispose();
}
}

View File

@@ -29,7 +29,7 @@ internal readonly struct TempFile : IDisposable
}
catch (UnauthorizedAccessException ex)
{
HutaoException.Throw(SH.CoreIOTempFileCreateFail, ex);
HutaoException.Throw(HutaoExceptionKind.FileSystemCreateFileInsufficientPermissions, SH.CoreIOTempFileCreateFail, ex);
}
if (delete)

View File

@@ -1,21 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core;
internal sealed class LazySlim<T>
{
private readonly Func<T> valueFactory;
[MaybeNull]
private T value;
private bool initialized;
private object? syncRoot;
public LazySlim(Func<T> valueFactory)
{
this.valueFactory = valueFactory;
}
public T Value { get => LazyInitializer.EnsureInitialized(ref value, ref initialized, ref syncRoot, valueFactory); }
}

View File

@@ -116,15 +116,15 @@ internal sealed partial class Activation : IActivation
// If it's the first time launch, we show the guide window anyway.
// Otherwise, we check if there's any unfulfilled resource category present.
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
{
if (StaticResource.IsAnyUnfulfilledCategoryPresent())
{
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.StaticResourceBegin);
}
}
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language) < GuideState.Completed)
{
await taskContext.SwitchToMainThreadAsync();
serviceProvider.GetRequiredService<GuideWindow>();

View File

@@ -49,7 +49,7 @@ internal sealed partial class PrivateNamedPipeServer : IDisposable
{
byte[] content = new byte[header->ContentLength];
serverStream.ReadAtLeast(content, header->ContentLength, false);
HutaoException.ThrowIf(XxHash64.HashToUInt64(content) != header->Checksum, "PipePacket Content Hash incorrect");
HutaoException.ThrowIf(XxHash64.HashToUInt64(content) != header->Checksum, HutaoExceptionKind.PrivateNamedPipeContentHashIncorrect, "PipePacket Content Hash incorrect");
return content;
}

View File

@@ -1,114 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
internal static class ConsoleVirtualTerminalSequences
{
public const string Default = "\u001b[0m";
public const string Bold = "\u001b[1m";
public const string Underline = "\u001b[4m";
public const string Negative = "\u001b[7m";
public const string NoBold = "\u001b[22m";
public const string NoUnderline = "\u001b[24m";
public const string Positive = "\u001b[27m";
public const string ForegroundBlack = "\u001b[30m";
public const string ForegroundRed = "\u001b[31m";
public const string ForegroundGreen = "\u001b[32m";
public const string ForegroundYellow = "\u001b[33m";
public const string ForegroundBlue = "\u001b[34m";
public const string ForegroundMagenta = "\u001b[35m";
public const string ForegroundCyan = "\u001b[36m";
public const string ForegroundWhite = "\u001b[37m";
public const string ForegroundExtended = "\u001b[38m";
public const string ForegroundDefault = "\u001b[39m";
public const string BackgroundBlack = "\u001b[40m";
public const string BackgroundRed = "\u001b[41m";
public const string BackgroundGreen = "\u001b[42m";
public const string BackgroundYellow = "\u001b[43m";
public const string BackgroundBlue = "\u001b[44m";
public const string BackgroundMagenta = "\u001b[45m";
public const string BackgroundCyan = "\u001b[46m";
public const string BackgroundWhite = "\u001b[47m";
public const string BackgroundExtended = "\u001b[48m";
public const string BackgroundDefault = "\u001b[49m";
public const string BrightForegroundBlack = "\u001b[1m\u001b[30m";
public const string BrightForegroundRed = "\u001b[1m\u001b[31m";
public const string BrightForegroundGreen = "\u001b[1m\u001b[32m";
public const string BrightForegroundYellow = "\u001b[1m\u001b[33m";
public const string BrightForegroundBlue = "\u001b[1m\u001b[34m";
public const string BrightForegroundMagenta = "\u001b[1m\u001b[35m";
public const string BrightForegroundCyan = "\u001b[1m\u001b[36m";
public const string BrightForegroundWhite = "\u001b[1m\u001b[37m";
public const string BrightBackgroundBlack = "\u001b[1m\u001b[40m";
public const string BrightBackgroundRed = "\u001b[1m\u001b[41m";
public const string BrightBackgroundGreen = "\u001b[1m\u001b[42m";
public const string BrightBackgroundYellow = "\u001b[1m\u001b[43m";
public const string BrightBackgroundBlue = "\u001b[1m\u001b[44m";
public const string BrightBackgroundMagenta = "\u001b[1m\u001b[45m";
public const string BrightBackgroundCyan = "\u001b[1m\u001b[46m";
public const string BrightBackgroundWhite = "\u001b[1m\u001b[47m";
public const string Dim = "\u001b[2m";
public const string Italic = "\u001b[3m";
public const string Blink = "\u001b[5m";
public const string Hidden = "\u001b[8m";
public const string StrikeThrough = "\u001b[9m";
public const string DoubleUnderline = "\u001b[21m";
public const string NoItalic = "\u001b[23m";
public const string NoBlink = "\u001b[25m";
public const string NoHidden = "\u001b[28m";
public const string NoStrikeThrough = "\u001b[29m";
public static string FromConsoleColor(ConsoleColor color, bool foreground)
{
return (foreground, color) switch
{
(true, ConsoleColor.Black) => ForegroundBlack,
(true, ConsoleColor.DarkBlue) => ForegroundBlue,
(true, ConsoleColor.DarkGreen) => ForegroundGreen,
(true, ConsoleColor.DarkCyan) => ForegroundCyan,
(true, ConsoleColor.DarkRed) => ForegroundRed,
(true, ConsoleColor.DarkMagenta) => ForegroundMagenta,
(true, ConsoleColor.DarkYellow) => ForegroundYellow,
(true, ConsoleColor.DarkGray) => BrightForegroundBlack,
(true, ConsoleColor.Gray) => ForegroundWhite,
(true, ConsoleColor.Blue) => BrightForegroundBlue,
(true, ConsoleColor.Green) => BrightForegroundGreen,
(true, ConsoleColor.Cyan) => BrightForegroundCyan,
(true, ConsoleColor.Red) => BrightForegroundRed,
(true, ConsoleColor.Magenta) => BrightForegroundMagenta,
(true, ConsoleColor.Yellow) => BrightForegroundYellow,
(true, ConsoleColor.White) => BrightForegroundWhite,
(false, ConsoleColor.Black) => BackgroundBlack,
(false, ConsoleColor.DarkBlue) => BackgroundBlue,
(false, ConsoleColor.DarkGreen) => BackgroundGreen,
(false, ConsoleColor.DarkCyan) => BackgroundCyan,
(false, ConsoleColor.DarkRed) => BackgroundRed,
(false, ConsoleColor.DarkMagenta) => BackgroundMagenta,
(false, ConsoleColor.DarkYellow) => BackgroundYellow,
(false, ConsoleColor.DarkGray) => BrightBackgroundBlack,
(false, ConsoleColor.Gray) => BackgroundWhite,
(false, ConsoleColor.Blue) => BrightBackgroundBlue,
(false, ConsoleColor.Green) => BrightBackgroundGreen,
(false, ConsoleColor.Cyan) => BrightBackgroundCyan,
(false, ConsoleColor.Red) => BrightBackgroundRed,
(false, ConsoleColor.Magenta) => BrightBackgroundMagenta,
(false, ConsoleColor.Yellow) => BrightBackgroundYellow,
(false, ConsoleColor.White) => BrightBackgroundWhite,
_ => string.Empty,
};
}
}

View File

@@ -22,7 +22,7 @@ internal sealed class ConsoleWindowLifeTime : IDisposable
HANDLE inputHandle = GetStdHandle(STD_HANDLE.STD_INPUT_HANDLE);
if (GetConsoleMode(inputHandle, out CONSOLE_MODE mode))
{
mode &= ~CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
mode &= ~CONSOLE_MODE.ENABLE_QUICK_EDIT_MODE;
SetConsoleMode(inputHandle, mode);
}
@@ -38,4 +38,4 @@ internal sealed class ConsoleWindowLifeTime : IDisposable
FreeConsole();
}
}
}
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
internal readonly struct LogArgument
{
public readonly object? Argument;
public readonly ConsoleColor? ForegroundColor;
public readonly ConsoleColor? BackgroundColor;
public LogArgument(object? argument, ConsoleColor? foreground = default, ConsoleColor? background = default)
{
Argument = argument;
ForegroundColor = foreground;
BackgroundColor = background;
}
public static implicit operator LogArgument(string argument)
{
return new(argument);
}
public static implicit operator LogArgument(double argument)
{
return new(argument);
}
public static implicit operator LogArgument((object? Argument, ConsoleColor Foreground) tuple)
{
return new(tuple.Argument, tuple.Foreground);
}
}

View File

@@ -1,28 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
internal readonly struct LogMessage
{
public readonly string Message;
public readonly ConsoleColor? ForegroundColor;
public readonly ConsoleColor? BackgroundColor;
public LogMessage(string message, ConsoleColor? foreground = default, ConsoleColor? background = default)
{
Message = message;
ForegroundColor = foreground;
BackgroundColor = background;
}
public static implicit operator LogMessage(string value)
{
return new(value);
}
public static implicit operator LogMessage((string Value, ConsoleColor? Foreground) tuple)
{
return new(tuple.Value, tuple.Foreground);
}
}

View File

@@ -1,172 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text;
namespace Snap.Hutao.Core.Logging;
[SuppressMessage("", "SH002")]
internal static class LoggerExtension
{
public static void LogColorizedDebug(this ILogger logger, Exception? exception, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Debug, exception, message, args);
}
public static void LogColorizedDebug(this ILogger logger, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Debug, message, args);
}
public static void LogColorizedTrace(this ILogger logger, Exception? exception, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Trace, exception, message, args);
}
public static void LogColorizedTrace(this ILogger logger, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Trace, message, args);
}
public static void LogColorizedInformation(this ILogger logger, Exception? exception, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Information, exception, message, args);
}
public static void LogColorizedInformation(this ILogger logger, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Information, message, args);
}
public static void LogColorizedWarning(this ILogger logger, Exception? exception, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Warning, exception, message, args);
}
public static void LogColorizedWarning(this ILogger logger, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Warning, message, args);
}
public static void LogColorizedError(this ILogger logger, Exception? exception, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Error, exception, message, args);
}
public static void LogColorizedError(this ILogger logger, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Error, message, args);
}
public static void LogColorizedCritical(this ILogger logger, Exception? exception, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Critical, exception, message, args);
}
public static void LogColorizedCritical(this ILogger logger, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(LogLevel.Critical, message, args);
}
public static void LogColorized(this ILogger logger, LogLevel logLevel, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(logLevel, 0, null, message, args);
}
public static void LogColorized(this ILogger logger, LogLevel logLevel, EventId eventId, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(logLevel, eventId, null, message, args);
}
public static void LogColorized(this ILogger logger, LogLevel logLevel, Exception? exception, LogMessage message, params LogArgument[] args)
{
logger.LogColorized(logLevel, 0, exception, message, args);
}
public static void LogColorized(this ILogger logger, LogLevel logLevel, EventId eventId, Exception? exception, LogMessage message, params LogArgument[] args)
{
string colorizedMessage = Colorize(message, args, out object?[] outArgs)!;
logger.Log(logLevel, eventId, exception, colorizedMessage, outArgs);
}
private static string? Colorize(LogMessage message, LogArgument[] args, out object?[] outArgs)
{
StringBuilder resultMessageBuilder = new(message.Message.Length);
ReadOnlySpan<char> messageSpan = message.Message.AsSpan();
// Message base colors
ConsoleColor? messageForeground = message.ForegroundColor;
ConsoleColor? messageBackground = message.BackgroundColor;
if (messageForeground.HasValue)
{
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.FromConsoleColor(messageForeground.Value, true));
}
if (messageBackground.HasValue)
{
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.FromConsoleColor(messageBackground.Value, false));
}
ReadOnlySpan<LogArgument> argSpan = args.AsSpan();
outArgs = new object?[args.Length];
int argIndex = 0;
for (int index = 0; index < messageSpan.Length; index++)
{
if (messageSpan[index] == '{')
{
ref readonly LogArgument arg = ref argSpan[argIndex];
outArgs[argIndex] = arg.Argument;
argIndex++;
if (arg.ForegroundColor.HasValue)
{
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.FromConsoleColor(arg.ForegroundColor.Value, true));
}
if (arg.BackgroundColor.HasValue)
{
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.FromConsoleColor(arg.BackgroundColor.Value, false));
}
int closingIndex = messageSpan[index..].IndexOf('}');
resultMessageBuilder.Append(messageSpan.Slice(index, closingIndex + 1));
index += closingIndex;
if (arg.ForegroundColor.HasValue || arg.BackgroundColor.HasValue)
{
// Restore message colors
if (messageForeground.HasValue || messageBackground.HasValue)
{
if (messageForeground.HasValue)
{
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.FromConsoleColor(messageForeground.Value, true));
}
if (messageBackground.HasValue)
{
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.FromConsoleColor(messageBackground.Value, false));
}
}
else
{
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.ForegroundWhite);
}
}
}
else
{
resultMessageBuilder.Append(messageSpan[index]);
}
}
// Restore default colors
if (message.ForegroundColor.HasValue || message.BackgroundColor.HasValue)
{
resultMessageBuilder.Append(ConsoleVirtualTerminalSequences.ForegroundWhite);
}
return resultMessageBuilder.ToString();
}
}

View File

@@ -3,17 +3,13 @@
namespace Snap.Hutao.Core.Logging;
internal static class LoggerFactoryExtension
internal static class LoggerFactoryExtensions
{
public static ILoggingBuilder AddConsoleWindow(this ILoggingBuilder builder)
{
builder.Services.AddSingleton<ConsoleWindowLifeTime>();
builder.AddSimpleConsole(options =>
{
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff ";
});
builder.AddSimpleConsole();
return builder;
}
}

View File

@@ -1,27 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core;
internal readonly ref struct ReadOnlySpan2D<T>
where T : unmanaged
{
private readonly ref T reference;
private readonly int length;
private readonly int columns;
public unsafe ReadOnlySpan2D(void* pointer, int length, int columns)
{
reference = ref *(T*)pointer;
this.length = length;
this.columns = columns;
}
public ReadOnlySpan<T> this[int row]
{
get => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref reference, row * columns), columns);
}
}

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