Compare commits

...

32 Commits

Author SHA1 Message Date
DismissedLight
848392f8d4 prevent user service capture scoped app db context 2022-10-30 15:19:21 +08:00
DismissedLight
62d0fb5d05 launch game phase 1 2022-10-28 14:59:31 +08:00
DismissedLight
7a99c44b29 collocation for wiki avatar 2022-10-25 15:47:12 +08:00
DismissedLight
792a701183 fix view model scope 2022-10-25 13:12:50 +08:00
DismissedLight
fa19f7e817 add api cache 2022-10-24 16:12:30 +08:00
DismissedLight
bf5fcb70f8 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2022-10-21 16:40:45 +08:00
DismissedLight
fda642b72f hutao api v2 page 2022-10-21 16:40:10 +08:00
Masterain
fa650a95c5 Update PublishDistribution.yml 2022-10-18 12:23:53 -07:00
Masterain
02fae69d1e Create PublishDistribution.yml 2022-10-18 12:05:43 -07:00
DismissedLight
76800de6ee separate primary and secondary properties 2022-10-16 21:53:40 +08:00
DismissedLight
72b660119f support homa api 2022-10-16 13:10:06 +08:00
DismissedLight
67a1d5dc74 update to hutao api v2 2022-10-13 21:14:57 +08:00
DismissedLight
6e6d125814 update to hoyolab 2.38.1 2022-10-10 18:55:51 +08:00
DismissedLight
55f16a6357 avatar info 2022-10-10 14:26:40 +08:00
DismissedLight
560068ca20 Merge pull request #90 from DGP-Studio/winui-120
update to was 1.2.0 preview
2022-10-06 11:59:52 +08:00
DismissedLight
2f1981108e update to was 1.2.0 preview 2022-10-06 11:59:17 +08:00
DismissedLight
b545c0d09b fix #88 2022-10-06 11:29:07 +08:00
DismissedLight
009feced08 Merge pull request #85 from Masterain98/main
Update bug_report.yml
2022-10-04 23:53:13 +08:00
Masterain
fa7fcbc9cc Update bug_report.yml 2022-10-04 06:03:38 -07:00
DismissedLight
d28624dea1 Merge pull request #84 from DGP-Studio/feat/avatar_info
Feat/avatar info
2022-10-04 16:48:22 +08:00
DismissedLight
d802d1af15 avatar info complete 1 2022-10-04 16:44:53 +08:00
DismissedLight
43e3df9cba avatar info phase 1 2022-10-04 00:09:25 +08:00
DismissedLight
8e5e59ad0d remove null check to cookie 2022-09-29 16:33:01 +08:00
DismissedLight
ed7d55ddd5 Merge pull request #64 from DGP-Studio/refactor/userserive
UserService v2 implementation
2022-09-29 16:14:27 +08:00
DismissedLight
94ef94a621 impl 2022-09-29 16:13:47 +08:00
Masterain
fd35213741 Merge pull request #52 from Masterain98/main
Update README.md
2022-09-27 19:23:15 -07:00
Masterain
0f752129b7 Update README.md 2022-09-27 19:20:49 -07:00
DismissedLight
331cc14532 Update Persistence.cs 2022-09-27 14:38:06 +08:00
DismissedLight
c0ddb24825 fix window issue 2022-09-27 14:36:13 +08:00
DismissedLight
e925c5909c update readme 2022-09-27 12:35:37 +08:00
DismissedLight
f29bfda4d9 Stoken upgradable 2022-09-26 16:55:27 +08:00
DismissedLight
cb6a9badc0 File IO API 2022-09-24 22:10:29 +08:00
353 changed files with 11254 additions and 2469 deletions

View File

@@ -24,8 +24,8 @@ body:
id: shver
attributes:
label: Snap Hutao 版本
description: 在应用程序的设置界面中靠下的位置可以找到
placeholder: 1.0.30.0
description: 在应用标题,应用程序的设置界面中靠下的位置可以找到
placeholder: 1.1.0
validations:
required: true
@@ -48,6 +48,42 @@ body:
- type: textarea
id: logs
attributes:
label: 相关的崩溃日志 位于 %HOMEPATH%/Documents/Hutao/Log.db
description: 如果应用程序崩溃了,可以将崩溃日志复制后粘贴在此处,文件包含了敏感信息,谨慎上传
label: 相关的崩溃日志 位于 `%HOMEPATH%/Documents/Hutao/Log.db`
description: |
在资源管理器中直接输入`%HOMEPATH%/Documents/Hutao`即可进入文件夹
如果应用程序崩溃了,请将`log.db` 文件上传,文件包含了敏感信息,谨慎上传
如果这个表单是关于导入祈愿记录的问题,请包含你导入的`Json`文件
**务必不要上传`user.db`文件,该文件包含你的帐号敏感信息**
render: shell
- type: checkboxes
id: confirm-issue
attributes:
label: 我确认已在表单中附上了充足的补充说明以帮助开发人员确定问题
description: 补充说明包括但不限于:日志文件、抛出的错误信息、截图和录屏
options:
- label:
required: true
- type: checkboxes
id: confirm-no-duplicated-issue
attributes:
label: 我确认没有他人提出相同或类似的问题
description: |
请先通过 Issue 搜索功能确认这不是相同的问题;
[BUG Issues](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aissue+is%3Aopen+label%3ABUG)
你应该在原始 Issue 中通过回复添加有助于解决问题的信息,而不是创建重复的问题;
**没有帮助的重复问题可能会被直接关闭**
options:
- label:
required: true
- type: checkboxes
id: confirm-docs
attributes:
label: 我确认该问题没有在文档中解释
description: Snap Hutao 官方文档:[https://hut.ao](https://hut.ao)
options:
- label:
required: true

View File

@@ -0,0 +1,39 @@
name: PublishDistribution
on:
release:
types: [published]
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
Publish:
runs-on: ubuntu-latest
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Checkout Repo
uses: actions/checkout@v3
# Download Publish.zip
- name: Download Release
uses: robinraju/release-downloader@v1.5
with:
repository: "DGP-Studio/Snap.Hutao"
latest: true
fileName: "*.zip"
out-file-path: ./release-download
# Upload to OD21 (Testing)
- name: Upload OD21
env:
RCCONF: ${{ secrets.RCCONF }}
run: |
curl https://rclone.org/install.sh | sudo bash
mkdir -p ~/.config/rclone/
cat << EOF > ~/.config/rclone/rclone.conf
$RCCONF
EOF
rclone copy ./release-download/* dgpODCN:/snaphutao/Releases/

View File

@@ -3,10 +3,18 @@
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)
## 项目首页(文档)
[![Deploy Docs](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml/badge.svg)](https://github.com/DGP-Studio/Snap.Hutao.Docs/actions/workflows/deploy-docs.yml)
[HUT.AO](https://hut.ao)
## 安装
* 前往 [下载页面](https://go.hut.ao/archive) 下载最新版本的 `胡桃` 安装包
* 完全解压后,使用 powershell 运行 `install.ps1` 文件
* 前往 [下载页面](https://go.hut.ao/down) 下载最新版本的 `胡桃` 安装包
* (曾启用的可以跳过此步骤)在系统设置中打开 **开发者选项** 界面,勾选 `开发人员模式``允许 PowerShell 脚本`
* 完全解压后,右键使用 powershell 运行 `install.ps1` 文件
* 安装完成后可以关闭 `允许 PowerShell 脚本`
## 特别感谢

View File

@@ -159,6 +159,7 @@ dotnet_diagnostic.CA1805.severity = suggestion
# VSTHRD111: Use ConfigureAwait(bool)
dotnet_diagnostic.VSTHRD111.severity = suggestion
csharp_style_prefer_top_level_statements = true:silent
[*.vb]
#### 命名样式 ####

View File

@@ -22,6 +22,7 @@ public class InjectionGenerator : ISourceGenerator
{
private const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
private const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
private const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
@@ -74,39 +75,46 @@ internal static partial class ServiceCollectionExtensions
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
{
lineBuilder
.Clear()
.Append("\r\n");
AttributeData injectionInfo = classSymbol
IEnumerable<AttributeData> datas = classSymbol
.GetAttributes()
.Single(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
.Where(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.ToCSharpString();
switch (injectAsName)
foreach (AttributeData injectionInfo in datas)
{
case InjectAsSingletonName:
lineBuilder.Append(@" services.AddSingleton(");
break;
case InjectAsTransientName:
lineBuilder.Append(@" services.AddTransient(");
break;
default:
throw new InvalidOperationException($"非法的InjectAs值: [{injectAsName}]");
lineBuilder
.Clear()
.Append("\r\n");
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.ToCSharpString();
switch (injectAsName)
{
case InjectAsSingletonName:
lineBuilder.Append(@" services.AddSingleton(");
break;
case InjectAsTransientName:
lineBuilder.Append(@" services.AddTransient(");
break;
case InjectAsScopedName:
lineBuilder.Append(@" services.AddScoped(");
break;
default:
throw new InvalidOperationException($"非法的 InjectAs 值: [{injectAsName}]");
}
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.ToCSharpString()}, ");
}
lineBuilder.Append($"typeof({classSymbol.ToDisplayString()}));");
lines.Add(lineBuilder.ToString());
}
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.ToCSharpString()}, ");
}
lineBuilder.Append($"typeof({classSymbol.ToDisplayString()}));");
lines.Add(lineBuilder.ToString());
}
foreach (string line in lines.OrderBy(x => x))

View File

@@ -21,7 +21,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -2,7 +2,10 @@
x:Class="Snap.Hutao.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
xmlns:shvc="using:Snap.Hutao.View.Converter">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
@@ -13,21 +16,36 @@
<!--Modify Window title bar color-->
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush"/>
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush"/>
<!--Page Transparent Background-->
<StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush"/>
<!--IconFont-->
<FontFamily x:Key="SymbolThemeFontFamily">ms-appx:///Resource/Font/Segoe Fluent Icons.ttf#Segoe Fluent Icons</FontFamily>
<!--InfoBar Resource-->
<Thickness x:Key="InfoBarIconMargin">6,16,16,16</Thickness>
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
<!--Pivot Resource-->
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
<!--CornerRadius-->
<CornerRadius x:Key="CompatCornerRadius">6</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusTop">6,6,0,0</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusRight">0,6,6,0</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusBottom">0,0,6,6</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
<!--Converters-->
<cwuc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<shmmc:AchievementIconConverter x:Key="AchievementIconConverter"/>
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
<shmmc:DescParamDescriptor x:Key="DescParamDescriptor"/>
<shmmc:ElementNameIconConverter x:Key="ElementNameIconConverter"/>
<shmmc:GachaAvatarImgConverter x:Key="GachaAvatarImgConverter"/>
<shmmc:GachaAvatarIconConverter x:Key="GachaAvatarIconConverter"/>
<shmmc:ItemIconConverter x:Key="ItemIconConverter"/>
<shmmc:PropertyInfoDescriptor x:Key="PropertyDescriptor"/>
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -3,10 +3,13 @@
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Exception;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.AppCenter;
using Snap.Hutao.Service.Metadata;
using System.Diagnostics;
using Windows.Storage;
@@ -25,13 +28,14 @@ public partial class App : Application
/// Initializes the singleton application object.
/// </summary>
/// <param name="logger">日志器</param>
public App(ILogger<App> logger)
/// <param name="appCenter">App Center</param>
public App(ILogger<App> logger, AppCenter appCenter)
{
// load app resource
InitializeComponent();
this.logger = logger;
_ = new ExceptionRecorder(this, logger);
_ = new ExceptionRecorder(this, logger, appCenter);
}
/// <inheritdoc/>
@@ -47,13 +51,20 @@ public partial class App : Application
Activation.Activate(firstInstance, activatedEventArgs);
firstInstance.Activated += Activation.Activate;
logger.LogInformation(EventIds.CommonLog, "Snap Hutao : {version}", CoreEnvironment.Version);
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.TemporaryFolder.Path);
JumpListHelper.ConfigAsync().SafeForget(logger);
Ioc.Default
.GetRequiredService<IMetadataService>()
.ImplictAs<IMetadataInitializer>()?
.InitializeInternalAsync()
.SafeForget(logger);
Ioc.Default
.GetRequiredService<AppCenter>()
.Initialize();
}
else
{

View File

@@ -3,6 +3,7 @@
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Configuration;
namespace Snap.Hutao.Context.Database;
@@ -50,6 +51,16 @@ public class AppDbContext : DbContext
/// </summary>
public DbSet<GachaArchive> GachaArchives { get; set; } = default!;
/// <summary>
/// 角色信息
/// </summary>
public DbSet<AvatarInfo> AvatarInfos { get; set; } = default!;
/// <summary>
/// 游戏内账号
/// </summary>
public DbSet<GameAccount> GameAccounts { get; set; } = default!;
/// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>
@@ -59,4 +70,12 @@ public class AppDbContext : DbContext
{
return new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options);
}
/// <inheritdoc/>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.ApplyConfiguration(new AvatarInfoConfiguration())
.ApplyConfiguration(new UserConfiguration());
}
}

View File

@@ -55,4 +55,4 @@ internal class AutoHeightBehavior : BehaviorBase<FrameworkElement>
{
AssociatedObject.Height = (double)AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
}
}
}

View File

@@ -0,0 +1,58 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control.Behavior;
/// <summary>
/// 按给定比例自动调整高度的行为
/// </summary>
internal class AutoWidthBehavior : BehaviorBase<FrameworkElement>
{
private static readonly DependencyProperty TargetWidthProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetWidth), 320D);
private static readonly DependencyProperty TargetHeightProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetHeight), 1024D);
/// <summary>
/// 目标宽度
/// </summary>
public double TargetWidth
{
get => (double)GetValue(TargetWidthProperty);
set => SetValue(TargetWidthProperty, value);
}
/// <summary>
/// 目标高度
/// </summary>
public double TargetHeight
{
get => (double)GetValue(TargetHeightProperty);
set => SetValue(TargetHeightProperty, value);
}
/// <inheritdoc/>
protected override void OnAssociatedObjectLoaded()
{
UpdateElementWidth();
AssociatedObject.SizeChanged += OnSizeChanged;
}
/// <inheritdoc/>
protected override void OnDetaching()
{
AssociatedObject.SizeChanged -= OnSizeChanged;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateElementWidth();
}
private void UpdateElementWidth()
{
AssociatedObject.Width = (double)AssociatedObject.Height * (TargetWidth / TargetHeight);
}
}

View File

@@ -1,40 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Behaviors;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Shapes;
namespace Snap.Hutao.Control.Behavior;
/// <summary>
/// Make ContentDialog's SmokeLayerBackground dsiplay over custom titleBar
/// </summary>
public class ContentDialogBehavior : BehaviorBase<FrameworkElement>
{
/// <inheritdoc/>
protected override void OnAssociatedObjectLoaded()
{
DependencyObject parent = VisualTreeHelper.GetParent(AssociatedObject);
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
DependencyObject current = VisualTreeHelper.GetChild(parent, i);
if (current is Rectangle { Name: "SmokeLayerBackground" } background)
{
background.ClearValue(FrameworkElement.MarginProperty);
background.RegisterPropertyChangedCallback(FrameworkElement.MarginProperty, OnMarginChanged);
break;
}
}
}
private static void OnMarginChanged(DependencyObject sender, DependencyProperty property)
{
if (property == FrameworkElement.MarginProperty)
{
sender.ClearValue(property);
}
}
}

View File

@@ -3,8 +3,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Xaml.Interactivity;
using Snap.Hutao.Control.Behavior;
using Snap.Hutao.Core.Threading;
namespace Snap.Hutao.Control.Extension;
@@ -23,7 +21,6 @@ internal static class ContentDialogExtensions
public static ContentDialog InitializeWithWindow(this ContentDialog contentDialog, Window window)
{
contentDialog.XamlRoot = window.Content.XamlRoot;
Interaction.SetBehaviors(contentDialog, new() { new ContentDialogBehavior() });
return contentDialog;
}

View File

@@ -121,19 +121,27 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{
await HideAsync(token);
LoadedImageSurface? imageSurface = null;
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
if (uri != null)
{
StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri);
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
LoadedImageSurface? imageSurface = null;
try
if (uri.Scheme == "ms-appx")
{
imageSurface = await LoadImageSurfaceAsync(storageFile, token);
imageSurface = LoadedImageSurface.StartLoadFromUri(uri);
}
catch (COMException)
else
{
await imageCache.RemoveAsync(uri.Enumerate());
StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri);
try
{
imageSurface = await LoadImageSurfaceAsync(storageFile, token);
}
catch (COMException)
{
await imageCache.RemoveAsync(uri.Enumerate());
}
}
if (imageSurface != null)

View File

@@ -29,7 +29,6 @@ public class MonoChrome : CompositionImage
{
CompositionColorBrush blackLayerBursh = compositor.CreateColorBrush(Colors.Black);
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.Uniform, vRatio: 0f);
CompositionEffectBrush overlayBrush = compositor.CompositeBlendEffectBrush(blackLayerBursh, imageSurfaceBrush, BlendEffectMode.Overlay);
CompositionEffectBrush opacityBrush = compositor.CompositeLuminanceToAlphaEffectBrush(overlayBrush);

View File

@@ -24,8 +24,8 @@ internal class I18NExtension : MarkupExtension
static I18NExtension()
{
string currentName = CultureInfo.CurrentUICulture.Name;
Type languageType = EnumerableExtensions.GetValueOrDefault(TranslationMap, currentName, typeof(LanguagezhCN));
Translation = (ITranslation)Activator.CreateInstance(languageType)!;
Type? languageType = TranslationMap.GetValueOrDefault2(currentName, typeof(LanguagezhCN));
Translation = (ITranslation)Activator.CreateInstance(languageType!)!;
}
/// <summary>

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.Control.Markup;
/// <summary>
/// Uri扩展
/// </summary>
[MarkupExtensionReturnType(ReturnType = typeof(Uri))]
public sealed class UriExtension : MarkupExtension
{
/// <summary>
/// 构造一个新的Uri扩展
/// </summary>
public UriExtension()
{
}
/// <summary>
/// 地址
/// </summary>
public string? Value { get; set; }
/// <inheritdoc/>
protected override object ProvideValue()
{
return new Uri(Value ?? string.Empty);
}
}

View File

@@ -0,0 +1,29 @@
<UserControl
x:Class="Snap.Hutao.Control.Panel.PanelSelector"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
mc:Ignorable="d">
<SplitButton Padding="0,6" Click="SplitButtonClick" Loaded="SplitButtonLoaded">
<SplitButton.Content>
<FontIcon Name="IconPresenter" Glyph="&#xE8FD;"/>
</SplitButton.Content>
<SplitButton.Flyout>
<MenuFlyout>
<RadioMenuFlyoutItem
Tag="List"
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
Text="列表"/>
<RadioMenuFlyoutItem
Tag="Grid"
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Text="网格"/>
</MenuFlyout>
</SplitButton.Flyout>
</SplitButton>
</UserControl>

View File

@@ -0,0 +1,81 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
namespace Snap.Hutao.Control.Panel;
/// <summary>
/// 面板选择器
/// </summary>
public sealed partial class PanelSelector : UserControl
{
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List");
/// <summary>
/// 构造一个新的面板选择器
/// </summary>
public PanelSelector()
{
InitializeComponent();
}
/// <summary>
/// 当前选择
/// </summary>
public string Current
{
get => (string)GetValue(CurrentProperty);
set => SetValue(CurrentProperty, value);
}
private void SplitButtonLoaded(object sender, RoutedEventArgs e)
{
MenuFlyout menuFlyout = (MenuFlyout)((SplitButton)sender).Flyout;
((RadioMenuFlyoutItem)menuFlyout.Items[0]).IsChecked = true;
}
private void SplitButtonClick(SplitButton sender, SplitButtonClickEventArgs args)
{
MenuFlyout menuFlyout = (MenuFlyout)sender.Flyout;
int i = 0;
for (; i < menuFlyout.Items.Count; i++)
{
RadioMenuFlyoutItem current = (RadioMenuFlyoutItem)menuFlyout.Items[i];
if (current.IsChecked)
{
break;
}
}
i++;
if (i > menuFlyout.Items.Count)
{
i = 1;
}
if (i == menuFlyout.Items.Count)
{
i = 0;
}
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)menuFlyout.Items[i];
item.IsChecked = true;
UpdateState(item);
}
private void RadioMenuFlyoutItemClick(object sender, RoutedEventArgs e)
{
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)sender;
UpdateState(item);
}
private void UpdateState(RadioMenuFlyoutItem item)
{
Current = (string)item.Tag;
IconPresenter.Glyph = ((FontIcon)item.Icon).Glyph;
}
}

View File

@@ -11,6 +11,9 @@ namespace Snap.Hutao.Control;
/// <summary>
/// 表示支持取消加载的异步页面
/// 在被导航到其他页面前触发取消异步通知
/// <para/>
/// InitializeWith{T}();
/// InitializeComponent();
/// </summary>
public class ScopedPage : Page
{
@@ -25,9 +28,6 @@ public class ScopedPage : Page
serviceScope = Ioc.Default.CreateScope();
}
/// <inheritdoc cref="IServiceScope.ServiceProvider"/>
public IServiceProvider ServiceProvider { get => serviceScope.ServiceProvider; }
/// <summary>
/// 初始化
/// </summary>
@@ -35,7 +35,7 @@ public class ScopedPage : Page
public void InitializeWith<TViewModel>()
where TViewModel : class, ISupportCancellation
{
ISupportCancellation viewModel = ServiceProvider.GetRequiredService<TViewModel>();
ISupportCancellation viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token;
DataContext = viewModel;
}

View File

@@ -102,7 +102,7 @@ public class DescriptionTextBlock : ContentControl
if (i == description.Length - 1)
{
AppendText(text, description[last..i]);
AppendText(text, description[last..(i + 1)]);
}
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
namespace Snap.Hutao.Control;
/// <summary>
/// 值转换器
/// </summary>
/// <typeparam name="TFrom">源类型</typeparam>
/// <typeparam name="TTo">目标类型</typeparam>
public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
{
/// <inheritdoc/>
public object? Convert(object value, Type targetType, object parameter, string language)
{
#if DEBUG
try
{
return Convert((TFrom)value);
}
catch (Exception ex)
{
Ioc.Default
.GetRequiredService<ILogger<ValueConverterBase<TFrom, TTo>>>()
.LogError(ex, "值转换器异常");
}
return null;
#else
return Convert((TFrom)value);
#endif
}
/// <inheritdoc/>
public object? ConvertBack(object value, Type targetType, object parameter, string language)
{
return ConvertBack((TTo)value);
}
/// <summary>
/// 从源类型转换到目标类型
/// </summary>
/// <param name="from">源</param>
/// <returns>目标</returns>
public abstract TTo Convert(TFrom from);
/// <summary>
/// 从目标类型转换到源类型
/// 重写时请勿调用基类方法
/// </summary>
/// <param name="to">目标</param>
/// <returns>源</returns>
public virtual TFrom ConvertBack(TTo to)
{
throw Must.NeverHappen();
}
}

View File

@@ -3,9 +3,10 @@
// See the LICENSE file in the project root for more information.
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
using Snap.Hutao.Core.Threading;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Windows.Storage;
using Windows.Storage.FileProperties;
@@ -22,6 +23,8 @@ public abstract class CacheBase<T>
{
private readonly SemaphoreSlim cacheFolderSemaphore = new(1);
private readonly ILogger logger;
// violate di rule
private readonly HttpClient httpClient;
private StorageFolder? baseFolder;
@@ -138,7 +141,7 @@ public abstract class CacheBase<T>
IStorageItem? item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
if (item == null)
if (item == null || (await item.GetBasicPropertiesAsync()).Size == 0)
{
StorageFile baseFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false);
await DownloadFileAsync(uri, baseFile).ConfigureAwait(false);
@@ -169,34 +172,21 @@ public abstract class CacheBase<T>
private static string GetCacheFileName(Uri uri)
{
return CreateHash64(uri.ToString()).ToString();
}
private static ulong CreateHash64(string str)
{
byte[] utf8 = Encoding.UTF8.GetBytes(str);
ulong value = (ulong)utf8.Length;
for (int n = 0; n < utf8.Length; n++)
{
value += (ulong)utf8[n] << ((n * 5) % 56);
}
return value;
string url = uri.ToString();
byte[] chars = Encoding.UTF8.GetBytes(url);
byte[] hash = SHA1.HashData(chars);
return System.Convert.ToHexString(hash);
}
private async Task DownloadFileAsync(Uri uri, StorageFile baseFile)
{
logger.LogInformation(EventIds.FileCaching, "Begin downloading for {uri}", uri);
using (Stream httpStream = await httpClient.GetStreamAsync(uri))
using (Stream httpStream = await httpClient.GetStreamAsync(uri).ConfigureAwait(false))
{
using (Stream fileStream = await baseFile.OpenStreamForWriteAsync())
using (FileStream fileStream = File.Create(baseFile.Path))
{
await httpStream.CopyToAsync(fileStream);
// Call this before dispose fileStream.
await fileStream.FlushAsync();
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Core.Caching;
/// </summary>
[Injection(InjectAs.Singleton, typeof(IImageCache))]
[HttpClient(HttpClientConfigration.Default)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 20)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 16)]
public class ImageCache : CacheBase<BitmapImage>, IImageCache
{
private const string DateAccessedProperty = "System.DateAccessed";

View File

@@ -0,0 +1,63 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text;
namespace Snap.Hutao.Core;
/// <summary>
/// 命令行建造器
/// </summary>
public class CommandLineBuilder
{
private const char WhiteSpace = ' ';
private readonly Dictionary<string, string?> options = new();
/// <summary>
/// 当符合条件时添加参数
/// </summary>
/// <param name="name">参数名称</param>
/// <param name="condition">条件</param>
/// <param name="value">值</param>
/// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIf(string name, bool condition, object? value = null)
{
return condition ? Append(name, value) : this;
}
/// <summary>
/// 添加参数
/// </summary>
/// <param name="name">参数名称</param>
/// <param name="value">值</param>
/// <returns>命令行建造器</returns>
public CommandLineBuilder Append(string name, object? value = null)
{
options.Add(name, value?.ToString());
return this;
}
/// <inheritdoc cref="ToString"/>
public string Build()
{
return ToString();
}
/// <inheritdoc/>
public override string ToString()
{
StringBuilder s = new();
foreach ((string key, string? value) in options)
{
s.Append(WhiteSpace);
s.Append(key);
if (!string.IsNullOrEmpty(value))
{
s.Append(WhiteSpace);
s.Append(value);
}
}
return s.ToString();
}
}

View File

@@ -1,7 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Extension;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using Windows.ApplicationModel;
namespace Snap.Hutao.Core;
@@ -11,17 +15,20 @@ namespace Snap.Hutao.Core;
/// </summary>
internal static class CoreEnvironment
{
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
private const string MachineGuidValue = "MachineGuid";
// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// <summary>
/// 动态密钥1的盐
/// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// </summary>
public const string DynamicSecret1Salt = "n0KjuIrKgLHh08LWSCYP0WXlVXaYvV64";
public const string DynamicSecret1Salt = "yUZ3s0Sna1IrSNfk29Vo6vRapdOyqyhB";
/// <summary>
/// 动态密钥2的盐
/// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// </summary>
public const string DynamicSecret2Salt = "YVEIkzDFNHLeKXLxzqCA9TzxCpWwbIbk";
public const string DynamicSecret2Salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
/// <summary>
/// 米游社请求UA
@@ -31,7 +38,7 @@ internal static class CoreEnvironment
/// <summary>
/// 米游社 Rpc 版本
/// </summary>
public const string HoyolabXrpcVersion = "2.36.1";
public const string HoyolabXrpcVersion = "2.38.1";
/// <summary>
/// 标准UA
@@ -48,6 +55,22 @@ internal static class CoreEnvironment
/// </summary>
public static readonly string HoyolabDeviceId;
/// <summary>
/// AppCenter 设备Id
/// </summary>
public static readonly string AppCenterDeviceId;
/// <summary>
/// 默认的Json序列化选项
/// </summary>
public static readonly JsonSerializerOptions JsonOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNameCaseInsensitive = true,
WriteIndented = true,
};
static CoreEnvironment()
{
Version = Package.Current.Id.Version.ToVersion();
@@ -55,5 +78,15 @@ internal static class CoreEnvironment
// simply assign a random guid
HoyolabDeviceId = Guid.NewGuid().ToString();
AppCenterDeviceId = GetUniqueUserID();
}
private static string GetUniqueUserID()
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
byte[] bytes = Encoding.UTF8.GetBytes($"{userName}{machineGuid}");
byte[] hash = MD5.Create().ComputeHash(bytes);
return System.Convert.ToHexString(hash);
}
}

View File

@@ -14,9 +14,8 @@ namespace Snap.Hutao.Core.Database;
/// <typeparam name="TMessage">消息的类型</typeparam>
internal class DbCurrent<TEntity, TMessage>
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntity>
where TMessage : Message.ValueChangedMessage<TEntity>, new()
{
private readonly DbContext dbContext;
private readonly DbSet<TEntity> dbSet;
private readonly IMessenger messenger;
@@ -25,12 +24,11 @@ internal class DbCurrent<TEntity, TMessage>
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="dbSet">数据集</param>
/// <param name="messenger">消息器</param>
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, IMessenger messenger)
///
public DbCurrent(DbSet<TEntity> dbSet, IMessenger messenger)
{
this.dbContext = dbContext;
this.dbSet = dbSet;
this.messenger = messenger;
}
@@ -55,19 +53,18 @@ internal class DbCurrent<TEntity, TMessage>
if (current != null)
{
current.IsSelected = false;
dbSet.Update(current);
dbContext.SaveChanges();
dbSet.UpdateAndSave(current);
}
}
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
TMessage message = new() { OldValue = current, NewValue = value };
current = value;
if (current != null)
{
current.IsSelected = true;
dbSet.Update(current);
dbContext.SaveChanges();
dbSet.UpdateAndSave(current);
}
messenger.Send(message);

View File

@@ -0,0 +1,94 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 数据库集合上下文
/// </summary>
public static class DbSetExtension
{
/// <summary>
/// 获取对应的数据库上下文
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <returns>对应的数据库上下文</returns>
public static DbContext Context<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class
{
return dbSet.GetService<ICurrentDbContext>().Context;
}
/// <summary>
/// 获取或添加一个对应的实体
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="predicate">谓词</param>
/// <param name="entityFactory">实体工厂</param>
/// <param name="added">是否添加</param>
/// <returns>实体</returns>
public static TEntity SingleOrAdd<TEntity>(this DbSet<TEntity> dbSet, Func<TEntity, bool> predicate, Func<TEntity> entityFactory, out bool added)
where TEntity : class
{
added = false;
TEntity? entry = dbSet.SingleOrDefault(predicate);
if (entry == null)
{
entry = entityFactory();
dbSet.Add(entry);
dbSet.Context().SaveChanges();
added = true;
}
return entry;
}
/// <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
{
dbSet.Add(entity);
return dbSet.Context().SaveChanges();
}
/// <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
{
dbSet.Remove(entity);
return dbSet.Context().SaveChanges();
}
/// <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
{
dbSet.Update(entity);
return dbSet.Context().SaveChanges();
}
}

View File

@@ -17,4 +17,9 @@ public enum InjectAs
/// 指示应注册为短期对象
/// </summary>
Transient,
/// <summary>
/// 指示应注册为范围对象
/// </summary>
Scoped,
}

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Annotation;
/// 指示被标注的类型可注入
/// 由源生成器生成注入代码
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class InjectionAttribute : Attribute
{
/// <summary>

View File

@@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Context.FileSystem;
using System.Diagnostics;
using System.Text.Encodings.Web;
namespace Snap.Hutao.Core.DependencyInjection;
@@ -22,14 +21,7 @@ internal static class IocConfiguration
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddJsonSerializerOptions(this IServiceCollection services)
{
return services
.AddSingleton(new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
PropertyNameCaseInsensitive = true,
WriteIndented = true,
});
return services.AddSingleton(CoreEnvironment.JsonOptions);
}
/// <summary>

View File

@@ -36,6 +36,7 @@ internal static partial class IocHttpClientConfiguration
{
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);

View File

@@ -3,8 +3,9 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Service.AppCenter;
namespace Snap.Hutao.Core.LifeCycle;
namespace Snap.Hutao.Core.Exception;
/// <summary>
/// 异常记录器
@@ -12,36 +13,35 @@ namespace Snap.Hutao.Core.LifeCycle;
internal class ExceptionRecorder
{
private readonly ILogger logger;
private readonly AppCenter appCenter;
/// <summary>
/// 构造一个新的异常记录器
/// </summary>
/// <param name="application">应用程序</param>
/// <param name="logger">日志器</param>
public ExceptionRecorder(Application application, ILogger logger)
/// <param name="appCenter">App Center</param>
public ExceptionRecorder(Application application, ILogger logger, AppCenter appCenter)
{
this.logger = logger;
this.appCenter = appCenter;
application.UnhandledException += OnAppUnhandledException;
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
}
/// <summary>
/// 当应用程序未经处理的异常引发时调用
/// </summary>
/// <param name="sender">实例</param>
/// <param name="e">事件参数</param>
public void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常: [HResult:{code}]", e.Exception.HResult);
appCenter.TrackCrash(e.Exception);
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())
{
provider.Dispose();
}
}
/// <summary>
/// Xaml 绑定失败时触发
/// </summary>
/// <param name="sender">实例</param>
/// <param name="e">事件参数</param>
public void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e)
private void OnXamlBindingFailed(object? sender, BindingFailedEventArgs e)
{
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);
}

View File

@@ -25,4 +25,16 @@ internal static class Clipboard
string json = await view.GetTextAsync();
return JsonSerializer.Deserialize<T>(json, options);
}
/// <summary>
/// 设置文本
/// </summary>
/// <param name="text">文本</param>
public static void SetText(string text)
{
DataPackage content = new();
content.SetText(text);
Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(content);
Windows.ApplicationModel.DataTransfer.Clipboard.Flush();
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.IO.Ini;
/// <summary>
/// Ini 注释
/// </summary>
internal class IniComment : IniElement
{
/// <summary>
/// 构造一个新的 Ini 注释
/// </summary>
/// <param name="comment">注释</param>
public IniComment(string comment)
{
Comment = comment;
}
/// <summary>
/// 注释
/// </summary>
public string Comment { get; set; }
/// <inheritdoc/>
public override string ToString()
{
return $";{Comment}";
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.IO.Ini;
/// <summary>
/// Ini 元素
/// </summary>
internal abstract class IniElement
{
/// <summary>
/// 将当前元素转换到等价的字符串表示
/// </summary>
/// <returns>字符串</returns>
public new abstract string ToString();
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.IO.Ini;
/// <summary>
/// Ini 参数
/// </summary>
internal class IniParameter : IniElement
{
/// <summary>
/// Ini 参数
/// </summary>
/// <param name="key">键</param>
/// <param name="value">值</param>
public IniParameter(string key, string value)
{
Key = key;
Value = value;
}
/// <summary>
/// 键
/// </summary>
public string Key { get; set; }
/// <summary>
/// 值
/// </summary>
public string Value { get; set; }
/// <inheritdoc/>
public override string ToString()
{
return $"{Key}={Value}";
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.IO.Ini;
/// <summary>
/// Ini 节
/// </summary>
internal class IniSection : IniElement
{
/// <summary>
/// 构造一个新的Ini 节
/// </summary>
/// <param name="name">名称</param>
/// <param name="elements">元素</param>
public IniSection(string name)
{
Name = name;
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <inheritdoc/>
public override string ToString()
{
return $"[{Name}]";
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO.Ini;
/// <summary>
/// Ini 序列化器
/// </summary>
internal static class IniSerializer
{
/// <summary>
/// 异步反序列化
/// </summary>
/// <param name="fileStream">文件流</param>
/// <returns>Ini 元素集合</returns>
public static IEnumerable<IniElement> Deserialize(FileStream fileStream)
{
using (TextReader reader = new StreamReader(fileStream))
{
while (reader.ReadLine() is string line)
{
if (line.Length > 0)
{
if (line[0] == '[')
{
yield return new IniSection(line[1..^1]);
}
if (line[0] == ';')
{
yield return new IniComment(line[1..]);
}
if (line.IndexOf('=') > 0)
{
string[] parameters = line.Split('=', 2);
yield return new IniParameter(parameters[0], parameters[1]);
}
}
continue;
}
}
}
}

View File

@@ -1,9 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading;
using System.IO;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Snap.Hutao.Core.IO;
@@ -18,27 +18,47 @@ public static class StorageFileExtensions
/// <typeparam name="T">内容的类型</typeparam>
/// <param name="file">文件</param>
/// <param name="options">序列化选项</param>
/// <param name="onException">错误时调用</param>
/// <returns>反序列化后的内容</returns>
public static async Task<T?> DeserializeJsonAsync<T>(this StorageFile file, JsonSerializerOptions options, Action<System.Exception>? onException = null)
/// <returns>操作是否成功,反序列化后的内容</returns>
public static async Task<ValueResult<bool, T?>> DeserializeFromJsonAsync<T>(this StorageFile file, JsonSerializerOptions options)
where T : class
{
T? t = null;
try
{
using (IRandomAccessStreamWithContentType fileSream = await file.OpenReadAsync())
using (FileStream stream = File.OpenRead(file.Path))
{
using (Stream stream = fileSream.AsStream())
{
t = await JsonSerializer.DeserializeAsync<T>(stream, options).ConfigureAwait(false);
}
T? t = await JsonSerializer.DeserializeAsync<T>(stream, options).ConfigureAwait(false);
return new(true, t);
}
}
catch (System.Exception ex)
{
onException?.Invoke(ex);
_ = ex;
return new(false, null);
}
}
return t;
/// <summary>
/// 将对象异步序列化入文件
/// </summary>
/// <typeparam name="T">对象的类型</typeparam>
/// <param name="file">文件</param>
/// <param name="obj">对象</param>
/// <param name="options">序列化选项</param>
/// <returns>操作是否成功</returns>
public static async Task<bool> SerializeToJsonAsync<T>(this StorageFile file, T obj, JsonSerializerOptions options)
{
try
{
using (FileStream stream = File.Create(file.Path))
{
await JsonSerializer.SerializeAsync(stream, obj, options).ConfigureAwait(false);
}
return true;
}
catch (System.Exception)
{
return false;
}
}
}

View File

@@ -24,4 +24,4 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
}
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 枚举 - 字符串数字 转换器
/// </summary>
/// <typeparam name="TEnum">枚举的类型</typeparam>
internal class EnumStringValueConverter<TEnum> : JsonConverter<TEnum>
where TEnum : struct, Enum
{
/// <inheritdoc/>
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.GetString() is string str)
{
return Enum.Parse<TEnum>(str);
}
throw Must.NeverHappen();
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("D"));
}
}

View File

@@ -21,4 +21,4 @@ internal class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEnumerabl
{
writer.WriteStringValue(string.Join(',', value));
}
}
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Reflection;
namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
@@ -18,7 +16,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
return false;
}
if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
if (typeToConvert.GetGenericTypeDefinition() != typeof(IDictionary<,>))
{
return false;
}
@@ -33,24 +31,19 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
Type valueType = type.GetGenericArguments()[1];
Type innerConverterType = typeof(StringEnumDictionaryConverterInner<,>).MakeGenericType(keyType, valueType);
JsonConverter converter = (JsonConverter)Activator.CreateInstance(innerConverterType, BindingFlags.Instance | BindingFlags.Public, null, new object[] { options }, null)!;
JsonConverter converter = (JsonConverter)Activator.CreateInstance(innerConverterType)!;
return converter;
}
private class StringEnumDictionaryConverterInner<TKey, TValue> : JsonConverter<IDictionary<TKey, TValue>>
where TKey : struct, Enum
{
private readonly JsonConverter<TValue>? valueConverter;
private readonly Type keyType;
private readonly Type valueType;
public StringEnumDictionaryConverterInner(JsonSerializerOptions options)
public StringEnumDictionaryConverterInner()
{
valueConverter = (JsonConverter<TValue>)options.GetConverter(typeof(TValue));
// Cache the key and value types.
keyType = typeof(TKey);
valueType = typeof(TValue);
}
public override IDictionary<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
@@ -77,22 +70,13 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
string? propertyName = reader.GetString();
if (!Enum.TryParse(propertyName, out TKey key))
if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) && !Enum.TryParse(propertyName, ignoreCase: true, out key))
{
throw new JsonException($"Unable to convert \"{propertyName}\" to Enum \"{keyType}\".");
}
// Get the value.
TValue value;
if (valueConverter != null)
{
reader.Read();
value = valueConverter.Read(ref reader, valueType, options)!;
}
else
{
value = JsonSerializer.Deserialize<TValue>(ref reader, options)!;
}
TValue value = JsonSerializer.Deserialize<TValue>(ref reader, options)!;
// Add to dictionary.
dictionary.Add(key, value);
@@ -111,15 +95,7 @@ public class StringEnumKeyDictionaryConverter : JsonConverterFactory
string? convertedName = options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName;
writer.WritePropertyName(convertedName);
if (valueConverter != null)
{
valueConverter.Write(writer, value, options);
}
else
{
JsonSerializer.Serialize(writer, value, options);
}
JsonSerializer.Serialize(writer, value, options);
}
writer.WriteEndObject();

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.LifeCycle;
using Windows.UI.StartScreen;
namespace Snap.Hutao.Core;
/// <summary>
/// 跳转列表帮助类
/// </summary>
public static class JumpListHelper
{
/// <summary>
/// 异步配置跳转列表
/// </summary>
/// <returns>任务</returns>
public static async Task ConfigAsync()
{
if (JumpList.IsSupported())
{
JumpList list = await JumpList.LoadCurrentAsync();
list.Items.Clear();
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, "启动游戏");
launchGameItem.GroupName = "快捷操作";
launchGameItem.Logo = new("ms-appx:///Resource/Icon/UI_GuideIcon_PlayMethod.png");
list.Items.Add(launchGameItem);
await list.SaveAsync();
}
}
}

View File

@@ -3,7 +3,6 @@
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Navigation;
@@ -14,6 +13,11 @@ namespace Snap.Hutao.Core.LifeCycle;
/// </summary>
internal static class Activation
{
/// <summary>
/// 启动游戏启动参数
/// </summary>
public const string LaunchGame = "LaunchGame";
private static readonly SemaphoreSlim ActivateSemaphore = new(1);
/// <summary>
@@ -45,16 +49,36 @@ internal static class Activation
private static async Task HandleActivationCoreAsync(AppActivationArguments args)
{
_ = Ioc.Default.GetRequiredService<MainWindow>();
string argument = string.Empty;
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
await infoBarService.WaitInitializationAsync().ConfigureAwait(false);
if (args.Kind == ExtendedActivationKind.Launch)
{
if (args.TryGetLaunchActivatedArgument(out string? arguments))
{
argument = arguments;
}
}
switch (argument)
{
case "":
{
_ = Ioc.Default.GetRequiredService<MainWindow>();
await Ioc.Default.GetRequiredService<IInfoBarService>().WaitInitializationAsync().ConfigureAwait(false);
break;
}
case LaunchGame:
{
break;
}
}
if (args.Kind == ExtendedActivationKind.Protocol)
{
if (args.TryGetProtocolActivatedUri(out Uri? uri))
{
infoBarService.Information(uri.ToString());
Ioc.Default.GetRequiredService<IInfoBarService>().Information(uri.ToString());
await HandleUrlActivationAsync(uri).ConfigureAwait(false);
}
}

View File

@@ -28,4 +28,22 @@ public static class AppActivationArgumentsExtensions
return false;
}
/// <summary>
/// 尝试获取启动的参数
/// </summary>
/// <param name="activatedEventArgs">应用程序激活参数</param>
/// <param name="arguments">参数</param>
/// <returns>是否存在参数</returns>
public static bool TryGetLaunchActivatedArgument(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
{
arguments = null;
if (activatedEventArgs.Data is ILaunchActivatedEventArgs launchArgs)
{
arguments = launchArgs.Arguments;
return true;
}
return false;
}
}

View File

@@ -88,6 +88,16 @@ internal static class EventIds
/// 成就
/// </summary>
public static readonly EventId Achievement = 100130;
/// <summary>
/// 祈愿统计生成
/// </summary>
public static readonly EventId GachaStatisticGeneration = 100140;
/// <summary>
/// 祈愿统计生成
/// </summary>
public static readonly EventId AvatarInfoGeneration = 100150;
#endregion
#region

View File

@@ -20,6 +20,8 @@ public sealed class LogEntryQueue : IDisposable
private readonly TaskCompletionSource writeDbCompletionSource = new();
private readonly LogDbContext logDbContext;
private bool disposed;
/// <summary>
/// 构造一个新的日志队列
/// </summary>
@@ -27,7 +29,7 @@ public sealed class LogEntryQueue : IDisposable
{
logDbContext = InitializeDbContext();
Task.Run(async () => await WritePendingLogsAsync(disposeTokenSource.Token)).SafeForget();
Task.Run(() => WritePendingLogsAsync(disposeTokenSource.Token)).SafeForget();
}
/// <summary>
@@ -43,6 +45,11 @@ public sealed class LogEntryQueue : IDisposable
[SuppressMessage("", "VSTHRD002")]
public void Dispose()
{
if (disposed)
{
return;
}
// notify the write task to complete.
disposeTokenSource.Cancel();
@@ -50,6 +57,7 @@ public sealed class LogEntryQueue : IDisposable
writeDbCompletionSource.Task.GetAwaiter().GetResult();
logDbContext.Dispose();
disposed = true;
}
private static LogDbContext InitializeDbContext()
@@ -62,9 +70,8 @@ public sealed class LogEntryQueue : IDisposable
logDbContext.Database.Migrate();
}
logDbContext.Logs.RemoveRange(logDbContext.Logs);
logDbContext.SaveChanges();
// only raw sql can pass
logDbContext.Database.ExecuteSqlRaw("DELETE FROM logs WHERE Exception IS NULL");
return logDbContext;
}

View File

@@ -41,4 +41,4 @@ public static class ProcessHelper
};
return Process.Start(processInfo);
}
}
}

View File

@@ -10,10 +10,6 @@ namespace Snap.Hutao.Core.Setting;
/// </summary>
internal static class LocalSetting
{
/// <summary>
/// 由于 <see cref="Windows.Foundation.Collections.IPropertySet"/> 没有 nullable context,
/// 在处理引用类型时需要格外小心
/// </summary>
private static readonly ApplicationDataContainer Container;
static LocalSetting()
@@ -21,6 +17,198 @@ internal static class LocalSetting
Container = ApplicationData.Current.LocalSettings;
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static byte Get(string key, byte defaultValue)
{
return Get<byte>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static short Get(string key, short defaultValue)
{
return Get<short>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static ushort Get(string key, ushort defaultValue)
{
return Get<ushort>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static int Get(string key, int defaultValue)
{
return Get<int>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static uint Get(string key, uint defaultValue)
{
return Get<uint>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static ulong Get(string key, ulong defaultValue)
{
return Get<ulong>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static float Get(string key, float defaultValue)
{
return Get<float>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static double Get(string key, double defaultValue)
{
return Get<double>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static bool Get(string key, bool defaultValue)
{
return Get<bool>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static char Get(string key, char defaultValue)
{
return Get<char>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static DateTimeOffset Get(string key, DateTimeOffset defaultValue)
{
return Get<DateTimeOffset>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static TimeSpan Get(string key, TimeSpan defaultValue)
{
return Get<TimeSpan>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static Guid Get(string key, Guid defaultValue)
{
return Get<Guid>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static Windows.Foundation.Point Get(string key, Windows.Foundation.Point defaultValue)
{
return Get<Windows.Foundation.Point>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static Windows.Foundation.Size Get(string key, Windows.Foundation.Size defaultValue)
{
return Get<Windows.Foundation.Size>(key, defaultValue);
}
/// <inheritdoc cref="Get{T}(string, T)"/>
public static Windows.Foundation.Rect Get(string key, Windows.Foundation.Rect defaultValue)
{
return Get<Windows.Foundation.Rect>(key, defaultValue);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, byte value)
{
Set<byte>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, short value)
{
Set<short>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, ushort value)
{
Set<ushort>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, int value)
{
Set<int>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, uint value)
{
Set<uint>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, ulong value)
{
Set<ulong>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, float value)
{
Set<float>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, double value)
{
Set<double>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, bool value)
{
Set<bool>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, char value)
{
Set<char>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, DateTimeOffset value)
{
Set<DateTimeOffset>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, TimeSpan value)
{
Set<TimeSpan>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, Guid value)
{
Set<Guid>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, Windows.Foundation.Point value)
{
Set<Windows.Foundation.Point>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, Windows.Foundation.Size value)
{
Set<Windows.Foundation.Size>(key, value);
}
/// <inheritdoc cref="Set{T}(string, T)"/>
public static void Set(string key, Windows.Foundation.Rect value)
{
Set<Windows.Foundation.Rect>(key, value);
}
/// <summary>
/// 获取设置项的值
/// </summary>
@@ -28,8 +216,8 @@ internal static class LocalSetting
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>获取的值</returns>
[return: MaybeNull]
public static T Get<T>(string key, [AllowNull] T defaultValue = default)
private static T Get<T>(string key, T defaultValue = default)
where T : struct
{
if (Container.Values.TryGetValue(key, out object? value))
{
@@ -49,9 +237,9 @@ internal static class LocalSetting
/// <typeparam name="T">设置项的类型</typeparam>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <returns>设置的值</returns>
public static object? Set<T>(string key, T value)
private static void Set<T>(string key, T value)
where T : struct
{
return Container.Values[key] = value;
Container.Values[key] = value;
}
}

View File

@@ -8,11 +8,6 @@ namespace Snap.Hutao.Core.Setting;
/// </summary>
internal static class SettingKeys
{
/// <summary>
/// 上次打开时App的版本
/// </summary>
public const string LastAppVersion = "LastAppVersion";
/// <summary>
/// 窗体左侧
/// </summary>

View File

@@ -28,4 +28,4 @@ internal class ConcurrentCancellationTokenSource<TItem>
return waitingItems.GetOrAdd(item, new CancellationTokenSource()).Token;
}
}
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Extension;
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 信号量扩展
@@ -15,7 +15,7 @@ public static class SemaphoreSlimExtensions
/// <returns>可释放的对象,用于释放信号量</returns>
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim)
{
await semaphoreSlim.WaitAsync();
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
return new SemaphoreSlimReleaser(semaphoreSlim);
}

View File

@@ -9,8 +9,9 @@ namespace Snap.Hutao.Core.Threading;
internal static class ThreadHelper
{
/// <summary>
/// 异步切换到主线程
/// 使用此静态方法以 异步切换到 主线程
/// </summary>
/// <remarks>使用 <see cref="Task.Yield"/> 异步切换到 后台线程</remarks>
/// <returns>等待体</returns>
public static DispatherQueueSwitchOperation SwitchToMainThreadAsync()
{

View File

@@ -12,8 +12,6 @@ namespace Snap.Hutao.Core.Threading;
/// <typeparam name="TResult">结果类型</typeparam>
/// <typeparam name="TValue">值类型</typeparam>
public readonly struct ValueResult<TResult, TValue> : IDeconstructable<TResult, TValue>
where TResult : notnull
where TValue : notnull
{
/// <summary>
/// 是否成功

View File

@@ -38,18 +38,6 @@ public static class Must
}
}
/// <summary>
/// 任务异常
/// </summary>
/// <param name="message">异常消息</param>
/// <returns>异常的任务</returns>
[SuppressMessage("", "VSTHRD200")]
public static Task Fault(string message)
{
InvalidOperationException exception = new(message);
return Task.FromException(exception);
}
/// <summary>
/// 任务异常
/// </summary>

View File

@@ -5,6 +5,8 @@ using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
using Snap.Hutao.Win32;
using Windows.Graphics;
using Windows.UI;
using Windows.Win32.Foundation;
@@ -16,7 +18,7 @@ namespace Snap.Hutao.Core.Windowing;
/// 窗口管理器
/// 主要包含了针对窗体的 P/Inoke 逻辑
/// </summary>
internal sealed class WindowManager : IDisposable
internal sealed class ExtendedWindow
{
private readonly HWND handle;
private readonly AppWindow appWindow;
@@ -24,7 +26,7 @@ internal sealed class WindowManager : IDisposable
private readonly Window window;
private readonly FrameworkElement titleBar;
private readonly ILogger<WindowManager> logger;
private readonly ILogger<ExtendedWindow> logger;
private readonly WindowSubclassManager subclassManager;
private readonly bool useLegacyDragBar;
@@ -34,15 +36,15 @@ internal sealed class WindowManager : IDisposable
/// </summary>
/// <param name="window">窗口</param>
/// <param name="titleBar">充当标题栏的元素</param>
public WindowManager(Window window, FrameworkElement titleBar)
private ExtendedWindow(Window window, FrameworkElement titleBar)
{
this.window = window;
this.titleBar = titleBar;
logger = Ioc.Default.GetRequiredService<ILogger<WindowManager>>();
logger = Ioc.Default.GetRequiredService<ILogger<ExtendedWindow>>();
handle = (HWND)WindowNative.GetWindowHandle(window);
Microsoft.UI.WindowId windowId = Win32Interop.GetWindowIdFromWindow(handle);
WindowId windowId = Win32Interop.GetWindowIdFromWindow(handle);
appWindow = AppWindow.GetFromWindowId(windowId);
useLegacyDragBar = !AppWindowTitleBar.IsCustomizationSupported();
@@ -51,11 +53,15 @@ internal sealed class WindowManager : IDisposable
InitializeWindow();
}
/// <inheritdoc/>
public void Dispose()
/// <summary>
/// 初始化
/// </summary>
/// <param name="window">窗口</param>
/// <param name="titleBar">标题栏</param>
/// <returns>实例</returns>
public static ExtendedWindow Initialize(Window window, FrameworkElement titleBar)
{
Persistence.Save(appWindow);
subclassManager?.Dispose();
return new(window, titleBar);
}
private static void UpdateTitleButtonColor(AppWindowTitleBar appTitleBar)
@@ -105,11 +111,19 @@ internal sealed class WindowManager : IDisposable
appWindow.Show(true);
bool micaApplied = new SystemBackdrop(window).TrySetBackdrop();
bool micaApplied = new SystemBackdrop(window).TryApply();
logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
bool subClassApplied = subclassManager.TrySetWindowSubclass();
logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager), subClassApplied ? "succeed" : "failed");
window.Closed += OnWindowClosed;
}
private void OnWindowClosed(object sender, WindowEventArgs args)
{
Persistence.Save(appWindow);
subclassManager?.Dispose();
}
private void ExtendsContentIntoTitleBar()
@@ -137,14 +151,8 @@ internal sealed class WindowManager : IDisposable
{
double scale = Persistence.GetScaleForWindow(handle);
List<RectInt32> dragRectsList = new();
// 48 is the navigation button leftInset
RectInt32 dragRect = new((int)(48 * scale), 0, (int)(titleBar.ActualWidth * scale), (int)(titleBar.ActualHeight * scale));
dragRectsList.Add(dragRect);
RectInt32[] dragRects = dragRectsList.ToArray();
appTitleBar.SetDragRectangles(dragRects);
RectInt32 dragRect = new RectInt32(48, 0, (int)titleBar.ActualWidth, (int)titleBar.ActualHeight).Scale(scale);
appTitleBar.SetDragRectangles(dragRect.Enumerate().ToArray());
}
}

View File

@@ -4,6 +4,7 @@
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Win32;
using System.Runtime.InteropServices;
using Windows.Graphics;
using Windows.Win32.Foundation;
@@ -25,11 +26,15 @@ internal static class Persistence
// Set first launch size.
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
SizeInt32 size = TransformSizeForWindow(new(1200, 741), hwnd);
RectInt32 rect = new(0, 0, size.Width, size.Height);
RectInt32 rect = StructMarshal.RectInt32(size);
// Make it centralized
TransformToCenterScreen(ref rect);
RectInt32 target = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
if (target.Width * target.Height < 848 * 524)
{
target = rect;
}
TransformToCenterScreen(ref target);
appWindow.MoveAndResize(target);
}
@@ -39,7 +44,7 @@ internal static class Persistence
/// <param name="appWindow">应用窗体</param>
public static void Save(AppWindow appWindow)
{
LocalSetting.Set(SettingKeys.WindowRect, (ulong)(CompactRect)appWindow.GetRect());
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
}
/// <summary>
@@ -119,7 +124,7 @@ internal static class Persistence
return new(rect.X, rect.Y, rect.Width, rect.Height);
}
public static explicit operator ulong(CompactRect rect)
public static implicit operator ulong(CompactRect rect)
{
return rect.Value;
}

View File

@@ -34,7 +34,7 @@ public class SystemBackdrop
/// 尝试设置背景
/// </summary>
/// <returns>是否设置成功</returns>
public bool TrySetBackdrop()
public bool TryApply()
{
if (!MicaController.IsSupported())
{
@@ -58,7 +58,7 @@ public class SystemBackdrop
backdropController = new()
{
// Mica Alt
Kind = MicaKind.BaseAlt
Kind = MicaKind.BaseAlt,
};
backdropController.AddSystemBackdropTarget(window.As<ICompositionSupportsSystemBackdrop>());
backdropController.SetSystemBackdropConfiguration(configuration);

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="BinaryReader"/> 扩展
/// </summary>
public static class BinaryReaderExtensions
public static class BinaryReaderExtension
{
/// <summary>
/// 判断是否处于流的结尾

View File

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="DateTimeOffset"/> 扩展
/// </summary>
public static class DateTimeOffsetExtensions
public static class DateTimeOffsetExtension
{
/// <summary>
/// Converts the current <see cref="DateTimeOffset"/> to a <see cref="DateTimeOffset"/> that represents the local time.

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Extension;
/// <summary>
/// 枚举拓展
/// </summary>
public static class EnumExtensions
public static class EnumExtension
{
/// <summary>
/// 获取枚举的描述

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Extension;
@@ -8,7 +9,7 @@ namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="IEnumerable{T}"/> 扩展
/// </summary>
public static partial class EnumerableExtensions
public static partial class EnumerableExtension
{
/// <inheritdoc cref="Enumerable.Average(IEnumerable{int})"/>
public static double AverageNoThrow(this List<int> source)
@@ -107,7 +108,7 @@ public static partial class EnumerableExtensions
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>结果值</returns>
public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue = default!)
public static TValue? GetValueOrDefault2<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue? defaultValue = default)
where TKey : notnull
{
if (dictionary.TryGetValue(key, out TValue? value))
@@ -118,6 +119,53 @@ public static partial class EnumerableExtensions
return defaultValue;
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <param name="value">增加的值</param>
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key, int value)
where TKey : notnull
{
// ref the value, so that we can manipulate it outside the dict.
ref int current = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
current += value;
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <returns>是否存在键值</returns>
public static bool TryIncrease<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
ref int value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
if (!Unsafe.IsNullRef(ref value))
{
++value;
return true;
}
return false;
}
/// <summary>
/// 移除表中首个满足条件的项
/// </summary>
@@ -153,6 +201,20 @@ public static partial class EnumerableExtensions
return dictionary;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey, TElement}(IEnumerable{TSource}, Func{TSource, TKey}, Func{TSource, TElement})"/>
public static Dictionary<TKey, TValue> ToDictionaryOverride<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> valueSelector)
where TKey : notnull
{
Dictionary<TKey, TValue> dictionary = new();
foreach (TSource value in source)
{
dictionary[keySelector(value)] = valueSelector(value);
}
return dictionary;
}
/// <summary>
/// 表示一个对 <see cref="TItem"/> 类型的计数器
/// </summary>

View File

@@ -6,8 +6,19 @@ namespace Snap.Hutao.Extension;
/// <summary>
/// 数高性能扩展
/// </summary>
public static class NumberExtensions
public static class NumberExtension
{
/// <summary>
/// 获取从右向左某位上的数字
/// </summary>
/// <param name="x">源</param>
/// <param name="place">位</param>
/// <returns>数字</returns>
public static int AtPlace(this int x, int place)
{
return (int)(x / Math.Pow(10, place - 1)) % 10;
}
/// <summary>
/// 计算给定整数的位数
/// </summary>

View File

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Extension;
/// <summary>
/// 对象扩展
/// </summary>
public static class ObjectExtensions
public static class ObjectExtension
{
/// <summary>
/// <see langword="as"/> 的链式调用扩展

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Extension;
/// <summary>
/// 包版本扩展
/// </summary>
public static class PackageVersionExtensions
public static class PackageVersionExtension
{
/// <summary>
/// 将包版本转换为版本

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Service.AppCenter;
namespace Snap.Hutao.Factory;
@@ -11,15 +12,18 @@ namespace Snap.Hutao.Factory;
[Injection(InjectAs.Transient, typeof(IAsyncRelayCommandFactory))]
internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
{
private readonly ILogger logger;
private readonly ILogger<AsyncRelayCommandFactory> logger;
private readonly AppCenter appCenter;
/// <summary>
/// 构造一个新的异步命令工厂
/// </summary>
/// <param name="logger">日志器</param>
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger)
/// <param name="appCenter">App Center</param>
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger, AppCenter appCenter)
{
this.logger = logger;
this.appCenter = appCenter;
}
/// <inheritdoc/>
@@ -93,7 +97,8 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
if (asyncRelayCommand.ExecutionTask?.Exception is AggregateException exception)
{
Exception baseException = exception.GetBaseException();
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(asyncRelayCommand));
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(AsyncRelayCommand));
appCenter.TrackError(exception);
}
}
}

View File

@@ -0,0 +1,12 @@
<Window
x:Class="Snap.Hutao.LaunchGameWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Snap.Hutao"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
namespace Snap.Hutao;
/// <summary>
/// 启动游戏窗口
/// </summary>
public sealed partial class LaunchGameWindow : Window
{
/// <summary>
/// 构造一个新的启动游戏窗口
/// </summary>
public LaunchGameWindow()
{
InitializeComponent();
}
}

View File

@@ -1,18 +1,18 @@
<Window
x:Class="Snap.Hutao.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:view="using:Snap.Hutao.View"
xmlns:shv="using:Snap.Hutao.View"
mc:Ignorable="d">
<Grid>
<view:TitleView
<shv:TitleView
Margin="48,0,0,0"
Height="44"
x:Name="TitleBarView"/>
<view:MainView/>
<shv:MainView/>
</Grid>
</Window>

View File

@@ -13,21 +13,12 @@ namespace Snap.Hutao;
[SuppressMessage("", "CA1001")]
public sealed partial class MainWindow : Window
{
private readonly WindowManager windowManager;
/// <summary>
/// 构造一个新的主窗体
/// </summary>
public MainWindow()
{
InitializeComponent();
Closed += MainWindowClosed;
windowManager = new WindowManager(this, TitleBarView.DragArea);
}
private void MainWindowClosed(object sender, WindowEventArgs args)
{
// Must dispose it before window is completely closed
windowManager?.Dispose();
ExtendedWindow.Initialize(this, TitleBarView.DragArea);
}
}

View File

@@ -8,16 +8,7 @@ namespace Snap.Hutao.Message;
/// <summary>
/// 成就存档切换消息
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
internal class AchievementArchiveChangedMessage : ValueChangedMessage<AchievementArchive>
{
/// <summary>
/// 构造一个新的用户切换消息
/// </summary>
/// <param name="oldArchive">老用户</param>
/// <param name="newArchive">新用户</param>
public AchievementArchiveChangedMessage(AchievementArchive? oldArchive, AchievementArchive? newArchive)
: base(oldArchive, newArchive)
{
}
}

View File

@@ -8,16 +8,7 @@ namespace Snap.Hutao.Message;
/// <summary>
/// 祈愿记录存档切换消息
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
internal class GachaArchiveChangedMessage : ValueChangedMessage<GachaArchive>
{
/// <summary>
/// 构造一个新的用户切换消息
/// </summary>
/// <param name="oldArchive">老用户</param>
/// <param name="newArchive">新用户</param>
public GachaArchiveChangedMessage(GachaArchive? oldArchive, GachaArchive? newArchive)
: base(oldArchive, newArchive)
{
}
}

View File

@@ -8,15 +8,7 @@ namespace Snap.Hutao.Message;
/// <summary>
/// 用户切换消息
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
internal class UserChangedMessage : ValueChangedMessage<User>
{
/// <summary>
/// 构造一个新的用户切换消息
/// </summary>
/// <param name="oldUser">老用户</param>
/// <param name="newUser">新用户</param>
public UserChangedMessage(User? oldUser, User? newUser)
: base(oldUser, newUser)
{
}
}

View File

@@ -7,9 +7,17 @@ namespace Snap.Hutao.Message;
/// 值变化消息
/// </summary>
/// <typeparam name="TValue">值的类型</typeparam>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
internal abstract class ValueChangedMessage<TValue>
where TValue : class
{
/// <summary>
/// 动态访问
/// </summary>
public ValueChangedMessage()
{
}
/// <summary>
/// 构造一个新的值变化消息
/// </summary>
@@ -24,10 +32,10 @@ internal abstract class ValueChangedMessage<TValue>
/// <summary>
/// 旧的值
/// </summary>
public TValue? OldValue { get; private set; }
public TValue? OldValue { get; set; }
/// <summary>
/// 新的值
/// </summary>
public TValue? NewValue { get; private set; }
public TValue? NewValue { get; set; }
}

View File

@@ -0,0 +1,190 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Context.Database;
#nullable disable
namespace Snap.Hutao.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20220924135810_AddAvatarInfo")]
partial class AddAvatarInfo
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("Current")
.HasColumnType("INTEGER");
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("gacha_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("GachaType")
.HasColumnType("INTEGER");
b.Property<long>("Id")
.HasColumnType("INTEGER");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("QueryType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Cookie")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("users");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,32 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
public partial class AddAvatarInfo : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "avatar_infos",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
Uid = table.Column<string>(type: "TEXT", nullable: false),
Info = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_avatar_infos", x => x.InnerId);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "avatar_infos");
}
}
}

View File

@@ -63,6 +63,25 @@ namespace Snap.Hutao.Migrations
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")

View File

@@ -1,12 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
namespace Snap.Hutao.Model.Binding;
/// <summary>
/// 用于视图绑定的成就
/// </summary>
public class Achievement : Observable
public class Achievement : ObservableObject
{
/// <summary>
/// 满进度占位符
@@ -28,7 +30,7 @@ public class Achievement : Observable
this.inner = inner;
this.entity = entity;
// Property should only be set when is user checking.
// Property should only be set when it's user checking.
isChecked = (int)entity.Status >= 2;
}
@@ -50,7 +52,7 @@ public class Achievement : Observable
get => isChecked;
set
{
Set(ref isChecked, value);
SetProperty(ref isChecked, value);
// Only update state when checked
if (value)
@@ -67,6 +69,6 @@ public class Achievement : Observable
/// </summary>
public string Time
{
get => entity.Time.ToString("yyyy-MM-dd HH:mm:ss");
get => entity.Time.ToString("yyyy.MM.dd HH:mm:ss");
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 词条评分
/// </summary>
public struct AffixScore
{
/// <summary>
/// 构造一个新的圣遗物评分
/// </summary>
/// <param name="score">评分</param>
/// <param name="weight">最大值</param>
public AffixScore(double score, double weight)
{
Score = score;
Weight = weight;
}
/// <summary>
/// 评分
/// </summary>
public double Score { get; }
/// <summary>
/// 权重
/// </summary>
public double Weight { get; }
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 角色信息
/// </summary>
public class Avatar
{
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
public Uri Icon { get; set; } = default!;
/// <summary>
/// 侧面图标
/// </summary>
public Uri SideIcon { get; set; } = default!;
/// <summary>
/// 星级
/// </summary>
public ItemQuality Quality { get; set; }
/// <summary>
/// 等级
/// </summary>
public string Level { get; set; } = default!;
/// <summary>
/// 好感度等级
/// </summary>
public int FetterLevel { get; set; }
/// <summary>
/// 武器
/// </summary>
public Weapon Weapon { get; set; } = default!;
/// <summary>
/// 圣遗物列表
/// </summary>
public List<Reliquary> Reliquaries { get; set; } = default!;
/// <summary>
/// 命之座列表
/// </summary>
public List<Constellation> Constellations { get; set; } = default!;
/// <summary>
/// 技能列表
/// </summary>
public List<Skill> Skills { get; set; } = default!;
/// <summary>
/// 属性
/// </summary>
public List<Pair2<string, string, string?>> Properties { get; set; } = default!;
/// <summary>
/// 评分
/// </summary>
public string Score { get; set; } = default!;
/// <summary>
/// 双爆评分
/// </summary>
public string CritScore { get; set; } = default!;
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 命座信息
/// </summary>
public class Constellation : NameIconDescription
{
/// <summary>
/// 是否激活
/// </summary>
public bool IsActiviated { get; set; }
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 装备基类
/// </summary>
public class EquipBase : NameIconDescription
{
/// <summary>
/// 等级
/// </summary>
public string Level { get; set; } = default!;
/// <summary>
/// 品质
/// </summary>
public ItemQuality Quality { get; set; }
/// <summary>
/// 主属性
/// </summary>
public Pair<string, string> MainProperty { get; set; } = default!;
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 名称与描述抽象
/// </summary>
public abstract class NameIconDescription
{
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
public Uri Icon { get; set; } = default!;
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; } = default!;
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 玩家信息
/// </summary>
public class Player
{
/// <summary>
/// 昵称
/// </summary>
public string Nickname { get; set; } = default!;
/// <summary>
/// 等级
/// </summary>
public int Level { get; set; }
/// <summary>
/// 签名
/// </summary>
public string Signature { get; set; } = default!;
/// <summary>
/// 完成成就数
/// </summary>
public int FinishAchievementNumber { get; set; }
/// <summary>
/// 深渊层间
/// </summary>
public string SipralAbyssFloorLevel { get; set; } = default!;
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 圣遗物
/// </summary>
public class Reliquary : EquipBase
{
/// <summary>
/// 副属性列表
/// </summary>
[Obsolete]
public List<ReliquarySubProperty> SubProperties { get; set; } = default!;
/// <summary>
/// 初始词条
/// </summary>
public List<ReliquarySubProperty> PrimarySubProperties { get; set; } = default!;
/// <summary>
/// 强化词条
/// </summary>
public List<ReliquarySubProperty> SecondarySubProperties { get; set; } = default!;
/// <summary>
/// 评分
/// </summary>
public double Score { get; set; }
/// <summary>
/// 格式化评分
/// </summary>
public string ScoreFormatted { get => $"{Score:F2}"; }
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 圣遗物副词条
/// </summary>
public class ReliquarySubProperty
{
/// <summary>
/// 构造副属性
/// </summary>
/// <param name="name">名称</param>
/// <param name="value">值</param>
/// <param name="score">评分</param>
public ReliquarySubProperty(string name, string value, double score)
{
Name = name;
Value = value;
Score = score;
// only 0.2 | 0.4 | 0.6 | 0.8 | 1.0
Opacity = score switch
{
< 25 => 0.25,
< 50 => 0.5,
< 75 => 0.75,
_ => 1,
};
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; }
/// <summary>
/// 值
/// </summary>
public string Value { get; }
/// <summary>
/// 评分
/// </summary>
public double Score { get; }
/// <summary>
/// 透明度
/// </summary>
public double Opacity { get; }
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata;
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 天赋
/// </summary>
public class Skill : NameIconDescription
{
/// <summary>
/// 技能属性
/// </summary>
public LevelParam<string, ParameterInfo> Info { get; set; } = default!;
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 玩家与角色列表的包装器
/// </summary>
public class Summary
{
/// <summary>
/// 玩家信息
/// </summary>
public Player Player { get; set; } = default!;
/// <summary>
/// 角色列表
/// </summary>
public List<Avatar> Avatars { get; set; } = default!;
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 武器
/// </summary>
public class Weapon : EquipBase
{
/// <summary>
/// 副属性
/// </summary>
public Pair<string, string> SubProperty { get; set; } = default!;
/// <summary>
/// 精炼属性
/// </summary>
public string AffixLevel { get; set; } = default!;
/// <summary>
/// 精炼名称
/// </summary>
public string AffixName { get; set; } = default!;
/// <summary>
/// 精炼被动
/// </summary>
public string AffixDescription { get; set; } = default!;
}

View File

@@ -8,11 +8,6 @@ namespace Snap.Hutao.Model.Binding.Gacha;
/// </summary>
public class GachaStatistics
{
/// <summary>
/// 默认的空祈愿统计
/// </summary>
public static readonly GachaStatistics Default = new();
/// <summary>
/// 角色活动
/// </summary>
@@ -29,7 +24,7 @@ public class GachaStatistics
public TypedWishSummary PermanentWish { get; set; } = default!;
/// <summary>
/// 历史
/// 历史卡池
/// </summary>
public List<HistoryWish> HistoryWishes { get; set; } = default!;

View File

@@ -10,6 +10,16 @@ namespace Snap.Hutao.Model.Binding.Gacha;
/// </summary>
public class HistoryWish : WishBase
{
/// <summary>
/// 版本
/// </summary>
public string Version { get; set; } = default!;
/// <summary>
/// 卡池图片
/// </summary>
public Uri BannerImage { get; set; } = default!;
/// <summary>
/// 五星Up
/// </summary>

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Converter;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 角色
/// </summary>
public class ComplexAvatar
{
/// <summary>
/// 构造一个胡桃数据库角色
/// </summary>
/// <param name="avatar">元数据角色</param>
/// <param name="rate">率</param>
public ComplexAvatar(Avatar avatar, double rate)
{
Name = avatar.Name;
Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
Quality = avatar.Quality;
Rate = $"{rate:P3}";
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
public Uri Icon { get; set; } = default!;
/// <summary>
/// 星级
/// </summary>
public ItemQuality Quality { get; set; }
/// <summary>
/// 比率
/// </summary>
public string Rate { get; set; } = default!;
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 角色搭配
/// </summary>
public class ComplexAvatarCollocation : ComplexAvatar
{
/// <summary>
/// 构造一个新的角色搭配
/// </summary>
/// <param name="avatar">角色</param>
/// <param name="rate">比率</param>
public ComplexAvatarCollocation(Avatar avatar)
: base(avatar, 0)
{
}
/// <summary>
/// 角色Id
/// </summary>
public AvatarId AvatarId { get; set; }
/// <summary>
/// 角色
/// </summary>
public List<ComplexAvatar> Avatars { get; set; } = default!;
/// <summary>
/// 武器
/// </summary>
public List<ComplexWeapon> Weapons { get; set; } = default!;
/// <summary>
/// 圣遗物套装
/// </summary>
public List<ComplexReliquarySet> ReliquarySets { get; set; } = default!;
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Avatar;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 角色命座信息
/// </summary>
internal class ComplexAvatarConstellationInfo : ComplexAvatar
{
/// <summary>
/// 构造一个新的角色命座信息
/// </summary>
/// <param name="avatar">角色</param>
/// <param name="rate">持有率</param>
/// <param name="rates">命座比率</param>
public ComplexAvatarConstellationInfo(Avatar avatar, double rate, IEnumerable<double> rates)
: base(avatar, rate)
{
Rates = rates.Select(r => $"{r:P3}").ToList();
}
/// <summary>
/// 命座比率
/// </summary>
public List<string> Rates { get; set; }
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 角色榜
/// </summary>
internal class ComplexAvatarRank
{
/// <summary>
/// 层数
/// </summary>
public string Floor { get; set; } = default!;
/// <summary>
/// 排行信息
/// </summary>
public List<ComplexAvatar> Avatars { get; set; } = default!;
}

View File

@@ -0,0 +1,66 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Web.Hutao.Model;
using System.Text;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 圣遗物套装
/// </summary>
public class ComplexReliquarySet
{
/// <summary>
/// 构造一个新的胡桃数据库圣遗物套装
/// </summary>
/// <param name="reliquarySetRate">圣遗物套装率</param>
/// <param name="idReliquarySetMap">圣遗物套装映射</param>
public ComplexReliquarySet(ItemRate<ReliquarySets, double> reliquarySetRate, Dictionary<int, Metadata.Reliquary.ReliquarySet> idReliquarySetMap)
{
ReliquarySets sets = reliquarySetRate.Item;
if (sets.Count >= 1)
{
StringBuilder setStringBuilder = new();
List<Uri> icons = new();
foreach (ReliquarySet set in sets)
{
Metadata.Reliquary.ReliquarySet metaSet = idReliquarySetMap[set.EquipAffixId / 10];
if (setStringBuilder.Length != 0)
{
setStringBuilder.Append(Environment.NewLine);
}
setStringBuilder.Append(set.Count).Append('×').Append(metaSet.Name);
icons.Add(RelicIconConverter.IconNameToUri(metaSet.Icon));
}
Name = setStringBuilder.ToString();
Icons = icons;
}
else
{
Name = "无圣遗物";
}
Rate = $"{reliquarySetRate.Rate:P3}";
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
public List<Uri> Icons { get; set; } = default!;
/// <summary>
/// 比率
/// </summary>
public string Rate { get; set; } = default!;
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 队伍排行
/// </summary>
internal class ComplexTeamRank
{
/// <summary>
/// 构造一个新的队伍排行
/// </summary>
/// <param name="teamRank">队伍排行</param>
/// <param name="idAvatarMap">映射</param>
public ComplexTeamRank(TeamAppearance teamRank, Dictionary<int, Avatar> idAvatarMap)
{
Floor = $"第 {teamRank.Floor} 层";
Up = teamRank.Up.Select(teamRate => new Team(teamRate, idAvatarMap)).ToList();
Down = teamRank.Down.Select(teamRate => new Team(teamRate, idAvatarMap)).ToList();
}
/// <summary>
/// 层数
/// </summary>
public string Floor { get; set; } = default!;
/// <summary>
/// 上半阵容
/// </summary>
public List<Team> Up { get; set; } = default!;
/// <summary>
/// 下半阵容
/// </summary>
public List<Team> Down { get; set; } = default!;
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Weapon;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 胡桃数据库武器
/// </summary>
public class ComplexWeapon
{
/// <summary>
/// 构造一个胡桃数据库武器
/// </summary>
/// <param name="weapon">元数据武器</param>
/// <param name="rate">率</param>
public ComplexWeapon(Weapon weapon, double rate)
{
Name = weapon.Name;
Icon = EquipIconConverter.IconNameToUri(weapon.Icon);
Quality = weapon.Quality;
Rate = $"{rate:P3}";
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
public Uri Icon { get; set; } = default!;
/// <summary>
/// 星级
/// </summary>
public ItemQuality Quality { get; set; }
/// <summary>
/// 比率
/// </summary>
public string Rate { get; set; } = default!;
}

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