mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb2665b75e | ||
|
|
d22ac39c1d | ||
|
|
a312603d61 | ||
|
|
0732ea0e06 | ||
|
|
e4d2b3055c | ||
|
|
5668931230 | ||
|
|
5126337138 | ||
|
|
4d634d3264 | ||
|
|
15a69fd0de | ||
|
|
c232891fe7 | ||
|
|
c35c2a5700 | ||
|
|
42305094f8 | ||
|
|
9ef48ab05c | ||
|
|
eec010870a | ||
|
|
a24fbf535d | ||
|
|
f7bd184a3c | ||
|
|
267f285101 | ||
|
|
2a1e77a9db | ||
|
|
abdc8e2e9f | ||
|
|
64f1af293b | ||
|
|
e0336d6b30 | ||
|
|
bf08ffa89e | ||
|
|
af4180bdeb | ||
|
|
a70593c529 |
8
.github/ISSUE_TEMPLATE/network-issue.yml
vendored
8
.github/ISSUE_TEMPLATE/network-issue.yml
vendored
@@ -22,7 +22,7 @@ body:
|
||||
**这个工具将会生成一份报告,请将这份报告拖入下面的框中,让其与你的工单一起被上传提交**
|
||||
- 你可以点击下面的链接以下载网络诊断工具:
|
||||
- [胡桃资源站](https://d.hut.ao/d/tools/network-diagnosis-tool.exe)
|
||||
- [GitHub](https://github.com/DGP-Studio/Snap.Hutao/files/10081999/network-diagnosis-hutao.zip)
|
||||
- [GitHub](https://github.com/Masterain98/network-diagnosis-tool/releases/latest/download/network-diagnosis-hutao.exe)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -41,7 +41,7 @@ body:
|
||||
id: user-isp
|
||||
attributes:
|
||||
label: 你的运营商
|
||||
description: 中国用户请精确到省级行政区,海外地区请精确到国家
|
||||
description: 海外用户请选其它
|
||||
options:
|
||||
- 中国电信
|
||||
- 中国联通
|
||||
@@ -60,7 +60,9 @@ body:
|
||||
- 完全无法连接服务器
|
||||
- 连接速度慢
|
||||
- 获取到了不正确的页面或数据
|
||||
- 图片下载错误(429 Error)
|
||||
- 客户端提示 429 Error
|
||||
- 客户端图片下载错误
|
||||
- 客户端图片预下载错误
|
||||
- 其它
|
||||
validations:
|
||||
required: true
|
||||
|
||||
16
.github/workflows/PublishDistribution.yml
vendored
16
.github/workflows/PublishDistribution.yml
vendored
@@ -16,10 +16,10 @@ jobs:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Download Publish.zip
|
||||
# Download Assets
|
||||
- name: Download Release
|
||||
timeout-minutes: 5
|
||||
uses: robinraju/release-downloader@v1.5
|
||||
uses: robinraju/release-downloader@v1.7
|
||||
with:
|
||||
repository: "DGP-Studio/Snap.Hutao"
|
||||
latest: true
|
||||
@@ -38,4 +38,14 @@ jobs:
|
||||
$RCCONF
|
||||
EOF
|
||||
|
||||
rclone copy ./release-download/* dgpODCN:/snaphutao/Releases/
|
||||
rclone copy ./release-download/* dgpODCN:/releases/
|
||||
|
||||
# Purge Patch System Cache
|
||||
- name: Purge Patch
|
||||
env:
|
||||
PATCH_HOSTS: ${{ secrets.PATCH_HOSTS }}
|
||||
PURGE_TOKEN: ${{ secrets.PURGE_TOKEN }}
|
||||
PURGE_URL: ${{ secrets.PURGE_URL }}
|
||||
run: |
|
||||
sudo echo "$PATCH_HOSTS" | sudo tee -a /etc/hosts
|
||||
curl --header "Authorization: token $PURGE_TOKEN" $PURGE_URL
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# [Snap.Hutao](https://hut.ao)
|
||||
|
||||

|
||||
|
||||
> 唷,找本堂主有何贵干呀?
|
||||
|
||||

|
||||
|
||||
# 特别感谢
|
||||
|
||||
### 原神组织与个人
|
||||
|
||||
* [HolographicHat](https://github.com/HolographicHat)
|
||||
* [UIGF organization](https://uigf.org)
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ steps:
|
||||
"Package/Identity/@Publisher": "CN=DGP Studio CI",
|
||||
"Package/Identity/@Version": "$(build_date).$(rev_number)",
|
||||
"Package/Properties/DisplayName": "胡桃 Alpha",
|
||||
"Package/Properties/PublisherDisplayName":"DGP Studio CI"
|
||||
"Package/Properties/PublisherDisplayName":"DGP Studio CI",
|
||||
"Package/Applications/Application/uap:VisualElements/@DisplayName": "胡桃 Alpha"
|
||||
}
|
||||
|
||||
- task: CmdLine@2
|
||||
@@ -165,3 +166,15 @@ steps:
|
||||
isPreRelease: true
|
||||
changeLogCompareToRelease: 'lastFullRelease'
|
||||
changeLogType: 'commitBased'
|
||||
|
||||
- task: DownloadSecureFile@1
|
||||
name: RcloneConfigFile
|
||||
displayName: Download Rclone Config
|
||||
inputs:
|
||||
secureFile: 'rclone.conf'
|
||||
|
||||
- task: rclone@1
|
||||
displayName: Upload CI via Rclone
|
||||
inputs:
|
||||
arguments: 'copy $(Build.ArtifactStagingDirectory)/* downloadDGPCN:/releases/Alpha/'
|
||||
configPath: '$(RcloneConfigFile.secureFilePath)/rclone.conf'
|
||||
@@ -14,9 +14,9 @@ csharp_style_expression_bodied_accessors = when_on_single_line:silent
|
||||
csharp_style_expression_bodied_lambdas = when_on_single_line:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
csharp_style_var_for_built_in_types = false:silent
|
||||
csharp_style_var_when_type_is_apparent = false:silent
|
||||
csharp_style_var_elsewhere = false:silent
|
||||
csharp_style_var_for_built_in_types = false:warning
|
||||
csharp_style_var_when_type_is_apparent = false:warning
|
||||
csharp_style_var_elsewhere = false:warning
|
||||
csharp_prefer_simple_using_statement = false:suggestion
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_namespace_declarations = file_scoped:silent
|
||||
@@ -24,11 +24,11 @@ csharp_prefer_static_local_function = false:suggestion
|
||||
|
||||
[*.{cs,vb}]
|
||||
end_of_line = crlf
|
||||
dotnet_style_qualification_for_field = false:silent
|
||||
dotnet_style_qualification_for_property = false:silent
|
||||
dotnet_style_qualification_for_method = false:silent
|
||||
dotnet_style_qualification_for_event = false:silent
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||
dotnet_code_quality_unused_parameters = non_public:suggestion
|
||||
dotnet_style_readonly_field = true:suggestion
|
||||
indent_size = 4
|
||||
@@ -165,6 +165,7 @@ dotnet_diagnostic.CA1805.severity = suggestion
|
||||
dotnet_diagnostic.VSTHRD111.severity = suggestion
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_prefer_readonly_struct = true:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:启用分析器发布跟踪", Justification = "<挂起>", Scope = "member", Target = "~F:Snap.Hutao.SourceGeneration.TodoAnalyzer.Descriptor")]
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// 高亮TODO
|
||||
/// </summary>
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
internal class TodoAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor Descriptor =
|
||||
new("SH0001", "TODO 项尚未实现", "此 TODO 项需要实现", "Standard", DiagnosticSeverity.Info, true);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get => ImmutableArray.Create(Descriptor); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterSyntaxTreeAction(HandleSyntaxTree);
|
||||
}
|
||||
|
||||
private static void HandleSyntaxTree(SyntaxTreeAnalysisContext context)
|
||||
{
|
||||
SyntaxNode root = context.Tree.GetCompilationUnitRoot(context.CancellationToken);
|
||||
foreach (SyntaxTrivia node in root.DescendantTrivia(descendIntoTrivia: true))
|
||||
{
|
||||
switch (node.Kind())
|
||||
{
|
||||
case SyntaxKind.SingleLineCommentTrivia:
|
||||
case SyntaxKind.MultiLineCommentTrivia:
|
||||
string text = node.ToString().ToLowerInvariant();
|
||||
if (text.Contains("todo:"))
|
||||
{
|
||||
string hint = node.ToString().Substring(text.IndexOf("todo:") + 6);
|
||||
DiagnosticDescriptor descriptor = new("SH0001", "TODO 项尚未实现", hint, "Standard", DiagnosticSeverity.Info, true);
|
||||
context.ReportDiagnostic(Diagnostic.Create(descriptor, node.GetLocation()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<muxc:XamlControlsResources/>
|
||||
<ResourceDictionary Source="ms-appx:///SettingsUI/Themes/Generic.xaml"/>
|
||||
<ResourceDictionary Source="Control/Theme/FontStyle.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
@@ -27,8 +28,6 @@
|
||||
<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>
|
||||
@@ -90,6 +89,7 @@
|
||||
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
|
||||
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
|
||||
<!-- Styles -->
|
||||
|
||||
<Style
|
||||
x:Key="LargeGridViewItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
using CommunityToolkit.WinUI.Notifications;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Exception;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此类支持取消任务
|
||||
/// </summary>
|
||||
public interface ISupportCancellation
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于通知事件取消的取消令牌
|
||||
/// </summary>
|
||||
CancellationToken CancellationToken { get; set; }
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.ViewModel.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Control;
|
||||
|
||||
@@ -33,9 +34,9 @@ public class ScopedPage : Page
|
||||
/// </summary>
|
||||
/// <typeparam name="TViewModel">视图模型类型</typeparam>
|
||||
public void InitializeWith<TViewModel>()
|
||||
where TViewModel : class, ISupportCancellation
|
||||
where TViewModel : class, IViewModel
|
||||
{
|
||||
ISupportCancellation viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
IViewModel viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
@@ -59,11 +60,22 @@ public class ScopedPage : Page
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
base.OnNavigatingFrom(e);
|
||||
viewLoadingCancellationTokenSource.Cancel();
|
||||
using (viewLoadingCancellationTokenSource)
|
||||
{
|
||||
// Cancel tasks executed by the view model
|
||||
viewLoadingCancellationTokenSource.Cancel();
|
||||
IViewModel viewModel = (IViewModel)DataContext;
|
||||
|
||||
// Try dispose scope when page is not presented
|
||||
serviceScope.Dispose();
|
||||
viewLoadingCancellationTokenSource.Dispose();
|
||||
using (SemaphoreSlim locker = viewModel.DisposeLock)
|
||||
{
|
||||
// Wait to ensure viewmodel operation is completed
|
||||
locker.Wait();
|
||||
viewModel.IsViewDisposed = true;
|
||||
|
||||
// Dispose the scope
|
||||
serviceScope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
210
src/Snap.Hutao/Snap.Hutao/Control/Theme/FontStyle.xaml
Normal file
210
src/Snap.Hutao/Snap.Hutao/Control/Theme/FontStyle.xaml
Normal file
@@ -0,0 +1,210 @@
|
||||
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
|
||||
<!-- Licensed under the MIT License. -->
|
||||
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:wsc="using:WinUICommunity.SettingsUI.Controls">
|
||||
<FontFamily x:Key="MiSans">ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
|
||||
<FontFamily x:Key="CascadiaMonoAndMiSans">ms-appx:///Resource/Font/CascadiaMono.ttf#Cascadia Mono, ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
|
||||
|
||||
<StaticResource x:Key="PivotHeaderItemFontFamily" ResourceKey="MiSans"/>
|
||||
<StaticResource x:Key="ContentControlThemeFontFamily" ResourceKey="MiSans"/>
|
||||
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock"/>
|
||||
<Style x:Key="BaseTextBlockStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
<Setter Property="FontSize" Value="{StaticResource BodyTextBlockFontSize}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="LineStackingStrategy" Value="MaxHeight"/>
|
||||
<Setter Property="TextLineBounds" Value="Full"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="HeaderTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="46"/>
|
||||
<Setter Property="FontWeight" Value="Light"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="SubheaderTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="34"/>
|
||||
<Setter Property="FontWeight" Value="Light"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="TitleTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource TitleTextBlockFontSize}"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="SubtitleTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource SubtitleTextBlockFontSize}"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="BodyTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontWeight" Value="Normal"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="CaptionTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource CaptionTextBlockFontSize}"/>
|
||||
<Setter Property="FontWeight" Value="Normal"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="BodyStrongTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource BodyStrongTextBlockFontSize}"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="TitleLargeTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource TitleLargeTextBlockFontSize}"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="DisplayTextBlockStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="{StaticResource DisplayTextBlockFontSize}"/>
|
||||
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultMenuFlyoutItemStyle}" TargetType="MenuFlyoutItem">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultMenuFlyoutSubItemStyle}" TargetType="MenuFlyoutSubItem">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="InfoBar">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultSettingStyle}" TargetType="wsc:Setting"/>
|
||||
|
||||
<Style x:Key="DefaultSettingStyle" TargetType="wsc:Setting">
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource CardBorderThickness}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Padding" Value="16"/>
|
||||
<Setter Property="Margin" Value="0,0,0,0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="wsc:Setting">
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
MinHeight="48"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<!-- Icon -->
|
||||
<ColumnDefinition Width="*"/>
|
||||
<!-- Header and subtitle -->
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<!-- Action control -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentPresenter
|
||||
x:Name="IconPresenter"
|
||||
MaxWidth="20"
|
||||
Margin="2,0,18,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Content="{TemplateBinding Icon}"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="20"
|
||||
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
|
||||
IsTextScaleFactorEnabled="False"/>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="0,0,16,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<TextBlock
|
||||
x:Name="HeaderPresenter"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource MiSans}"
|
||||
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
|
||||
Text="{TemplateBinding Header}"/>
|
||||
|
||||
<ContentPresenter
|
||||
x:Name="DescriptionPresenter"
|
||||
Content="{TemplateBinding Description}"
|
||||
FontFamily="{StaticResource MiSans}"
|
||||
FontSize="{StaticResource SecondaryTextFontSize}"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="WrapWholeWords">
|
||||
<ContentPresenter.Resources>
|
||||
<Style BasedOn="{StaticResource CaptionTextBlockStyle}" TargetType="TextBlock">
|
||||
<Style.Setters>
|
||||
<Setter Property="TextWrapping" Value="WrapWholeWords"/>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
<Style BasedOn="{StaticResource TextButtonStyle}" TargetType="HyperlinkButton">
|
||||
<Style.Setters>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Padding" Value="0,0,0,0"/>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
</ContentPresenter.Resources>
|
||||
</ContentPresenter>
|
||||
</StackPanel>
|
||||
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding ActionContent}"/>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal"/>
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="HeaderPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
|
||||
<Setter Target="DescriptionPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
|
||||
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
|
||||
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Control;
|
||||
/// </summary>
|
||||
/// <typeparam name="TFrom">源类型</typeparam>
|
||||
/// <typeparam name="TTo">目标类型</typeparam>
|
||||
public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
|
||||
public abstract class ValueConverter<TFrom, TTo> : IValueConverter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
@@ -23,7 +23,7 @@ public abstract class ValueConverterBase<TFrom, TTo> : IValueConverter
|
||||
catch (Exception ex)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<ILogger<ValueConverterBase<TFrom, TTo>>>()
|
||||
.GetRequiredService<ILogger<ValueConverter<TFrom, TTo>>>()
|
||||
.LogError(ex, "值转换器异常");
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
@@ -24,7 +25,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
{
|
||||
private const string CacheFolderName = nameof(ImageCache);
|
||||
|
||||
private static readonly ImmutableDictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
|
||||
private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
|
||||
{
|
||||
[0] = TimeSpan.FromSeconds(4),
|
||||
[1] = TimeSpan.FromSeconds(16),
|
||||
@@ -32,13 +33,13 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
[3] = TimeSpan.FromSeconds(4),
|
||||
[4] = TimeSpan.FromSeconds(16),
|
||||
[5] = TimeSpan.FromSeconds(64),
|
||||
}.ToImmutableDictionary();
|
||||
};
|
||||
|
||||
private readonly ILogger logger;
|
||||
|
||||
// violate di rule
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
|
||||
|
||||
private string? baseFolder;
|
||||
private string? cacheFolder;
|
||||
|
||||
@@ -100,11 +101,30 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
/// <inheritdoc/>
|
||||
public async Task<string> GetFileFromCacheAsync(Uri uri)
|
||||
{
|
||||
string filePath = Path.Combine(GetCacheFolder(), GetCacheFileName(uri));
|
||||
string fileName = GetCacheFileName(uri);
|
||||
string filePath = Path.Combine(GetCacheFolder(), fileName);
|
||||
|
||||
if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0)
|
||||
{
|
||||
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
try
|
||||
{
|
||||
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
|
||||
{
|
||||
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (concurrentTasks.TryGetValue(fileName, out Task? task))
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
taskCompletionSource.TrySetResult();
|
||||
}
|
||||
}
|
||||
|
||||
return filePath;
|
||||
@@ -191,7 +211,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
|
||||
if (retryCount == 3)
|
||||
{
|
||||
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao, }.Uri;
|
||||
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao }.Uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,13 @@ internal static class IocConfiguration
|
||||
}
|
||||
}
|
||||
|
||||
return services.AddDbContext<AppDbContext>(builder => builder.UseSqlite(sqlConnectionString));
|
||||
return services.AddDbContext<AppDbContext>(builder =>
|
||||
{
|
||||
builder
|
||||
#if DEBUG
|
||||
.EnableSensitiveDataLogging()
|
||||
#endif
|
||||
.UseSqlite(sqlConnectionString);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@ namespace Snap.Hutao.Core.DependencyInjection;
|
||||
/// </summary>
|
||||
public static class ServiceScopeExtension
|
||||
{
|
||||
private static IServiceScope? scopeReference;
|
||||
// Allow GC to Collect the IServiceScope
|
||||
private static readonly WeakReference<IServiceScope> ScopeReference = new(null!);
|
||||
|
||||
/// <summary>
|
||||
/// 追踪服务范围
|
||||
@@ -19,7 +20,7 @@ public static class ServiceScopeExtension
|
||||
public static void Track(this IServiceScope scope)
|
||||
{
|
||||
DisposeLast();
|
||||
scopeReference = scope;
|
||||
ScopeReference.SetTarget(scope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -27,6 +28,9 @@ public static class ServiceScopeExtension
|
||||
/// </summary>
|
||||
public static void DisposeLast()
|
||||
{
|
||||
scopeReference?.Dispose();
|
||||
if (ScopeReference.TryGetTarget(out IServiceScope? scope))
|
||||
{
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,8 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
|
||||
namespace Snap.Hutao.Core.Exception;
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
/// <summary>
|
||||
/// 异常记录器
|
||||
@@ -27,10 +26,13 @@ internal class ExceptionRecorder
|
||||
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD002")]
|
||||
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Ioc.Default.GetRequiredService<HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
|
||||
#if RELEASE
|
||||
#pragma warning disable VSTHRD002
|
||||
Ioc.Default.GetRequiredService<Web.Hutao.HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002
|
||||
#endif
|
||||
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
|
||||
|
||||
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())
|
||||
@@ -43,4 +45,4 @@ internal class ExceptionRecorder
|
||||
{
|
||||
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
/// <summary>
|
||||
/// 用户数据损坏异常
|
||||
/// </summary>
|
||||
internal class UserdataCorruptedException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的用户数据损坏异常
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="innerException">内部错误</param>
|
||||
public UserdataCorruptedException(string message, Exception innerException)
|
||||
: base($"用户数据已损坏: {message}", innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Snap.Hutao.Core.ExpressionService;
|
||||
|
||||
/// <summary>
|
||||
/// 枚举帮助类
|
||||
/// </summary>
|
||||
public static class EnumExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断枚举是否有对应的Flag
|
||||
/// </summary>
|
||||
/// <typeparam name="T">枚举类型</typeparam>
|
||||
/// <param name="enum">待检查的枚举</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>是否有对应的Flag</returns>
|
||||
public static bool HasOption<T>(this T @enum, T value)
|
||||
where T : struct, Enum
|
||||
{
|
||||
return ExpressionCache<T>.Entry(@enum, value);
|
||||
}
|
||||
|
||||
private static class ExpressionCache<T>
|
||||
{
|
||||
public static readonly Func<T, T, bool> Entry = Get();
|
||||
|
||||
private static Func<T, T, bool> Get()
|
||||
{
|
||||
ParameterExpression paramSource = Expression.Parameter(typeof(T));
|
||||
ParameterExpression paramValue = Expression.Parameter(typeof(T));
|
||||
|
||||
BinaryExpression logicalAnd = Expression.AndAssign(paramSource, paramValue);
|
||||
BinaryExpression equal = Expression.Equal(logicalAnd, paramValue);
|
||||
|
||||
// 生成一个源类型入,目标类型出的 lamdba
|
||||
return Expression.Lambda<Func<T, T, bool>>(equal, paramSource, paramValue).Compile();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,11 @@ namespace Snap.Hutao.Core.IO.Bits;
|
||||
[SuppressMessage("", "SA1600")]
|
||||
internal class BitsJob : DisposableObject, IBackgroundCopyCallback
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务名称前缀
|
||||
/// </summary>
|
||||
public const string JobNamePrefix = "SnapHutaoBitsJob";
|
||||
|
||||
private const uint BitsEngineNoProgressTimeout = 120;
|
||||
private const int MaxResumeAttempts = 10;
|
||||
|
||||
@@ -43,12 +48,14 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
|
||||
public static BitsJob CreateJob(IServiceProvider serviceProvider, IBackgroundCopyManager backgroundCopyManager, Uri uri, string filePath)
|
||||
{
|
||||
ILogger<BitsJob> service = serviceProvider.GetRequiredService<ILogger<BitsJob>>();
|
||||
string text = $"BitsDownloadJob - {uri}";
|
||||
string text = $"{JobNamePrefix} - {uri}";
|
||||
IBackgroundCopyJob ppJob;
|
||||
try
|
||||
{
|
||||
backgroundCopyManager.CreateJob(text, BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out Guid _, out ppJob);
|
||||
ppJob.SetNotifyFlags(11u);
|
||||
|
||||
// BG_NOTIFY_JOB_TRANSFERRED & BG_NOTIFY_JOB_ERROR & BG_NOTIFY_JOB_MODIFICATION
|
||||
ppJob.SetNotifyFlags(0b1011);
|
||||
ppJob.SetNoProgressTimeout(BitsEngineNoProgressTimeout);
|
||||
ppJob.SetPriority(BG_JOB_PRIORITY.BG_JOB_PRIORITY_FOREGROUND);
|
||||
ppJob.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_AUTODETECT, null, null);
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Networking.BackgroundIntelligentTransferService;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Bits;
|
||||
@@ -40,6 +43,41 @@ internal class BitsManager
|
||||
return new(result, tempFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消所有先前创建的任务
|
||||
/// </summary>
|
||||
public void CancelAllJobs()
|
||||
{
|
||||
IBackgroundCopyManager value;
|
||||
try
|
||||
{
|
||||
value = lazyBackgroundCopyManager.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
value.EnumJobs(0, out IEnumBackgroundCopyJobs pJobs);
|
||||
pJobs.GetCount(out uint count);
|
||||
|
||||
List<IBackgroundCopyJob> jobsToCancel = new();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
uint actualFetched = 0;
|
||||
pJobs.Next(1, out IBackgroundCopyJob pJob, ref actualFetched);
|
||||
pJob.GetDisplayName(out PWSTR name);
|
||||
if (name.AsSpan().StartsWith(BitsJob.JobNamePrefix))
|
||||
{
|
||||
jobsToCancel.Add(pJob);
|
||||
}
|
||||
}
|
||||
|
||||
jobsToCancel.ForEach(job => job.Cancel());
|
||||
}
|
||||
|
||||
private bool DownloadCore(Uri uri, string tempFile, Action<ProgressUpdateStatus> progress, CancellationToken token)
|
||||
{
|
||||
IBackgroundCopyManager value;
|
||||
@@ -48,29 +86,37 @@ internal class BitsManager
|
||||
{
|
||||
value = lazyBackgroundCopyManager.Value;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogWarning("BITS download engine not supported: {message}", ex.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
|
||||
try
|
||||
{
|
||||
try
|
||||
using (BitsJob bitsJob = BitsJob.CreateJob(serviceProvider, value, uri, tempFile))
|
||||
{
|
||||
bitsJob.WaitForCompletion(progress, token);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "BITS download failed:");
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
bitsJob.WaitForCompletion(progress, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "BITS download failed:");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bitsJob.ErrorCode != 0)
|
||||
{
|
||||
return false;
|
||||
if (bitsJob.ErrorCode != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// BITS job creation failed
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
112
src/Snap.Hutao/Snap.Hutao/Core/IO/FilePath.cs
Normal file
112
src/Snap.Hutao/Snap.Hutao/Core/IO/FilePath.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 文件路径
|
||||
/// </summary>
|
||||
internal readonly struct FilePath : IEquatable<FilePath>
|
||||
{
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public readonly string Value;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FilePath"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="value">value</param>
|
||||
public FilePath(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator string(FilePath value)
|
||||
{
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
public static implicit operator FilePath(string value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
public static bool operator ==(FilePath left, FilePath right)
|
||||
{
|
||||
return left.Value == right.Value;
|
||||
}
|
||||
|
||||
public static bool operator !=(FilePath left, FilePath right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步反序列化文件中的内容
|
||||
/// </summary>
|
||||
/// <typeparam name="T">内容的类型</typeparam>
|
||||
/// <param name="options">序列化选项</param>
|
||||
/// <returns>操作是否成功,反序列化后的内容</returns>
|
||||
public async Task<ValueResult<bool, T?>> DeserializeFromJsonAsync<T>(JsonSerializerOptions options)
|
||||
where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream stream = File.OpenRead(Value))
|
||||
{
|
||||
T? t = await JsonSerializer.DeserializeAsync<T>(stream, options).ConfigureAwait(false);
|
||||
return new(true, t);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ = ex;
|
||||
return new(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将对象异步序列化入文件
|
||||
/// </summary>
|
||||
/// <typeparam name="T">对象的类型</typeparam>
|
||||
/// <param name="obj">对象</param>
|
||||
/// <param name="options">序列化选项</param>
|
||||
/// <returns>操作是否成功</returns>
|
||||
public async Task<bool> SerializeToJsonAsync<T>(T obj, JsonSerializerOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream stream = File.Create(Value))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(stream, obj, options).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(FilePath other)
|
||||
{
|
||||
return Value == other.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is FilePath other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
}
|
||||
77
src/Snap.Hutao/Snap.Hutao/Core/IO/PickerExtension.cs
Normal file
77
src/Snap.Hutao/Snap.Hutao/Core/IO/PickerExtension.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Pickers;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 选择器拓展
|
||||
/// </summary>
|
||||
internal static class PickerExtension
|
||||
{
|
||||
/// <inheritdoc cref="FileOpenPicker.PickSingleFileAsync"/>
|
||||
public static async Task<ValueResult<bool, FilePath>> TryPickSingleFileAsync(this FileOpenPicker picker)
|
||||
{
|
||||
StorageFile? file;
|
||||
Exception? exception = null;
|
||||
try
|
||||
{
|
||||
file = await picker.PickSingleFileAsync().AsTask().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
file = null;
|
||||
}
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
return new(true, file.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (exception != null)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<Service.Abstraction.IInfoBarService>()
|
||||
.Warning($"无法打开文件选择器 {exception.Message}");
|
||||
}
|
||||
|
||||
return new(false, null!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="FileSavePicker.PickSaveFileAsync"/>
|
||||
public static async Task<ValueResult<bool, FilePath>> TryPickSaveFileAsync(this FileSavePicker picker)
|
||||
{
|
||||
StorageFile? file;
|
||||
Exception? exception = null;
|
||||
try
|
||||
{
|
||||
file = await picker.PickSaveFileAsync().AsTask().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
file = null;
|
||||
}
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
return new(true, file.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (exception != null)
|
||||
{
|
||||
Ioc.Default
|
||||
.GetRequiredService<Service.Abstraction.IInfoBarService>()
|
||||
.Warning($"无法打开文件选择器 {exception.Message}");
|
||||
}
|
||||
|
||||
return new(false, null!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 文件拓展
|
||||
/// </summary>
|
||||
public static class StorageFileExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步反序列化文件中的内容
|
||||
/// </summary>
|
||||
/// <typeparam name="T">内容的类型</typeparam>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="options">序列化选项</param>
|
||||
/// <returns>操作是否成功,反序列化后的内容</returns>
|
||||
public static async Task<ValueResult<bool, T?>> DeserializeFromJsonAsync<T>(this StorageFile file, JsonSerializerOptions options)
|
||||
where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream stream = File.OpenRead(file.Path))
|
||||
{
|
||||
T? t = await JsonSerializer.DeserializeAsync<T>(stream, options).ConfigureAwait(false);
|
||||
return new(true, t);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_ = ex;
|
||||
return new(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将对象异步序列化入文件
|
||||
/// </summary>
|
||||
/// <typeparam name="T">对象的类型</typeparam>
|
||||
/// <param name="file">文件</param>
|
||||
/// <param name="obj">对象</param>
|
||||
/// <param name="options">序列化选项</param>
|
||||
/// <returns>操作是否成功</returns>
|
||||
public static async Task<bool> SerializeToJsonAsync<T>(this StorageFile file, T obj, JsonSerializerOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (FileStream stream = File.Create(file.Path))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(stream, obj, options).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ internal sealed class TempFile : IDisposable
|
||||
File.Copy(file, temporaryFile.Path, true);
|
||||
return temporaryFile;
|
||||
}
|
||||
catch (System.Exception)
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -55,4 +55,4 @@ internal sealed class TempFile : IDisposable
|
||||
{
|
||||
File.Delete(Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,27 +14,26 @@ namespace Snap.Hutao.Core.LifeCycle;
|
||||
/// </summary>
|
||||
internal static class AppInstanceExtension
|
||||
{
|
||||
// Hold the reference here to prevent memory corruption.
|
||||
private static HANDLE redirectEventHandle = HANDLE.Null;
|
||||
|
||||
/// <summary>
|
||||
/// 同步非阻塞重定向
|
||||
/// </summary>
|
||||
/// <param name="appInstance">app实例</param>
|
||||
/// <param name="args">参数</param>
|
||||
[SuppressMessage("", "VSTHRD002")]
|
||||
[SuppressMessage("", "VSTHRD110")]
|
||||
public static void RedirectActivationTo(this AppInstance appInstance, AppActivationArguments args)
|
||||
public static unsafe void RedirectActivationTo(this AppInstance appInstance, AppActivationArguments args)
|
||||
{
|
||||
HANDLE redirectEventHandle = UnsafeCreateEvent();
|
||||
Task.Run(async () =>
|
||||
redirectEventHandle = CreateEvent(default(SECURITY_ATTRIBUTES*), true, false, null);
|
||||
Task.Run(() =>
|
||||
{
|
||||
await appInstance.RedirectActivationToAsync(args);
|
||||
appInstance.RedirectActivationToAsync(args).AsTask().Wait();
|
||||
SetEvent(redirectEventHandle);
|
||||
});
|
||||
|
||||
ReadOnlySpan<HANDLE> handles = new(in redirectEventHandle);
|
||||
CoWaitForMultipleObjects((uint)CWMO_FLAGS.CWMO_DEFAULT, INFINITE, handles, out uint _);
|
||||
}
|
||||
|
||||
private static unsafe HANDLE UnsafeCreateEvent()
|
||||
{
|
||||
return CreateEvent(default(SECURITY_ATTRIBUTES*), true, false, null);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Win32.TaskScheduler;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using SchedulerTask = Microsoft.Win32.TaskScheduler.Task;
|
||||
|
||||
@@ -23,6 +24,7 @@ internal static class ScheduleTaskHelper
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: 似乎可以不删除任务,直接注册已经包含了更新功能
|
||||
SchedulerTask? targetTask = TaskService.Instance.GetTask(DailyNoteRefreshTaskName);
|
||||
if (targetTask != null)
|
||||
{
|
||||
@@ -36,12 +38,9 @@ internal static class ScheduleTaskHelper
|
||||
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);
|
||||
return true;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (COMException)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ = ex;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,9 @@ internal static class SettingKeys
|
||||
/// 静态资源合约V2 成就图标与物品图标
|
||||
/// </summary>
|
||||
public const string StaticResourceV2Contract = "StaticResourceV2Contract";
|
||||
|
||||
/// <summary>
|
||||
/// 静态资源合约V3 刷新 Skill Talent
|
||||
/// </summary>
|
||||
public const string StaticResourceV3Contract = "StaticResourceV3Contract";
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Threading.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// 在复杂的异步方法环境下
|
||||
/// 指示方法的线程访问状态
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
internal class ThreadAccessAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 指示方法的进入线程访问状态
|
||||
/// </summary>
|
||||
/// <param name="enter">进入状态</param>
|
||||
public ThreadAccessAttribute(ThreadAccessState enter)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Threading.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// 线程访问情况
|
||||
/// </summary>
|
||||
internal enum ThreadAccessState
|
||||
{
|
||||
/// <summary>
|
||||
/// 任何线程均有可能访问该方法
|
||||
/// </summary>
|
||||
AnyThread,
|
||||
|
||||
/// <summary>
|
||||
/// 仅主线程有机会访问该方法
|
||||
/// 仅允许主线程访问该方法
|
||||
/// </summary>
|
||||
MainThread,
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 调度器队列拓展
|
||||
/// </summary>
|
||||
public static class DispatcherQueueExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 在调度器队列同步调用,直到执行结束,会持续阻塞当前线程
|
||||
/// </summary>
|
||||
/// <param name="dispatcherQueue">调度器队列</param>
|
||||
/// <param name="action">执行的回调</param>
|
||||
public static void Invoke(this DispatcherQueue dispatcherQueue, Action action)
|
||||
{
|
||||
using (ManualResetEventSlim blockEvent = new())
|
||||
{
|
||||
dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
action();
|
||||
blockEvent.Set();
|
||||
});
|
||||
|
||||
blockEvent.Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,9 +43,6 @@ public readonly struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueue
|
||||
/// <inheritdoc/>
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
dispatherQueue.TryEnqueue(() =>
|
||||
{
|
||||
continuation();
|
||||
});
|
||||
dispatherQueue.TryEnqueue(() => continuation());
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,19 @@ public static class SemaphoreSlimExtensions
|
||||
/// 异步进入信号量
|
||||
/// </summary>
|
||||
/// <param name="semaphoreSlim">信号量</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>可释放的对象,用于释放信号量</returns>
|
||||
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim)
|
||||
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim, CancellationToken token = default)
|
||||
{
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await semaphoreSlim.WaitAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
throw new OperationCanceledException("信号量已经被释放,操作取消", ex);
|
||||
}
|
||||
|
||||
return new SemaphoreSlimReleaser(semaphoreSlim);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,4 +95,4 @@ public static class TaskExtensions
|
||||
onException?.Invoke(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,4 +31,21 @@ internal static class ThreadHelper
|
||||
{
|
||||
return new(Program.DispatcherQueue!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在主线程上同步等待执行操作
|
||||
/// </summary>
|
||||
/// <param name="action">操作</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void InvokeOnMainThread(Action action)
|
||||
{
|
||||
if (Program.DispatcherQueue!.HasThreadAccess)
|
||||
{
|
||||
action();
|
||||
}
|
||||
else
|
||||
{
|
||||
Program.DispatcherQueue.Invoke(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
|
||||
logger.LogInformation(EventIds.WindowState, "Postion: [{pos}], Size: [{size}]", pos, size);
|
||||
|
||||
// appWindow.Show(true);
|
||||
// appWindow.Show can't bring window to top.
|
||||
window.Activate();
|
||||
|
||||
systemBackdrop = new(window);
|
||||
@@ -190,7 +191,7 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
|
||||
}
|
||||
else
|
||||
{
|
||||
double scale = Persistence.GetScaleForWindow(handle);
|
||||
double scale = Persistence.GetScaleForWindowHandle(handle);
|
||||
|
||||
// 48 is the navigation button leftInset
|
||||
RectInt32 dragRect = StructMarshal.RectInt32(new(48, 0), titleBar.ActualSize).Scale(scale);
|
||||
|
||||
@@ -8,6 +8,7 @@ using Snap.Hutao.Win32;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using static Windows.Win32.PInvoke;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
@@ -22,28 +23,24 @@ internal static class Persistence
|
||||
/// </summary>
|
||||
/// <param name="appWindow">应用窗体</param>
|
||||
/// <param name="persistSize">持久化尺寸</param>
|
||||
/// <param name="size">初始尺寸</param>
|
||||
public static void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 size)
|
||||
/// <param name="initialSize">初始尺寸</param>
|
||||
public static unsafe void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 initialSize)
|
||||
{
|
||||
// Set first launch size.
|
||||
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
|
||||
SizeInt32 transformedSize = TransformSizeForWindow(size, hwnd);
|
||||
SizeInt32 transformedSize = TransformSizeForWindow(initialSize, hwnd);
|
||||
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
|
||||
|
||||
if (persistSize)
|
||||
{
|
||||
RectInt32 persistedSize = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (persistedSize.Width * persistedSize.Height > 848 * 524)
|
||||
RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (persistedRect.Size() >= initialSize.Size())
|
||||
{
|
||||
rect = persistedSize;
|
||||
rect = persistedRect;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
TransformToCenterScreen(&rect);
|
||||
}
|
||||
|
||||
TransformToCenterScreen(&rect);
|
||||
appWindow.MoveAndResize(rect);
|
||||
}
|
||||
|
||||
@@ -53,7 +50,15 @@ internal static class Persistence
|
||||
/// <param name="appWindow">应用窗体</param>
|
||||
public static void Save(AppWindow appWindow)
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
|
||||
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
|
||||
WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT();
|
||||
GetWindowPlacement(hwnd, ref windowPlacement);
|
||||
|
||||
// prevent save value when we are maximized.
|
||||
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,7 +66,7 @@ internal static class Persistence
|
||||
/// </summary>
|
||||
/// <param name="hwnd">窗体句柄</param>
|
||||
/// <returns>缩放比</returns>
|
||||
public static double GetScaleForWindow(HWND hwnd)
|
||||
public static double GetScaleForWindowHandle(HWND hwnd)
|
||||
{
|
||||
uint dpi = GetDpiForWindow(hwnd);
|
||||
return Math.Round(dpi / 96d, 2, MidpointRounding.AwayFromZero);
|
||||
@@ -69,7 +74,7 @@ internal static class Persistence
|
||||
|
||||
private static SizeInt32 TransformSizeForWindow(SizeInt32 size, HWND hwnd)
|
||||
{
|
||||
double scale = GetScaleForWindow(hwnd);
|
||||
double scale = GetScaleForWindowHandle(hwnd);
|
||||
return new((int)(size.Width * scale), (int)(size.Height * scale));
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ internal class WindowSubclassManager<TWindow> : IDisposable
|
||||
{
|
||||
case WM_GETMINMAXINFO:
|
||||
{
|
||||
double scalingFactor = Persistence.GetScaleForWindow(hwnd);
|
||||
double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd);
|
||||
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// 对象扩展
|
||||
/// </summary>
|
||||
public static class ObjectExtension
|
||||
internal static class ObjectExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// <see langword="as"/> 的链式调用扩展
|
||||
@@ -19,4 +19,4 @@ public static class ObjectExtension
|
||||
{
|
||||
return obj as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,26 +11,26 @@ namespace Snap.Hutao.Factory.Abstraction;
|
||||
internal interface IContentDialogFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建一个新的内容对话框,用于确认
|
||||
/// 异步确认
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="content">内容</param>
|
||||
/// <returns>内容对话框</returns>
|
||||
ContentDialog CreateForConfirm(string title, string content);
|
||||
/// <returns>结果</returns>
|
||||
ValueTask<ContentDialogResult> ConfirmAsync(string title, string content);
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的内容对话框,用于确认或取消
|
||||
/// 异步确认或取消
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="content">内容</param>
|
||||
/// <param name="defaultButton">默认按钮</param>
|
||||
/// <returns>内容对话框</returns>
|
||||
ContentDialog CreateForConfirmCancel(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close);
|
||||
/// <returns>结果</returns>
|
||||
ValueTask<ContentDialogResult> ConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close);
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的内容对话框,用于提示未知的进度
|
||||
/// 异步创建一个新的内容对话框,用于提示未知的进度
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <returns>内容对话框</returns>
|
||||
ContentDialog CreateForIndeterminateProgress(string title);
|
||||
ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title);
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
|
||||
namespace Snap.Hutao.Factory;
|
||||
|
||||
@@ -83,7 +82,6 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
|
||||
return command;
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD002")]
|
||||
private void ReportException(IAsyncRelayCommand command)
|
||||
{
|
||||
command.PropertyChanged += (sender, args) =>
|
||||
@@ -96,7 +94,6 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,38 @@ internal class ContentDialogFactory : IContentDialogFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ContentDialog CreateForConfirm(string title, string content)
|
||||
public async ValueTask<ContentDialogResult> ConfirmAsync(string title, string content)
|
||||
{
|
||||
ContentDialog dialog = await CreateForConfirmAsync(title, content).ConfigureAwait(false);
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
return await dialog.ShowAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ContentDialogResult> ConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
|
||||
{
|
||||
ContentDialog dialog = await CreateForConfirmCancelAsync(title, content, defaultButton).ConfigureAwait(false);
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
return await dialog.ShowAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = mainWindow.Content.XamlRoot,
|
||||
Title = title,
|
||||
Content = new ProgressBar() { IsIndeterminate = true },
|
||||
};
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private async ValueTask<ContentDialog> CreateForConfirmAsync(string title, string content)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = mainWindow.Content.XamlRoot,
|
||||
@@ -36,9 +66,9 @@ internal class ContentDialogFactory : IContentDialogFactory
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ContentDialog CreateForConfirmCancel(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
|
||||
private async ValueTask<ContentDialog> CreateForConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = mainWindow.Content.XamlRoot,
|
||||
@@ -51,17 +81,4 @@ internal class ContentDialogFactory : IContentDialogFactory
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ContentDialog CreateForIndeterminateProgress(string title)
|
||||
{
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = mainWindow.Content.XamlRoot,
|
||||
Title = title,
|
||||
Content = new ProgressBar() { IsIndeterminate = true },
|
||||
};
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class Avatar : ICalculableSource<ICalculableAvatar>
|
||||
/// <summary>
|
||||
/// 武器
|
||||
/// </summary>
|
||||
public Weapon Weapon { get; set; } = default!;
|
||||
public Weapon? Weapon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物列表
|
||||
|
||||
@@ -22,5 +22,8 @@ public class RankAvatar : Avatar
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 排行
|
||||
/// </summary>
|
||||
public int Value { get; set; }
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Passport;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using EntityUser = Snap.Hutao.Model.Entity.User;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.User;
|
||||
@@ -98,8 +99,8 @@ public class User : ObservableObject
|
||||
/// </summary>
|
||||
/// <param name="inner">数据库实体</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>用户是否初始化完成,若Cookie失效会返回 <see langword="false"/> </returns>
|
||||
internal static async Task<User?> ResumeAsync(EntityUser inner, CancellationToken token = default)
|
||||
/// <returns>用户</returns>
|
||||
internal static async Task<User> ResumeAsync(EntityUser inner, CancellationToken token = default)
|
||||
{
|
||||
User user = new(inner);
|
||||
bool isOk = await user.InitializeCoreAsync(token).ConfigureAwait(false);
|
||||
@@ -107,6 +108,7 @@ public class User : ObservableObject
|
||||
if (!isOk)
|
||||
{
|
||||
user.UserInfo = new UserInfo() { Nickname = "网络异常" };
|
||||
user.UserGameRoles = new();
|
||||
}
|
||||
|
||||
return user;
|
||||
@@ -117,7 +119,7 @@ public class User : ObservableObject
|
||||
/// </summary>
|
||||
/// <param name="cookie">cookie</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>用户是否初始化完成,若Cookie失效会返回 <see langword="null"/> </returns>
|
||||
/// <returns>用户</returns>
|
||||
internal static async Task<User?> CreateAsync(Cookie cookie, CancellationToken token = default)
|
||||
{
|
||||
// 这里只负责创建实体用户,稍后在用户服务中保存到数据库
|
||||
@@ -153,49 +155,76 @@ public class User : ObservableObject
|
||||
|
||||
using (IServiceScope scope = Ioc.Default.CreateScope())
|
||||
{
|
||||
UserInfo = await scope.ServiceProvider
|
||||
Response<UserFullInfoWrapper> response = await scope.ServiceProvider
|
||||
.GetRequiredService<UserClient2>()
|
||||
.GetUserFullInfoAsync(Entity, token)
|
||||
.ConfigureAwait(false);
|
||||
UserInfo = response.Data?.UserInfo;
|
||||
|
||||
// 自动填充 Ltoken
|
||||
if (Ltoken == null)
|
||||
{
|
||||
string? ltoken = await scope.ServiceProvider
|
||||
Response<LtokenWrapper> ltokenResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<PassportClient2>()
|
||||
.GetLtokenBySTokenAsync(Entity, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (ltoken != null)
|
||||
if (ltokenResponse.IsOk())
|
||||
{
|
||||
Cookie ltokenCookie = Cookie.Parse($"ltuid={Entity.Aid};ltoken={ltoken}");
|
||||
Cookie ltokenCookie = Cookie.Parse($"ltuid={Entity.Aid};ltoken={ltokenResponse.Data.Ltoken}");
|
||||
Entity.Ltoken = ltokenCookie;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
string? actionTicket = await scope.ServiceProvider
|
||||
Response<ActionTicketWrapper> actionTicketResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<AuthClient>()
|
||||
.GetActionTicketByStokenAsync("game_role", Entity)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
UserGameRoles = await scope.ServiceProvider
|
||||
.GetRequiredService<BindingClient>()
|
||||
.GetUserGameRolesByActionTicketAsync(actionTicket!, Entity, token)
|
||||
.ConfigureAwait(false);
|
||||
if (actionTicketResponse.IsOk())
|
||||
{
|
||||
string actionTicket = actionTicketResponse.Data.Ticket;
|
||||
|
||||
Response<ListWrapper<UserGameRole>> userGameRolesResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<BindingClient>()
|
||||
.GetUserGameRolesByActionTicketAsync(actionTicket, Entity, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (userGameRolesResponse.IsOk())
|
||||
{
|
||||
UserGameRoles = userGameRolesResponse.Data.List;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 自动填充 CookieToken
|
||||
if (CookieToken == null)
|
||||
{
|
||||
string? cookieToken = await scope.ServiceProvider
|
||||
Response<UidCookieToken> cookieTokenResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<PassportClient2>()
|
||||
.GetCookieAccountInfoBySTokenAsync(Entity, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (cookieToken != null)
|
||||
if (cookieTokenResponse.IsOk())
|
||||
{
|
||||
Cookie cookieTokenCookie = Cookie.Parse($"account_id={Entity.Aid};cookie_token={cookieToken}");
|
||||
Cookie cookieTokenCookie = Cookie.Parse($"account_id={Entity.Aid};cookie_token={cookieTokenResponse.Data.CookieToken}");
|
||||
Entity.CookieToken = cookieTokenCookie;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using EntityUser = Snap.Hutao.Model.Entity.User;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.User;
|
||||
|
||||
/// <summary>
|
||||
/// 角色与实体用户
|
||||
/// 实体用户与角色
|
||||
/// 由于许多操作需要同时用到ck与uid
|
||||
/// 抽象此类用于简化这类调用
|
||||
/// </summary>
|
||||
public class UserAndRole
|
||||
public class UserAndUid
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的实体用户与角色
|
||||
/// </summary>
|
||||
/// <param name="user">实体用户</param>
|
||||
/// <param name="role">角色</param>
|
||||
public UserAndRole(EntityUser user, UserGameRole role)
|
||||
public UserAndUid(EntityUser user, PlayerUid role)
|
||||
{
|
||||
User = user;
|
||||
Role = role;
|
||||
Uid = role;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -30,33 +32,33 @@ public class UserAndRole
|
||||
/// <summary>
|
||||
/// 角色
|
||||
/// </summary>
|
||||
public UserGameRole Role { get; private set; }
|
||||
public PlayerUid Uid { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 从用户与选中的角色转换
|
||||
/// </summary>
|
||||
/// <param name="user">角色</param>
|
||||
/// <returns>用户与角色</returns>
|
||||
public static UserAndRole FromUser(User user)
|
||||
public static UserAndUid FromUser(User user)
|
||||
{
|
||||
return new UserAndRole(user.Entity, user.SelectedUserGameRole!);
|
||||
return new UserAndUid(user.Entity, user.SelectedUserGameRole!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试转换到用户与角色
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="userAndRole">用户与角色</param>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <returns>是否转换成功</returns>
|
||||
public static bool TryFromUser(User? user, [NotNullWhen(true)]out UserAndRole? userAndRole)
|
||||
public static bool TryFromUser(User? user, [NotNullWhen(true)] out UserAndUid? userAndUid)
|
||||
{
|
||||
if (user != null && user.SelectedUserGameRole != null)
|
||||
{
|
||||
userAndRole = FromUser(user);
|
||||
userAndUid = FromUser(user);
|
||||
return true;
|
||||
}
|
||||
|
||||
userAndRole = null;
|
||||
userAndUid = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -108,14 +108,14 @@ public class DailyNoteEntry : ObservableObject
|
||||
/// <summary>
|
||||
/// 构造一个新的实时便笺
|
||||
/// </summary>
|
||||
/// <param name="userAndRole">用户与角色</param>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <returns>新的实时便笺</returns>
|
||||
public static DailyNoteEntry Create(UserAndRole userAndRole)
|
||||
public static DailyNoteEntry Create(UserAndUid userAndUid)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
UserId = userAndRole.User.InnerId,
|
||||
Uid = userAndRole.Role.GameUid,
|
||||
UserId = userAndUid.User.InnerId,
|
||||
Uid = userAndUid.Uid.Value,
|
||||
ResinNotifyThreshold = 160,
|
||||
HomeCoinNotifyThreshold = 2400,
|
||||
};
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Model.Entity.Configuration;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Model.Entity.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序数据库上下文
|
||||
/// </summary>
|
||||
[DebuggerDisplay("Id = {ContextId}")]
|
||||
public sealed class AppDbContext : DbContext
|
||||
{
|
||||
private readonly Guid contextId;
|
||||
private readonly ILogger<AppDbContext>? logger;
|
||||
|
||||
/// <summary>
|
||||
@@ -31,9 +32,8 @@ public sealed class AppDbContext : DbContext
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options, ILogger<AppDbContext> logger)
|
||||
: this(options)
|
||||
{
|
||||
contextId = Guid.NewGuid();
|
||||
this.logger = logger;
|
||||
logger.LogInformation("AppDbContext[{id}] created.", contextId);
|
||||
logger.LogInformation("AppDbContext[{id}] created.", ContextId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -135,7 +135,7 @@ public sealed class AppDbContext : DbContext
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
logger?.LogInformation("AppDbContext[{id}] disposed.", contextId);
|
||||
logger?.LogInformation("AppDbContext[{id}] disposed.", ContextId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Intrinsic;
|
||||
/// <summary>
|
||||
/// 不可变的原生枚举
|
||||
/// </summary>
|
||||
public static class ImmutableIntrinsics
|
||||
public static class IntrinsicImmutables
|
||||
{
|
||||
/// <summary>
|
||||
/// 所属地区
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 角色头像转换器
|
||||
/// </summary>
|
||||
internal class AchievementIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class AchievementIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 角色卡片转换器
|
||||
/// </summary>
|
||||
internal class AvatarCardConverter : ValueConverterBase<string, Uri>
|
||||
internal class AvatarCardConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
private const string CostumeCard = "UI_AvatarIcon_Costume_Card.png";
|
||||
private static readonly Uri UIAvatarIconCostumeCard = new(Web.HutaoEndpoints.StaticFile("AvatarCard", CostumeCard));
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 角色头像转换器
|
||||
/// </summary>
|
||||
internal class AvatarIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class AvatarIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 角色名片转换器
|
||||
/// </summary>
|
||||
internal class AvatarNameCardPicConverter : ValueConverterBase<Avatar.Avatar?, Uri>
|
||||
internal class AvatarNameCardPicConverter : ValueConverter<Avatar.Avatar?, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 从角色转换到名片
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 角色侧面头像转换器
|
||||
/// </summary>
|
||||
internal class AvatarSideIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class AvatarSideIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 描述参数解析器
|
||||
/// </summary>
|
||||
internal sealed partial class DescParamDescriptor : ValueConverterBase<DescParam, IList<LevelParam<string, ParameterInfo>>>
|
||||
internal sealed partial class DescParamDescriptor : ValueConverter<DescParam, IList<LevelParam<string, ParameterInfo>>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取特定等级的解释
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 元素名称图标转换器
|
||||
/// </summary>
|
||||
internal class ElementNameIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class ElementNameIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 将中文元素名称转换为图标链接
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 表情图片转换器
|
||||
/// </summary>
|
||||
internal class EmotionIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class EmotionIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 武器图片转换器
|
||||
/// </summary>
|
||||
internal class EquipIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class EquipIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 立绘图标转换器
|
||||
/// </summary>
|
||||
internal class GachaAvatarIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class GachaAvatarIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 立绘转换器
|
||||
/// </summary>
|
||||
internal class GachaAvatarImgConverter : ValueConverterBase<string, Uri>
|
||||
internal class GachaAvatarImgConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 武器祈愿图片转换器
|
||||
/// </summary>
|
||||
internal class GachaEquipIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class GachaEquipIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 物品图片转换器
|
||||
/// </summary>
|
||||
internal class ItemIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class ItemIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 基础属性翻译器
|
||||
/// </summary>
|
||||
internal class PropertyInfoDescriptor : ValueConverterBase<PropertyInfo, IList<LevelParam<string, ParameterInfo>>?>
|
||||
internal class PropertyInfoDescriptor : ValueConverter<PropertyInfo, IList<LevelParam<string, ParameterInfo>>?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 格式化对
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 品质颜色转换器
|
||||
/// </summary>
|
||||
internal class QualityColorConverter : ValueConverterBase<ItemQuality, Color>
|
||||
internal class QualityColorConverter : ValueConverter<ItemQuality, Color>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Color Convert(ItemQuality from)
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 物品等级转换器
|
||||
/// </summary>
|
||||
internal class QualityConverter : ValueConverterBase<ItemQuality, Uri>
|
||||
internal class QualityConverter : ValueConverter<ItemQuality, Uri>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Uri Convert(ItemQuality from)
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 武器图片转换器
|
||||
/// </summary>
|
||||
internal class RelicIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class RelicIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 技能图标转换器
|
||||
/// </summary>
|
||||
internal class SkillIconConverter : ValueConverterBase<string, Uri>
|
||||
internal class SkillIconConverter : ValueConverter<string, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// <summary>
|
||||
/// 元素名称图标转换器
|
||||
/// </summary>
|
||||
internal class WeaponTypeIconConverter : ValueConverterBase<WeaponType, Uri>
|
||||
internal class WeaponTypeIconConverter : ValueConverter<WeaponType, Uri>
|
||||
{
|
||||
/// <summary>
|
||||
/// 将武器类型转换为图标链接
|
||||
|
||||
@@ -55,4 +55,38 @@ public class Material
|
||||
/// 效果描述
|
||||
/// </summary>
|
||||
public string? EffectDescription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否为物品栏物品
|
||||
/// </summary>
|
||||
/// <returns>是否为物品栏物品</returns>
|
||||
public bool IsInventoryItem()
|
||||
{
|
||||
// 原质
|
||||
if (Id == 112001)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 摩拉
|
||||
if (Id == 202)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TypeDescription.EndsWith("区域特产"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return TypeDescription switch
|
||||
{
|
||||
"角色经验素材" => true,
|
||||
"角色培养素材" => true,
|
||||
"天赋培养素材" => true,
|
||||
"武器强化素材" => true,
|
||||
"武器突破素材" => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,11 @@ CoWaitForMultipleObjects
|
||||
// USER32
|
||||
FindWindowEx
|
||||
GetDpiForWindow
|
||||
GetWindowPlacement
|
||||
|
||||
// COM BITS
|
||||
BackgroundCopyManager
|
||||
IBackgroundCopyCallback
|
||||
IBackgroundCopyFile5
|
||||
IBackgroundCopyJobHttpOptions
|
||||
IBackgroundCopyManager
|
||||
|
||||
// WinRT
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.3.4.0" />
|
||||
Version="1.3.10.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>胡桃</DisplayName>
|
||||
|
||||
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Font/CascadiaMono.ttf
Normal file
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Font/CascadiaMono.ttf
Normal file
Binary file not shown.
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Font/MiSans-Regular.ttf
Normal file
BIN
src/Snap.Hutao/Snap.Hutao/Resource/Font/MiSans-Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
@@ -5,7 +5,9 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Model.InterChange.Achievement;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -52,22 +54,27 @@ internal class AchievementService : IAchievementService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<EntityArchive> GetArchiveCollection()
|
||||
public async Task<ObservableCollection<EntityArchive>> GetArchiveCollectionAsync()
|
||||
{
|
||||
return archiveCollection ??= new(appDbContext.AchievementArchives.AsNoTracking().ToList());
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
return archiveCollection ??= appDbContext.AchievementArchives.AsNoTracking().ToObservableCollection();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task RemoveArchiveAsync(EntityArchive archive)
|
||||
{
|
||||
// Sync cache
|
||||
// Keep this on main thread.
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
archiveCollection!.Remove(archive);
|
||||
|
||||
// Sync database
|
||||
await ThreadHelper.SwitchToBackgroundAsync();
|
||||
await appDbContext.AchievementArchives.RemoveAndSaveAsync(archive).ConfigureAwait(false);
|
||||
|
||||
// Cascade deleted the achievements.
|
||||
await appDbContext.AchievementArchives
|
||||
.Where(a => a.InnerId == archive.InnerId)
|
||||
.ExecuteDeleteAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -108,7 +115,17 @@ internal class AchievementService : IAchievementService
|
||||
List<BindingAchievement> results = new();
|
||||
foreach (MetadataAchievement meta in metadata)
|
||||
{
|
||||
EntityAchievement entity = entities.SingleOrDefault(e => e.Id == meta.Id) ?? EntityAchievement.Create(archiveId, meta.Id);
|
||||
EntityAchievement? entity;
|
||||
try
|
||||
{
|
||||
entity = entities.SingleOrDefault(e => e.Id == meta.Id);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
throw new UserdataCorruptedException("单个成就存档内发现多个相同的成就 Id", ex);
|
||||
}
|
||||
|
||||
entity ??= EntityAchievement.Create(archiveId, meta.Id);
|
||||
|
||||
results.Add(new(meta, entity));
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ internal interface IAchievementService
|
||||
List<BindingAchievement> GetAchievements(EntityArchive archive, IList<MetadataAchievement> metadata);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于绑定的成就存档集合
|
||||
/// 异步获取用于绑定的成就存档集合
|
||||
/// </summary>
|
||||
/// <returns>成就存档集合</returns>
|
||||
ObservableCollection<EntityArchive> GetArchiveCollection();
|
||||
Task<ObservableCollection<EntityArchive>> GetArchiveCollectionAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步导入UIAF数据
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Snap.Hutao.Service;
|
||||
@@ -34,29 +35,39 @@ internal partial class AnnouncementService : IAnnouncementService
|
||||
// 缓存中存在记录,直接返回
|
||||
if (memoryCache.TryGetValue(CacheKey, out object? cache))
|
||||
{
|
||||
return Must.NotNull((AnnouncementWrapper)cache!);
|
||||
return (AnnouncementWrapper)cache!;
|
||||
}
|
||||
|
||||
await ThreadHelper.SwitchToBackgroundAsync();
|
||||
AnnouncementWrapper? wrapper = await announcementClient
|
||||
Response<AnnouncementWrapper> announcementWrapperResponse = await announcementClient
|
||||
.GetAnnouncementsAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
List<AnnouncementContent> contents = await announcementClient
|
||||
.GetAnnouncementContentsAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Dictionary<int, string> contentMap = contents
|
||||
.ToDictionary(id => id.AnnId, content => content.Content);
|
||||
if (announcementWrapperResponse.IsOk())
|
||||
{
|
||||
AnnouncementWrapper wrapper = announcementWrapperResponse.Data;
|
||||
Response<ListWrapper<AnnouncementContent>> announcementContentResponse = await announcementClient
|
||||
.GetAnnouncementContentsAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Must.NotNull(wrapper!);
|
||||
if (announcementContentResponse.IsOk())
|
||||
{
|
||||
List<AnnouncementContent> contents = announcementContentResponse.Data.List;
|
||||
|
||||
// 将活动公告置于上方
|
||||
wrapper.List.Reverse();
|
||||
Dictionary<int, string> contentMap = contents
|
||||
.ToDictionary(id => id.AnnId, content => content.Content);
|
||||
|
||||
// 将公告内容联入公告列表
|
||||
JoinAnnouncements(contentMap, wrapper.List);
|
||||
// 将活动公告置于上方
|
||||
wrapper.List.Reverse();
|
||||
|
||||
return memoryCache.Set(CacheKey, wrapper, TimeSpan.FromMinutes(30));
|
||||
// 将公告内容联入公告列表
|
||||
JoinAnnouncements(contentMap, wrapper.List);
|
||||
|
||||
return memoryCache.Set(CacheKey, wrapper, TimeSpan.FromMinutes(30));
|
||||
}
|
||||
}
|
||||
|
||||
return null!;
|
||||
}
|
||||
|
||||
private static void JoinAnnouncements(Dictionary<int, string> contentMap, List<AnnouncementListWrapper> announcementListWrappers)
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Model.Binding.User;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Service.AvatarInfo.Composer;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using CalculateAvatar = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar;
|
||||
using EnkaAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
|
||||
using ModelAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
|
||||
using RecordCharacter = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar.Character;
|
||||
using RecordPlayerInfo = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.PlayerInfo;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo;
|
||||
|
||||
/// <summary>
|
||||
/// 角色信息数据库操作
|
||||
/// </summary>
|
||||
public class AvatarInfoDbOperation
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的角色信息数据库操作
|
||||
/// </summary>
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
public AvatarInfoDbOperation(AppDbContext appDbContext)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新数据库角色信息
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="webInfos">Enka信息</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色列表</returns>
|
||||
public List<EnkaAvatarInfo> UpdateDbAvatarInfos(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
|
||||
|
||||
foreach (EnkaAvatarInfo webInfo in webInfos)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(webInfo.AvatarId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId);
|
||||
if (entity == null)
|
||||
{
|
||||
entity = ModelAvatarInfo.Create(uid, webInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = webInfo;
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 米游社我的角色方式 更新数据库角色信息
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色列表</returns>
|
||||
public async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
string uid = userAndUid.Uid.Value;
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
|
||||
|
||||
GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService<GameRecordClient>();
|
||||
Response<RecordPlayerInfo> playerInfoResponse = await gameRecordClient
|
||||
.GetPlayerInfoAsync(userAndUid, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (playerInfoResponse.IsOk())
|
||||
{
|
||||
Response<Web.Hoyolab.Takumi.GameRecord.Avatar.CharacterWrapper> charactersResponse = await gameRecordClient
|
||||
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (charactersResponse.IsOk())
|
||||
{
|
||||
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
|
||||
|
||||
GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
|
||||
|
||||
foreach (RecordCharacter character in characters)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(character.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
EnkaAvatarInfo avatarInfo = new() { AvatarId = character.Id };
|
||||
avatarInfo = await composer.ComposeAsync(avatarInfo, character).ConfigureAwait(false);
|
||||
entity = ModelAvatarInfo.Create(uid, avatarInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = await composer.ComposeAsync(entity.Info, character).ConfigureAwait(false);
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 米游社养成计算方式 更新数据库角色信息
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色列表</returns>
|
||||
public async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
string uid = userAndUid.Uid.Value;
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
|
||||
|
||||
CalculateClient calculateClient = Ioc.Default.GetRequiredService<CalculateClient>();
|
||||
List<CalculateAvatar> avatars = await calculateClient.GetAvatarsAsync(userAndUid, token).ConfigureAwait(false);
|
||||
|
||||
CalculateAvatarDetailAvatarInfoComposer composer = Ioc.Default.GetRequiredService<CalculateAvatarDetailAvatarInfoComposer>();
|
||||
|
||||
foreach (CalculateAvatar avatar in avatars)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(avatar.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
Response<AvatarDetail> detailAvatarResponse = await calculateClient.GetAvatarDetailAsync(userAndUid, avatar, token).ConfigureAwait(false);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (!detailAvatarResponse.IsOk())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == avatar.Id);
|
||||
AvatarDetail detailAvatar = detailAvatarResponse.Data;
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatar.Id };
|
||||
avatarInfo = await composer.ComposeAsync(avatarInfo, detailAvatar).ConfigureAwait(false);
|
||||
entity = ModelAvatarInfo.Create(uid, avatarInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = await composer.ComposeAsync(entity.Info, detailAvatar).ConfigureAwait(false);
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数据库角色信息
|
||||
/// </summary>
|
||||
/// <param name="uid">Uid</param>
|
||||
/// <returns>角色列表</returns>
|
||||
public List<EnkaAvatarInfo> GetDbAvatarInfos(string uid)
|
||||
{
|
||||
return appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.Select(i => i.Info)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void EnsureItemsAvatarIdDistinct(ref List<ModelAvatarInfo> dbInfos, string uid)
|
||||
{
|
||||
int distinctCount = dbInfos.Select(info => info.Info.AvatarId).ToHashSet().Count;
|
||||
|
||||
// Avatars are actually less than the list told us.
|
||||
if (distinctCount < dbInfos.Count)
|
||||
{
|
||||
appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ExecuteDelete();
|
||||
|
||||
dbInfos = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
using Snap.Hutao.Model.Binding.User;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Service.AvatarInfo.Composer;
|
||||
using Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Web.Enka;
|
||||
using Snap.Hutao.Web.Enka.Model;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
using CalculateAvatar = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar;
|
||||
using EnkaAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
|
||||
using EnkaPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo;
|
||||
using ModelAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
|
||||
using RecordCharacter = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar.Character;
|
||||
using RecordPlayerInfo = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.PlayerInfo;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo;
|
||||
|
||||
@@ -31,11 +22,12 @@ namespace Snap.Hutao.Service.AvatarInfo;
|
||||
[Injection(InjectAs.Scoped, typeof(IAvatarInfoService))]
|
||||
internal class AvatarInfoService : IAvatarInfoService
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
private readonly ISummaryFactory summaryFactory;
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly ILogger<AvatarInfoService> logger;
|
||||
|
||||
private readonly AvatarInfoDbOperation avatarInfoDbOperation;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的角色信息服务
|
||||
/// </summary>
|
||||
@@ -49,14 +41,15 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
ISummaryFactory summaryFactory,
|
||||
ILogger<AvatarInfoService> logger)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
this.metadataService = metadataService;
|
||||
this.summaryFactory = summaryFactory;
|
||||
this.logger = logger;
|
||||
|
||||
avatarInfoDbOperation = new(appDbContext);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndRole userAndRole, RefreshOption refreshOption, CancellationToken token = default)
|
||||
public async Task<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default)
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
@@ -66,46 +59,44 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
{
|
||||
case RefreshOption.RequestFromEnkaAPI:
|
||||
{
|
||||
EnkaResponse? resp = await GetEnkaResponseAsync(userAndRole.Role, token).ConfigureAwait(false);
|
||||
EnkaResponse? resp = await GetEnkaResponseAsync(userAndUid.Uid, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (resp == null)
|
||||
{
|
||||
return new(RefreshResult.APIUnavailable, null);
|
||||
}
|
||||
|
||||
if (resp.IsValid)
|
||||
{
|
||||
IList<EnkaAvatarInfo> list = UpdateDbAvatarInfos(userAndRole.Role.GameUid, resp.AvatarInfoList);
|
||||
Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
return new(RefreshResult.Ok, summary);
|
||||
}
|
||||
else
|
||||
if (!resp.IsValid)
|
||||
{
|
||||
return new(RefreshResult.ShowcaseNotOpen, null);
|
||||
}
|
||||
|
||||
List<EnkaAvatarInfo> list = avatarInfoDbOperation.UpdateDbAvatarInfos(userAndUid.Uid.Value, resp.AvatarInfoList, token);
|
||||
Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list, token).ConfigureAwait(false);
|
||||
return new(RefreshResult.Ok, summary);
|
||||
}
|
||||
|
||||
case RefreshOption.RequestFromHoyolabGameRecord:
|
||||
{
|
||||
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndRole.Role.GameUid);
|
||||
IList<EnkaAvatarInfo> list = await UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndRole).ConfigureAwait(false);
|
||||
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
|
||||
List<EnkaAvatarInfo> list = await avatarInfoDbOperation.UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false);
|
||||
Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
|
||||
return new(RefreshResult.Ok, summary);
|
||||
}
|
||||
|
||||
case RefreshOption.RequestFromHoyolabCalculate:
|
||||
{
|
||||
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndRole.Role.GameUid);
|
||||
IList<EnkaAvatarInfo> list = await UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndRole).ConfigureAwait(false);
|
||||
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
|
||||
List<EnkaAvatarInfo> list = await avatarInfoDbOperation.UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false);
|
||||
Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
|
||||
return new(RefreshResult.Ok, summary);
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndRole.Role.GameUid);
|
||||
Summary summary = await GetSummaryCoreAsync(info, GetDbAvatarInfos(userAndRole.Role.GameUid), token).ConfigureAwait(false);
|
||||
EnkaPlayerInfo info = EnkaPlayerInfo.CreateEmpty(userAndUid.Uid.Value);
|
||||
List<EnkaAvatarInfo> list = avatarInfoDbOperation.GetDbAvatarInfos(userAndUid.Uid.Value);
|
||||
Summary summary = await GetSummaryCoreAsync(info, list, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
return new(RefreshResult.Ok, summary.Avatars.Count == 0 ? null : summary);
|
||||
}
|
||||
@@ -133,138 +124,4 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
private List<EnkaAvatarInfo> UpdateDbAvatarInfos(string uid, IEnumerable<EnkaAvatarInfo> webInfos)
|
||||
{
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
|
||||
foreach (EnkaAvatarInfo webInfo in webInfos)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(webInfo.AvatarId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
entity = ModelAvatarInfo.Create(uid, webInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = webInfo;
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
private async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndRole userAndRole)
|
||||
{
|
||||
string uid = userAndRole.Role.GameUid;
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
|
||||
GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService<GameRecordClient>();
|
||||
RecordPlayerInfo? playerInfo = await gameRecordClient
|
||||
.GetPlayerInfoAsync(userAndRole)
|
||||
.ConfigureAwait(false);
|
||||
List<RecordCharacter> characters = await gameRecordClient
|
||||
.GetCharactersAsync(userAndRole, playerInfo!)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
|
||||
|
||||
foreach (RecordCharacter character in characters)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(character.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
EnkaAvatarInfo avatarInfo = new() { AvatarId = character.Id };
|
||||
avatarInfo = await composer.ComposeAsync(avatarInfo, character).ConfigureAwait(false);
|
||||
entity = ModelAvatarInfo.Create(uid, avatarInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = await composer.ComposeAsync(entity.Info, character).ConfigureAwait(false);
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
private async Task<List<EnkaAvatarInfo>> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndRole userAndRole)
|
||||
{
|
||||
string uid = userAndRole.Role.GameUid;
|
||||
List<ModelAvatarInfo> dbInfos = appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.ToList();
|
||||
|
||||
CalculateClient calculateClient = Ioc.Default.GetRequiredService<CalculateClient>();
|
||||
List<CalculateAvatar> avatars = await calculateClient.GetAvatarsAsync(userAndRole.User, userAndRole.Role).ConfigureAwait(false);
|
||||
|
||||
CalculateAvatarDetailAvatarInfoComposer composer = Ioc.Default.GetRequiredService<CalculateAvatarDetailAvatarInfoComposer>();
|
||||
|
||||
foreach (CalculateAvatar avatar in avatars)
|
||||
{
|
||||
if (AvatarIds.IsPlayer(avatar.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AvatarDetail? detailAvatar = await calculateClient.GetAvatarDetailAsync(userAndRole.User, userAndRole.Role, avatar).ConfigureAwait(false);
|
||||
|
||||
if (detailAvatar == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == avatar.Id);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatar.Id };
|
||||
avatarInfo = await composer.ComposeAsync(avatarInfo, detailAvatar).ConfigureAwait(false);
|
||||
entity = ModelAvatarInfo.Create(uid, avatarInfo);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = await composer.ComposeAsync(entity.Info, detailAvatar).ConfigureAwait(false);
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
private List<EnkaAvatarInfo> GetDbAvatarInfos(string uid)
|
||||
{
|
||||
try
|
||||
{
|
||||
return appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.Select(i => i.Info)
|
||||
.ToList();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// appDbContext can be disposed unexpectedly
|
||||
return new();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,10 +40,10 @@ internal class SummaryAvatarFactory
|
||||
/// <returns>角色</returns>
|
||||
public PropertyAvatar CreateAvatar()
|
||||
{
|
||||
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList);
|
||||
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
|
||||
MetadataAvatar avatar = metadataContext.IdAvatarMap[avatarInfo.AvatarId];
|
||||
|
||||
return new()
|
||||
PropertyAvatar propertyAvatar = new()
|
||||
{
|
||||
Id = avatar.Id,
|
||||
Name = avatar.Name,
|
||||
@@ -52,8 +52,6 @@ internal class SummaryAvatarFactory
|
||||
NameCard = AvatarNameCardPicConverter.AvatarToUri(avatar),
|
||||
Quality = avatar.Quality,
|
||||
Element = ElementNameIconConverter.ElementNameToElementType(avatar.FetterInfo.VisionBefore),
|
||||
Level = $"Lv.{avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value}",
|
||||
LevelNumber = int.Parse(avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value ?? string.Empty),
|
||||
FetterLevel = avatarInfo.FetterInfo.ExpLevel,
|
||||
Weapon = reliquaryAndWeapon.Weapon,
|
||||
Reliquaries = reliquaryAndWeapon.Reliquaries,
|
||||
@@ -62,14 +60,19 @@ internal class SummaryAvatarFactory
|
||||
Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap),
|
||||
Score = reliquaryAndWeapon.Reliquaries.Sum(r => r.Score).ToString("F2"),
|
||||
CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}",
|
||||
LevelNumber = int.Parse(avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].Value ?? string.Empty),
|
||||
};
|
||||
|
||||
propertyAvatar.Level = $"Lv.{propertyAvatar.LevelNumber}";
|
||||
return propertyAvatar;
|
||||
}
|
||||
|
||||
private ReliquaryAndWeapon ProcessEquip(IList<Equip> equipments)
|
||||
private ReliquaryAndWeapon ProcessEquip(List<Equip> equipments)
|
||||
{
|
||||
List<PropertyReliquary> reliquaryList = new();
|
||||
PropertyWeapon? weapon = null;
|
||||
|
||||
// equipments can be null
|
||||
foreach (Equip equip in equipments)
|
||||
{
|
||||
switch (equip.Flat.ItemType)
|
||||
@@ -84,7 +87,7 @@ internal class SummaryAvatarFactory
|
||||
}
|
||||
}
|
||||
|
||||
return new(reliquaryList, weapon!);
|
||||
return new(reliquaryList, weapon);
|
||||
}
|
||||
|
||||
private PropertyWeapon CreateWeapon(Equip equip)
|
||||
@@ -134,9 +137,9 @@ internal class SummaryAvatarFactory
|
||||
private struct ReliquaryAndWeapon
|
||||
{
|
||||
public List<PropertyReliquary> Reliquaries;
|
||||
public PropertyWeapon Weapon;
|
||||
public PropertyWeapon? Weapon;
|
||||
|
||||
public ReliquaryAndWeapon(List<PropertyReliquary> reliquaries, PropertyWeapon weapon)
|
||||
public ReliquaryAndWeapon(List<PropertyReliquary> reliquaries, PropertyWeapon? weapon)
|
||||
{
|
||||
Reliquaries = reliquaries;
|
||||
Weapon = weapon;
|
||||
|
||||
@@ -14,9 +14,9 @@ internal interface IAvatarInfoService
|
||||
/// <summary>
|
||||
/// 异步获取总览数据
|
||||
/// </summary>
|
||||
/// <param name="userAndRole">uid</param>
|
||||
/// <param name="userAndUid">uid</param>
|
||||
/// <param name="refreshOption">刷新选项</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>总览数据</returns>
|
||||
Task<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndRole userAndRole, RefreshOption refreshOption, CancellationToken token = default);
|
||||
Task<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default);
|
||||
}
|
||||
@@ -118,7 +118,7 @@ internal class CultivationService : ICultivationService
|
||||
.ToList();
|
||||
|
||||
List<BindingInventoryItem> results = new();
|
||||
foreach (Model.Metadata.Material meta in metadata.Where(IsInventoryItem).OrderBy(m => m.Id))
|
||||
foreach (Model.Metadata.Material meta in metadata.Where(m => m.IsInventoryItem()).OrderBy(m => m.Id))
|
||||
{
|
||||
InventoryItem entity = entities.SingleOrDefault(e => e.ItemId == meta.Id) ?? InventoryItem.Create(projectId, meta.Id);
|
||||
results.Add(new(meta, entity));
|
||||
@@ -309,34 +309,4 @@ internal class CultivationService : ICultivationService
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsInventoryItem(Model.Metadata.Material material)
|
||||
{
|
||||
// 原质
|
||||
if (material.Id == 112001)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 摩拉
|
||||
if (material.Id == 202)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (material.TypeDescription.EndsWith("区域特产"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return material.TypeDescription switch
|
||||
{
|
||||
"角色经验素材" => true,
|
||||
"角色培养素材" => true,
|
||||
"天赋培养素材" => true,
|
||||
"武器强化素材" => true,
|
||||
"武器突破素材" => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using Snap.Hutao.Model.Metadata.Converter;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using Windows.Foundation.Metadata;
|
||||
|
||||
namespace Snap.Hutao.Service.DailyNote;
|
||||
@@ -145,16 +146,24 @@ internal class DailyNoteNotifier
|
||||
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService<BindingClient>();
|
||||
AuthClient authClient = scope.ServiceProvider.GetRequiredService<AuthClient>();
|
||||
|
||||
string? actionTicket = await authClient
|
||||
Response<ActionTicketWrapper> actionTicketResponse = await authClient
|
||||
.GetActionTicketByStokenAsync("game_role", entry.User)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
List<UserGameRole> roles = await scope.ServiceProvider
|
||||
.GetRequiredService<BindingClient>()
|
||||
.GetUserGameRolesByActionTicketAsync(actionTicket!, entry.User)
|
||||
.ConfigureAwait(false);
|
||||
string? attribution = "请求异常";
|
||||
if (actionTicketResponse.IsOk())
|
||||
{
|
||||
Response<ListWrapper<UserGameRole>> rolesResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<BindingClient>()
|
||||
.GetUserGameRolesByActionTicketAsync(actionTicketResponse.Data.Ticket, entry.User)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
string attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? "未知角色";
|
||||
if (rolesResponse.IsOk())
|
||||
{
|
||||
List<UserGameRole> roles = rolesResponse.Data.List;
|
||||
attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? "未知角色";
|
||||
}
|
||||
}
|
||||
|
||||
ToastContentBuilder builder = new ToastContentBuilder()
|
||||
.AddHeader("DAILYNOTE", "实时便笺提醒", "DAILYNOTE")
|
||||
@@ -181,11 +190,11 @@ internal class DailyNoteNotifier
|
||||
{
|
||||
HintWeight = 1,
|
||||
Children =
|
||||
{
|
||||
new AdaptiveImage() { Source = info.AdaptiveIcon, HintRemoveMargin = true, },
|
||||
new AdaptiveText() { Text = info.AdaptiveHint, HintAlign = AdaptiveTextAlign.Center, },
|
||||
new AdaptiveText() { Text = info.Title, HintAlign = AdaptiveTextAlign.Center, HintStyle = AdaptiveTextStyle.CaptionSubtle, },
|
||||
},
|
||||
{
|
||||
new AdaptiveImage() { Source = info.AdaptiveIcon, HintRemoveMargin = true, },
|
||||
new AdaptiveText() { Text = info.AdaptiveHint, HintAlign = AdaptiveTextAlign.Center, },
|
||||
new AdaptiveText() { Text = info.Title, HintAlign = AdaptiveTextAlign.Center, HintStyle = AdaptiveTextStyle.CaptionSubtle, },
|
||||
},
|
||||
};
|
||||
|
||||
group.Children.Add(subgroup);
|
||||
|
||||
@@ -45,13 +45,17 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
|
||||
/// <inheritdoc/>
|
||||
public void Receive(UserRemovedMessage message)
|
||||
{
|
||||
entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
|
||||
ThreadHelper.InvokeOnMainThread(() =>
|
||||
{
|
||||
// Database items have been deleted by cascade deleting.
|
||||
entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task AddDailyNoteAsync(UserAndRole role)
|
||||
public async Task AddDailyNoteAsync(UserAndUid role)
|
||||
{
|
||||
string roleUid = role.Role.GameUid;
|
||||
string roleUid = role.Uid.Value;
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
@@ -60,7 +64,16 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
|
||||
if (!appDbContext.DailyNotes.Any(n => n.Uid == roleUid))
|
||||
{
|
||||
DailyNoteEntry newEntry = DailyNoteEntry.Create(role);
|
||||
newEntry.DailyNote = await gameRecordClient.GetDailyNoteAsync(role.User, newEntry.Uid).ConfigureAwait(false);
|
||||
|
||||
Web.Response.Response<WebDailyNote> dailyNoteResponse = await gameRecordClient
|
||||
.GetDailyNoteAsync(role)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (dailyNoteResponse.IsOk())
|
||||
{
|
||||
newEntry.DailyNote = dailyNoteResponse.Data;
|
||||
}
|
||||
|
||||
newEntry.UserGameRole = userService.GetUserGameRoleByUid(roleUid);
|
||||
await appDbContext.DailyNotes.AddAndSaveAsync(newEntry).ConfigureAwait(false);
|
||||
|
||||
@@ -106,18 +119,25 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
|
||||
|
||||
foreach (DailyNoteEntry entry in appDbContext.DailyNotes.Include(n => n.User))
|
||||
{
|
||||
WebDailyNote? dailyNote = await gameRecordClient.GetDailyNoteAsync(entry.User, entry.Uid).ConfigureAwait(false);
|
||||
Web.Response.Response<WebDailyNote> dailyNoteResponse = await gameRecordClient
|
||||
.GetDailyNoteAsync(new(entry.User, entry.Uid))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// database
|
||||
entry.DailyNote = dailyNote;
|
||||
|
||||
// cache
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
entries?.SingleOrDefault(e => e.UserId == entry.UserId && e.Uid == entry.Uid)?.UpdateDailyNote(dailyNote);
|
||||
|
||||
if (notify)
|
||||
if (dailyNoteResponse.IsOk())
|
||||
{
|
||||
await new DailyNoteNotifier(scopeFactory, entry).NotifyAsync().ConfigureAwait(false);
|
||||
WebDailyNote dailyNote = dailyNoteResponse.Data;
|
||||
|
||||
// database
|
||||
entry.DailyNote = dailyNote;
|
||||
|
||||
// cache
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
entries?.SingleOrDefault(e => e.UserId == entry.UserId && e.Uid == entry.Uid)?.UpdateDailyNote(dailyNote);
|
||||
|
||||
if (notify)
|
||||
{
|
||||
await new DailyNoteNotifier(scopeFactory, entry).NotifyAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +152,7 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
|
||||
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
// DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s)
|
||||
scope.ServiceProvider.GetRequiredService<AppDbContext>().DailyNotes.RemoveAndSave(entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public interface IDailyNoteService
|
||||
/// </summary>
|
||||
/// <param name="role">角色</param>
|
||||
/// <returns>任务</returns>
|
||||
Task AddDailyNoteAsync(UserAndRole role);
|
||||
Task AddDailyNoteAsync(UserAndUid role);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取实时便笺列表
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
@@ -28,7 +28,7 @@ namespace Snap.Hutao.Service.GachaLog;
|
||||
/// 祈愿记录服务
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Scoped, typeof(IGachaLogService))]
|
||||
internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
internal class GachaLogService : IGachaLogService
|
||||
{
|
||||
/// <summary>
|
||||
/// 祈愿记录查询的类型
|
||||
@@ -95,9 +95,6 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
set => dbCurrent.Current = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInitialized { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<UIGF> ExportToUIGFAsync(GachaArchive archive)
|
||||
{
|
||||
@@ -117,30 +114,29 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<GachaArchive> GetArchiveCollection()
|
||||
public async Task<ObservableCollection<GachaArchive>> GetArchiveCollectionAsync()
|
||||
{
|
||||
return archiveCollection ??= new(appDbContext.GachaArchives.ToList());
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
return archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeAsync()
|
||||
public async ValueTask<bool> InitializeAsync(CancellationToken token)
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
nameAvatarMap = await metadataService.GetNameToAvatarMapAsync().ConfigureAwait(false);
|
||||
nameWeaponMap = await metadataService.GetNameToWeaponMapAsync().ConfigureAwait(false);
|
||||
nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
nameWeaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
|
||||
idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
|
||||
idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
|
||||
IsInitialized = true;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsInitialized = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsInitialized;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -224,11 +220,10 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
|
||||
// Sync database
|
||||
await ThreadHelper.SwitchToBackgroundAsync();
|
||||
await appDbContext.GachaItems
|
||||
.Where(item => item.ArchiveId == archive.InnerId)
|
||||
await appDbContext.GachaArchives
|
||||
.Where(a => a.InnerId == archive.InnerId)
|
||||
.ExecuteDeleteAsync()
|
||||
.ConfigureAwait(false);
|
||||
await appDbContext.GachaArchives.RemoveAndSaveAsync(archive).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static Task RandomDelayAsync(CancellationToken token)
|
||||
@@ -250,13 +245,15 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
|
||||
do
|
||||
{
|
||||
Response<GachaLogPage>? response = await gachaInfoClient.GetGachaLogPageAsync(configration, token).ConfigureAwait(false);
|
||||
Response<GachaLogPage> response = await gachaInfoClient.GetGachaLogPageAsync(configration, token).ConfigureAwait(false);
|
||||
|
||||
if (response?.Data is GachaLogPage page)
|
||||
if (response.IsOk())
|
||||
{
|
||||
GachaLogPage page = response.Data;
|
||||
|
||||
state.Items.Clear();
|
||||
List<GachaLogItem> items = page.List;
|
||||
bool completedCurrentTypeAdding = false;
|
||||
bool currentTypeAddingCompleted = false;
|
||||
|
||||
foreach (GachaLogItem item in items)
|
||||
{
|
||||
@@ -271,14 +268,14 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
}
|
||||
else
|
||||
{
|
||||
completedCurrentTypeAdding = true;
|
||||
currentTypeAddingCompleted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(state);
|
||||
|
||||
if (completedCurrentTypeAdding || items.Count < GachaLogConfigration.Size)
|
||||
if (currentTypeAddingCompleted || items.Count < GachaLogConfigration.Size)
|
||||
{
|
||||
// exit current type fetch loop
|
||||
break;
|
||||
@@ -300,6 +297,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
break;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
SaveGachaItems(itemsToAdd, isLazy, archive, configration.EndId);
|
||||
await RandomDelayAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
@@ -320,7 +318,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
|
||||
archive = appDbContext.GachaArchives.Single(a => a.Uid == uid);
|
||||
GachaArchive temp = archive;
|
||||
Program.DispatcherQueue!.TryEnqueue(() => archiveCollection!.Add(temp));
|
||||
ThreadHelper.InvokeOnMainThread(() => archiveCollection!.Add(temp));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,14 +329,21 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
|
||||
if (archive != null)
|
||||
{
|
||||
// TODO: replace with MaxBy
|
||||
// https://github.com/dotnet/efcore/issues/25566
|
||||
// .MaxBy(i => i.Id);
|
||||
item = appDbContext.GachaItems
|
||||
.Where(i => i.ArchiveId == archive.InnerId)
|
||||
.Where(i => i.QueryType == configType)
|
||||
.OrderByDescending(i => i.Id)
|
||||
.FirstOrDefault();
|
||||
try
|
||||
{
|
||||
// TODO: replace with MaxBy
|
||||
// https://github.com/dotnet/efcore/issues/25566
|
||||
// .MaxBy(i => i.Id);
|
||||
item = appDbContext.GachaItems
|
||||
.Where(i => i.ArchiveId == archive.InnerId)
|
||||
.Where(i => i.QueryType == configType)
|
||||
.OrderByDescending(i => i.Id)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
catch (SqliteException ex)
|
||||
{
|
||||
throw new Core.ExceptionService.UserdataCorruptedException("无法获取祈愿记录 End Id", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return item?.Id ?? 0L;
|
||||
@@ -373,11 +378,12 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
|
||||
private INameQuality GetNameQualityByItemId(int id)
|
||||
{
|
||||
return id.Place() switch
|
||||
int place = id.Place();
|
||||
return place switch
|
||||
{
|
||||
8 => idAvatarMap![id],
|
||||
5 => idWeaponMap![id],
|
||||
_ => throw Must.NeverHappen(),
|
||||
_ => throw Must.NeverHappen($"Id places: {place}"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ internal interface IGachaLogService
|
||||
Task<UIGF> ExportToUIGFAsync(GachaArchive archive);
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用于绑定的存档集合
|
||||
/// 异步获取可用于绑定的存档集合
|
||||
/// </summary>
|
||||
/// <returns>存档集合</returns>
|
||||
ObservableCollection<GachaArchive> GetArchiveCollection();
|
||||
Task<ObservableCollection<GachaArchive>> GetArchiveCollectionAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取祈愿日志Url提供器
|
||||
@@ -58,7 +58,7 @@ internal interface IGachaLogService
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>是否初始化成功</returns>
|
||||
ValueTask<bool> InitializeAsync();
|
||||
ValueTask<bool> InitializeAsync(CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// 刷新祈愿记录
|
||||
|
||||
@@ -17,6 +17,8 @@ internal class GachaLogUrlManualInputProvider : IGachaLogUrlProvider
|
||||
/// <inheritdoc/>
|
||||
public async Task<ValueResult<bool, string>> GetQueryAsync()
|
||||
{
|
||||
// ContentDialog must be created by main thread.
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
ValueResult<bool, string> result = await new GachaLogUrlDialog().GetInputUrlAsync().ConfigureAwait(false);
|
||||
|
||||
if (result.IsOk)
|
||||
|
||||
@@ -42,10 +42,10 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
|
||||
PlayerUid uid = (PlayerUid)user.SelectedUserGameRole;
|
||||
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(uid);
|
||||
|
||||
GameAuthKey? authkey = await bindingClient2.GenerateAuthenticationKeyAsync(user.Entity, data).ConfigureAwait(false);
|
||||
if (authkey != null)
|
||||
Web.Response.Response<GameAuthKey> authkeyResponse = await bindingClient2.GenerateAuthenticationKeyAsync(user.Entity, data).ConfigureAwait(false);
|
||||
if (authkeyResponse.IsOk())
|
||||
{
|
||||
return new(true, GachaLogConfigration.AsQuery(data, authkey));
|
||||
return new(true, GachaLogConfigration.AsQuery(data, authkeyResponse.Data));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -25,6 +25,12 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
|
||||
this.gameService = gameService;
|
||||
}
|
||||
|
||||
private enum UrlMatch
|
||||
{
|
||||
Chinese,
|
||||
Oversea,
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name { get => nameof(GachaLogUrlWebCacheProvider); }
|
||||
|
||||
@@ -36,7 +42,10 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
|
||||
public static string GetCacheFile(string path)
|
||||
{
|
||||
string folder = Path.GetDirectoryName(path) ?? string.Empty;
|
||||
return Path.Combine(folder, @"YuanShen_Data\webCaches\Cache\Cache_Data\data_2");
|
||||
string cacheDataPathChinese = Path.Combine(folder, @"YuanShen_Data\webCaches\Cache\Cache_Data\data_2");
|
||||
string cacheDataPathOversea = Path.Combine(folder, @"GenshinImpact_Data\webCaches\Cache\Cache_Data\data_2");
|
||||
|
||||
return File.Exists(cacheDataPathChinese) ? cacheDataPathChinese : cacheDataPathOversea;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -60,7 +69,7 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
|
||||
using (MemoryStream memoryStream = new())
|
||||
{
|
||||
await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
string? result = Match(memoryStream);
|
||||
string? result = Match(memoryStream, !tempFile.Path.Contains("GenshinImpact_Data"));
|
||||
return new(!string.IsNullOrEmpty(result), result!);
|
||||
}
|
||||
}
|
||||
@@ -72,10 +81,12 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
|
||||
}
|
||||
}
|
||||
|
||||
private static string? Match(MemoryStream stream)
|
||||
private static string? Match(MemoryStream stream, bool isOversea)
|
||||
{
|
||||
ReadOnlySpan<byte> span = stream.ToArray();
|
||||
ReadOnlySpan<byte> match = "https://webstatic.mihoyo.com/hk4e/event/e20190909gacha-v2/index.html"u8;
|
||||
ReadOnlySpan<byte> match = isOversea
|
||||
? "https://webstatic-sea.hoyoverse.com/genshin/event/e20190909gacha-v2/index.html"u8
|
||||
: "https://webstatic.mihoyo.com/hk4e/event/e20190909gacha-v2/index.html"u8;
|
||||
ReadOnlySpan<byte> zero = "\0"u8;
|
||||
|
||||
int index = span.LastIndexOf(match);
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.IO.Ini;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Binding.LaunchGame;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
@@ -142,6 +143,11 @@ internal class GameService : IGameService, IDisposable
|
||||
string gamePath = GetGamePathSkipLocator();
|
||||
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFile);
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
return new(null, null, configPath);
|
||||
}
|
||||
|
||||
using (FileStream stream = File.OpenRead(configPath))
|
||||
{
|
||||
List<IniElement> elements = IniSerializer.Deserialize(stream).ToList();
|
||||
@@ -200,6 +206,7 @@ internal class GameService : IGameService, IDisposable
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[SuppressMessage("", "IDE0046")]
|
||||
public bool IsGameRunning()
|
||||
{
|
||||
if (gameSemaphore.CurrentCount == 0)
|
||||
@@ -207,18 +214,20 @@ internal class GameService : IGameService, IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
return Process.GetProcessesByName("YuanShen.exe").Any();
|
||||
return Process.GetProcessesByName("YuanShen.exe").Any()
|
||||
|| Process.GetProcessesByName("GenshinImpact.exe").Any();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<GameAccount> GetGameAccountCollection()
|
||||
public async Task<ObservableCollection<GameAccount>> GetGameAccountCollectionAsync()
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
if (gameAccounts == null)
|
||||
{
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
gameAccounts = new(appDbContext.GameAccounts.AsNoTracking().ToList());
|
||||
gameAccounts = appDbContext.GameAccounts.AsNoTracking().ToObservableCollection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +313,8 @@ internal class GameService : IGameService, IDisposable
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
// ContentDialog must be created by main thread.
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
(bool isOk, string name) = await new GameAccountNameDialog().GetInputNameAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
|
||||
@@ -27,10 +27,10 @@ internal interface IGameService
|
||||
ValueTask DetectGameAccountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取游戏内账号集合
|
||||
/// 异步获取游戏内账号集合
|
||||
/// </summary>
|
||||
/// <returns>游戏内账号集合</returns>
|
||||
ObservableCollection<GameAccount> GetGameAccountCollection();
|
||||
Task<ObservableCollection<GameAccount>> GetGameAccountCollectionAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取游戏路径
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Pickers;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Locator;
|
||||
@@ -30,18 +30,21 @@ internal class ManualGameLocator : IGameLocator
|
||||
/// <inheritdoc/>
|
||||
public Task<ValueResult<bool, string>> LocateGamePathAsync()
|
||||
{
|
||||
return LocateInternalAsync("YuanShen.exe");
|
||||
List<string> filenames = new(2) { "YuanShen.exe", "GenshinImpact.exe", };
|
||||
return LocateInternalAsync(filenames);
|
||||
}
|
||||
|
||||
private async Task<ValueResult<bool, string>> LocateInternalAsync(string fileName)
|
||||
private async Task<ValueResult<bool, string>> LocateInternalAsync(List<string> fileNames)
|
||||
{
|
||||
FileOpenPicker picker = pickerFactory.GetFileOpenPicker(PickerLocationId.Desktop, "选择游戏本体", ".exe");
|
||||
if (await picker.PickSingleFileAsync() is StorageFile file)
|
||||
(bool isPickerOk, FilePath file) = await picker.TryPickSingleFileAsync().ConfigureAwait(false);
|
||||
|
||||
if (isPickerOk)
|
||||
{
|
||||
string path = file.Path;
|
||||
if (path.Contains(fileName))
|
||||
string fileName = System.IO.Path.GetFileName(file);
|
||||
if (fileNames.Contains(fileName))
|
||||
{
|
||||
return new(true, path);
|
||||
return new(true, file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,29 +21,35 @@ internal partial class UnityLogGameLocator : IGameLocator
|
||||
{
|
||||
await ThreadHelper.SwitchToBackgroundAsync();
|
||||
string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
string logFilePath = Path.Combine(appDataPath, @"..\LocalLow\miHoYo\原神\output_log.txt");
|
||||
string logFilePathChinese = Path.Combine(appDataPath, @"..\LocalLow\miHoYo\原神\output_log.txt");
|
||||
string logFilePathOvsesea = Path.Combine(appDataPath, @"..\LocalLow\miHoYo\Genshin Impact\output_log.txt");
|
||||
|
||||
using (TempFile? tempFile = TempFile.CreateFromFileCopy(logFilePath))
|
||||
// We need to fallback the the cn server rather than os server.
|
||||
string logFilePathFinal = File.Exists(logFilePathOvsesea) ? logFilePathOvsesea : logFilePathChinese;
|
||||
|
||||
using (TempFile? tempFile = TempFile.CreateFromFileCopy(logFilePathFinal))
|
||||
{
|
||||
if (tempFile == null)
|
||||
if (tempFile != null)
|
||||
{
|
||||
return new(false, $"找不到 Unity 日志文件:\n{logFilePath}");
|
||||
string content = File.ReadAllText(tempFile.Path);
|
||||
|
||||
Match matchResult = WarmupFileLine().Match(content);
|
||||
if (!matchResult.Success)
|
||||
{
|
||||
return new(false, $"在 Unity 日志文件中找不到游戏路径");
|
||||
}
|
||||
|
||||
string entryName = matchResult.Groups[0].Value.Replace("_Data", ".exe");
|
||||
string fullPath = Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName));
|
||||
return new(true, fullPath);
|
||||
}
|
||||
|
||||
string content = File.ReadAllText(tempFile.Path);
|
||||
|
||||
Match matchResult = WarmupFileLine().Match(content);
|
||||
if (!matchResult.Success)
|
||||
else
|
||||
{
|
||||
return new(false, $"在 Unity 日志文件中找不到游戏路径");
|
||||
return new(false, $"找不到 Unity 日志文件");
|
||||
}
|
||||
|
||||
string entryName = matchResult.Groups[0].Value.Replace("_Data", ".exe");
|
||||
string fullPath = Path.GetFullPath(Path.Combine(matchResult.Value, "..", entryName));
|
||||
return new(true, fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(?m).:/.+YuanShen_Data")]
|
||||
[GeneratedRegex(@"(?m).:/.+(GenshinImpact_Data|YuanShen_Data)")]
|
||||
private static partial Regex WarmupFileLine();
|
||||
}
|
||||
@@ -18,14 +18,21 @@ public struct MultiChannel
|
||||
/// </summary>
|
||||
public string SubChannel;
|
||||
|
||||
/// <summary>
|
||||
/// 配置文件路径 当不为 null 时则存在文件读写问题
|
||||
/// </summary>
|
||||
public string? ConfigFilePath;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的多通道
|
||||
/// </summary>
|
||||
/// <param name="channel">通道</param>
|
||||
/// <param name="subChannel">子通道</param>
|
||||
public MultiChannel(string? channel, string? subChannel)
|
||||
/// <param name="configFilePath">配置文件路径</param>
|
||||
public MultiChannel(string? channel, string? subChannel, string? configFilePath = null)
|
||||
{
|
||||
Channel = channel ?? string.Empty;
|
||||
SubChannel = subChannel ?? string.Empty;
|
||||
ConfigFilePath = configFilePath;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Win32;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Diagnostics.ToolHelp;
|
||||
using static Windows.Win32.PInvoke;
|
||||
|
||||
@@ -75,21 +76,24 @@ internal class GameFpsUnlocker : IGameFpsUnlocker
|
||||
|
||||
private static unsafe MODULEENTRY32 FindModule(int processId, string moduleName)
|
||||
{
|
||||
using (SafeFileHandle snapshot = CreateToolhelp32Snapshot_SafeHandle(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, (uint)processId))
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, (uint)processId);
|
||||
using (snapshot.AutoClose())
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
|
||||
|
||||
MODULEENTRY32 entry = StructMarshal.MODULEENTRY32();
|
||||
bool found = false;
|
||||
|
||||
// First module must be exe. Ignoring it.
|
||||
for (Module32First(snapshot, ref entry); Module32Next(snapshot, ref entry);)
|
||||
bool loop = Module32First(snapshot, &entry);
|
||||
while (loop)
|
||||
{
|
||||
if (entry.th32ProcessID == processId && entry.szModule.AsString() == moduleName)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
loop = Module32Next(snapshot, &entry);
|
||||
}
|
||||
|
||||
return found ? entry : default;
|
||||
|
||||
@@ -23,9 +23,9 @@ internal class HutaoCache : IHutaoCache
|
||||
|
||||
private Dictionary<AvatarId, Avatar>? idAvatarExtendedMap;
|
||||
|
||||
private bool isDatabaseViewModelInitialized;
|
||||
private bool isWikiAvatarViewModelInitiaized;
|
||||
private bool isWikiWeaponViewModelInitiaized;
|
||||
private TaskCompletionSource<bool>? databaseViewModelTaskSource;
|
||||
private TaskCompletionSource<bool>? wikiAvatarViewModelTaskSource;
|
||||
private TaskCompletionSource<bool>? wikiWeaponViewModelTaskSource;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃 API 缓存
|
||||
@@ -62,11 +62,12 @@ internal class HutaoCache : IHutaoCache
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeForDatabaseViewModelAsync()
|
||||
{
|
||||
if (isDatabaseViewModelInitialized)
|
||||
if (databaseViewModelTaskSource != null)
|
||||
{
|
||||
return true;
|
||||
return await databaseViewModelTaskSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
databaseViewModelTaskSource = new();
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
|
||||
@@ -81,20 +82,23 @@ internal class HutaoCache : IHutaoCache
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
return isDatabaseViewModelInitialized = true;
|
||||
databaseViewModelTaskSource.TrySetResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
databaseViewModelTaskSource.TrySetResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeForWikiAvatarViewModelAsync()
|
||||
{
|
||||
if (isWikiAvatarViewModelInitiaized)
|
||||
if (wikiAvatarViewModelTaskSource != null)
|
||||
{
|
||||
return true;
|
||||
return await wikiAvatarViewModelTaskSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
wikiAvatarViewModelTaskSource = new();
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
|
||||
@@ -116,21 +120,23 @@ internal class HutaoCache : IHutaoCache
|
||||
ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
|
||||
}).ToList();
|
||||
|
||||
isWikiAvatarViewModelInitiaized = true;
|
||||
wikiAvatarViewModelTaskSource.TrySetResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
wikiAvatarViewModelTaskSource.TrySetResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeForWikiWeaponViewModelAsync()
|
||||
{
|
||||
if (isWikiWeaponViewModelInitiaized)
|
||||
if (wikiWeaponViewModelTaskSource != null)
|
||||
{
|
||||
return true;
|
||||
return await wikiWeaponViewModelTaskSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
wikiWeaponViewModelTaskSource = new();
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
|
||||
@@ -148,10 +154,11 @@ internal class HutaoCache : IHutaoCache
|
||||
Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
|
||||
}).ToList();
|
||||
|
||||
isWikiWeaponViewModelInitiaized = true;
|
||||
wikiWeaponViewModelTaskSource.TrySetResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
wikiWeaponViewModelTaskSource.TrySetResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
using Snap.Hutao.Web.Hutao.Model;
|
||||
using Snap.Hutao.Web.Response;
|
||||
|
||||
namespace Snap.Hutao.Service.Hutao;
|
||||
|
||||
@@ -37,7 +38,7 @@ internal class HutaoService : IHutaoService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Overview?> GetOverviewAsync()
|
||||
public ValueTask<Overview> GetOverviewAsync()
|
||||
{
|
||||
return FromCacheOrWebAsync(nameof(Overview), homaClient.GetOverviewAsync);
|
||||
}
|
||||
@@ -78,7 +79,8 @@ internal class HutaoService : IHutaoService
|
||||
return FromCacheOrWebAsync(nameof(TeamAppearance), homaClient.GetTeamCombinationsAsync);
|
||||
}
|
||||
|
||||
private async ValueTask<T> FromCacheOrWebAsync<T>(string typeName, Func<CancellationToken, Task<T>> taskFunc)
|
||||
private async ValueTask<T> FromCacheOrWebAsync<T>(string typeName, Func<CancellationToken, Task<Response<T>>> taskFunc)
|
||||
where T : new()
|
||||
{
|
||||
string key = $"{nameof(HutaoService)}.Cache.{typeName}";
|
||||
if (memoryCache.TryGetValue(key, out object? cache))
|
||||
@@ -99,13 +101,25 @@ internal class HutaoService : IHutaoService
|
||||
}
|
||||
}
|
||||
|
||||
T web = await taskFunc(default).ConfigureAwait(false);
|
||||
appDbContext.ObjectCache.AddAndSave(new()
|
||||
Response<T> webResponse = await taskFunc(default).ConfigureAwait(false);
|
||||
T web = webResponse.IsOk() ? webResponse.Data : new();
|
||||
|
||||
try
|
||||
{
|
||||
Key = key,
|
||||
ExpireTime = DateTimeOffset.Now.AddHours(4),
|
||||
Value = JsonSerializer.Serialize(web, options),
|
||||
});
|
||||
appDbContext.ObjectCache.AddAndSave(new()
|
||||
{
|
||||
Key = key,
|
||||
|
||||
// we hold the cache for 4 hours, then just expire it.
|
||||
ExpireTime = DateTimeOffset.Now.AddHours(4),
|
||||
Value = JsonSerializer.Serialize(web, options),
|
||||
});
|
||||
}
|
||||
catch (Microsoft.EntityFrameworkCore.DbUpdateException)
|
||||
{
|
||||
// DbUpdateException: An error occurred while saving the entity changes.
|
||||
// TODO: Not ignore it.
|
||||
}
|
||||
|
||||
return memoryCache.Set(key, web, TimeSpan.FromMinutes(30));
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ internal interface IHutaoService
|
||||
/// 异步获取统计数据
|
||||
/// </summary>
|
||||
/// <returns>统计数据</returns>
|
||||
ValueTask<Overview?> GetOverviewAsync();
|
||||
ValueTask<Overview> GetOverviewAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取队伍上场
|
||||
|
||||
@@ -181,7 +181,7 @@ internal class NavigationService : INavigationService
|
||||
/// <inheritdoc/>
|
||||
public void GoBack()
|
||||
{
|
||||
Program.DispatcherQueue!.TryEnqueue(() =>
|
||||
ThreadHelper.InvokeOnMainThread(() =>
|
||||
{
|
||||
bool canGoBack = Frame?.CanGoBack ?? false;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user