Compare commits

...

21 Commits

Author SHA1 Message Date
DismissedLight
9205d51cd5 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2022-11-11 18:55:08 +08:00
DismissedLight
a66a0b8a23 reduce using statements 2022-11-11 18:55:01 +08:00
Masterain
17c53dce4c Optimize Issue Template 2022-11-11 01:33:22 -08:00
Masterain
5049aa9cb6 Create artifact-rating-rules.yml
Inspired by #198
2022-11-11 01:32:47 -08:00
DismissedLight
aac1e62fd2 was 1.2.0 2022-11-11 17:20:36 +08:00
DismissedLight
5c1f861956 dailynote notification logic 2022-11-10 23:36:00 +08:00
DismissedLight
9667917559 file nesting 2022-11-09 12:08:59 +08:00
DismissedLight
9a3183e917 add launcher resource 2022-11-06 18:25:41 +08:00
DismissedLight
77db918178 update bug report template 2022-11-05 21:35:57 +08:00
DismissedLight
77158fc708 add installer 2022-11-05 20:45:38 +08:00
DismissedLight
f2d63e69ea bug fixes 2022-11-04 17:45:17 +08:00
DismissedLight
9e344f56e0 LaunchGame 2022-11-03 18:19:36 +08:00
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
282 changed files with 9809 additions and 1687 deletions

View File

@@ -0,0 +1,65 @@
name: Artifact Rating Rules
description: 圣遗物评分细则建议
title: "[Artifact Rating] 请在这里填写角色名称"
labels: area-AvatarInfo
assignees: Lightczx
body:
- type: markdown
attributes:
value: |
请按下方的要求填写完整的问题表单
- type: textarea
id: your-suggested-rule
attributes:
label: 评分细则
description: |
请修改下方表格中的**角色名称**和**各属性权重**,并在表格后添加合适的说明
你可以点击预览按钮preview来查看表格最终会显示出的内容
value: |
|项目|评分权重(0-100)|
|-----|-----|
|角色名称| 旅行者 |
|生命值| 10 |
|攻击力| 10 |
|防御力| 10 |
|暴击率| 10 |
|暴击伤害| 10 |
|元素精通| 10 |
|充能效率| 10 |
|治疗加成| 10 |
|元素伤害| 10 |
validations:
required: true
- type: dropdown
id: no-duplicated-dropdown
attributes:
label: 我确认当前没有其它的该角色的圣遗物评分细则建议
description: 如果有,你应该在已有的工单内回复以提出你的建议
options:
-
-
validations:
required: true
- type: dropdown
id: title-filled-dropdown
attributes:
label: 我确认已设置合适的标题
options:
-
-
validations:
required: true
- type: dropdown
id: all-filled-dropdown
attributes:
label: 我确认已完整填写表格
options:
-
-
validations:
required: true

View File

@@ -48,24 +48,24 @@ body:
- type: textarea
id: logs
attributes:
label: 相关的崩溃日志 位于 `%HOMEPATH%/Documents/Hutao/Log.db`
label: 相关的崩溃日志
description: |
在资源管理器中直接输入`%HOMEPATH%/Documents/Hutao`即可进入文件夹
在资源管理器中直接输入`%userprofile%/Documents/Hutao`即可进入文件夹
如果应用程序崩溃了,请将`log.db` 文件上传,文件包含了敏感信息,谨慎上传
如果这个表单是关于导入祈愿记录的问题,请包含你导入的`Json`文件
**务必不要上传`user.db`文件,该文件包含你的帐号敏感信息**
render: shell
- type: checkboxes
- type: dropdown
id: confirm-issue
attributes:
label: 我确认已在表单中附上了充足的补充说明以帮助开发人员确定问题
description: 补充说明包括但不限于:日志文件、抛出的错误信息、截图和录屏
options:
- label:
required: true
-
validations:
required: true
- type: checkboxes
- type: dropdown
id: confirm-no-duplicated-issue
attributes:
label: 我确认没有他人提出相同或类似的问题
@@ -75,15 +75,17 @@ body:
你应该在原始 Issue 中通过回复添加有助于解决问题的信息,而不是创建重复的问题;
**没有帮助的重复问题可能会被直接关闭**
options:
- label:
required: true
-
validations:
required: true
- type: checkboxes
- type: dropdown
id: confirm-docs
attributes:
label: 我确认该问题没有在文档中解释
description: Snap Hutao 官方文档:[https://hut.ao](https://hut.ao)
options:
- label:
required: true
-
validations:
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/

4
.gitignore vendored
View File

@@ -11,6 +11,10 @@ src/Snap.Hutao/Snap.Hutao/bin/
src/Snap.Hutao/Snap.Hutao/obj/
src/Snap.Hutao/Snap.Hutao/Snap.Hutao_TemporaryKey.pfx
src/Snap.Hutao/Snap.Hutao.Installer/bin/
src/Snap.Hutao/Snap.Hutao.Installer/obj/
src/Snap.Hutao/Snap.Hutao.Installer/Properties/PublishProfiles/FolderProfile.pubxml.user
src/Snap.Hutao/Snap.Hutao.SourceGeneration/bin/
src/Snap.Hutao/Snap.Hutao.SourceGeneration/obj/

View File

@@ -36,5 +36,4 @@
* [microsoft/vs-threading](https://github.com/microsoft/vs-threading)
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
* [MiniExcel/MiniExcel](https://github.com/MiniExcel/MiniExcel)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0-windows10.0.17763.0</TargetFrameworks>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<TargetFrameworks>net7.0-windows10.0.18362.0</TargetFrameworks>
<TargetPlatformMinVersion>10.0.18362.0</TargetPlatformMinVersion>
<RootNamespace>SettingsUI</RootNamespace>
<Platforms>x64</Platforms>
<RuntimeIdentifiers>win10-x64</RuntimeIdentifiers>
@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.4" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221109.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,73 @@
using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace Snap.Hutao.Installer;
internal class Program
{
private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Appx";
private const string ValueName = "AllowDevelopmentWithoutDevLicense";
public static async Task Main(string[] args)
{
_ = args;
string ps1File = Path.Combine(AppContext.BaseDirectory, "Install.ps1");
if (!File.Exists(ps1File))
{
Console.WriteLine("未检测到 Install.ps1 文件");
Console.WriteLine("请勿移动该安装程序,按下任意键退出...");
Console.ReadKey();
return;
}
try
{
//以管理策略打开开发者模式
Registry.SetValue(AppxKey, ValueName, 1, RegistryValueKind.DWord);
}
catch (Exception)
{
Console.WriteLine("开发者模式未开启,请手动开启,参阅下方链接");
Console.WriteLine("https://learn.microsoft.com/zh-CN/windows/apps/get-started/developer-mode-features-and-debugging");
}
await InstallAsync(ps1File).ConfigureAwait(false);
Console.WriteLine();
Console.WriteLine("官方文档与使用教程");
Console.WriteLine("https://hut.ao");
Console.WriteLine();
Console.WriteLine("在开始菜单中启动 Snap.Hutao ,按下任意键退出...");
Console.ReadKey();
}
private static async Task InstallAsync(string ps1File)
{
Console.WriteLine("请注意 PowerShell 中的提示");
Process ps = new()
{
StartInfo = new ProcessStartInfo()
{
FileName = "powershell.exe",
Arguments = $"-ExecutionPolicy Unrestricted \"{ps1File}\"",
UseShellExecute = true,
}
};
try
{
ps.Start();
await ps.WaitForExitAsync();
Console.WriteLine("安装脚本运行完成");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishTrimmed>true</PublishTrimmed>
<ApplicationManifest>app.manifest</ApplicationManifest>
<DebugType>embedded</DebugType>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC 清单选项
如果想要更改 Windows 用户帐户控制级别,请使用
以下节点之一替换 requestedExecutionLevel 节点。
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
元素。
-->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
Windows 版本的列表。取消评论适当的元素,
Windows 将自动选择最兼容的环境。 -->
<!-- Windows Vista -->
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!-- Windows 7 -->
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
<!-- Windows 8 -->
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
<!-- Windows 8.1 -->
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI无需
选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
<!--
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
-->
<!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
<!--
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
-->
</assembly>

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)
@@ -51,7 +52,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
internal static partial class ServiceCollectionExtensions
internal static partial class ServiceCollectionExtension
{{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{toolName}"",""1.0.0.0"")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
@@ -64,7 +65,7 @@ internal static partial class ServiceCollectionExtensions
}
}");
context.AddSource("ServiceCollectionExtensions.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
context.AddSource("ServiceCollectionExtension.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
}
private static void FillWithInjectionServices(InjectionSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
@@ -97,8 +98,11 @@ internal static partial class ServiceCollectionExtensions
case InjectAsTransientName:
lineBuilder.Append(@" services.AddTransient(");
break;
case InjectAsScopedName:
lineBuilder.Append(@" services.AddScoped(");
break;
default:
throw new InvalidOperationException($"非法的InjectAs值: [{injectAsName}]");
throw new InvalidOperationException($"非法的 InjectAs 值: [{injectAsName}]");
}
if (arguments.Length == 2)

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

@@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SettingsUI", "SettingsUI\Se
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Installer", "Snap.Hutao.Installer\Snap.Hutao.Installer.csproj", "{CEC01691-F65E-4874-9AE2-F571369A7631}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -82,6 +84,22 @@ Global
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.ActiveCfg = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.Build.0 = Debug|x64
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x86.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x86.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1,31 @@
{
"help": "https://go.microsoft.com/fwlink/?linkid=866610",
"dependentFileProviders": {
"add": {
"extensionToExtension": {
"add": {
".json": [ ".txt" ]
}
},
"pathSegment": {
"add": {
".*": [ ".cs" ]
}
},
"fileSuffixToExtension": {
"add": {
"DesignTimeFactory.cs": [".cs"]
}
},
"fileToFile": {
"add": {
"app.manifest": [ "App.xaml.cs" ],
"Package.appxmanifest": [ "App.xaml.cs" ],
"GlobalUsing.cs": [ "Program.cs" ],
".filenesting.json": [ "Program.cs" ],
".editorconfig": [ "Program.cs" ]
}
}
}
}
}

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,54 @@
<!--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>
<Thickness x:Key="PivotHeaderItemMargin">16,0,0,0</Thickness>
<Thickness x:Key="PivotItemMargin">0</Thickness>
<!--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"/>
<!--Styles-->
<Style
x:Key="LargeGridViewItemStyle"
TargetType="GridViewItem"
BasedOn="{StaticResource DefaultGridViewItemStyle}">
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
<!--ItemsPanelTemplate-->
<ItemsPanelTemplate x:Key="ItemsStackPanelTemplate">
<ItemsStackPanel/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="HorizontalStackPanelTemplate">
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,15 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Notifications;
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.Metadata;
using System.Diagnostics;
using Windows.Storage;
@@ -27,6 +25,7 @@ public partial class App : Application
/// Initializes the singleton application object.
/// </summary>
/// <param name="logger">日志器</param>
/// <param name="appCenter">App Center</param>
public App(ILogger<App> logger)
{
// load app resource
@@ -48,15 +47,12 @@ public partial class App : Application
// manually invoke
Activation.Activate(firstInstance, activatedEventArgs);
firstInstance.Activated += Activation.Activate;
ToastNotificationManagerCompat.OnActivated += Activation.NotificationActivate;
logger.LogInformation(EventIds.CommonLog, "Snap Hutao : {version}", CoreEnvironment.Version);
logger.LogInformation(EventIds.CommonLog, "Snap Hutao | {name} : {version}", CoreEnvironment.FamilyName, CoreEnvironment.Version);
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.TemporaryFolder.Path);
Ioc.Default
.GetRequiredService<IMetadataService>()
.ImplictAs<IMetadataInitializer>()?
.InitializeInternalAsync()
.SafeForget(logger);
JumpListHelper.ConfigAsync().SafeForget(logger);
}
else
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -56,6 +56,16 @@ public class AppDbContext : DbContext
/// </summary>
public DbSet<AvatarInfo> AvatarInfos { get; set; } = default!;
/// <summary>
/// 游戏内账号
/// </summary>
public DbSet<GameAccount> GameAccounts { get; set; } = default!;
/// <summary>
/// 实时便笺
/// </summary>
public DbSet<DailyNoteEntry> DailyNotes { get; set; } = default!;
/// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>
@@ -71,6 +81,7 @@ public class AppDbContext : DbContext
{
modelBuilder
.ApplyConfiguration(new AvatarInfoConfiguration())
.ApplyConfiguration(new UserConfiguration());
.ApplyConfiguration(new UserConfiguration())
.ApplyConfiguration(new DailyNoteEntryConfiguration());
}
}

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

@@ -3,7 +3,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.Threading;
namespace Snap.Hutao.Control.Extension;
@@ -37,7 +36,7 @@ internal static class ContentDialogExtensions
return new ContentDialogHider(contentDialog);
}
private struct ContentDialogHider : IAsyncDisposable
private readonly struct ContentDialogHider : IAsyncDisposable
{
private readonly ContentDialog contentDialog;

View File

@@ -8,7 +8,6 @@ using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using System.Net.Http;
@@ -67,7 +66,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
/// <returns>加载的图像表面</returns>
protected virtual async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
{
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token).ConfigureAwait(true))
{
return LoadedImageSurface.StartLoadFromStream(imageStream);
}
@@ -119,7 +118,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
private async Task ApplyImageInternalAsync(Uri? uri, CancellationToken token)
{
await HideAsync(token);
await HideAsync(token).ConfigureAwait(true);
LoadedImageSurface? imageSurface = null;
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
@@ -132,15 +131,15 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
}
else
{
StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri);
StorageFile storageFile = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
try
{
imageSurface = await LoadImageSurfaceAsync(storageFile, token);
imageSurface = await LoadImageSurfaceAsync(storageFile, token).ConfigureAwait(true);
}
catch (COMException)
{
await imageCache.RemoveAsync(uri.Enumerate());
await imageCache.RemoveAsync(uri.Enumerate()).ConfigureAwait(true);
}
}
@@ -150,7 +149,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
OnUpdateVisual(spriteVisual);
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
await ShowAsync(token);
await ShowAsync(token).ConfigureAwait(true);
}
}
}
@@ -159,7 +158,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{
if (!isShow)
{
await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token);
await AnimationBuilder.Create().Opacity(1d).StartAsync(this, token).ConfigureAwait(true);
isShow = true;
}
}
@@ -168,7 +167,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
{
if (isShow)
{
await AnimationBuilder.Create().Opacity(0d).StartAsync(this, token);
await AnimationBuilder.Create().Opacity(0d).StartAsync(this, token).ConfigureAwait(true);
isShow = false;
}
}

View File

@@ -31,9 +31,9 @@ public class Gradient : CompositionImage
/// <inheritdoc/>
protected override async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
{
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token).ConfigureAwait(true))
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token);
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token).ConfigureAwait(true);
imageAspectRatio = decoder.PixelWidth / (double)decoder.PixelHeight;
return LoadedImageSurface.StartLoadFromStream(imageStream);

View File

@@ -12,6 +12,7 @@ namespace Snap.Hutao.Control;
/// 表示支持取消加载的异步页面
/// 在被导航到其他页面前触发取消异步通知
/// </summary>
[SuppressMessage("", "CA1001")]
public class ScopedPage : Page
{
private readonly CancellationTokenSource viewLoadingCancellationTokenSource = new();
@@ -23,19 +24,18 @@ public class ScopedPage : Page
public ScopedPage()
{
serviceScope = Ioc.Default.CreateScope();
serviceScope.Track();
}
/// <inheritdoc cref="IServiceScope.ServiceProvider"/>
public IServiceProvider ServiceProvider { get => serviceScope.ServiceProvider; }
/// <summary>
/// 初始化
/// 应当在 InitializeComponent() 前调用
/// </summary>
/// <typeparam name="TViewModel">视图模型类型</typeparam>
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;
}
@@ -63,6 +63,7 @@ public class ScopedPage : Page
// Try dispose scope when page is not presented
serviceScope.Dispose();
viewLoadingCancellationTokenSource.Dispose();
}
/// <inheritdoc/>

View File

@@ -18,5 +18,5 @@ internal interface ISupportAsyncInitialization
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>初始化任务</returns>
ValueTask<bool> InitializeAsync(CancellationToken token = default);
ValueTask<bool> InitializeAsync();
}

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
@@ -18,11 +17,14 @@ namespace Snap.Hutao.Core.Caching;
/// 经过简化
/// </summary>
/// <typeparam name="T">Generic type as supplied by consumer of the class</typeparam>
[SuppressMessage("", "CA1001")]
public abstract class CacheBase<T>
where T : class
{
private readonly SemaphoreSlim cacheFolderSemaphore = new(1);
private readonly ILogger logger;
// violate di rule
private readonly HttpClient httpClient;
private StorageFolder? baseFolder;

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,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Core.Convert;
using Snap.Hutao.Extension;
using System.Text.Encodings.Web;
using Windows.ApplicationModel;
@@ -12,12 +14,12 @@ namespace Snap.Hutao.Core;
/// </summary>
internal static class CoreEnvironment
{
// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
// 计算过程https://github.com/UIGF-org/Hoyolab.Salt
/// <summary>
/// 动态密钥1的盐
/// </summary>
public const string DynamicSecret1Salt = "yUZ3s0Sna1IrSNfk29Vo6vRapdOyqyhB";
public const string DynamicSecret1Salt = "jEpJb9rRARU2rXDA9qYbZ3selxkuct9a";
/// <summary>
/// 动态密钥2的盐
@@ -32,7 +34,7 @@ internal static class CoreEnvironment
/// <summary>
/// 米游社 Rpc 版本
/// </summary>
public const string HoyolabXrpcVersion = "2.38.1";
public const string HoyolabXrpcVersion = "2.40.1";
/// <summary>
/// 标准UA
@@ -49,6 +51,16 @@ internal static class CoreEnvironment
/// </summary>
public static readonly string HoyolabDeviceId;
/// <summary>
/// 胡桃设备Id
/// </summary>
public static readonly string HutaoDeviceId;
/// <summary>
/// 包家族名称
/// </summary>
public static readonly string FamilyName;
/// <summary>
/// 默认的Json序列化选项
/// </summary>
@@ -60,12 +72,24 @@ internal static class CoreEnvironment
WriteIndented = true,
};
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
private const string MachineGuidValue = "MachineGuid";
static CoreEnvironment()
{
Version = Package.Current.Id.Version.ToVersion();
FamilyName = Package.Current.Id.FamilyName;
CommonUA = $"Snap Hutao/{Version}";
// simply assign a random guid
HoyolabDeviceId = Guid.NewGuid().ToString();
HutaoDeviceId = GetUniqueUserID();
}
private static string GetUniqueUserID()
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
return Md5Convert.ToHexString($"{userName}{machineGuid}");
}
}

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,10 @@ 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,96 +52,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();
}
messenger.Send(message);
}
}
}
/// <summary>
/// 数据库当前项
/// 简化对数据库中选中项的管理
/// </summary>
/// <typeparam name="TObservable">绑定类型</typeparam>
/// <typeparam name="TEntity">实体的类型</typeparam>
/// <typeparam name="TMessage">消息的类型</typeparam>
[SuppressMessage("", "SA1402")]
internal class DbCurrent<TObservable, TEntity, TMessage>
where TObservable : class
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TObservable>
{
private readonly DbContext dbContext;
private readonly DbSet<TEntity> dbSet;
private readonly IMessenger messenger;
private readonly Func<TObservable, TEntity> selector;
private TObservable? current;
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="dbSet">数据集</param>
/// <param name="selector">选择器</param>
/// <param name="messenger">消息器</param>
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, Func<TObservable, TEntity> selector, IMessenger messenger)
{
this.dbContext = dbContext;
this.dbSet = dbSet;
this.selector = selector;
this.messenger = messenger;
}
/// <summary>
/// 当前选中的项
/// </summary>
public TObservable? Current
{
get => current;
set
{
// prevent useless sets
if (current == value)
{
return;
}
// only update when not processing a deletion
if (value != null)
{
if (current != null)
{
TEntity entity = selector(current);
entity.IsSelected = false;
dbSet.Update(entity);
dbContext.SaveChanges();
}
}
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
current = value;
if (current != null)
{
TEntity entity = selector(current);
entity.IsSelected = true;
dbSet.Update(entity);
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

@@ -0,0 +1,95 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 设置帮助类
/// </summary>
public static class SettingEntryHelper
{
/// <summary>
/// 获取或添加一个对应的设置
/// </summary>
/// <param name="dbSet">设置集</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <returns>设置</returns>
public static SettingEntry SingleOrAdd(this DbSet<SettingEntry> dbSet, string key, string value)
{
SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key);
if (entry == null)
{
entry = new(key, value);
dbSet.Add(entry);
dbSet.Context().SaveChanges();
}
return entry;
}
/// <summary>
/// 获取或添加一个对应的设置
/// </summary>
/// <param name="dbSet">设置集</param>
/// <param name="key">键</param>
/// <param name="valueFactory">值工厂</param>
/// <returns>设置</returns>
public static SettingEntry SingleOrAdd(this DbSet<SettingEntry> dbSet, string key, Func<string> valueFactory)
{
SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key);
if (entry == null)
{
entry = new(key, valueFactory());
dbSet.Add(entry);
dbSet.Context().SaveChanges();
}
return entry;
}
/// <summary>
/// 获取 Boolean 值
/// </summary>
/// <param name="entry">设置</param>
/// <returns>值</returns>
public static bool GetBoolean(this SettingEntry entry)
{
return bool.Parse(entry.Value!);
}
/// <summary>
/// 设置 Boolean 值
/// </summary>
/// <param name="entry">设置</param>
/// <param name="value">值</param>
public static void SetBoolean(this SettingEntry entry, bool value)
{
entry.Value = value.ToString();
}
/// <summary>
/// 获取 Int32 值
/// </summary>
/// <param name="entry">设置</param>
/// <returns>值</returns>
public static int GetInt32(this SettingEntry entry)
{
return int.Parse(entry.Value!);
}
/// <summary>
/// 设置 Int32 值
/// </summary>
/// <param name="entry">设置</param>
/// <param name="value">值</param>
public static void SetInt32(this SettingEntry entry, int value)
{
entry.Value = value.ToString();
}
}

View File

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

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.DependencyInjection;
/// 服务管理器
/// 依赖注入的核心管理类
/// </summary>
internal static partial class ServiceCollectionExtensions
internal static partial class ServiceCollectionExtension
{
/// <summary>
/// 向容器注册服务
@@ -17,4 +17,4 @@ internal static partial class ServiceCollectionExtensions
/// <param name="services">容器</param>
/// <returns>可继续操作的服务集合</returns>
public static partial IServiceCollection AddInjections(this IServiceCollection services);
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 服务范围扩展
/// </summary>
public static class ServiceScopeExtension
{
private static IServiceScope? scopeReference;
/// <summary>
/// 追踪服务范围
/// </summary>
/// <param name="scope">范围</param>
public static void Track(this IServiceScope scope)
{
DisposeLast();
scopeReference = scope;
}
/// <summary>
/// 释放上个范围
/// </summary>
public static void DisposeLast()
{
scopeReference?.Dispose();
}
}

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Diagnostics;
/// <summary>
/// 值类型的<see cref="Stopwatch"/>
/// </summary>
internal struct ValueStopwatch
internal readonly struct ValueStopwatch
{
private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;

View File

@@ -3,6 +3,7 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Web.Hutao;
namespace Snap.Hutao.Core.Exception;
@@ -26,11 +27,10 @@ internal class ExceptionRecorder
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
}
[SuppressMessage("", "VSTHRD002")]
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
// string fileName = $"ex-{DateTimeOffset.Now:yyyyMMddHHmmssffff}.txt";
// File.WriteAllText(Path.Combine(path, fileName), $"{e.Exception}\r\n{e.Exception.StackTrace}");
Ioc.Default.GetRequiredService<HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading;
using Windows.ApplicationModel.DataTransfer;
namespace Snap.Hutao.Core.IO.DataTransfer;

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core.IO.Ini;
internal static class IniSerializer
{
/// <summary>
/// 异步反序列化
/// 反序列化
/// </summary>
/// <param name="fileStream">文件流</param>
/// <returns>Ini 元素集合</returns>
@@ -44,4 +44,20 @@ internal static class IniSerializer
}
}
}
/// <summary>
/// 序列化
/// </summary>
/// <param name="fileStream">写入的流</param>
/// <param name="elements">元素</param>
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
{
using (TextWriter writer = new StreamWriter(fileStream))
{
foreach (IniElement element in elements)
{
writer.WriteLine(element.ToString());
}
}
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading;
using System.IO;
using Windows.Storage;

View File

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

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

@@ -1,10 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Notifications;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Navigation;
using System.Security.Principal;
namespace Snap.Hutao.Core.LifeCycle;
@@ -13,8 +17,26 @@ 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>
/// 获取是否提升了权限
/// </summary>
/// <returns>是否提升了权限</returns>
public static bool GetElevated()
{
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
/// <summary>
/// 响应激活事件
/// 激活事件一般不会在UI线程上触发
@@ -24,7 +46,29 @@ internal static class Activation
public static void Activate(object? sender, AppActivationArguments args)
{
_ = sender;
HandleActivationAsync(args).SafeForget();
if (!ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
HandleActivationAsync(args).SafeForget();
}
}
/// <summary>
/// 响应通知激活事件
/// </summary>
/// <param name="args">参数</param>
public static void NotificationActivate(ToastNotificationActivatedEventArgsCompat args)
{
ToastArguments toastArgs = ToastArguments.Parse(args.Argument);
_ = toastArgs;
if (toastArgs.TryGetValue("Action", out string? action))
{
if (action == LaunchGame)
{
_ = toastArgs.TryGetValue("Uid", out string? uid);
HandleLaunchGameActionAsync(uid).SafeForget();
}
}
}
/// <summary>
@@ -44,35 +88,69 @@ internal static class Activation
private static async Task HandleActivationCoreAsync(AppActivationArguments args)
{
_ = Ioc.Default.GetRequiredService<MainWindow>();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
await infoBarService.WaitInitializationAsync().ConfigureAwait(false);
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);
}
}
else if (args.Kind == ExtendedActivationKind.Launch)
{
if (args.TryGetLaunchActivatedArgument(out string? arguments))
{
switch (arguments)
{
case "":
{
await WaitMainWindowAsync().ConfigureAwait(false);
break;
}
case LaunchGame:
{
await HandleLaunchGameActionAsync().ConfigureAwait(false);
break;
}
}
}
}
}
private static async Task WaitMainWindowAsync()
{
await ThreadHelper.SwitchToMainThreadAsync();
_ = Ioc.Default.GetRequiredService<MainWindow>();
await Ioc.Default.GetRequiredService<IInfoBarService>().WaitInitializationAsync().ConfigureAwait(false);
Ioc.Default
.GetRequiredService<IMetadataService>()
.ImplictAs<IMetadataInitializer>()?
.InitializeInternalAsync()
.SafeForget();
}
private static async Task HandleUrlActivationAsync(Uri uri)
{
UriBuilder builder = new(uri);
Must.Argument(builder.Scheme == "hutao", "uri 的协议不正确");
string category = builder.Host.ToLowerInvariant();
string action = builder.Path.ToLowerInvariant();
string rawParameter = builder.Query.ToLowerInvariant();
string parameter = builder.Query.ToLowerInvariant();
switch (category)
{
case "achievement":
{
await HandleAchievementActionAsync(action, rawParameter).ConfigureAwait(false);
await WaitMainWindowAsync().ConfigureAwait(false);
await HandleAchievementActionAsync(action, parameter).ConfigureAwait(false);
break;
}
case "dailynote":
{
await HandleDailyNoteActionAsync(action, parameter).ConfigureAwait(false);
break;
}
}
@@ -80,6 +158,7 @@ internal static class Activation
private static async Task HandleAchievementActionAsync(string action, string parameter)
{
_ = parameter;
switch (action)
{
case "/import":
@@ -95,4 +174,37 @@ internal static class Activation
}
}
}
private static async Task HandleDailyNoteActionAsync(string action, string parameter)
{
_ = parameter;
switch (action)
{
case "/refresh":
{
await Ioc.Default
.GetRequiredService<IDailyNoteService>()
.RefreshDailyNotesAsync(true)
.ConfigureAwait(false);
break;
}
}
}
private static async Task HandleLaunchGameActionAsync(string? uid = null)
{
await ThreadHelper.SwitchToMainThreadAsync();
// TODO auto switch to account
if (!MainWindow.IsPresent)
{
_ = Ioc.Default.GetRequiredService<LaunchGameWindow>();
}
else
{
await Ioc.Default
.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.LaunchGamePage>(INavigationAwaiter.Default, true).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

@@ -24,6 +24,7 @@ internal sealed partial class DatebaseLogger : ILogger
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state)
where TState : notnull
{
return new NullScope();
}

View File

@@ -4,7 +4,6 @@
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Core.Threading;
using System.Collections.Concurrent;
using System.Diagnostics;

View File

@@ -1,44 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Core;
/// <summary>
/// 进程帮助类
/// </summary>
public static class ProcessHelper
{
/// <summary>
/// 启动进程
/// </summary>
/// <param name="path">路径</param>
/// <param name="useShellExecute">使用shell</param>
/// <returns>进程</returns>
public static Process? Start(string path, bool useShellExecute = true)
{
ProcessStartInfo processInfo = new(path)
{
UseShellExecute = useShellExecute,
};
return Process.Start(processInfo);
}
/// <summary>
/// 启动进程
/// </summary>
/// <param name="path">路径</param>
/// <param name="arguments">命令行参数</param>
/// <param name="useShellExecute">使用shell</param>
/// <returns>进程</returns>
public static Process? Start(string path, string arguments, bool useShellExecute = true)
{
ProcessStartInfo processInfo = new(path)
{
UseShellExecute = useShellExecute,
Arguments = arguments,
};
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

@@ -0,0 +1,48 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Win32.TaskScheduler;
using SchedulerTask = Microsoft.Win32.TaskScheduler.Task;
namespace Snap.Hutao.Core;
/// <summary>
/// 任务计划器服务
/// </summary>
internal static class TaskSchedulerHelper
{
private const string DailyNoteRefreshTaskName = "SnapHutaoDailyNoteRefreshTask";
/// <summary>
/// 注册实时便笺刷新任务
/// </summary>
/// <param name="interval">间隔(秒)</param>
/// <returns>是否注册或修改成功</returns>
public static bool RegisterForDailyNoteRefresh(int interval)
{
try
{
TimeSpan intervalTime = TimeSpan.FromSeconds(interval);
if (TaskService.Instance.GetTask(DailyNoteRefreshTaskName) is SchedulerTask targetTask)
{
TimeTrigger? trigger = targetTask.Definition.Triggers[0] as TimeTrigger;
trigger!.Repetition.Interval = intervalTime;
targetTask.RegisterChanges();
return true;
}
else
{
TaskDefinition task = TaskService.Instance.NewTask();
task.RegistrationInfo.Description = "胡桃实时便笺刷新任务 | 请勿编辑或删除。";
task.Triggers.Add(new TimeTrigger() { Repetition = new(intervalTime, TimeSpan.Zero), });
task.Actions.Add("explorer", "hutao://DailyNote/Refresh");
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);
return true;
}
}
catch (UnauthorizedAccessException)
{
return false;
}
}
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// Holds the task for a cancellation token, as well as the token registration. The registration is disposed when this instance is disposed.
/// </summary>
/// <typeparam name="T">包装类型</typeparam>
public sealed class CancellationTokenTaskCompletionSource : IDisposable
{
/// <summary>
/// The cancellation token registration, if any. This is <c>null</c> if the registration was not necessary.
/// </summary>
private readonly IDisposable? registration;
/// <summary>
/// Creates a task for the specified cancellation token, registering with the token if necessary.
/// </summary>
/// <param name="cancellationToken">The cancellation token to observe.</param>
public CancellationTokenTaskCompletionSource(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
Task = Task.CompletedTask;
return;
}
TaskCompletionSource tcs = new TaskCompletionSource();
registration = cancellationToken.Register(() => tcs.TrySetResult(), useSynchronizationContext: false);
Task = tcs.Task;
}
/// <summary>
/// Gets the task for the source cancellation token.
/// </summary>
public Task Task { get; private set; }
/// <summary>
/// Disposes the cancellation token registration, if any. Note that this may cause <see cref="Task"/> to never complete.
/// </summary>
public void Dispose()
{
registration?.Dispose();
}
}

View File

@@ -14,16 +14,8 @@ internal class ThreadAccessAttribute : Attribute
/// 指示方法的进入线程访问状态
/// </summary>
/// <param name="enter">进入状态</param>
[SuppressMessage("", "IDE0060")]
public ThreadAccessAttribute(ThreadAccessState enter)
{
}
/// <summary>
/// 指示方法的进入退出线程访问状态
/// </summary>
/// <param name="enter">进入状态</param>
/// <param name="leave">离开状态</param>
public ThreadAccessAttribute(ThreadAccessState enter, ThreadAccessState leave)
{
}
}

View File

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

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 调度器队列切换操作
/// </summary>
public struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueueSwitchOperation>, IAwaiter
public readonly struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueueSwitchOperation>, IAwaiter
{
private readonly DispatcherQueue dispatherQueue;

View File

@@ -19,7 +19,7 @@ public static class SemaphoreSlimExtensions
return new SemaphoreSlimReleaser(semaphoreSlim);
}
private struct SemaphoreSlimReleaser : IDisposable
private readonly struct SemaphoreSlimReleaser : IDisposable
{
private readonly SemaphoreSlim semaphoreSlim;

View File

@@ -20,7 +20,7 @@ public static class TaskExtensions
{
try
{
await task;
await task.ConfigureAwait(false);
}
catch (System.Exception ex)
{
@@ -37,7 +37,7 @@ public static class TaskExtensions
{
try
{
await task;
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
{
@@ -59,7 +59,7 @@ public static class TaskExtensions
{
try
{
await task;
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
{
@@ -83,7 +83,7 @@ public static class TaskExtensions
{
try
{
await task;
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
{

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

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

@@ -14,8 +14,8 @@ namespace Snap.Hutao.Core;
/// </summary>
internal abstract class WebView2Helper
{
private static bool hasEverDetected = false;
private static bool isSupported = false;
private static bool hasEverDetected;
private static bool isSupported;
private static string version = "未检测到 WebView2 运行时";
/// <summary>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Windowing;
using Snap.Hutao.Win32;
using Windows.Graphics;
namespace Snap.Hutao.Core.Windowing;
@@ -18,9 +19,6 @@ public static class AppWindowExtensions
/// <returns>呈现矩形</returns>
public static RectInt32 GetRect(this AppWindow appWindow)
{
PointInt32 postion = appWindow.Position;
SizeInt32 size = appWindow.Size;
return new RectInt32(postion.X, postion.Y, size.Width, size.Height);
return StructMarshal.RectInt32(appWindow.Position, appWindow.Size);
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 背景类型
/// </summary>
public enum BackdropType
{
/// <summary>
/// 无
/// </summary>
None = 0,
/// <summary>
/// 亚克力
/// </summary>
Acrylic,
/// <summary>
/// 云母
/// </summary>
Mica,
/// <summary>
/// 变种云母
/// </summary>
MicaAlt,
}

View File

@@ -1,12 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
using Snap.Hutao.Message;
using Snap.Hutao.Win32;
using System.IO;
using Windows.ApplicationModel;
using Windows.Graphics;
using Windows.UI;
using Windows.Win32.Foundation;
@@ -15,32 +19,36 @@ using WinRT.Interop;
namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 窗口管理器
/// 主要包含了针对窗体的 P/Inoke 逻辑
/// 扩展窗口
/// </summary>
internal sealed class ExtendedWindow
/// <typeparam name="TWindow">窗体类型</typeparam>
[SuppressMessage("", "CA1001")]
internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMessage>
where TWindow : Window, IExtendedWindowSource
{
private readonly HWND handle;
private readonly AppWindow appWindow;
private readonly Window window;
private readonly TWindow window;
private readonly FrameworkElement titleBar;
private readonly ILogger<ExtendedWindow> logger;
private readonly WindowSubclassManager subclassManager;
private readonly ILogger<ExtendedWindow<TWindow>> logger;
private readonly WindowSubclassManager<TWindow> subclassManager;
private readonly bool useLegacyDragBar;
private SystemBackdrop? systemBackdrop;
/// <summary>
/// 构造一个新的窗口状态管理器
/// 构造一个新的扩展窗口
/// </summary>
/// <param name="window">窗口</param>
/// <param name="titleBar">充当标题栏的元素</param>
private ExtendedWindow(Window window, FrameworkElement titleBar)
private ExtendedWindow(TWindow window, FrameworkElement titleBar)
{
this.window = window;
this.titleBar = titleBar;
logger = Ioc.Default.GetRequiredService<ILogger<ExtendedWindow>>();
logger = Ioc.Default.GetRequiredService<ILogger<ExtendedWindow<TWindow>>>();
handle = (HWND)WindowNative.GetWindowHandle(window);
@@ -48,7 +56,7 @@ internal sealed class ExtendedWindow
appWindow = AppWindow.GetFromWindowId(windowId);
useLegacyDragBar = !AppWindowTitleBar.IsCustomizationSupported();
subclassManager = new(handle, useLegacyDragBar);
subclassManager = new(window, handle, useLegacyDragBar);
InitializeWindow();
}
@@ -57,11 +65,21 @@ internal sealed class ExtendedWindow
/// 初始化
/// </summary>
/// <param name="window">窗口</param>
/// <param name="titleBar">标题栏</param>
/// <returns>实例</returns>
public static ExtendedWindow Initialize(Window window, FrameworkElement titleBar)
public static ExtendedWindow<TWindow> Initialize(TWindow window)
{
return new(window, titleBar);
return new(window, window.TitleBar);
}
/// <inheritdoc/>
public void Receive(BackdropTypeChangedMessage message)
{
if (systemBackdrop != null)
{
systemBackdrop.BackdropType = message.BackdropType;
bool micaApplied = systemBackdrop.TryApply();
logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
}
}
private static void UpdateTitleButtonColor(AppWindowTitleBar appTitleBar)
@@ -101,9 +119,10 @@ internal sealed class ExtendedWindow
private void InitializeWindow()
{
appWindow.Title = "胡桃";
appWindow.SetIcon(Path.Combine(Package.Current.InstalledLocation.Path, "Assets/Logos/Logo.ico"));
ExtendsContentIntoTitleBar();
Persistence.RecoverOrInit(appWindow);
Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize);
// Log basic window state here.
(string pos, string size) = GetPostionAndSize(appWindow);
@@ -111,18 +130,24 @@ internal sealed class ExtendedWindow
appWindow.Show(true);
bool micaApplied = new SystemBackdrop(window).TryApply();
systemBackdrop = new(window);
bool micaApplied = systemBackdrop.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");
logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager<TWindow>), subClassApplied ? "succeed" : "failed");
Ioc.Default.GetRequiredService<IMessenger>().Register(this);
window.Closed += OnWindowClosed;
}
private void OnWindowClosed(object sender, WindowEventArgs args)
{
Persistence.Save(appWindow);
if (window.PersistSize)
{
Persistence.Save(appWindow);
}
subclassManager?.Dispose();
}
@@ -147,12 +172,17 @@ internal sealed class ExtendedWindow
}
}
private void UpdateDragRectangles(AppWindowTitleBar appTitleBar)
private unsafe void UpdateDragRectangles(AppWindowTitleBar appTitleBar)
{
double scale = Persistence.GetScaleForWindow(handle);
// 48 is the navigation button leftInset
RectInt32 dragRect = new RectInt32(48, 0, (int)titleBar.ActualWidth, (int)titleBar.ActualHeight).Scale(scale);
RectInt32 dragRect = StructMarshal.RectInt32(new(48, 0), titleBar.ActualSize).Scale(scale);
appTitleBar.SetDragRectangles(dragRect.Enumerate().ToArray());
// workaround for https://github.com/microsoft/WindowsAppSDK/issues/2976
SizeInt32 size = appWindow.ClientSize;
size.Height -= 38;
appWindow.ResizeClient(size);
}
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Windows.Graphics;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 为扩展窗体提供必要的选项
/// </summary>
/// <typeparam name="TWindow">窗体类型</typeparam>
internal interface IExtendedWindowSource
{
/// <summary>
/// 提供的标题栏
/// </summary>
FrameworkElement TitleBar { get; }
/// <summary>
/// 是否持久化尺寸
/// </summary>
bool PersistSize { get; }
/// <summary>
/// 初始大小
/// </summary>
SizeInt32 InitSize { get; }
/// <summary>
/// 处理最大最小信息
/// </summary>
/// <param name="pInfo">信息指针</param>
/// <param name="scalingFactor">缩放比</param>
unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor);
}

View File

@@ -21,21 +21,30 @@ internal static class Persistence
/// 设置窗体位置
/// </summary>
/// <param name="appWindow">应用窗体</param>
public static void RecoverOrInit(AppWindow appWindow)
/// <param name="persistSize">持久化尺寸</param>
/// <param name="size">初始尺寸</param>
public static void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 size)
{
// Set first launch size.
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
SizeInt32 size = TransformSizeForWindow(new(1200, 741), hwnd);
RectInt32 rect = StructMarshal.RectInt32(size);
SizeInt32 transformedSize = TransformSizeForWindow(size, hwnd);
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
RectInt32 target = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
if (target.Width * target.Height < 848 * 524)
if (persistSize)
{
target = rect;
RectInt32 persistedSize = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
if (persistedSize.Width * persistedSize.Height > 848 * 524)
{
rect = persistedSize;
}
}
TransformToCenterScreen(ref target);
appWindow.MoveAndResize(target);
unsafe
{
TransformToCenterScreen(&rect);
}
appWindow.MoveAndResize(rect);
}
/// <summary>
@@ -44,7 +53,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>
@@ -64,13 +73,13 @@ internal static class Persistence
return new((int)(size.Width * scale), (int)(size.Height * scale));
}
private static void TransformToCenterScreen(ref RectInt32 rect)
private static unsafe void TransformToCenterScreen(RectInt32* rect)
{
DisplayArea displayArea = DisplayArea.GetFromRect(rect, DisplayAreaFallback.Primary);
DisplayArea displayArea = DisplayArea.GetFromRect(*rect, DisplayAreaFallback.Primary);
RectInt32 workAreaRect = displayArea.WorkArea;
rect.X = workAreaRect.X + ((workAreaRect.Width - rect.Width) / 2);
rect.Y = workAreaRect.Y + ((workAreaRect.Height - rect.Height) / 2);
rect->X = workAreaRect.X + ((workAreaRect.Width - rect->Width) / 2);
rect->Y = workAreaRect.Y + ((workAreaRect.Height - rect->Height) / 2);
}
[StructLayout(LayoutKind.Explicit)]
@@ -124,7 +133,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

@@ -1,9 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using System.Runtime.InteropServices;
using Windows.System;
using WinRT;
@@ -18,7 +22,7 @@ public class SystemBackdrop
private readonly Window window;
private DispatcherQueueHelper? dispatcherQueueHelper;
private MicaController? backdropController;
private ISystemBackdropControllerWithTargets? backdropController;
private SystemBackdropConfiguration? configuration;
/// <summary>
@@ -28,38 +32,68 @@ public class SystemBackdrop
public SystemBackdrop(Window window)
{
this.window = window;
using (IServiceScope scope = Ioc.Default.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.SystemBackdropType, BackdropType.Mica.ToString());
BackdropType = Enum.Parse<BackdropType>(entry.Value!);
}
}
/// <summary>
/// 背景类型
/// </summary>
public BackdropType BackdropType { get; set; }
/// <summary>
/// 尝试设置背景
/// </summary>
/// <returns>是否设置成功</returns>
public bool TryApply()
{
if (!MicaController.IsSupported())
bool isSupport = BackdropType switch
{
BackdropType.Acrylic => DesktopAcrylicController.IsSupported(),
BackdropType.Mica or BackdropType.MicaAlt => MicaController.IsSupported(),
_ => false,
};
if (!isSupport)
{
return false;
}
else
{
dispatcherQueueHelper = new();
dispatcherQueueHelper.Ensure();
// Previous one
if (backdropController != null)
{
backdropController.RemoveAllSystemBackdropTargets();
}
else
{
dispatcherQueueHelper = new();
dispatcherQueueHelper.Ensure();
}
// Hooking up the policy object
configuration = new();
configuration = new()
{
IsInputActive = true, // Initial configuration state.
};
SetConfigurationSourceTheme(configuration);
window.Activated += OnWindowActivated;
window.Closed += OnWindowClosed;
((FrameworkElement)window.Content).ActualThemeChanged += OnWindowThemeChanged;
// Initial configuration state.
configuration.IsInputActive = true;
SetConfigurationSourceTheme(configuration);
backdropController = new()
backdropController = BackdropType switch
{
// Mica Alt
Kind = MicaKind.BaseAlt,
BackdropType.Acrylic => new DesktopAcrylicController(),
BackdropType.Mica => new MicaController() { Kind = MicaKind.Base },
BackdropType.MicaAlt => new MicaController() { Kind = MicaKind.BaseAlt },
_ => throw Must.NeverHappen(),
};
backdropController.AddSystemBackdropTarget(window.As<ICompositionSupportsSystemBackdrop>());
backdropController.SetSystemBackdropConfiguration(configuration);
@@ -69,7 +103,7 @@ public class SystemBackdrop
private void OnWindowActivated(object sender, WindowActivatedEventArgs args)
{
Must.NotNull(configuration!).IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
configuration!.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
}
private void OnWindowClosed(object sender, WindowEventArgs args)
@@ -101,7 +135,7 @@ public class SystemBackdrop
private class DispatcherQueueHelper
{
private object? dispatcherQueueController = null;
private object? dispatcherQueueController;
/// <summary>
/// 确保系统调度队列控制器存在

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
@@ -11,14 +12,14 @@ namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 窗体子类管理器
/// </summary>
internal class WindowSubclassManager : IDisposable
/// <typeparam name="TWindow">窗体类型</typeparam>
internal class WindowSubclassManager<TWindow> : IDisposable
where TWindow : Window, IExtendedWindowSource
{
private const int WindowSubclassId = 101;
private const int DragBarSubclassId = 102;
private const int MinWidth = 848;
private const int MinHeight = 524;
private readonly TWindow window;
private readonly HWND hwnd;
private readonly bool isLegacyDragBar;
private HWND hwndDragBar;
@@ -30,12 +31,13 @@ internal class WindowSubclassManager : IDisposable
/// <summary>
/// 构造一个新的窗体子类管理器
/// </summary>
/// <param name="window">窗体实例</param>
/// <param name="hwnd">窗体句柄</param>
/// <param name="isLegacyDragBar">是否为经典标题栏区域</param>
public WindowSubclassManager(HWND hwnd, bool isLegacyDragBar)
public WindowSubclassManager(TWindow window, HWND hwnd, bool isLegacyDragBar)
{
Must.NotNull(hwnd);
this.hwnd = hwnd;
this.window = window;
this.hwnd = Must.NotNull(hwnd);
this.isLegacyDragBar = isLegacyDragBar;
}
@@ -50,9 +52,10 @@ internal class WindowSubclassManager : IDisposable
bool titleBarHooked = true;
// only hook up drag bar proc when use legacy Window.ExtendsContentIntoTitleBar
// only hook up drag bar proc when not use legacy Window.ExtendsContentIntoTitleBar
if (isLegacyDragBar)
{
titleBarHooked = false;
hwndDragBar = FindWindowEx(hwnd, default, "DRAG_BAR_WINDOW_CLASS", string.Empty);
if (!hwndDragBar.IsNull)
@@ -85,11 +88,15 @@ internal class WindowSubclassManager : IDisposable
case WM_GETMINMAXINFO:
{
double scalingFactor = Persistence.GetScaleForWindow(hwnd);
MINMAXINFO* info = (MINMAXINFO*)lParam.Value;
info->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, info->ptMinTrackSize.X);
info->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, info->ptMinTrackSize.Y);
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
break;
}
case WM_NCRBUTTONDOWN:
case WM_NCRBUTTONUP:
{
return new(0);
}
}
return DefSubclassProc(hwnd, uMsg, wParam, lParam);

View File

@@ -0,0 +1,34 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.ObjectModel;
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="Collection{T}"/> 部分
/// </summary>
public static partial class EnumerableExtension
{
/// <summary>
/// 移除集合中满足条件的项
/// </summary>
/// <typeparam name="T">集合项类型</typeparam>
/// <param name="collection">集合</param>
/// <param name="shouldRemovePredicate">是否应当移除</param>
/// <returns>移除的个数</returns>
public static int RemoveWhere<T>(this Collection<T> collection, Func<T, bool> shouldRemovePredicate)
{
int count = 0;
foreach (T item in collection.ToList())
{
if (shouldRemovePredicate.Invoke(item))
{
collection.Remove(item);
count++;
}
}
return count;
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="Dictionary{TKey, TValue}"/> 部分
/// </summary>
public static partial class EnumerableExtension
{
/// <summary>
/// 获取值或默认值
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <typeparam name="TValue">值类型</typeparam>
/// <param name="dictionary">字典</param>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>结果值</returns>
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))
{
return value;
}
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;
}
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="List{T}"/> 部分
/// </summary>
public static partial class EnumerableExtension
{
/// <inheritdoc cref="Enumerable.Average(IEnumerable{int})"/>
public static double AverageNoThrow(this List<int> source)
{
Span<int> span = CollectionsMarshal.AsSpan(source);
if (span.IsEmpty)
{
return 0;
}
long sum = 0;
for (int i = 0; i < span.Length; i++)
{
sum += span[i];
}
return (double)sum / span.Length;
}
/// <summary>
/// 如果传入列表不为空则原路返回,
/// 如果传入列表为空返回一个空的列表
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>源列表或空列表</returns>
public static List<TSource> EmptyIfNull<TSource>(this List<TSource>? source)
{
return source ?? new();
}
/// <summary>
/// 移除表中首个满足条件的项
/// </summary>
/// <typeparam name="T">项的类型</typeparam>
/// <param name="list">表</param>
/// <param name="shouldRemovePredicate">是否应当移除</param>
/// <returns>是否移除了元素</returns>
public static bool RemoveFirstWhere<T>(this IList<T> list, Func<T, bool> shouldRemovePredicate)
{
for (int i = 0; i < list.Count; i++)
{
if (shouldRemovePredicate.Invoke(list[i]))
{
list.RemoveAt(i);
return true;
}
}
return false;
}
}

View File

@@ -1,9 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Extension;
/// <summary>
@@ -11,26 +8,6 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static partial class EnumerableExtension
{
/// <inheritdoc cref="Enumerable.Average(IEnumerable{int})"/>
public static double AverageNoThrow(this List<int> source)
{
Span<int> span = CollectionsMarshal.AsSpan(source);
if (span.IsEmpty)
{
return 0;
}
long sum = 0;
for (int i = 0; i < span.Length; i++)
{
sum += span[i];
}
return (double)sum / span.Length;
}
/// <summary>
/// 计数
/// </summary>
@@ -63,18 +40,6 @@ public static partial class EnumerableExtension
return source ?? Enumerable.Empty<TSource>();
}
/// <summary>
/// 如果传入列表不为空则原路返回,
/// 如果传入列表为空返回一个空的列表
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>源列表或空列表</returns>
public static List<TSource> EmptyIfNull<TSource>(this List<TSource>? source)
{
return source ?? new();
}
/// <summary>
/// 将源转换为仅包含单个元素的枚举
/// </summary>
@@ -99,94 +64,6 @@ public static partial class EnumerableExtension
return source.FirstOrDefault(predicate) ?? source.FirstOrDefault();
}
/// <summary>
/// 获取值或默认值
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <typeparam name="TValue">值类型</typeparam>
/// <param name="dictionary">字典</param>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>结果值</returns>
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))
{
return value;
}
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>
/// <typeparam name="T">项的类型</typeparam>
/// <param name="list">表</param>
/// <param name="shouldRemovePredicate">是否应当移除</param>
/// <returns>是否移除了元素</returns>
public static bool RemoveFirstWhere<T>(this IList<T> list, Func<T, bool> shouldRemovePredicate)
{
for (int i = 0; i < list.Count; i++)
{
if (shouldRemovePredicate.Invoke(list[i]))
{
list.RemoveAt(i);
return true;
}
}
return false;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>
public static Dictionary<TKey, TSource> ToDictionaryOverride<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
where TKey : notnull

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text;
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="StringBuilder"/> 扩展方法
/// </summary>
public static class StringBuilderExtensions
{
/// <summary>
/// 当条件符合时执行 <see cref="StringBuilder.Append(string?)"/>
/// </summary>
/// <param name="sb">字符串建造器</param>
/// <param name="condition">条件</param>
/// <param name="value">附加的字符串</param>
/// <returns>同一个字符串建造器</returns>
public static StringBuilder AppendIf(this StringBuilder sb, bool condition, string? value)
{
return condition ? sb.Append(value) : sb;
}
}

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Web.Hutao;
namespace Snap.Hutao.Factory;
@@ -11,7 +12,7 @@ namespace Snap.Hutao.Factory;
[Injection(InjectAs.Transient, typeof(IAsyncRelayCommandFactory))]
internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
{
private readonly ILogger logger;
private readonly ILogger<AsyncRelayCommandFactory> logger;
/// <summary>
/// 构造一个新的异步命令工厂
@@ -82,6 +83,7 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
return command;
}
[SuppressMessage("", "VSTHRD002")]
private void ReportException(IAsyncRelayCommand command)
{
command.PropertyChanged += (sender, args) =>
@@ -94,6 +96,7 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
{
Exception baseException = exception.GetBaseException();
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(AsyncRelayCommand));
Ioc.Default.GetRequiredService<HomaClient2>().UploadLogAsync(baseException).GetAwaiter().GetResult();
}
}
}

View File

@@ -11,6 +11,7 @@ global using Microsoft.Extensions.Logging;
// Snap.Hutao
global using Snap.Hutao.Core.DependencyInjection;
global using Snap.Hutao.Core.DependencyInjection.Annotation;
global using Snap.Hutao.Core.Threading;
global using Snap.Hutao.Core.Validation;
// Runtime

View File

@@ -0,0 +1,63 @@
<Window
x:Class="Snap.Hutao.LaunchGameWindow"
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:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shv="using:Snap.Hutao.ViewModel"
mc:Ignorable="d">
<Grid
Name="RootGrid"
d:DataContext="{d:DesignInstance shv:LaunchGameViewModel}">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid
x:Name="DragableGrid"
Grid.Row="0"
Height="32">
<TextBlock
Text="选择账号并启动"
TextWrapping="NoWrap"
Style="{StaticResource CaptionTextBlockStyle}"
VerticalAlignment="Center"
Margin="12,0,0,0"/>
</Grid>
<ListView
Grid.Row="1"
ItemsSource="{Binding GameAccounts}"
SelectedItem="{Binding SelectedGameAccount,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<StackPanel Margin="0,12">
<TextBlock Text="{Binding Name}"/>
<TextBlock
Opacity="0.8"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding AttachUid,TargetNullValue=该账号尚未绑定 UID}"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button
Margin="16"
Grid.Row="2"
HorizontalAlignment="Stretch"
Content="启动游戏"
Command="{Binding LaunchCommand}"/>
</Grid>
</Window>

View File

@@ -0,0 +1,64 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.ViewModel;
using Windows.Graphics;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao;
/// <summary>
/// 启动游戏窗口
/// </summary>
[Injection(InjectAs.Singleton)]
public sealed partial class LaunchGameWindow : Window, IDisposable, IExtendedWindowSource
{
private const int MinWidth = 240;
private const int MinHeight = 240;
private const int MaxWidth = 320;
private const int MaxHeight = 320;
private readonly IServiceScope scope;
/// <summary>
/// 构造一个新的启动游戏窗口
/// </summary>
/// <param name="scopeFactory">范围工厂</param>
public LaunchGameWindow(IServiceScopeFactory scopeFactory)
{
InitializeComponent();
ExtendedWindow<LaunchGameWindow>.Initialize(this);
scope = scopeFactory.CreateScope();
RootGrid.DataContext = scope.ServiceProvider.GetRequiredService<LaunchGameViewModel>();
Closed += (s, e) => Dispose();
}
/// <inheritdoc/>
public FrameworkElement TitleBar { get => DragableGrid; }
/// <inheritdoc/>
public bool PersistSize { get => false; }
/// <inheritdoc/>
public SizeInt32 InitSize { get => new(320, 320); }
/// <inheritdoc/>
public void Dispose()
{
scope.Dispose();
}
/// <inheritdoc/>
public unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor)
{
pInfo->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo->ptMinTrackSize.X);
pInfo->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo->ptMinTrackSize.Y);
pInfo->ptMaxTrackSize.X = (int)Math.Min(MaxWidth * scalingFactor, pInfo->ptMaxTrackSize.X);
pInfo->ptMaxTrackSize.Y = (int)Math.Min(MaxHeight * scalingFactor, pInfo->ptMaxTrackSize.Y);
}
}

View File

@@ -3,6 +3,8 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Windows.Graphics;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao;
@@ -11,14 +13,40 @@ namespace Snap.Hutao;
/// </summary>
[Injection(InjectAs.Singleton)]
[SuppressMessage("", "CA1001")]
public sealed partial class MainWindow : Window
public sealed partial class MainWindow : Window, IExtendedWindowSource
{
private const int MinWidth = 848;
private const int MinHeight = 524;
/// <summary>
/// 构造一个新的主窗体
/// </summary>
public MainWindow()
{
InitializeComponent();
ExtendedWindow.Initialize(this, TitleBarView.DragArea);
ExtendedWindow<MainWindow>.Initialize(this);
IsPresent = true;
Closed += (s, e) => IsPresent = false;
}
/// <summary>
/// 是否打开
/// </summary>
public static bool IsPresent { get; private set; }
/// <inheritdoc/>
public FrameworkElement TitleBar { get => TitleBarView.DragArea; }
/// <inheritdoc/>
public bool PersistSize { get => true; }
/// <inheritdoc/>
public SizeInt32 InitSize { get => new(1200, 741); }
/// <inheritdoc/>
public unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor)
{
pInfo->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo->ptMinTrackSize.X);
pInfo->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo->ptMinTrackSize.Y);
}
}

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

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Windowing;
namespace Snap.Hutao.Message;
/// <summary>
/// 背景类型改变消息
/// </summary>
internal class BackdropTypeChangedMessage
{
/// <summary>
/// 构造一个新的背景类型改变消息
/// </summary>
/// <param name="backdropType">背景类型</param>
public BackdropTypeChangedMessage(BackdropType backdropType)
{
BackdropType = backdropType;
}
/// <summary>
/// 背景类型
/// </summary>
public BackdropType BackdropType { get; set; }
}

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

@@ -1,22 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Model.Binding.User;
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

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
namespace Snap.Hutao.Message;
/// <summary>
/// 用户删除消息
/// </summary>
internal class UserRemovedMessage
{
/// <summary>
/// 构造一个新的用户删除消息
/// </summary>
/// <param name="user">用户</param>
public UserRemovedMessage(User user)
{
RemovedUserId = user.InnerId;
}
/// <summary>
/// 删除的用户Id
/// </summary>
public Guid RemovedUserId { get; }
}

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,215 @@
// <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("20221031104940_GameAccount")]
partial class GameAccount
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
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.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
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,34 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
public partial class GameAccount : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "game_accounts",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
AttachUid = table.Column<string>(type: "TEXT", nullable: true),
Type = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
MihoyoSDK = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_game_accounts", x => x.InnerId);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "game_accounts");
}
}
}

View File

@@ -0,0 +1,282 @@
// <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("20221108081525_DailyNoteEntry")]
partial class DailyNoteEntry
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
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.DailyNoteEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DailyNote")
.HasColumnType("TEXT");
b.Property<bool>("DailyTaskNotify")
.HasColumnType("INTEGER");
b.Property<bool>("DailyTaskNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotify")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("HomeCoinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("HomeCoinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ResinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("ResinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ShowInHomeWidget")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotify")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("UserId");
b.ToTable("daily_notes");
});
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.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
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.DailyNoteEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
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,55 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
public partial class DailyNoteEntry : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "daily_notes",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
Uid = table.Column<string>(type: "TEXT", nullable: false),
DailyNote = table.Column<string>(type: "TEXT", nullable: true),
ResinNotifyThreshold = table.Column<int>(type: "INTEGER", nullable: false),
ResinNotifySuppressed = table.Column<bool>(type: "INTEGER", nullable: false),
HomeCoinNotifyThreshold = table.Column<int>(type: "INTEGER", nullable: false),
HomeCoinNotifySuppressed = table.Column<bool>(type: "INTEGER", nullable: false),
TransformerNotify = table.Column<bool>(type: "INTEGER", nullable: false),
TransformerNotifySuppressed = table.Column<bool>(type: "INTEGER", nullable: false),
DailyTaskNotify = table.Column<bool>(type: "INTEGER", nullable: false),
DailyTaskNotifySuppressed = table.Column<bool>(type: "INTEGER", nullable: false),
ExpeditionNotify = table.Column<bool>(type: "INTEGER", nullable: false),
ExpeditionNotifySuppressed = table.Column<bool>(type: "INTEGER", nullable: false),
ShowInHomeWidget = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_daily_notes", x => x.InnerId);
table.ForeignKey(
name: "FK_daily_notes_users_UserId",
column: x => x.UserId,
principalTable: "users",
principalColumn: "InnerId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_daily_notes_UserId",
table: "daily_notes",
column: "UserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "daily_notes");
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
@@ -82,6 +82,62 @@ namespace Snap.Hutao.Migrations
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DailyNote")
.HasColumnType("TEXT");
b.Property<bool>("DailyTaskNotify")
.HasColumnType("INTEGER");
b.Property<bool>("DailyTaskNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotify")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("HomeCoinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("HomeCoinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ResinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("ResinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ShowInHomeWidget")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotify")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("UserId");
b.ToTable("daily_notes");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
@@ -131,6 +187,31 @@ namespace Snap.Hutao.Migrations
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
@@ -172,6 +253,17 @@ namespace Snap.Hutao.Migrations
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")

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

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// <summary>
/// 词条评分
/// </summary>
public struct AffixScore
public readonly struct AffixScore
{
/// <summary>
/// 构造一个新的圣遗物评分

View File

@@ -8,12 +8,6 @@ namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// </summary>
public class Reliquary : EquipBase
{
/// <summary>
/// 副属性列表
/// </summary>
[Obsolete]
public List<ReliquarySubProperty> SubProperties { get; set; } = default!;
/// <summary>
/// 初始词条
/// </summary>

View File

@@ -24,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,53 @@
// 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);
SideIcon = AvatarSideIconConverter.IconNameToUri(avatar.SideIcon);
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 Uri SideIcon { 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!;
}

View File

@@ -0,0 +1,36 @@
// 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 Team : List<ComplexAvatar>
{
/// <summary>
/// 构造一个新的队伍
/// </summary>
/// <param name="team">队伍</param>
/// <param name="idAvatarMap">映射</param>
public Team(ItemRate<string, int> team, Dictionary<int, Avatar> idAvatarMap)
: base(4)
{
IEnumerable<int> ids = team.Item.Split(',').Select(i => int.Parse(i));
foreach (int id in ids)
{
Add(new(idAvatarMap[id], 0));
}
Rate = $"上场 {team.Rate} 次";
}
/// <summary>
/// 上场次数
/// </summary>
public string Rate { get; set; }
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.LaunchGame;
/// <summary>
/// 服务器方案
/// </summary>
/// <summary>
/// 启动方案
/// </summary>
public class LaunchScheme
{
/// <summary>
/// 构造一个新的启动方案
/// </summary>
/// <param name="name">名称</param>
/// <param name="channel">通道</param>
/// <param name="cps">通道描述字符串</param>
/// <param name="subChannel">子通道</param>
/// <param name="launcherId">启动器Id</param>
public LaunchScheme(string name, string channel, string subChannel, string launcherId)
{
Name = name;
Channel = channel;
SubChannel = subChannel;
LauncherId = launcherId;
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 通道
/// </summary>
public string Channel { get; set; }
/// <summary>
/// 子通道
/// </summary>
public string SubChannel { get; set; }
/// <summary>
/// 启动器Id
/// </summary>
public string LauncherId { get; set; }
}

View File

@@ -1,25 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Metadata.Achievement;
namespace Snap.Hutao.Model.Binding.LaunchGame;
/// <summary>
/// 成就触发器类型
/// 启动类型
/// </summary>
public enum AchievementTriggerType
public enum SchemeType
{
/// <summary>
/// 任务
/// 国际服
/// </summary>
Quest = 1,
Mihoyo,
/// <summary>
/// 子任务
/// 国服官服
/// </summary>
SubQuest = 2,
Officical,
/// <summary>
/// 日常任务
/// 渠道服
/// </summary>
DailyTask = 3,
Bilibili,
}

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