Compare commits

..

21 Commits

Author SHA1 Message Date
DismissedLight
5126337138 code style [skip ci] 2023-01-12 19:42:45 +08:00
DismissedLight
4d634d3264 improve concurrent execution 2023-01-12 19:38:06 +08:00
Masterain
15a69fd0de Delete PrereleaseDistribution.yml 2023-01-11 20:42:42 -08:00
Masterain
c232891fe7 Update azure-pipelines.yml for Azure Pipelines 2023-01-11 20:41:03 -08:00
Masterain
c35c2a5700 Update workflows 2023-01-11 19:36:23 -08:00
Masterain
42305094f8 Update network-issue.yml 2023-01-11 16:12:16 -08:00
DismissedLight
9ef48ab05c fix crashes 2023-01-11 16:02:14 +08:00
DismissedLight
eec010870a improve web request experience 2023-01-11 13:11:09 +08:00
DismissedLight
a24fbf535d fix up oversea api style 2023-01-10 15:52:10 +08:00
DismissedLight
f7bd184a3c Merge pull request #352 from solacens/globalization/gotcha-histories
[Globalization] Read gotcha history from input of url or from cache/data_2
2023-01-10 15:22:06 +08:00
DismissedLight
267f285101 fix up oversea launcher support 2023-01-10 15:05:21 +08:00
solacens
2a1e77a9db add intl version gatcha history support 2023-01-10 17:46:29 +11:00
DismissedLight
abdc8e2e9f Merge pull request #334 from solacens/globalization/support-intl-version-game-launching
[Globalization] Support intl version game launching
2023-01-10 14:01:07 +08:00
solacens
64f1af293b merge main 2023-01-10 16:36:40 +11:00
DismissedLight
e0336d6b30 fix content dialog thread issue 2023-01-10 10:21:32 +08:00
DismissedLight
23f3e5df77 ContentDialogFactory 2023-01-09 12:15:11 +08:00
DismissedLight
4a027a8d3f remove filesystem context 2023-01-08 14:54:16 +08:00
DismissedLight
80459708a7 fix panel selector global group name 2023-01-08 12:32:43 +08:00
solacens
bf08ffa89e Missing changes 2023-01-03 12:20:13 +11:00
solacens
af4180bdeb Add GenshinImpact.exe to check game running 2023-01-01 23:14:35 +11:00
solacens
a70593c529 Support international version game launching 2023-01-01 22:45:10 +11:00
196 changed files with 2281 additions and 2336 deletions

View File

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

View File

@@ -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,4 @@ jobs:
$RCCONF
EOF
rclone copy ./release-download/* dgpODCN:/snaphutao/Releases/
rclone copy ./release-download/* dgpODCN:/releases/

View File

@@ -165,3 +165,14 @@ steps:
isPreRelease: true
changeLogCompareToRelease: 'lastFullRelease'
changeLogType: 'commitBased'
- task: DownloadSecureFile@1
name: cerFile
displayName: Download Rclone Config
inputs:
secureFile: 'rclone.conf'
- task: rclone@1
inputs:
arguments: 'copy $(Build.ArtifactStagingDirectory)/* downloadDGPCN:/releases/Alpha/'
configPath: '$(cerFile.secureFilePath)/rclone.conf'

View File

@@ -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]
#### 命名样式 ####

View File

@@ -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")]

View File

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

View File

@@ -5,7 +5,7 @@ using CommunityToolkit.WinUI.Notifications;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Exception;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Logging;
using System.Diagnostics;

View File

@@ -1,174 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Context.FileSystem.Location;
using System.IO;
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 文件系统上下文
/// </summary>
/// <typeparam name="TLocation">路径位置类型</typeparam>
internal abstract class FileSystemContext
{
private readonly IFileSystemLocation location;
/// <summary>
/// 初始化文件系统上下文
/// </summary>
/// <param name="location">指定的文件系统位置</param>
public FileSystemContext(IFileSystemLocation location)
{
this.location = location;
EnsureDirectory();
}
/// <summary>
/// 创建文件,若已存在文件,则不会创建
/// </summary>
/// <param name="file">文件</param>
public void CreateFileOrIgnore(string file)
{
file = Locate(file);
if (!File.Exists(file))
{
File.Create(file).Dispose();
}
}
/// <summary>
/// 创建文件夹,若已存在文件,则不会创建
/// </summary>
/// <param name="folder">文件夹</param>
public void CreateFolderOrIgnore(string folder)
{
folder = Locate(folder);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
}
/// <summary>
/// 尝试删除文件夹
/// </summary>
/// <param name="folder">文件夹</param>
public void DeleteFolderOrIgnore(string folder)
{
folder = Locate(folder);
if (Directory.Exists(folder))
{
Directory.Delete(folder, true);
}
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="file">文件名称</param>
/// <returns>是否存在</returns>
public bool FileExists(string file)
{
return File.Exists(Locate(file));
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="folder">文件夹名称</param>
/// <param name="file">文件名称</param>
/// <returns>是否存在</returns>
public bool FileExists(string folder, string file)
{
return File.Exists(Locate(folder, file));
}
/// <summary>
/// 检查文件是否存在
/// </summary>
/// <param name="folder">文件夹名称</param>
/// <returns>是否存在</returns>
public bool FolderExists(string folder)
{
return Directory.Exists(Locate(folder));
}
/// <summary>
/// 定位根目录中的文件或文件夹
/// </summary>
/// <param name="fileOrFolder">文件或文件夹</param>
/// <returns>绝对路径</returns>
public string Locate(string fileOrFolder)
{
return Path.GetFullPath(fileOrFolder, location.GetPath());
}
/// <summary>
/// 定位根目录下子文件夹中的文件
/// </summary>
/// <param name="folder">文件夹</param>
/// <param name="file">文件</param>
/// <returns>绝对路径</returns>
public string Locate(string folder, string file)
{
return Path.GetFullPath(Path.Combine(folder, file), location.GetPath());
}
/// <summary>
/// 将文件移动到指定的子目录
/// </summary>
/// <param name="file">文件</param>
/// <param name="folder">文件夹</param>
/// <param name="overwrite">是否覆盖</param>
/// <returns>是否成功 当文件不存在时会失败</returns>
public bool MoveToFolderOrIgnore(string file, string folder, bool overwrite = true)
{
string target = Locate(folder, file);
file = Locate(file);
if (File.Exists(file))
{
File.Move(file, target, overwrite);
return true;
}
return false;
}
/// <summary>
/// 等效于 <see cref="File.OpenRead(string)"/> ,但路径经过解析
/// </summary>
/// <param name="file">文件名</param>
/// <returns>文件流</returns>
public FileStream OpenRead(string file)
{
return File.OpenRead(Locate(file));
}
/// <summary>
/// 等效于 <see cref="File.Create(string)"/> ,但路径经过解析
/// </summary>
/// <param name="file">文件名</param>
/// <returns>文件流</returns>
public FileStream Create(string file)
{
return File.Create(Locate(file));
}
/// <summary>
/// 检查根目录
/// </summary>
/// <returns>是否创建了路径</returns>
private bool EnsureDirectory()
{
string folder = location.GetPath();
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
return true;
}
return false;
}
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 我的文档上下文
/// </summary>
[Injection(InjectAs.Transient)]
internal class HutaoContext : FileSystemContext
{
/// <inheritdoc cref="FileSystemContext"/>
public HutaoContext(Location.HutaoLocation myDocument)
: base(myDocument)
{
}
}

View File

@@ -1,35 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using Windows.ApplicationModel;
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 我的文档位置
/// </summary>
[Injection(InjectAs.Transient)]
internal class HutaoLocation : IFileSystemLocation
{
private string? path;
/// <inheritdoc/>
public string GetPath()
{
if (string.IsNullOrEmpty(path))
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
path = Path.GetFullPath(Path.Combine(myDocument, folderName));
}
return path;
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 文件系统位置
/// </summary>
public interface IFileSystemLocation
{
/// <summary>
/// 获取路径
/// </summary>
/// <returns>路径</returns>
string GetPath();
}

View File

@@ -1,27 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 我的文档位置
/// </summary>
[Injection(InjectAs.Transient)]
internal class Metadata : IFileSystemLocation
{
private string? path;
/// <inheritdoc/>
public string GetPath()
{
if (string.IsNullOrEmpty(path))
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
path = Path.GetFullPath(Path.Combine(myDocument, "Hutao", "Metadata"));
}
return path;
}
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Context.FileSystem.Location;
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 元数据上下文
/// </summary>
[Injection(InjectAs.Transient)]
internal class MetadataContext : FileSystemContext
{
/// <inheritdoc cref="FileSystemContext"/>
public MetadataContext(Metadata metadata)
: base(metadata)
{
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Extension;
@@ -11,19 +10,6 @@ namespace Snap.Hutao.Control.Extension;
/// </summary>
internal static class ContentDialogExtensions
{
/// <summary>
/// 针对窗口进行初始化
/// </summary>
/// <param name="contentDialog">对话框</param>
/// <param name="window">窗口</param>
/// <returns>初始化完成的对话框</returns>
public static ContentDialog InitializeWithWindow(this ContentDialog contentDialog, Window window)
{
contentDialog.XamlRoot = window.Content.XamlRoot;
return contentDialog;
}
/// <summary>
/// 阻止用户交互
/// </summary>

View File

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

View File

@@ -7,7 +7,6 @@ using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Extension;
using System.Runtime.InteropServices;
using Windows.Storage;
namespace Snap.Hutao.Control.Image;

View File

@@ -12,8 +12,6 @@ using Snap.Hutao.Service.Abstraction;
using System.IO;
using System.Net.Http;
using System.Runtime.InteropServices;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Snap.Hutao.Control.Image;

View File

@@ -6,9 +6,7 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using System.IO;
using System.Numerics;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Snap.Hutao.Control.Image;
@@ -28,8 +26,8 @@ public class Gradient : CompositionImage
/// </summary>
public GradientDirection BackgroundDirection
{
get { return (GradientDirection)GetValue(BackgroundDirectionProperty); }
set { SetValue(BackgroundDirectionProperty, value); }
get => (GradientDirection)GetValue(BackgroundDirectionProperty);
set => SetValue(BackgroundDirectionProperty, value);
}
/// <summary>
@@ -37,8 +35,8 @@ public class Gradient : CompositionImage
/// </summary>
public GradientDirection ForegroundDirection
{
get { return (GradientDirection)GetValue(ForegroundDirectionProperty); }
set { SetValue(ForegroundDirectionProperty, value); }
get => (GradientDirection)GetValue(ForegroundDirectionProperty);
set => SetValue(ForegroundDirectionProperty, value);
}
/// <inheritdoc/>

View File

@@ -5,12 +5,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
Loaded="OnRootControlLoaded"
mc:Ignorable="d">
<SplitButton
Name="RootSplitButton"
Padding="0,6"
Click="SplitButtonClick"
Loaded="SplitButtonLoaded">
Click="SplitButtonClick">
<SplitButton.Content>
<FontIcon Name="IconPresenter" Glyph="&#xE8FD;"/>
</SplitButton.Content>

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Control.Panel;
/// </summary>
public sealed partial class PanelSelector : UserControl
{
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List");
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List", OnCurrentChanged);
/// <summary>
/// 构造一个新的面板选择器
@@ -30,51 +30,60 @@ public sealed partial class PanelSelector : UserControl
set => SetValue(CurrentProperty, value);
}
private void SplitButtonLoaded(object sender, RoutedEventArgs e)
private static void OnCurrentChanged(PanelSelector sender, string current)
{
MenuFlyout menuFlyout = (MenuFlyout)((SplitButton)sender).Flyout;
((RadioMenuFlyoutItem)menuFlyout.Items[0]).IsChecked = true;
MenuFlyout menuFlyout = (MenuFlyout)sender.RootSplitButton.Flyout;
RadioMenuFlyoutItem targetItem = menuFlyout.Items
.Cast<RadioMenuFlyoutItem>()
.Single(i => (string)i.Tag == current);
targetItem.IsChecked = true;
sender.IconPresenter.Glyph = ((FontIcon)targetItem.Icon).Glyph;
}
private static void OnCurrentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
OnCurrentChanged((PanelSelector)obj, (string)args.NewValue);
}
private void OnRootControlLoaded(object sender, RoutedEventArgs e)
{
// because the GroupName shares in global
// we have to impl a control scoped GroupName.
PanelSelector selector = (PanelSelector)sender;
MenuFlyout menuFlyout = (MenuFlyout)selector.RootSplitButton.Flyout;
int hash = GetHashCode();
foreach (RadioMenuFlyoutItem item in menuFlyout.Items.Cast<RadioMenuFlyoutItem>())
{
item.GroupName = $"PanelSelector{hash}Group";
}
OnCurrentChanged(selector, Current);
}
private void SplitButtonClick(SplitButton sender, SplitButtonClickEventArgs args)
{
MenuFlyout menuFlyout = (MenuFlyout)sender.Flyout;
int i = 0;
for (; i < menuFlyout.Items.Count; i++)
{
RadioMenuFlyoutItem current = (RadioMenuFlyoutItem)menuFlyout.Items[i];
if (current.IsChecked)
if ((string)menuFlyout.Items[i].Tag == Current)
{
break;
}
}
i++;
if (i > menuFlyout.Items.Count)
{
i = 1;
}
if (i == menuFlyout.Items.Count)
{
i = 0;
}
++i;
i %= menuFlyout.Items.Count; // move the count index to 0
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)menuFlyout.Items[i];
item.IsChecked = true;
UpdateState(item);
Current = (string)item.Tag;
}
private void RadioMenuFlyoutItemClick(object sender, RoutedEventArgs e)
{
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)sender;
UpdateState(item);
}
private void UpdateState(RadioMenuFlyoutItem item)
{
Current = (string)item.Tag;
IconPresenter.Glyph = ((FontIcon)item.Icon).Glyph;
}
}

View File

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

View File

@@ -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, "值转换器异常");
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Storage;
namespace Snap.Hutao.Core.Caching;
/// <summary>

View File

@@ -7,6 +7,7 @@ using Snap.Hutao.Core.Json;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Collections.Immutable;
using System.IO;
using System.Text.Json.Serialization.Metadata;
using Windows.ApplicationModel;
@@ -70,6 +71,11 @@ internal static class CoreEnvironment
/// </summary>
public static readonly string FamilyName;
/// <summary>
/// 数据文件夹
/// </summary>
public static readonly string DataFolder;
/// <summary>
/// 默认的Json序列化选项
/// </summary>
@@ -93,6 +99,7 @@ internal static class CoreEnvironment
static CoreEnvironment()
{
DataFolder = GetDocumentsHutaoPath();
Version = Package.Current.Id.Version.ToVersion();
FamilyName = Package.Current.Id.FamilyName;
CommonUA = $"Snap Hutao/{Version}";
@@ -108,4 +115,19 @@ internal static class CoreEnvironment
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
return Md5Convert.ToHexString($"{userName}{machineGuid}");
}
private static string GetDocumentsHutaoPath()
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
Directory.CreateDirectory(path);
return path;
}
}

View File

@@ -3,7 +3,6 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Model.Entity.Database;
using System.Diagnostics;
@@ -31,9 +30,7 @@ internal static class IocConfiguration
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddDatebase(this IServiceCollection services)
{
HutaoContext myDocument = new(new());
string dbFile = myDocument.Locate("Userdata.db");
string dbFile = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
// temporarily create a context
@@ -46,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);
});
}
}

View File

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

View File

@@ -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();
}
}
}

View File

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

View File

@@ -2,8 +2,6 @@
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core.IO;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
@@ -45,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;
@@ -53,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;
}

View 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();
}
}

View 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!);
}
}
}

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.Concurrent;
using System.Diagnostics;
@@ -61,8 +60,8 @@ public sealed class LogEntryQueue : IDisposable
private static LogDbContext InitializeDbContext()
{
HutaoContext myDocument = new(new());
LogDbContext logDbContext = LogDbContext.Create($"Data Source={myDocument.Locate("Log.db")}");
string logDbName = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Log.db");
LogDbContext logDbContext = LogDbContext.Create($"Data Source={logDbName}");
if (logDbContext.Database.GetPendingMigrations().Any())
{
Debug.WriteLine("[Debug] Performing LogDbContext Migrations");
@@ -70,7 +69,7 @@ public sealed class LogEntryQueue : IDisposable
}
// only raw sql can pass
logDbContext.Database.ExecuteSqlRaw("DELETE FROM logs WHERE Exception IS NULL");
logDbContext.Logs.Where(log => log.Exception == null).ExecuteDelete();
return logDbContext;
}

View File

@@ -27,4 +27,9 @@ internal static class SettingKeys
/// 静态资源合约V1
/// </summary>
public const string StaticResourceV1Contract = "StaticResourceV1Contract";
/// <summary>
/// 静态资源合约V2 成就图标与物品图标
/// </summary>
public const string StaticResourceV2Contract = "StaticResourceV2Contract";
}

View File

@@ -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)
{
}
}

View File

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

View File

@@ -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();
}
}
}

View File

@@ -43,9 +43,6 @@ public readonly struct DispatherQueueSwitchOperation : IAwaitable<DispatherQueue
/// <inheritdoc/>
public void OnCompleted(Action continuation)
{
dispatherQueue.TryEnqueue(() =>
{
continuation();
});
dispatherQueue.TryEnqueue(() => continuation());
}
}

View File

@@ -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);
}

View File

@@ -95,4 +95,4 @@ public static class TaskExtensions
onException?.Invoke(e);
}
}
}
}

View File

@@ -31,4 +31,14 @@ internal static class ThreadHelper
{
return new(Program.DispatcherQueue!);
}
/// <summary>
/// 在主线程上同步等待执行操作
/// </summary>
/// <param name="action">操作</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void InvokeOnMainThread(Action action)
{
Program.DispatcherQueue!.Invoke(action);
}
}

View File

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

View File

@@ -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));
}

View File

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

View File

@@ -77,4 +77,32 @@ public static partial class EnumerableExtension
return false;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>
public static Dictionary<TKey, TSource> ToDictionaryOverride<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
where TKey : notnull
{
Dictionary<TKey, TSource> dictionary = new();
foreach (TSource value in source)
{
dictionary[keySelector(value)] = value;
}
return dictionary;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey, TElement}(IEnumerable{TSource}, Func{TSource, TKey}, Func{TSource, TElement})"/>
public static Dictionary<TKey, TValue> ToDictionaryOverride<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> valueSelector)
where TKey : notnull
{
Dictionary<TKey, TValue> dictionary = new();
foreach (TSource value in source)
{
dictionary[keySelector(value)] = valueSelector(value);
}
return dictionary;
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.ObjectModel;
namespace Snap.Hutao.Extension;
/// <summary>
@@ -8,26 +10,6 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static partial class EnumerableExtension
{
/// <summary>
/// 计数
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <typeparam name="TKey">计数的键类型</typeparam>
/// <param name="source">源</param>
/// <param name="keySelector">键选择器</param>
/// <returns>计数表</returns>
public static IEnumerable<KeyValuePair<TKey, int>> CountBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
where TKey : notnull, IEquatable<TKey>
{
CounterInt32<TKey> counter = new();
foreach (TSource item in source)
{
counter.Increase(keySelector(item));
}
return counter;
}
/// <summary>
/// 如果传入集合不为空则原路返回,
/// 如果传入集合为空返回一个集合的空集
@@ -64,56 +46,14 @@ public static partial class EnumerableExtension
return source.FirstOrDefault(predicate) ?? source.FirstOrDefault();
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>
public static Dictionary<TKey, TSource> ToDictionaryOverride<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
where TKey : notnull
{
Dictionary<TKey, TSource> dictionary = new();
foreach (TSource value in source)
{
dictionary[keySelector(value)] = value;
}
return dictionary;
}
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey, TElement}(IEnumerable{TSource}, Func{TSource, TKey}, Func{TSource, TElement})"/>
public static Dictionary<TKey, TValue> ToDictionaryOverride<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> valueSelector)
where TKey : notnull
{
Dictionary<TKey, TValue> dictionary = new();
foreach (TSource value in source)
{
dictionary[keySelector(value)] = valueSelector(value);
}
return dictionary;
}
/// <summary>
/// 表示一个对 <see cref="TItem"/> 类型的计数器
/// 转换到 <see cref="ObservableCollection{T}"/>
/// </summary>
/// <typeparam name="TItem">待计数的类型</typeparam>
private class CounterInt32<TItem> : Dictionary<TItem, int>
where TItem : notnull, IEquatable<TItem>
/// <typeparam name="T">类型</typeparam>
/// <param name="source">源</param>
/// <returns><see cref="ObservableCollection{T}"/></returns>
public static ObservableCollection<T> ToObservableCollection<T>(this IEnumerable<T> source)
{
/// <summary>
/// 增加计数器
/// </summary>
/// <param name="item">物品</param>
public void Increase(TItem? item)
{
if (item != null)
{
if (!ContainsKey(item))
{
this[item] = 0;
}
this[item] += 1;
}
}
return new ObservableCollection<T>(source);
}
}

View File

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

View File

@@ -0,0 +1,36 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Factory.Abstraction;
/// <summary>
/// 内容对话框工厂
/// </summary>
internal interface IContentDialogFactory
{
/// <summary>
/// 异步确认
/// </summary>
/// <param name="title">标题</param>
/// <param name="content">内容</param>
/// <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>
ValueTask<ContentDialogResult> ConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close);
/// <summary>
/// 异步创建一个新的内容对话框,用于提示未知的进度
/// </summary>
/// <param name="title">标题</param>
/// <returns>内容对话框</returns>
ValueTask<ContentDialog> CreateForIndeterminateProgressAsync(string title);
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Factory.Abstraction;
namespace Snap.Hutao.Factory;
/// <inheritdoc cref="IContentDialogFactory"/>
[Injection(InjectAs.Transient, typeof(IContentDialogFactory))]
internal class ContentDialogFactory : IContentDialogFactory
{
private readonly MainWindow mainWindow;
/// <summary>
/// 构造一个新的内容对话框工厂
/// </summary>
/// <param name="mainWindow">主窗体</param>
public ContentDialogFactory(MainWindow mainWindow)
{
this.mainWindow = mainWindow;
}
/// <inheritdoc/>
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,
Title = title,
Content = content,
DefaultButton = ContentDialogButton.Primary,
PrimaryButtonText = "确认",
};
return dialog;
}
private async ValueTask<ContentDialog> CreateForConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
{
await ThreadHelper.SwitchToMainThreadAsync();
ContentDialog dialog = new()
{
XamlRoot = mainWindow.Content.XamlRoot,
Title = title,
Content = content,
DefaultButton = defaultButton,
PrimaryButtonText = "确认",
CloseButtonText = "取消",
};
return dialog;
}
}

View File

@@ -33,9 +33,11 @@ public sealed partial class MainWindow : Window, IExtendedWindowSource, IRecipie
Ioc.Default.GetRequiredService<IMessenger>().Register(this);
// Query the StaticResourceV1Contract.
// Query the StaticResourceV1Contract & StaticResourceV2Contract.
// If not complete we should present the welcome view.
ContentSwitchPresenter.Value = !LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false);
ContentSwitchPresenter.Value =
!LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false)
|| (!LocalSetting.Get(SettingKeys.StaticResourceV2Contract, false));
}
/// <summary>

View File

@@ -51,7 +51,7 @@ public class Avatar : ICalculableSource<ICalculableAvatar>
/// <summary>
/// 武器
/// </summary>
public Weapon Weapon { get; set; } = default!;
public Weapon? Weapon { get; set; } = default!;
/// <summary>
/// 圣遗物列表

View File

@@ -22,5 +22,8 @@ public class RankAvatar : Avatar
Value = value;
}
/// <summary>
/// 排行
/// </summary>
public int Value { get; set; }
}

View File

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

View File

@@ -1,44 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.Model.Binding.User;
/// <summary>
/// 角色与实体用户
/// </summary>
public class UserAndRole
{
/// <summary>
/// 构造一个新的实体用户与角色
/// </summary>
/// <param name="user">实体用户</param>
/// <param name="role">角色</param>
public UserAndRole(EntityUser user, UserGameRole role)
{
User = user;
Role = role;
}
/// <summary>
/// 实体用户
/// </summary>
public EntityUser User { get; private set; }
/// <summary>
/// 角色
/// </summary>
public UserGameRole Role { get; private set; }
/// <summary>
/// 从用户与选中的角色转换
/// </summary>
/// <param name="user">角色</param>
/// <returns>用户与角色</returns>
public static UserAndRole FromUser(User user)
{
return new UserAndRole(user.Entity, user.SelectedUserGameRole!);
}
}

View File

@@ -0,0 +1,64 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab;
using EntityUser = Snap.Hutao.Model.Entity.User;
namespace Snap.Hutao.Model.Binding.User;
/// <summary>
/// 实体用户与角色
/// 由于许多操作需要同时用到ck与uid
/// 抽象此类用于简化这类调用
/// </summary>
public class UserAndUid
{
/// <summary>
/// 构造一个新的实体用户与角色
/// </summary>
/// <param name="user">实体用户</param>
/// <param name="role">角色</param>
public UserAndUid(EntityUser user, PlayerUid role)
{
User = user;
Uid = role;
}
/// <summary>
/// 实体用户
/// </summary>
public EntityUser User { get; private set; }
/// <summary>
/// 角色
/// </summary>
public PlayerUid Uid { get; private set; }
/// <summary>
/// 从用户与选中的角色转换
/// </summary>
/// <param name="user">角色</param>
/// <returns>用户与角色</returns>
public static UserAndUid FromUser(User user)
{
return new UserAndUid(user.Entity, user.SelectedUserGameRole!);
}
/// <summary>
/// 尝试转换到用户与角色
/// </summary>
/// <param name="user">用户</param>
/// <param name="userAndUid">用户与角色</param>
/// <returns>是否转换成功</returns>
public static bool TryFromUser(User? user, [NotNullWhen(true)] out UserAndUid? userAndUid)
{
if (user != null && user.SelectedUserGameRole != null)
{
userAndUid = FromUser(user);
return true;
}
userAndUid = null;
return false;
}
}

View File

@@ -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,
};

View File

@@ -2,17 +2,17 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity;
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>
@@ -32,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>
@@ -136,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/>

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore.Design;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Model.Entity.Database;
namespace Snap.Hutao.Context.Database;
@@ -17,7 +16,7 @@ public class AppDbContextDesignTimeFactory : IDesignTimeDbContextFactory<AppDbCo
[EditorBrowsable(EditorBrowsableState.Never)]
public AppDbContext CreateDbContext(string[] args)
{
HutaoContext myDocument = new(new());
return AppDbContext.Create($"Data Source={myDocument.Locate("Userdata.db")}");
string userdataDbName = System.IO.Path.Combine(Core.CoreEnvironment.DataFolder, "Userdata.db");
return AppDbContext.Create($"Data Source={userdataDbName}");
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore.Design;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Model.Entity.Database;
namespace Snap.Hutao.Context.Database;
@@ -17,7 +16,7 @@ public class LogDbContextDesignTimeFactory : IDesignTimeDbContextFactory<LogDbCo
[EditorBrowsable(EditorBrowsableState.Never)]
public LogDbContext CreateDbContext(string[] args)
{
HutaoContext myDocument = new(new());
return LogDbContext.Create($"Data Source={myDocument.Locate("Log.db")}");
string logDbName = System.IO.Path.Combine(Core.CoreEnvironment.DataFolder, "Log.db");
return LogDbContext.Create($"Data Source={logDbName}");
}
}

View File

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

View File

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

View File

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

View File

@@ -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>
/// 从角色转换到名片

View File

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

View File

@@ -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>
/// 获取特定等级的解释

View File

@@ -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>
/// 将中文元素名称转换为图标链接

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
/// 格式化对

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
/// 将武器类型转换为图标链接

View File

@@ -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,
};
}
}

View File

@@ -28,12 +28,11 @@ CoWaitForMultipleObjects
// USER32
FindWindowEx
GetDpiForWindow
GetWindowPlacement
// COM BITS
BackgroundCopyManager
IBackgroundCopyCallback
IBackgroundCopyFile5
IBackgroundCopyJobHttpOptions
IBackgroundCopyManager
// WinRT

View File

@@ -12,7 +12,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.3.4.0" />
Version="1.3.9.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
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 +53,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/>

View File

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

View File

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

View File

@@ -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();
}
}
}

View File

@@ -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;
@@ -36,6 +27,8 @@ internal class AvatarInfoService : IAvatarInfoService
private readonly IMetadataService metadataService;
private readonly ILogger<AvatarInfoService> logger;
private readonly AvatarInfoDbOperation avatarInfoDbOperation;
/// <summary>
/// 构造一个新的角色信息服务
/// </summary>
@@ -53,10 +46,12 @@ internal class AvatarInfoService : IAvatarInfoService
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 +61,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 +126,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();
}
}
}

View File

@@ -1,15 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Annotation;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Converter;
using ModelPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>

View File

@@ -40,7 +40,7 @@ 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()
@@ -65,11 +65,12 @@ internal class SummaryAvatarFactory
};
}
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)
@@ -134,9 +135,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;

View File

@@ -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);
}

View File

@@ -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,
};
}
}
}

View File

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

View File

@@ -45,13 +45,16 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
/// <inheritdoc/>
public void Receive(UserRemovedMessage message)
{
entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
ThreadHelper.InvokeOnMainThread(() =>
{
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 +63,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 +118,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);
}
}
}

View File

@@ -17,7 +17,7 @@ public interface IDailyNoteService
/// </summary>
/// <param name="role">角色</param>
/// <returns>任务</returns>
Task AddDailyNoteAsync(UserAndRole role);
Task AddDailyNoteAsync(UserAndUid role);
/// <summary>
/// 异步获取实时便笺列表

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.Mvvm.Messaging;
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 +27,7 @@ namespace Snap.Hutao.Service.GachaLog;
/// 祈愿记录服务
/// </summary>
[Injection(InjectAs.Scoped, typeof(IGachaLogService))]
internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
internal class GachaLogService : IGachaLogService
{
/// <summary>
/// 祈愿记录查询的类型
@@ -95,9 +94,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 +113,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 +219,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 +244,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 +267,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;
@@ -320,7 +316,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));
}
}
}

View File

@@ -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>
/// 刷新祈愿记录

View File

@@ -17,8 +17,9 @@ internal class GachaLogUrlManualInputProvider : IGachaLogUrlProvider
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> GetQueryAsync()
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
ValueResult<bool, string> result = await new GachaLogUrlDialog(mainWindow).GetInputUrlAsync().ConfigureAwait(false);
// ContentDialog must be created by main thread.
await ThreadHelper.SwitchToMainThreadAsync();
ValueResult<bool, string> result = await new GachaLogUrlDialog().GetInputUrlAsync().ConfigureAwait(false);
if (result.IsOk)
{

View File

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

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