mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e344f56e0 | ||
|
|
848392f8d4 | ||
|
|
62d0fb5d05 | ||
|
|
7a99c44b29 | ||
|
|
792a701183 | ||
|
|
fa19f7e817 |
@@ -22,6 +22,7 @@ public class InjectionGenerator : ISourceGenerator
|
||||
{
|
||||
private const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
|
||||
private const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
|
||||
private const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
@@ -97,8 +98,11 @@ internal static partial class ServiceCollectionExtensions
|
||||
case InjectAsTransientName:
|
||||
lineBuilder.Append(@" services.AddTransient(");
|
||||
break;
|
||||
case InjectAsScopedName:
|
||||
lineBuilder.Append(@" services.AddScoped(");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"非法的InjectAs值: [{injectAsName}]");
|
||||
throw new InvalidOperationException($"非法的 InjectAs 值: [{injectAsName}]");
|
||||
}
|
||||
|
||||
if (arguments.Length == 2)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
x:Class="Snap.Hutao.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
|
||||
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
|
||||
xmlns:shvc="using:Snap.Hutao.View.Converter">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
@@ -13,21 +16,36 @@
|
||||
<!--Modify Window title bar color-->
|
||||
<StaticResource x:Key="WindowCaptionBackground" ResourceKey="ControlFillColorTransparentBrush"/>
|
||||
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush"/>
|
||||
|
||||
<!--Page Transparent Background-->
|
||||
<StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush"/>
|
||||
|
||||
<!--IconFont-->
|
||||
<FontFamily x:Key="SymbolThemeFontFamily">ms-appx:///Resource/Font/Segoe Fluent Icons.ttf#Segoe Fluent Icons</FontFamily>
|
||||
|
||||
<!--InfoBar Resource-->
|
||||
<Thickness x:Key="InfoBarIconMargin">6,16,16,16</Thickness>
|
||||
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
|
||||
|
||||
<!--Pivot Resource-->
|
||||
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
|
||||
|
||||
<!--CornerRadius-->
|
||||
<CornerRadius x:Key="CompatCornerRadius">6</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusTop">6,6,0,0</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusRight">0,6,6,0</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusBottom">0,0,6,6</CornerRadius>
|
||||
<CornerRadius x:Key="CompatCornerRadiusSmall">2</CornerRadius>
|
||||
<!--Converters-->
|
||||
<cwuc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
<shmmc:AchievementIconConverter x:Key="AchievementIconConverter"/>
|
||||
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
|
||||
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
|
||||
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
|
||||
<shmmc:DescParamDescriptor x:Key="DescParamDescriptor"/>
|
||||
<shmmc:ElementNameIconConverter x:Key="ElementNameIconConverter"/>
|
||||
<shmmc:GachaAvatarImgConverter x:Key="GachaAvatarImgConverter"/>
|
||||
<shmmc:GachaAvatarIconConverter x:Key="GachaAvatarIconConverter"/>
|
||||
<shmmc:ItemIconConverter x:Key="ItemIconConverter"/>
|
||||
<shmmc:PropertyInfoDescriptor x:Key="PropertyDescriptor"/>
|
||||
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
|
||||
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
|
||||
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -54,13 +54,17 @@ public partial class App : Application
|
||||
logger.LogInformation(EventIds.CommonLog, "Snap Hutao : {version}", CoreEnvironment.Version);
|
||||
logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.TemporaryFolder.Path);
|
||||
|
||||
JumpListHelper.ConfigAsync().SafeForget(logger);
|
||||
|
||||
Ioc.Default
|
||||
.GetRequiredService<IMetadataService>()
|
||||
.ImplictAs<IMetadataInitializer>()?
|
||||
.InitializeInternalAsync()
|
||||
.SafeForget(logger);
|
||||
|
||||
Ioc.Default.GetRequiredService<AppCenter>().Initialize();
|
||||
Ioc.Default
|
||||
.GetRequiredService<AppCenter>()
|
||||
.Initialize();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -56,6 +56,11 @@ public class AppDbContext : DbContext
|
||||
/// </summary>
|
||||
public DbSet<AvatarInfo> AvatarInfos { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏内账号
|
||||
/// </summary>
|
||||
public DbSet<GameAccount> GameAccounts { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个临时的应用程序数据库上下文
|
||||
/// </summary>
|
||||
|
||||
@@ -55,4 +55,4 @@ internal class AutoHeightBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
AssociatedObject.Height = (double)AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.UI.Behaviors;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core;
|
||||
|
||||
namespace Snap.Hutao.Control.Behavior;
|
||||
|
||||
/// <summary>
|
||||
/// 按给定比例自动调整高度的行为
|
||||
/// </summary>
|
||||
internal class AutoWidthBehavior : BehaviorBase<FrameworkElement>
|
||||
{
|
||||
private static readonly DependencyProperty TargetWidthProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetWidth), 320D);
|
||||
private static readonly DependencyProperty TargetHeightProperty = Property<AutoWidthBehavior>.Depend(nameof(TargetHeight), 1024D);
|
||||
|
||||
/// <summary>
|
||||
/// 目标宽度
|
||||
/// </summary>
|
||||
public double TargetWidth
|
||||
{
|
||||
get => (double)GetValue(TargetWidthProperty);
|
||||
set => SetValue(TargetWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标高度
|
||||
/// </summary>
|
||||
public double TargetHeight
|
||||
{
|
||||
get => (double)GetValue(TargetHeightProperty);
|
||||
set => SetValue(TargetHeightProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAssociatedObjectLoaded()
|
||||
{
|
||||
UpdateElementWidth();
|
||||
AssociatedObject.SizeChanged += OnSizeChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
AssociatedObject.SizeChanged -= OnSizeChanged;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateElementWidth();
|
||||
}
|
||||
|
||||
private void UpdateElementWidth()
|
||||
{
|
||||
AssociatedObject.Width = (double)AssociatedObject.Height * (TargetWidth / TargetHeight);
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,6 @@ public class ScopedPage : Page
|
||||
serviceScope = Ioc.Default.CreateScope();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IServiceScope.ServiceProvider"/>
|
||||
public IServiceProvider ServiceProvider { get => serviceScope.ServiceProvider; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// </summary>
|
||||
@@ -38,7 +35,7 @@ public class ScopedPage : Page
|
||||
public void InitializeWith<TViewModel>()
|
||||
where TViewModel : class, ISupportCancellation
|
||||
{
|
||||
ISupportCancellation viewModel = ServiceProvider.GetRequiredService<TViewModel>();
|
||||
ISupportCancellation viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
|
||||
viewModel.CancellationToken = viewLoadingCancellationTokenSource.Token;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ public abstract class CacheBase<T>
|
||||
{
|
||||
private readonly SemaphoreSlim cacheFolderSemaphore = new(1);
|
||||
private readonly ILogger logger;
|
||||
|
||||
// violate di rule
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
private StorageFolder? baseFolder;
|
||||
|
||||
63
src/Snap.Hutao/Snap.Hutao/Core/CommandLineBuilder.cs
Normal file
63
src/Snap.Hutao/Snap.Hutao/Core/CommandLineBuilder.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 命令行建造器
|
||||
/// </summary>
|
||||
public class CommandLineBuilder
|
||||
{
|
||||
private const char WhiteSpace = ' ';
|
||||
private readonly Dictionary<string, string?> options = new();
|
||||
|
||||
/// <summary>
|
||||
/// 当符合条件时添加参数
|
||||
/// </summary>
|
||||
/// <param name="name">参数名称</param>
|
||||
/// <param name="condition">条件</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>命令行建造器</returns>
|
||||
public CommandLineBuilder AppendIf(string name, bool condition, object? value = null)
|
||||
{
|
||||
return condition ? Append(name, value) : this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加参数
|
||||
/// </summary>
|
||||
/// <param name="name">参数名称</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>命令行建造器</returns>
|
||||
public CommandLineBuilder Append(string name, object? value = null)
|
||||
{
|
||||
options.Add(name, value?.ToString());
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ToString"/>
|
||||
public string Build()
|
||||
{
|
||||
return ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder s = new();
|
||||
foreach ((string key, string? value) in options)
|
||||
{
|
||||
s.Append(WhiteSpace);
|
||||
s.Append(key);
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
s.Append(WhiteSpace);
|
||||
s.Append(value);
|
||||
}
|
||||
}
|
||||
|
||||
return s.ToString();
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,6 @@ namespace Snap.Hutao.Core;
|
||||
/// </summary>
|
||||
internal static class CoreEnvironment
|
||||
{
|
||||
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
|
||||
private const string MachineGuidValue = "MachineGuid";
|
||||
|
||||
// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
|
||||
|
||||
/// <summary>
|
||||
@@ -71,6 +68,9 @@ internal static class CoreEnvironment
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
|
||||
private const string MachineGuidValue = "MachineGuid";
|
||||
|
||||
static CoreEnvironment()
|
||||
{
|
||||
Version = Package.Current.Id.Version.ToVersion();
|
||||
|
||||
@@ -14,9 +14,8 @@ namespace Snap.Hutao.Core.Database;
|
||||
/// <typeparam name="TMessage">消息的类型</typeparam>
|
||||
internal class DbCurrent<TEntity, TMessage>
|
||||
where TEntity : class, ISelectable
|
||||
where TMessage : Message.ValueChangedMessage<TEntity>
|
||||
where TMessage : Message.ValueChangedMessage<TEntity>, new()
|
||||
{
|
||||
private readonly DbContext dbContext;
|
||||
private readonly DbSet<TEntity> dbSet;
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
@@ -25,12 +24,10 @@ internal class DbCurrent<TEntity, TMessage>
|
||||
/// <summary>
|
||||
/// 构造一个新的数据库当前项
|
||||
/// </summary>
|
||||
/// <param name="dbContext">数据库上下文</param>
|
||||
/// <param name="dbSet">数据集</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, IMessenger messenger)
|
||||
public DbCurrent(DbSet<TEntity> dbSet, IMessenger messenger)
|
||||
{
|
||||
this.dbContext = dbContext;
|
||||
this.dbSet = dbSet;
|
||||
this.messenger = messenger;
|
||||
}
|
||||
@@ -55,96 +52,18 @@ internal class DbCurrent<TEntity, TMessage>
|
||||
if (current != null)
|
||||
{
|
||||
current.IsSelected = false;
|
||||
dbSet.Update(current);
|
||||
dbContext.SaveChanges();
|
||||
dbSet.UpdateAndSave(current);
|
||||
}
|
||||
}
|
||||
|
||||
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
|
||||
TMessage message = new() { OldValue = current, NewValue = value };
|
||||
|
||||
current = value;
|
||||
|
||||
if (current != null)
|
||||
{
|
||||
current.IsSelected = true;
|
||||
dbSet.Update(current);
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据库当前项
|
||||
/// 简化对数据库中选中项的管理
|
||||
/// </summary>
|
||||
/// <typeparam name="TObservable">绑定类型</typeparam>
|
||||
/// <typeparam name="TEntity">实体的类型</typeparam>
|
||||
/// <typeparam name="TMessage">消息的类型</typeparam>
|
||||
[SuppressMessage("", "SA1402")]
|
||||
internal class DbCurrent<TObservable, TEntity, TMessage>
|
||||
where TObservable : class
|
||||
where TEntity : class, ISelectable
|
||||
where TMessage : Message.ValueChangedMessage<TObservable>
|
||||
{
|
||||
private readonly DbContext dbContext;
|
||||
private readonly DbSet<TEntity> dbSet;
|
||||
private readonly IMessenger messenger;
|
||||
private readonly Func<TObservable, TEntity> selector;
|
||||
|
||||
private TObservable? current;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的数据库当前项
|
||||
/// </summary>
|
||||
/// <param name="dbContext">数据库上下文</param>
|
||||
/// <param name="dbSet">数据集</param>
|
||||
/// <param name="selector">选择器</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, Func<TObservable, TEntity> selector, IMessenger messenger)
|
||||
{
|
||||
this.dbContext = dbContext;
|
||||
this.dbSet = dbSet;
|
||||
this.selector = selector;
|
||||
this.messenger = messenger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的项
|
||||
/// </summary>
|
||||
public TObservable? Current
|
||||
{
|
||||
get => current;
|
||||
set
|
||||
{
|
||||
// prevent useless sets
|
||||
if (current == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// only update when not processing a deletion
|
||||
if (value != null)
|
||||
{
|
||||
if (current != null)
|
||||
{
|
||||
TEntity entity = selector(current);
|
||||
entity.IsSelected = false;
|
||||
dbSet.Update(entity);
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
|
||||
current = value;
|
||||
|
||||
if (current != null)
|
||||
{
|
||||
TEntity entity = selector(current);
|
||||
entity.IsSelected = true;
|
||||
dbSet.Update(entity);
|
||||
dbContext.SaveChanges();
|
||||
dbSet.UpdateAndSave(current);
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
|
||||
95
src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs
Normal file
95
src/Snap.Hutao/Snap.Hutao/Core/Database/DbSetExtension.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库集合上下文
|
||||
/// </summary>
|
||||
public static class DbSetExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取对应的数据库上下文
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <returns>对应的数据库上下文</returns>
|
||||
public static DbContext Context<TEntity>(this DbSet<TEntity> dbSet)
|
||||
where TEntity : class
|
||||
{
|
||||
return dbSet.GetService<ICurrentDbContext>().Context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或添加一个对应的实体
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="predicate">谓词</param>
|
||||
/// <param name="entityFactory">实体工厂</param>
|
||||
/// <param name="added">是否添加</param>
|
||||
/// <returns>实体</returns>
|
||||
public static TEntity SingleOrAdd<TEntity>(this DbSet<TEntity> dbSet, Func<TEntity, bool> predicate, Func<TEntity> entityFactory, out bool added)
|
||||
where TEntity : class
|
||||
{
|
||||
added = false;
|
||||
TEntity? entry = dbSet.SingleOrDefault(predicate);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
entry = entityFactory();
|
||||
dbSet.Add(entry);
|
||||
dbSet.Context().SaveChanges();
|
||||
|
||||
added = true;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static int AddAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Add(entity);
|
||||
return dbSet.Context().SaveChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Remove(entity);
|
||||
return dbSet.Context().SaveChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新并保存
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">实体类型</typeparam>
|
||||
/// <param name="dbSet">数据库集</param>
|
||||
/// <param name="entity">实体</param>
|
||||
/// <returns>影响条数</returns>
|
||||
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
|
||||
where TEntity : class
|
||||
{
|
||||
dbSet.Update(entity);
|
||||
return dbSet.Context().SaveChanges();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 设置帮助类
|
||||
/// </summary>
|
||||
public static class SettingEntryHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或添加一个对应的设置
|
||||
/// </summary>
|
||||
/// <param name="dbSet">设置集</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>设置</returns>
|
||||
public static SettingEntry SingleOrAdd(this DbSet<SettingEntry> dbSet, string key, string value)
|
||||
{
|
||||
SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
entry = new(key, value);
|
||||
dbSet.Add(entry);
|
||||
dbSet.Context().SaveChanges();
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或添加一个对应的设置
|
||||
/// </summary>
|
||||
/// <param name="dbSet">设置集</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="valueFactory">值工厂</param>
|
||||
/// <returns>设置</returns>
|
||||
public static SettingEntry SingleOrAdd(this DbSet<SettingEntry> dbSet, string key, Func<string> valueFactory)
|
||||
{
|
||||
SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
entry = new(key, valueFactory());
|
||||
dbSet.Add(entry);
|
||||
dbSet.Context().SaveChanges();
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Boolean 值
|
||||
/// </summary>
|
||||
/// <param name="entry">设置</param>
|
||||
/// <returns>值</returns>
|
||||
public static bool GetBoolean(this SettingEntry entry)
|
||||
{
|
||||
return bool.Parse(entry.Value!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 Boolean 值
|
||||
/// </summary>
|
||||
/// <param name="entry">设置</param>
|
||||
/// <param name="value">值</param>
|
||||
public static void SetBoolean(this SettingEntry entry, bool value)
|
||||
{
|
||||
entry.Value = value.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Int32 值
|
||||
/// </summary>
|
||||
/// <param name="entry">设置</param>
|
||||
/// <returns>值</returns>
|
||||
public static int GetInt32(this SettingEntry entry)
|
||||
{
|
||||
return int.Parse(entry.Value!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 Int32 值
|
||||
/// </summary>
|
||||
/// <param name="entry">设置</param>
|
||||
/// <param name="value">值</param>
|
||||
public static void SetInt32(this SettingEntry entry, int value)
|
||||
{
|
||||
entry.Value = value.ToString();
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,9 @@ public enum InjectAs
|
||||
/// 指示应注册为短期对象
|
||||
/// </summary>
|
||||
Transient,
|
||||
|
||||
/// <summary>
|
||||
/// 指示应注册为范围对象
|
||||
/// </summary>
|
||||
Scoped,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core.IO.Ini;
|
||||
internal static class IniSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步反序列化
|
||||
/// 反序列化
|
||||
/// </summary>
|
||||
/// <param name="fileStream">文件流</param>
|
||||
/// <returns>Ini 元素集合</returns>
|
||||
@@ -44,4 +44,20 @@ internal static class IniSerializer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 序列化
|
||||
/// </summary>
|
||||
/// <param name="fileStream">写入的流</param>
|
||||
/// <param name="elements">元素</param>
|
||||
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
|
||||
{
|
||||
using (TextWriter writer = new StreamWriter(fileStream))
|
||||
{
|
||||
foreach (IniElement element in elements)
|
||||
{
|
||||
writer.WriteLine(element.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,4 @@ internal class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEnumerabl
|
||||
{
|
||||
writer.WriteStringValue(string.Join(',', value));
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Snap.Hutao/Snap.Hutao/Core/JumpListHelper.cs
Normal file
35
src/Snap.Hutao/Snap.Hutao/Core/JumpListHelper.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Windows.UI.StartScreen;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 跳转列表帮助类
|
||||
/// </summary>
|
||||
public static class JumpListHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步配置跳转列表
|
||||
/// </summary>
|
||||
/// <returns>任务</returns>
|
||||
public static async Task ConfigAsync()
|
||||
{
|
||||
if (JumpList.IsSupported())
|
||||
{
|
||||
JumpList list = await JumpList.LoadCurrentAsync();
|
||||
|
||||
list.Items.Clear();
|
||||
|
||||
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, "启动游戏");
|
||||
launchGameItem.GroupName = "快捷操作";
|
||||
launchGameItem.Logo = new("ms-appx:///Resource/Icon/UI_GuideIcon_PlayMethod.png");
|
||||
|
||||
list.Items.Add(launchGameItem);
|
||||
|
||||
await list.SaveAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
@@ -13,8 +14,26 @@ namespace Snap.Hutao.Core.LifeCycle;
|
||||
/// </summary>
|
||||
internal static class Activation
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动游戏启动参数
|
||||
/// </summary>
|
||||
public const string LaunchGame = "LaunchGame";
|
||||
|
||||
private static readonly SemaphoreSlim ActivateSemaphore = new(1);
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否提升了权限
|
||||
/// </summary>
|
||||
/// <returns>是否提升了权限</returns>
|
||||
public static bool GetElevated()
|
||||
{
|
||||
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
|
||||
{
|
||||
WindowsPrincipal principal = new(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 响应激活事件
|
||||
/// 激活事件一般不会在UI线程上触发
|
||||
@@ -44,16 +63,48 @@ internal static class Activation
|
||||
|
||||
private static async Task HandleActivationCoreAsync(AppActivationArguments args)
|
||||
{
|
||||
_ = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
string argument = string.Empty;
|
||||
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
await infoBarService.WaitInitializationAsync().ConfigureAwait(false);
|
||||
if (args.Kind == ExtendedActivationKind.Launch)
|
||||
{
|
||||
if (args.TryGetLaunchActivatedArgument(out string? arguments))
|
||||
{
|
||||
argument = arguments;
|
||||
}
|
||||
}
|
||||
|
||||
switch (argument)
|
||||
{
|
||||
case "":
|
||||
{
|
||||
_ = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
await Ioc.Default.GetRequiredService<IInfoBarService>().WaitInitializationAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
case LaunchGame:
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
if (!MainWindow.IsPresent)
|
||||
{
|
||||
_ = Ioc.Default.GetRequiredService<LaunchGameWindow>();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Ioc.Default
|
||||
.GetRequiredService<INavigationService>()
|
||||
.NavigateAsync<View.Page.LaunchGamePage>(INavigationAwaiter.Default, true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (args.Kind == ExtendedActivationKind.Protocol)
|
||||
{
|
||||
if (args.TryGetProtocolActivatedUri(out Uri? uri))
|
||||
{
|
||||
infoBarService.Information(uri.ToString());
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Information(uri.ToString());
|
||||
await HandleUrlActivationAsync(uri).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,22 @@ public static class AppActivationArgumentsExtensions
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取启动的参数
|
||||
/// </summary>
|
||||
/// <param name="activatedEventArgs">应用程序激活参数</param>
|
||||
/// <param name="arguments">参数</param>
|
||||
/// <returns>是否存在参数</returns>
|
||||
public static bool TryGetLaunchActivatedArgument(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
|
||||
{
|
||||
arguments = null;
|
||||
if (activatedEventArgs.Data is ILaunchActivatedEventArgs launchArgs)
|
||||
{
|
||||
arguments = launchArgs.Arguments;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,4 +41,4 @@ public static class ProcessHelper
|
||||
};
|
||||
return Process.Start(processInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,6 @@ namespace Snap.Hutao.Core.Setting;
|
||||
/// </summary>
|
||||
internal static class LocalSetting
|
||||
{
|
||||
/// <summary>
|
||||
/// 由于 <see cref="Windows.Foundation.Collections.IPropertySet"/> 没有 nullable context,
|
||||
/// 在处理引用类型时需要格外小心
|
||||
/// </summary>
|
||||
private static readonly ApplicationDataContainer Container;
|
||||
|
||||
static LocalSetting()
|
||||
@@ -21,6 +17,198 @@ internal static class LocalSetting
|
||||
Container = ApplicationData.Current.LocalSettings;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static byte Get(string key, byte defaultValue)
|
||||
{
|
||||
return Get<byte>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static short Get(string key, short defaultValue)
|
||||
{
|
||||
return Get<short>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static ushort Get(string key, ushort defaultValue)
|
||||
{
|
||||
return Get<ushort>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static int Get(string key, int defaultValue)
|
||||
{
|
||||
return Get<int>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static uint Get(string key, uint defaultValue)
|
||||
{
|
||||
return Get<uint>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static ulong Get(string key, ulong defaultValue)
|
||||
{
|
||||
return Get<ulong>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static float Get(string key, float defaultValue)
|
||||
{
|
||||
return Get<float>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static double Get(string key, double defaultValue)
|
||||
{
|
||||
return Get<double>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static bool Get(string key, bool defaultValue)
|
||||
{
|
||||
return Get<bool>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static char Get(string key, char defaultValue)
|
||||
{
|
||||
return Get<char>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static DateTimeOffset Get(string key, DateTimeOffset defaultValue)
|
||||
{
|
||||
return Get<DateTimeOffset>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static TimeSpan Get(string key, TimeSpan defaultValue)
|
||||
{
|
||||
return Get<TimeSpan>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static Guid Get(string key, Guid defaultValue)
|
||||
{
|
||||
return Get<Guid>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static Windows.Foundation.Point Get(string key, Windows.Foundation.Point defaultValue)
|
||||
{
|
||||
return Get<Windows.Foundation.Point>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static Windows.Foundation.Size Get(string key, Windows.Foundation.Size defaultValue)
|
||||
{
|
||||
return Get<Windows.Foundation.Size>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Get{T}(string, T)"/>
|
||||
public static Windows.Foundation.Rect Get(string key, Windows.Foundation.Rect defaultValue)
|
||||
{
|
||||
return Get<Windows.Foundation.Rect>(key, defaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, byte value)
|
||||
{
|
||||
Set<byte>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, short value)
|
||||
{
|
||||
Set<short>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, ushort value)
|
||||
{
|
||||
Set<ushort>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, int value)
|
||||
{
|
||||
Set<int>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, uint value)
|
||||
{
|
||||
Set<uint>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, ulong value)
|
||||
{
|
||||
Set<ulong>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, float value)
|
||||
{
|
||||
Set<float>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, double value)
|
||||
{
|
||||
Set<double>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, bool value)
|
||||
{
|
||||
Set<bool>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, char value)
|
||||
{
|
||||
Set<char>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, DateTimeOffset value)
|
||||
{
|
||||
Set<DateTimeOffset>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, TimeSpan value)
|
||||
{
|
||||
Set<TimeSpan>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, Guid value)
|
||||
{
|
||||
Set<Guid>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, Windows.Foundation.Point value)
|
||||
{
|
||||
Set<Windows.Foundation.Point>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, Windows.Foundation.Size value)
|
||||
{
|
||||
Set<Windows.Foundation.Size>(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Set{T}(string, T)"/>
|
||||
public static void Set(string key, Windows.Foundation.Rect value)
|
||||
{
|
||||
Set<Windows.Foundation.Rect>(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取设置项的值
|
||||
/// </summary>
|
||||
@@ -28,8 +216,8 @@ internal static class LocalSetting
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>获取的值</returns>
|
||||
[return: MaybeNull]
|
||||
public static T Get<T>(string key, [AllowNull] T defaultValue = default)
|
||||
private static T Get<T>(string key, T defaultValue = default)
|
||||
where T : struct
|
||||
{
|
||||
if (Container.Values.TryGetValue(key, out object? value))
|
||||
{
|
||||
@@ -49,9 +237,9 @@ internal static class LocalSetting
|
||||
/// <typeparam name="T">设置项的类型</typeparam>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>设置的值</returns>
|
||||
public static object? Set<T>(string key, T value)
|
||||
private static void Set<T>(string key, T value)
|
||||
where T : struct
|
||||
{
|
||||
return Container.Values[key] = value;
|
||||
Container.Values[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,6 @@ namespace Snap.Hutao.Core.Setting;
|
||||
/// </summary>
|
||||
internal static class SettingKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// 上次打开时App的版本
|
||||
/// </summary>
|
||||
public const string LastAppVersion = "LastAppVersion";
|
||||
|
||||
/// <summary>
|
||||
/// 窗体左侧
|
||||
/// </summary>
|
||||
|
||||
@@ -9,8 +9,9 @@ namespace Snap.Hutao.Core.Threading;
|
||||
internal static class ThreadHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步切换到主线程
|
||||
/// 使用此静态方法以 异步切换到 主线程
|
||||
/// </summary>
|
||||
/// <remarks>使用 <see cref="Task.Yield"/> 异步切换到 后台线程</remarks>
|
||||
/// <returns>等待体</returns>
|
||||
public static DispatherQueueSwitchOperation SwitchToMainThreadAsync()
|
||||
{
|
||||
|
||||
@@ -38,18 +38,6 @@ public static class Must
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务异常
|
||||
/// </summary>
|
||||
/// <param name="message">异常消息</param>
|
||||
/// <returns>异常的任务</returns>
|
||||
[SuppressMessage("", "VSTHRD200")]
|
||||
public static Task Fault(string message)
|
||||
{
|
||||
InvalidOperationException exception = new(message);
|
||||
return Task.FromException(exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务异常
|
||||
/// </summary>
|
||||
|
||||
@@ -18,16 +18,18 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
/// 窗口管理器
|
||||
/// 主要包含了针对窗体的 P/Inoke 逻辑
|
||||
/// </summary>
|
||||
internal sealed class ExtendedWindow
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
internal sealed class ExtendedWindow<TWindow>
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
{
|
||||
private readonly HWND handle;
|
||||
private readonly AppWindow appWindow;
|
||||
|
||||
private readonly Window window;
|
||||
private readonly TWindow window;
|
||||
private readonly FrameworkElement titleBar;
|
||||
|
||||
private readonly ILogger<ExtendedWindow> logger;
|
||||
private readonly WindowSubclassManager subclassManager;
|
||||
private readonly ILogger<ExtendedWindow<TWindow>> logger;
|
||||
private readonly WindowSubclassManager<TWindow> subclassManager;
|
||||
|
||||
private readonly bool useLegacyDragBar;
|
||||
|
||||
@@ -36,11 +38,11 @@ internal sealed class ExtendedWindow
|
||||
/// </summary>
|
||||
/// <param name="window">窗口</param>
|
||||
/// <param name="titleBar">充当标题栏的元素</param>
|
||||
private ExtendedWindow(Window window, FrameworkElement titleBar)
|
||||
private ExtendedWindow(TWindow window, FrameworkElement titleBar)
|
||||
{
|
||||
this.window = window;
|
||||
this.titleBar = titleBar;
|
||||
logger = Ioc.Default.GetRequiredService<ILogger<ExtendedWindow>>();
|
||||
logger = Ioc.Default.GetRequiredService<ILogger<ExtendedWindow<TWindow>>>();
|
||||
|
||||
handle = (HWND)WindowNative.GetWindowHandle(window);
|
||||
|
||||
@@ -48,7 +50,7 @@ internal sealed class ExtendedWindow
|
||||
appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
|
||||
useLegacyDragBar = !AppWindowTitleBar.IsCustomizationSupported();
|
||||
subclassManager = new(handle, useLegacyDragBar);
|
||||
subclassManager = new(window, handle, useLegacyDragBar);
|
||||
|
||||
InitializeWindow();
|
||||
}
|
||||
@@ -57,11 +59,10 @@ internal sealed class ExtendedWindow
|
||||
/// 初始化
|
||||
/// </summary>
|
||||
/// <param name="window">窗口</param>
|
||||
/// <param name="titleBar">标题栏</param>
|
||||
/// <returns>实例</returns>
|
||||
public static ExtendedWindow Initialize(Window window, FrameworkElement titleBar)
|
||||
public static ExtendedWindow<TWindow> Initialize(TWindow window)
|
||||
{
|
||||
return new(window, titleBar);
|
||||
return new(window, window.TitleBar);
|
||||
}
|
||||
|
||||
private static void UpdateTitleButtonColor(AppWindowTitleBar appTitleBar)
|
||||
@@ -103,7 +104,8 @@ internal sealed class ExtendedWindow
|
||||
appWindow.Title = "胡桃";
|
||||
|
||||
ExtendsContentIntoTitleBar();
|
||||
Persistence.RecoverOrInit(appWindow);
|
||||
|
||||
Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize);
|
||||
|
||||
// Log basic window state here.
|
||||
(string pos, string size) = GetPostionAndSize(appWindow);
|
||||
@@ -115,14 +117,18 @@ internal sealed class ExtendedWindow
|
||||
logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
|
||||
|
||||
bool subClassApplied = subclassManager.TrySetWindowSubclass();
|
||||
logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager), subClassApplied ? "succeed" : "failed");
|
||||
logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager<TWindow>), subClassApplied ? "succeed" : "failed");
|
||||
|
||||
window.Closed += OnWindowClosed;
|
||||
}
|
||||
|
||||
private void OnWindowClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
Persistence.Save(appWindow);
|
||||
if (window.PersistSize)
|
||||
{
|
||||
Persistence.Save(appWindow);
|
||||
}
|
||||
|
||||
subclassManager?.Dispose();
|
||||
}
|
||||
|
||||
@@ -155,4 +161,4 @@ internal sealed class ExtendedWindow
|
||||
RectInt32 dragRect = new RectInt32(48, 0, (int)titleBar.ActualWidth, (int)titleBar.ActualHeight).Scale(scale);
|
||||
appTitleBar.SetDragRectangles(dragRect.Enumerate().ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
/// <summary>
|
||||
/// 为扩展窗体提供必要的选项
|
||||
/// </summary>
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
internal interface IExtendedWindowSource
|
||||
{
|
||||
/// <summary>
|
||||
/// 提供的标题栏
|
||||
/// </summary>
|
||||
FrameworkElement TitleBar { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否持久化尺寸
|
||||
/// </summary>
|
||||
bool PersistSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始大小
|
||||
/// </summary>
|
||||
SizeInt32 InitSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理最大最小信息
|
||||
/// </summary>
|
||||
/// <param name="pInfo">信息指针</param>
|
||||
/// <param name="scalingFactor">缩放比</param>
|
||||
unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor);
|
||||
}
|
||||
@@ -21,21 +21,26 @@ internal static class Persistence
|
||||
/// 设置窗体位置
|
||||
/// </summary>
|
||||
/// <param name="appWindow">应用窗体</param>
|
||||
public static void RecoverOrInit(AppWindow appWindow)
|
||||
/// <param name="persistSize">持久化尺寸</param>
|
||||
/// <param name="size">初始尺寸</param>
|
||||
public static void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 size)
|
||||
{
|
||||
// Set first launch size.
|
||||
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
|
||||
SizeInt32 size = TransformSizeForWindow(new(1200, 741), hwnd);
|
||||
RectInt32 rect = StructMarshal.RectInt32(size);
|
||||
SizeInt32 transformedSize = TransformSizeForWindow(size, hwnd);
|
||||
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
|
||||
|
||||
RectInt32 target = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (target.Width * target.Height < 848 * 524)
|
||||
if (persistSize)
|
||||
{
|
||||
target = rect;
|
||||
RectInt32 persistedSize = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (persistedSize.Width * persistedSize.Height > 848 * 524)
|
||||
{
|
||||
rect = persistedSize;
|
||||
}
|
||||
}
|
||||
|
||||
TransformToCenterScreen(ref target);
|
||||
appWindow.MoveAndResize(target);
|
||||
TransformToCenterScreen(ref rect);
|
||||
appWindow.MoveAndResize(rect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -44,7 +49,7 @@ internal static class Persistence
|
||||
/// <param name="appWindow">应用窗体</param>
|
||||
public static void Save(AppWindow appWindow)
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (ulong)(CompactRect)appWindow.GetRect());
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -124,7 +129,7 @@ internal static class Persistence
|
||||
return new(rect.X, rect.Y, rect.Width, rect.Height);
|
||||
}
|
||||
|
||||
public static explicit operator ulong(CompactRect rect)
|
||||
public static implicit operator ulong(CompactRect rect)
|
||||
{
|
||||
return rect.Value;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
@@ -11,14 +12,14 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
/// <summary>
|
||||
/// 窗体子类管理器
|
||||
/// </summary>
|
||||
internal class WindowSubclassManager : IDisposable
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
internal class WindowSubclassManager<TWindow> : IDisposable
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
{
|
||||
private const int WindowSubclassId = 101;
|
||||
private const int DragBarSubclassId = 102;
|
||||
|
||||
private const int MinWidth = 848;
|
||||
private const int MinHeight = 524;
|
||||
|
||||
private readonly TWindow window;
|
||||
private readonly HWND hwnd;
|
||||
private readonly bool isLegacyDragBar;
|
||||
private HWND hwndDragBar;
|
||||
@@ -30,12 +31,13 @@ internal class WindowSubclassManager : IDisposable
|
||||
/// <summary>
|
||||
/// 构造一个新的窗体子类管理器
|
||||
/// </summary>
|
||||
/// <param name="window">窗体实例</param>
|
||||
/// <param name="hwnd">窗体句柄</param>
|
||||
/// <param name="isLegacyDragBar">是否为经典标题栏区域</param>
|
||||
public WindowSubclassManager(HWND hwnd, bool isLegacyDragBar)
|
||||
public WindowSubclassManager(TWindow window, HWND hwnd, bool isLegacyDragBar)
|
||||
{
|
||||
Must.NotNull(hwnd);
|
||||
this.hwnd = hwnd;
|
||||
this.window = window;
|
||||
this.hwnd = Must.NotNull(hwnd);
|
||||
this.isLegacyDragBar = isLegacyDragBar;
|
||||
}
|
||||
|
||||
@@ -85,9 +87,7 @@ internal class WindowSubclassManager : IDisposable
|
||||
case WM_GETMINMAXINFO:
|
||||
{
|
||||
double scalingFactor = Persistence.GetScaleForWindow(hwnd);
|
||||
MINMAXINFO* info = (MINMAXINFO*)lParam.Value;
|
||||
info->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, info->ptMinTrackSize.X);
|
||||
info->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, info->ptMinTrackSize.Y);
|
||||
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
63
src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml
Normal file
63
src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml
Normal file
@@ -0,0 +1,63 @@
|
||||
<Window
|
||||
x:Class="Snap.Hutao.LaunchGameWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
xmlns:shv="using:Snap.Hutao.ViewModel"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid
|
||||
Name="RootGrid"
|
||||
d:DataContext="{d:DesignInstance shv:LaunchGameViewModel}">
|
||||
|
||||
<mxi:Interaction.Behaviors>
|
||||
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid
|
||||
x:Name="DragableGrid"
|
||||
Grid.Row="0"
|
||||
Height="32">
|
||||
<TextBlock
|
||||
Text="选择账号并启动"
|
||||
TextWrapping="NoWrap"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<ListView
|
||||
Grid.Row="1"
|
||||
ItemsSource="{Binding GameAccounts}"
|
||||
SelectedItem="{Binding SelectedGameAccount,Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<StackPanel Margin="0,12">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
<TextBlock
|
||||
Opacity="0.8"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding AttachUid,TargetNullValue=该账号尚未绑定 UID}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<Button
|
||||
Margin="16"
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
Content="启动游戏"
|
||||
Command="{Binding LaunchCommand}"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
63
src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml.cs
Normal file
63
src/Snap.Hutao/Snap.Hutao/LaunchGameWindow.xaml.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Snap.Hutao.ViewModel;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏窗口
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
public sealed partial class LaunchGameWindow : Window, IDisposable, IExtendedWindowSource
|
||||
{
|
||||
private const int MinWidth = 240;
|
||||
private const int MinHeight = 240;
|
||||
|
||||
private const int MaxWidth = 320;
|
||||
private const int MaxHeight = 320;
|
||||
|
||||
private readonly IServiceScope scope;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的启动游戏窗口
|
||||
/// </summary>
|
||||
/// <param name="scopeFactory">范围工厂</param>
|
||||
public LaunchGameWindow(IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
InitializeComponent();
|
||||
ExtendedWindow<LaunchGameWindow>.Initialize(this);
|
||||
|
||||
scope = scopeFactory.CreateScope();
|
||||
RootGrid.DataContext = scope.ServiceProvider.GetRequiredService<LaunchGameViewModel>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FrameworkElement TitleBar { get => DragableGrid; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PersistSize { get => false; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SizeInt32 InitSize { get => new(320, 320); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
scope.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor)
|
||||
{
|
||||
pInfo->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo->ptMinTrackSize.X);
|
||||
pInfo->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo->ptMinTrackSize.Y);
|
||||
pInfo->ptMaxTrackSize.X = (int)Math.Min(MaxWidth * scalingFactor, pInfo->ptMaxTrackSize.X);
|
||||
pInfo->ptMaxTrackSize.Y = (int)Math.Min(MaxHeight * scalingFactor, pInfo->ptMaxTrackSize.Y);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -11,14 +13,40 @@ namespace Snap.Hutao;
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
[SuppressMessage("", "CA1001")]
|
||||
public sealed partial class MainWindow : Window
|
||||
public sealed partial class MainWindow : Window, IExtendedWindowSource
|
||||
{
|
||||
private const int MinWidth = 848;
|
||||
private const int MinHeight = 524;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的主窗体
|
||||
/// </summary>
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
ExtendedWindow.Initialize(this, TitleBarView.DragArea);
|
||||
ExtendedWindow<MainWindow>.Initialize(this);
|
||||
IsPresent = true;
|
||||
Closed += (s, e) => IsPresent = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否打开
|
||||
/// </summary>
|
||||
public static bool IsPresent { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FrameworkElement TitleBar { get => TitleBarView.DragArea; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PersistSize { get => true; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SizeInt32 InitSize { get => new(1200, 741); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor)
|
||||
{
|
||||
pInfo->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo->ptMinTrackSize.X);
|
||||
pInfo->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo->ptMinTrackSize.Y);
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,7 @@ namespace Snap.Hutao.Message;
|
||||
/// <summary>
|
||||
/// 成就存档切换消息
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
internal class AchievementArchiveChangedMessage : ValueChangedMessage<AchievementArchive>
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的用户切换消息
|
||||
/// </summary>
|
||||
/// <param name="oldArchive">老用户</param>
|
||||
/// <param name="newArchive">新用户</param>
|
||||
public AchievementArchiveChangedMessage(AchievementArchive? oldArchive, AchievementArchive? newArchive)
|
||||
: base(oldArchive, newArchive)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,7 @@ namespace Snap.Hutao.Message;
|
||||
/// <summary>
|
||||
/// 祈愿记录存档切换消息
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
internal class GachaArchiveChangedMessage : ValueChangedMessage<GachaArchive>
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的用户切换消息
|
||||
/// </summary>
|
||||
/// <param name="oldArchive">老用户</param>
|
||||
/// <param name="newArchive">新用户</param>
|
||||
public GachaArchiveChangedMessage(GachaArchive? oldArchive, GachaArchive? newArchive)
|
||||
: base(oldArchive, newArchive)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,7 @@ namespace Snap.Hutao.Message;
|
||||
/// <summary>
|
||||
/// 用户切换消息
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
internal class UserChangedMessage : ValueChangedMessage<User>
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的用户切换消息
|
||||
/// </summary>
|
||||
/// <param name="oldUser">老用户</param>
|
||||
/// <param name="newUser">新用户</param>
|
||||
public UserChangedMessage(User? oldUser, User? newUser)
|
||||
: base(oldUser, newUser)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,17 @@ namespace Snap.Hutao.Message;
|
||||
/// 值变化消息
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">值的类型</typeparam>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
internal abstract class ValueChangedMessage<TValue>
|
||||
where TValue : class
|
||||
{
|
||||
/// <summary>
|
||||
/// 动态访问
|
||||
/// </summary>
|
||||
public ValueChangedMessage()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的值变化消息
|
||||
/// </summary>
|
||||
@@ -24,10 +32,10 @@ internal abstract class ValueChangedMessage<TValue>
|
||||
/// <summary>
|
||||
/// 旧的值
|
||||
/// </summary>
|
||||
public TValue? OldValue { get; private set; }
|
||||
public TValue? OldValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 新的值
|
||||
/// </summary>
|
||||
public TValue? NewValue { get; private set; }
|
||||
public TValue? NewValue { get; set; }
|
||||
}
|
||||
215
src/Snap.Hutao/Snap.Hutao/Migrations/20221031104940_GameAccount.Designer.cs
generated
Normal file
215
src/Snap.Hutao/Snap.Hutao/Migrations/20221031104940_GameAccount.Designer.cs
generated
Normal file
@@ -0,0 +1,215 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Snap.Hutao.Context.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Snap.Hutao.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20221031104940_GameAccount")]
|
||||
partial class GameAccount
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ArchiveId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Current")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Time")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ArchiveId");
|
||||
|
||||
b.ToTable("achievements");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("achievement_archives");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Info")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("avatar_infos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("gacha_archives");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ArchiveId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("GachaType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("QueryType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Time")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ArchiveId");
|
||||
|
||||
b.ToTable("gacha_items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachUid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MihoyoSDK")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("game_accounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Cookie")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
|
||||
.WithMany()
|
||||
.HasForeignKey("ArchiveId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Archive");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
|
||||
.WithMany()
|
||||
.HasForeignKey("ArchiveId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Archive");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Snap.Hutao.Migrations
|
||||
{
|
||||
public partial class GameAccount : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "game_accounts",
|
||||
columns: table => new
|
||||
{
|
||||
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
AttachUid = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
MihoyoSDK = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_game_accounts", x => x.InnerId);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "game_accounts");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
@@ -131,6 +131,31 @@ namespace Snap.Hutao.Migrations
|
||||
b.ToTable("gacha_items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachUid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MihoyoSDK")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("game_accounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding;
|
||||
|
||||
/// <summary>
|
||||
/// 用于视图绑定的成就
|
||||
/// </summary>
|
||||
public class Achievement : Observable
|
||||
public class Achievement : ObservableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// 满进度占位符
|
||||
@@ -28,7 +30,7 @@ public class Achievement : Observable
|
||||
this.inner = inner;
|
||||
this.entity = entity;
|
||||
|
||||
// Property should only be set when is user checking.
|
||||
// Property should only be set when it's user checking.
|
||||
isChecked = (int)entity.Status >= 2;
|
||||
}
|
||||
|
||||
@@ -50,7 +52,7 @@ public class Achievement : Observable
|
||||
get => isChecked;
|
||||
set
|
||||
{
|
||||
Set(ref isChecked, value);
|
||||
SetProperty(ref isChecked, value);
|
||||
|
||||
// Only update state when checked
|
||||
if (value)
|
||||
@@ -67,6 +69,6 @@ public class Achievement : Observable
|
||||
/// </summary>
|
||||
public string Time
|
||||
{
|
||||
get => entity.Time.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
get => entity.Time.ToString("yyyy.MM.dd HH:mm:ss");
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public class GachaStatistics
|
||||
public TypedWishSummary PermanentWish { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 历史
|
||||
/// 历史卡池
|
||||
/// </summary>
|
||||
public List<HistoryWish> HistoryWishes { get; set; } = default!;
|
||||
|
||||
|
||||
@@ -10,6 +10,16 @@ namespace Snap.Hutao.Model.Binding.Gacha;
|
||||
/// </summary>
|
||||
public class HistoryWish : WishBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 版本
|
||||
/// </summary>
|
||||
public string Version { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 卡池图片
|
||||
/// </summary>
|
||||
public Uri BannerImage { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 五星Up
|
||||
/// </summary>
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Binding.Hutao;
|
||||
/// <summary>
|
||||
/// 角色
|
||||
/// </summary>
|
||||
internal class ComplexAvatar
|
||||
public class ComplexAvatar
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个胡桃数据库角色
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Converter;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 角色搭配
|
||||
/// </summary>
|
||||
internal class ComplexAvatarCollocation : ComplexAvatar
|
||||
public class ComplexAvatarCollocation : ComplexAvatar
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的角色搭配
|
||||
@@ -22,6 +21,11 @@ internal class ComplexAvatarCollocation : ComplexAvatar
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 角色Id
|
||||
/// </summary>
|
||||
public AvatarId AvatarId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色
|
||||
/// </summary>
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Binding.Hutao;
|
||||
/// <summary>
|
||||
/// 圣遗物套装
|
||||
/// </summary>
|
||||
internal class ComplexReliquarySet
|
||||
public class ComplexReliquarySet
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃数据库圣遗物套装
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Model.Binding.Hutao;
|
||||
/// <summary>
|
||||
/// 胡桃数据库武器
|
||||
/// </summary>
|
||||
internal class ComplexWeapon
|
||||
public class ComplexWeapon
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个胡桃数据库武器
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.LaunchGame;
|
||||
|
||||
/// <summary>
|
||||
/// 服务器方案
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// 启动方案
|
||||
/// </summary>
|
||||
public class LaunchScheme
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的启动方案
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <param name="channel">通道</param>
|
||||
/// <param name="cps">通道描述字符串</param>
|
||||
/// <param name="subChannel">子通道</param>
|
||||
public LaunchScheme(string name, string channel, string subChannel)
|
||||
{
|
||||
Name = name;
|
||||
Channel = channel;
|
||||
SubChannel = subChannel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 通道
|
||||
/// </summary>
|
||||
public string Channel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 子通道
|
||||
/// </summary>
|
||||
public string SubChannel { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.LaunchGame;
|
||||
|
||||
/// <summary>
|
||||
/// 启动类型
|
||||
/// </summary>
|
||||
public enum SchemeType
|
||||
{
|
||||
/// <summary>
|
||||
/// 国际服
|
||||
/// </summary>
|
||||
Mihoyo,
|
||||
|
||||
/// <summary>
|
||||
/// 国服官服
|
||||
/// </summary>
|
||||
Officical,
|
||||
|
||||
/// <summary>
|
||||
/// 渠道服
|
||||
/// </summary>
|
||||
Bilibili,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
@@ -12,7 +13,7 @@ namespace Snap.Hutao.Model.Binding;
|
||||
/// <summary>
|
||||
/// 用于视图绑定的用户
|
||||
/// </summary>
|
||||
public class User : Observable
|
||||
public class User : ObservableObject
|
||||
{
|
||||
private readonly EntityUser inner;
|
||||
|
||||
@@ -44,7 +45,7 @@ public class User : Observable
|
||||
public UserGameRole? SelectedUserGameRole
|
||||
{
|
||||
get => selectedUserGameRole;
|
||||
private set => Set(ref selectedUserGameRole, value);
|
||||
private set => SetProperty(ref selectedUserGameRole, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityUser.IsSelected"/>
|
||||
@@ -55,10 +56,22 @@ public class User : Observable
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityUser.Cookie"/>
|
||||
public Cookie Cookie
|
||||
public Cookie? Cookie
|
||||
{
|
||||
get => inner.Cookie;
|
||||
set => inner.Cookie = value;
|
||||
set
|
||||
{
|
||||
inner.Cookie = value;
|
||||
OnPropertyChanged(nameof(HasSToken));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否拥有 SToken
|
||||
/// </summary>
|
||||
public bool HasSToken
|
||||
{
|
||||
get => inner.Cookie!.ContainsSToken();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,11 +92,7 @@ public class User : Observable
|
||||
/// <param name="userGameRoleClient">角色客户端</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>用户是否初始化完成,若Cookie失效会返回 <see langword="false"/> </returns>
|
||||
internal static async Task<User?> ResumeAsync(
|
||||
EntityUser inner,
|
||||
UserClient userClient,
|
||||
BindingClient userGameRoleClient,
|
||||
CancellationToken token = default)
|
||||
internal static async Task<User?> ResumeAsync(EntityUser inner, UserClient userClient, BindingClient userGameRoleClient, CancellationToken token = default)
|
||||
{
|
||||
User user = new(inner);
|
||||
bool successful = await user.InitializeCoreAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
@@ -98,36 +107,31 @@ public class User : Observable
|
||||
/// <param name="userGameRoleClient">角色客户端</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>用户是否初始化完成,若Cookie失效会返回 <see langword="null"/> </returns>
|
||||
internal static async Task<User?> CreateAsync(
|
||||
Cookie cookie,
|
||||
UserClient userClient,
|
||||
BindingClient userGameRoleClient,
|
||||
CancellationToken token = default)
|
||||
internal static async Task<User?> CreateAsync(Cookie cookie, UserClient userClient, BindingClient userGameRoleClient, CancellationToken token = default)
|
||||
{
|
||||
User user = new(EntityUser.Create(cookie));
|
||||
bool successful = await user.InitializeCoreAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
return successful ? user : null;
|
||||
}
|
||||
|
||||
private async Task<bool> InitializeCoreAsync(
|
||||
UserClient userClient,
|
||||
BindingClient userGameRoleClient,
|
||||
CancellationToken token = default)
|
||||
/// <summary>
|
||||
/// 更新SToken
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="cookie">cookie</param>
|
||||
internal void UpdateSToken(string uid, Cookie cookie)
|
||||
{
|
||||
Cookie!.InsertSToken(uid, cookie);
|
||||
OnPropertyChanged(nameof(HasSToken));
|
||||
}
|
||||
|
||||
private async Task<bool> InitializeCoreAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token = default)
|
||||
{
|
||||
if (isInitialized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
await InitializeUserInfoAndUserGameRolesAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
return UserInfo != null && UserGameRoles.Any();
|
||||
}
|
||||
|
||||
private async Task InitializeUserInfoAndUserGameRolesAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token)
|
||||
{
|
||||
UserInfo = await userClient
|
||||
.GetUserFullInfoAsync(this, token)
|
||||
.ConfigureAwait(false);
|
||||
@@ -137,5 +141,9 @@ public class User : Observable
|
||||
.ConfigureAwait(false);
|
||||
|
||||
SelectedUserGameRole = UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen);
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
return UserInfo != null && UserGameRoles.Any();
|
||||
}
|
||||
}
|
||||
81
src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs
Normal file
81
src/Snap.Hutao/Snap.Hutao/Model/Entity/GameAccount.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.LaunchGame;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Snap.Hutao.Model.Entity;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏内账号
|
||||
/// </summary>
|
||||
[Table("game_accounts")]
|
||||
public class GameAccount : INotifyPropertyChanged
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 内部Id
|
||||
/// </summary>
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public Guid InnerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对应的Uid
|
||||
/// </summary>
|
||||
public string? AttachUid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务器类型
|
||||
/// </summary>
|
||||
public SchemeType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// [MIHOYOSDK_ADL_PROD_CN_h3123967166]
|
||||
/// see <see cref="Service.Game.GameAccountRegistryInterop.SdkKey"/>
|
||||
/// </summary>
|
||||
public string MihoyoSDK { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的游戏内账号
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <param name="sdk">sdk</param>
|
||||
/// <returns>游戏内账号</returns>
|
||||
public static GameAccount Create(string name, string sdk)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Name = name,
|
||||
MihoyoSDK = sdk,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新绑定的Uid
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
public void UpdateAttachUid(string? uid)
|
||||
{
|
||||
AttachUid = uid;
|
||||
PropertyChanged?.Invoke(this, new(nameof(AttachUid)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新名称
|
||||
/// </summary>
|
||||
/// <param name="name">新名称</param>
|
||||
public void UpdateName(string name)
|
||||
{
|
||||
Name = name;
|
||||
PropertyChanged?.Invoke(this, new(nameof(Name)));
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,46 @@ namespace Snap.Hutao.Model.Entity;
|
||||
[Table("settings")]
|
||||
public class SettingEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏路径
|
||||
/// </summary>
|
||||
public const string GamePath = "GamePath";
|
||||
|
||||
/// <summary>
|
||||
/// 空的历史记录卡池是否可见
|
||||
/// </summary>
|
||||
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 全屏
|
||||
/// </summary>
|
||||
public const string LaunchIsFullScreen = "Launch.IsFullScreen";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 无边框
|
||||
/// </summary>
|
||||
public const string LaunchIsBorderless = "Launch.IsBorderless";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 宽度
|
||||
/// </summary>
|
||||
public const string LaunchScreenWidth = "Launch.ScreenWidth";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 高度
|
||||
/// </summary>
|
||||
public const string LaunchScreenHeight = "Launch.ScreenHeight";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 解锁帧率
|
||||
/// </summary>
|
||||
public const string LaunchUnlockFps = "Launch.UnlockFps";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 目标帧率
|
||||
/// </summary>
|
||||
public const string LaunchTargetFps = "Launch.TargetFps";
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的设置入口
|
||||
/// </summary>
|
||||
|
||||
@@ -29,7 +29,7 @@ public class User : ISelectable
|
||||
/// <summary>
|
||||
/// 用户的Cookie
|
||||
/// </summary>
|
||||
public Cookie Cookie { get; set; } = default!;
|
||||
public Cookie? Cookie { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的用户
|
||||
@@ -40,4 +40,4 @@ public class User : ISelectable
|
||||
{
|
||||
return new() { Cookie = cookie };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
using Snap.Hutao.Model.Binding.Hutao;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Abstraction;
|
||||
using Snap.Hutao.Model.Metadata.Converter;
|
||||
@@ -84,6 +85,11 @@ public class Avatar : IStatisticsItemSource, ISummaryItemSource, INameQuality
|
||||
/// </summary>
|
||||
public IEnumerable<Costume> Costumes { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// [非元数据] 搭配数据
|
||||
/// </summary>
|
||||
public ComplexAvatarCollocation? Collocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 转换为基础物品
|
||||
/// </summary>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Avatar;
|
||||
|
||||
/// <summary>
|
||||
/// 角色ID
|
||||
/// </summary>
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public static class AvatarIds
|
||||
{
|
||||
public const int Ayaka = 10000002;
|
||||
public const int Qin = 10000003;
|
||||
|
||||
public const int Lisa = 10000006;
|
||||
|
||||
public const int Barbara = 10000014;
|
||||
public const int Kaeya = 10000015;
|
||||
public const int Diluc = 10000016;
|
||||
|
||||
public const int Razor = 10000020;
|
||||
public const int Ambor = 10000021;
|
||||
public const int Venti = 10000022;
|
||||
public const int Xiangling = 10000023;
|
||||
public const int Beidou = 10000024;
|
||||
public const int Xingqiu = 10000025;
|
||||
public const int Xiao = 10000026;
|
||||
public const int Ningguang = 10000027;
|
||||
|
||||
public const int Klee = 10000029;
|
||||
public const int Zhongli = 10000030;
|
||||
public const int Fischl = 10000031;
|
||||
public const int Bennett = 10000032;
|
||||
public const int Tartaglia = 10000033;
|
||||
public const int Noel = 10000034;
|
||||
public const int Qiqi = 10000035;
|
||||
public const int Chongyun = 10000036;
|
||||
public const int Ganyu = 10000037;
|
||||
public const int Albedo = 10000038;
|
||||
public const int Diona = 10000039;
|
||||
|
||||
public const int Mona = 10000041;
|
||||
public const int Keqing = 10000042;
|
||||
public const int Sucrose = 10000043;
|
||||
public const int Xinyan = 10000044;
|
||||
public const int Rosaria = 10000045;
|
||||
public const int Hutao = 10000046;
|
||||
public const int Kazuha = 10000047;
|
||||
public const int Feiyan = 10000048;
|
||||
public const int Yoimiya = 10000049;
|
||||
public const int Tohma = 10000050;
|
||||
public const int Eula = 10000051;
|
||||
public const int Shougun = 10000052;
|
||||
public const int Sayu = 10000053;
|
||||
public const int Kokomi = 10000054;
|
||||
public const int Gorou = 10000055;
|
||||
public const int Sara = 10000056;
|
||||
public const int Itto = 10000057;
|
||||
public const int Yae = 10000058;
|
||||
public const int Heizou = 10000059;
|
||||
public const int Yelan = 10000060;
|
||||
|
||||
public const int Aloy = 10000062;
|
||||
public const int Shenhe = 10000063;
|
||||
public const int Yunjin = 10000064;
|
||||
public const int Shinobu = 10000065;
|
||||
public const int Ayato = 10000066;
|
||||
public const int Collei = 10000067;
|
||||
public const int Dori = 10000068;
|
||||
public const int Tighnari = 10000069;
|
||||
public const int Nilou = 10000070;
|
||||
public const int Cyno = 10000071;
|
||||
public const int Candace = 10000072;
|
||||
public const int Nahida = 10000073;
|
||||
public const int Layla = 10000074;
|
||||
}
|
||||
57
src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/CookBonus.cs
Normal file
57
src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/CookBonus.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Avatar;
|
||||
|
||||
/// <summary>
|
||||
/// 料理奖励
|
||||
/// </summary>
|
||||
public class CookBonus
|
||||
{
|
||||
/// <summary>
|
||||
/// 原型名称
|
||||
/// </summary>
|
||||
public string OriginName { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 原型描述
|
||||
/// </summary>
|
||||
public string OriginDescription { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 原型图标
|
||||
/// </summary>
|
||||
public string OriginIcon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 描述
|
||||
/// </summary>
|
||||
public string Description { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 效果描述
|
||||
/// </summary>
|
||||
public string EffectDescription { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 图标
|
||||
/// </summary>
|
||||
public string Icon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 物品等级
|
||||
/// </summary>
|
||||
public ItemQuality RankLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 材料列表
|
||||
/// </summary>
|
||||
public List<ItemWithCount> InputList { get; set; } = default!;
|
||||
}
|
||||
@@ -76,6 +76,11 @@ public class FetterInfo
|
||||
/// </summary>
|
||||
public string CvKorean { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 料理
|
||||
/// </summary>
|
||||
public CookBonus? CookBonus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 好感语音
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Avatar;
|
||||
|
||||
/// <summary>
|
||||
/// 带有个数的物品
|
||||
/// </summary>
|
||||
public class ItemWithCount
|
||||
{
|
||||
/// <summary>
|
||||
/// 物品Id
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 图标
|
||||
/// </summary>
|
||||
public string Icon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 物品等级
|
||||
/// </summary>
|
||||
public ItemQuality RankLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数量
|
||||
/// </summary>
|
||||
public int Count { get; set; }
|
||||
}
|
||||
78
src/Snap.Hutao/Snap.Hutao/Model/Metadata/AvatarIds.cs
Normal file
78
src/Snap.Hutao/Snap.Hutao/Model/Metadata/AvatarIds.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata;
|
||||
|
||||
/// <summary>
|
||||
/// 角色ID
|
||||
/// </summary>
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public static class AvatarIds
|
||||
{
|
||||
public static readonly AvatarId Ayaka = 10000002;
|
||||
public static readonly AvatarId Qin = 10000003;
|
||||
|
||||
public static readonly AvatarId Lisa = 10000006;
|
||||
|
||||
public static readonly AvatarId Barbara = 10000014;
|
||||
public static readonly AvatarId Kaeya = 10000015;
|
||||
public static readonly AvatarId Diluc = 10000016;
|
||||
|
||||
public static readonly AvatarId Razor = 10000020;
|
||||
public static readonly AvatarId Ambor = 10000021;
|
||||
public static readonly AvatarId Venti = 10000022;
|
||||
public static readonly AvatarId Xiangling = 10000023;
|
||||
public static readonly AvatarId Beidou = 10000024;
|
||||
public static readonly AvatarId Xingqiu = 10000025;
|
||||
public static readonly AvatarId Xiao = 10000026;
|
||||
public static readonly AvatarId Ningguang = 10000027;
|
||||
|
||||
public static readonly AvatarId Klee = 10000029;
|
||||
public static readonly AvatarId Zhongli = 10000030;
|
||||
public static readonly AvatarId Fischl = 10000031;
|
||||
public static readonly AvatarId Bennett = 10000032;
|
||||
public static readonly AvatarId Tartaglia = 10000033;
|
||||
public static readonly AvatarId Noel = 10000034;
|
||||
public static readonly AvatarId Qiqi = 10000035;
|
||||
public static readonly AvatarId Chongyun = 10000036;
|
||||
public static readonly AvatarId Ganyu = 10000037;
|
||||
public static readonly AvatarId Albedo = 10000038;
|
||||
public static readonly AvatarId Diona = 10000039;
|
||||
|
||||
public static readonly AvatarId Mona = 10000041;
|
||||
public static readonly AvatarId Keqing = 10000042;
|
||||
public static readonly AvatarId Sucrose = 10000043;
|
||||
public static readonly AvatarId Xinyan = 10000044;
|
||||
public static readonly AvatarId Rosaria = 10000045;
|
||||
public static readonly AvatarId Hutao = 10000046;
|
||||
public static readonly AvatarId Kazuha = 10000047;
|
||||
public static readonly AvatarId Feiyan = 10000048;
|
||||
public static readonly AvatarId Yoimiya = 10000049;
|
||||
public static readonly AvatarId Tohma = 10000050;
|
||||
public static readonly AvatarId Eula = 10000051;
|
||||
public static readonly AvatarId Shougun = 10000052;
|
||||
public static readonly AvatarId Sayu = 10000053;
|
||||
public static readonly AvatarId Kokomi = 10000054;
|
||||
public static readonly AvatarId Gorou = 10000055;
|
||||
public static readonly AvatarId Sara = 10000056;
|
||||
public static readonly AvatarId Itto = 10000057;
|
||||
public static readonly AvatarId Yae = 10000058;
|
||||
public static readonly AvatarId Heizou = 10000059;
|
||||
public static readonly AvatarId Yelan = 10000060;
|
||||
|
||||
public static readonly AvatarId Aloy = 10000062;
|
||||
public static readonly AvatarId Shenhe = 10000063;
|
||||
public static readonly AvatarId Yunjin = 10000064;
|
||||
public static readonly AvatarId Shinobu = 10000065;
|
||||
public static readonly AvatarId Ayato = 10000066;
|
||||
public static readonly AvatarId Collei = 10000067;
|
||||
public static readonly AvatarId Dori = 10000068;
|
||||
public static readonly AvatarId Tighnari = 10000069;
|
||||
public static readonly AvatarId Nilou = 10000070;
|
||||
public static readonly AvatarId Cyno = 10000071;
|
||||
public static readonly AvatarId Candace = 10000072;
|
||||
public static readonly AvatarId Nahida = 10000073;
|
||||
public static readonly AvatarId Layla = 10000074;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Control;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// 立绘图标转换器
|
||||
/// </summary>
|
||||
internal class GachaAvatarIconConverter : ValueConverterBase<string, Uri>
|
||||
{
|
||||
private const string BaseUrl = "https://static.snapgenshin.com/GachaAvatarIcon/UI_Gacha_AvatarIcon_{0}.png";
|
||||
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <returns>链接</returns>
|
||||
public static Uri IconNameToUri(string name)
|
||||
{
|
||||
name = name["UI_AvatarIcon_".Length..];
|
||||
return new Uri(string.Format(BaseUrl, name));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Uri Convert(string from)
|
||||
{
|
||||
return IconNameToUri(from);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Control;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// 立绘转换器
|
||||
/// </summary>
|
||||
internal class GachaAvatarImgConverter : ValueConverterBase<string, Uri>
|
||||
{
|
||||
private const string BaseUrl = "https://static.snapgenshin.com/GachaAvatarImg/UI_Gacha_AvatarImg_{0}.png";
|
||||
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <returns>链接</returns>
|
||||
public static Uri IconNameToUri(string name)
|
||||
{
|
||||
name = name["UI_AvatarIcon_".Length..];
|
||||
return new Uri(string.Format(BaseUrl, name));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Uri Convert(string from)
|
||||
{
|
||||
return IconNameToUri(from);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Control;
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// 物品图片转换器
|
||||
/// </summary>
|
||||
internal class ItemIconConverter : ValueConverterBase<string, Uri>
|
||||
{
|
||||
private const string BaseUrl = "https://static.snapgenshin.com/ItemIcon/{0}.png";
|
||||
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <returns>链接</returns>
|
||||
public static Uri IconNameToUri(string name)
|
||||
{
|
||||
return new Uri(string.Format(BaseUrl, name));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Uri Convert(string from)
|
||||
{
|
||||
return IconNameToUri(from);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ internal class SkillIconConverter : ValueConverterBase<string, Uri>
|
||||
private const string SkillUrl = "https://static.snapgenshin.com/Skill/{0}.png";
|
||||
private const string TalentUrl = "https://static.snapgenshin.com/Talent/{0}.png";
|
||||
|
||||
private static readonly Uri UIIconNone = new("https://static.snapgenshin.com/Bg/UI_Icon_None.png");
|
||||
|
||||
/// <summary>
|
||||
/// 名称转Uri
|
||||
/// </summary>
|
||||
@@ -20,6 +22,11 @@ internal class SkillIconConverter : ValueConverterBase<string, Uri>
|
||||
/// <returns>链接</returns>
|
||||
public static Uri IconNameToUri(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return UIIconNone;
|
||||
}
|
||||
|
||||
if (name.StartsWith("UI_Talent_"))
|
||||
{
|
||||
return new Uri(string.Format(TalentUrl, name));
|
||||
|
||||
@@ -15,6 +15,16 @@ public class GachaEvent
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 版本
|
||||
/// </summary>
|
||||
public string Version { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 卡池图
|
||||
/// </summary>
|
||||
public Uri Banner { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
|
||||
35
src/Snap.Hutao/Snap.Hutao/Model/NamedValue.cs
Normal file
35
src/Snap.Hutao/Snap.Hutao/Model/NamedValue.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Snap.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 封装带有名称描述的值
|
||||
/// 在绑定枚举变量时非常有用
|
||||
/// </summary>
|
||||
/// <typeparam name="T">包含值的类型</typeparam>
|
||||
public class NamedValue<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的命名的值
|
||||
/// </summary>
|
||||
/// <param name="name">命名</param>
|
||||
/// <param name="value">值</param>
|
||||
public NamedValue(string name, T value)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public T Value { get; }
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 简单的实现了 <see cref="INotifyPropertyChanged"/> 接口
|
||||
/// </summary>
|
||||
public abstract class Observable : INotifyPropertyChanged
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 设置字段的值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">字段类型</typeparam>
|
||||
/// <param name="storage">现有值</param>
|
||||
/// <param name="value">新的值</param>
|
||||
/// <param name="propertyName">属性名称</param>
|
||||
/// <returns>项是否更新</returns>
|
||||
protected bool Set<T>([NotNullIfNotNull("value")] ref T storage, T value, [CallerMemberName] string propertyName = default!)
|
||||
{
|
||||
if (Equals(storage, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
storage = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发 <see cref="PropertyChanged"/>
|
||||
/// </summary>
|
||||
/// <param name="propertyName">属性名称</param>
|
||||
protected void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
65
src/Snap.Hutao/Snap.Hutao/Model/Primitive/AvatarId.cs
Normal file
65
src/Snap.Hutao/Snap.Hutao/Model/Primitive/AvatarId.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive.Converter;
|
||||
|
||||
namespace Snap.Hutao.Model.Primitive;
|
||||
|
||||
/// <summary>
|
||||
/// 角色Id
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(AvatarIdConverter))]
|
||||
public readonly struct AvatarId : IEquatable<AvatarId>
|
||||
{
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public readonly int Value;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AvatarId"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="value">value</param>
|
||||
public AvatarId(int value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator int(AvatarId value)
|
||||
{
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
public static implicit operator AvatarId(int value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
public static bool operator ==(AvatarId left, AvatarId right)
|
||||
{
|
||||
return left.Value == right.Value;
|
||||
}
|
||||
|
||||
public static bool operator !=(AvatarId left, AvatarId right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(AvatarId other)
|
||||
{
|
||||
return Value == other.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is AvatarId other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Primitive.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// 角色Id转换器
|
||||
/// </summary>
|
||||
internal class AvatarIdConverter : JsonConverter<AvatarId>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override AvatarId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.GetInt32();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, AvatarId value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteNumberValue(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Primitive.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// 武器Id转换器
|
||||
/// </summary>
|
||||
internal class WeaponIdConverter : JsonConverter<WeaponId>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override WeaponId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.GetInt32();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, WeaponId value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteNumberValue(value);
|
||||
}
|
||||
}
|
||||
65
src/Snap.Hutao/Snap.Hutao/Model/Primitive/WeaponId.cs
Normal file
65
src/Snap.Hutao/Snap.Hutao/Model/Primitive/WeaponId.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive.Converter;
|
||||
|
||||
namespace Snap.Hutao.Model.Primitive;
|
||||
|
||||
/// <summary>
|
||||
/// 武器Id
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(WeaponIdConverter))]
|
||||
public readonly struct WeaponId : IEquatable<WeaponId>
|
||||
{
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public readonly int Value;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WeaponId"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="value">value</param>
|
||||
public WeaponId(int value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator int(WeaponId value)
|
||||
{
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
public static implicit operator WeaponId(int value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
public static bool operator ==(WeaponId left, WeaponId right)
|
||||
{
|
||||
return left.Value == right.Value;
|
||||
}
|
||||
|
||||
public static bool operator !=(WeaponId left, WeaponId right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(WeaponId other)
|
||||
{
|
||||
return Value == other.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is WeaponId other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Snap.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
@@ -8,7 +10,7 @@ namespace Snap.Hutao.Model;
|
||||
/// 默认为选中状态
|
||||
/// </summary>
|
||||
/// <typeparam name="T">值的类型</typeparam>
|
||||
public class Selectable<T> : Observable
|
||||
public class Selectable<T> : ObservableObject
|
||||
where T : class
|
||||
{
|
||||
private readonly Action? selectedChanged;
|
||||
@@ -35,7 +37,7 @@ public class Selectable<T> : Observable
|
||||
get => isSelected;
|
||||
set
|
||||
{
|
||||
Set(ref isSelected, value);
|
||||
SetProperty(ref isSelected, value);
|
||||
selectedChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -43,5 +45,5 @@ public class Selectable<T> : Observable
|
||||
/// <summary>
|
||||
/// 存放的对象
|
||||
/// </summary>
|
||||
public T Value { get => value; set => Set(ref this.value, value); }
|
||||
}
|
||||
public T Value { get => value; set => SetProperty(ref this.value, value); }
|
||||
}
|
||||
@@ -2,19 +2,22 @@
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
IgnorableNamespaces="uap desktop6 rescap">
|
||||
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.1.13.0" />
|
||||
Version="1.1.18.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>胡桃</DisplayName>
|
||||
<PublisherDisplayName>DGP Studio</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
|
||||
<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
@@ -50,6 +53,7 @@
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
<rescap:Capability Name="runFullTrust"/>
|
||||
<rescap:Capability Name="unvirtualizedResources"/>
|
||||
</Capabilities>
|
||||
</Package>
|
||||
|
||||
@@ -7,7 +7,6 @@ using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.UI.ViewManagement;
|
||||
using WinRT;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
@@ -32,6 +31,7 @@ public static partial class Program
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
_ = args;
|
||||
|
||||
XamlCheckProcessRequirements();
|
||||
ComWrappersSupport.InitializeComWrappers();
|
||||
|
||||
@@ -75,7 +75,6 @@ public static partial class Program
|
||||
|
||||
// Discrete services
|
||||
.AddSingleton<IMessenger>(WeakReferenceMessenger.Default)
|
||||
.AddSingleton(new UISettings())
|
||||
|
||||
.BuildServiceProvider();
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Model.InterChange.Achievement;
|
||||
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
|
||||
|
||||
@@ -63,7 +64,7 @@ public class AchievementDbOperation
|
||||
|
||||
if (entity == null && uiaf != null)
|
||||
{
|
||||
AddEntity(EntityAchievement.Create(archiveId, uiaf));
|
||||
appDbContext.Achievements.AddAndSave(EntityAchievement.Create(archiveId, uiaf));
|
||||
add++;
|
||||
continue;
|
||||
}
|
||||
@@ -85,8 +86,8 @@ public class AchievementDbOperation
|
||||
|
||||
if (aggressive)
|
||||
{
|
||||
RemoveEntity(entity);
|
||||
AddEntity(EntityAchievement.Create(archiveId, uiaf));
|
||||
appDbContext.Achievements.RemoveAndSave(entity);
|
||||
appDbContext.Achievements.AddAndSave(EntityAchievement.Create(archiveId, uiaf));
|
||||
update++;
|
||||
}
|
||||
}
|
||||
@@ -96,7 +97,7 @@ public class AchievementDbOperation
|
||||
moveEntity = false;
|
||||
moveUIAF = true;
|
||||
|
||||
AddEntity(EntityAchievement.Create(archiveId, uiaf));
|
||||
appDbContext.Achievements.AddAndSave(EntityAchievement.Create(archiveId, uiaf));
|
||||
add++;
|
||||
}
|
||||
}
|
||||
@@ -115,7 +116,7 @@ public class AchievementDbOperation
|
||||
/// <returns>导入结果</returns>
|
||||
public ImportResult Overwrite(Guid archiveId, IEnumerable<EntityAchievement> items)
|
||||
{
|
||||
IQueryable<EntityAchievement> oldData = appDbContext.Achievements
|
||||
IOrderedQueryable<EntityAchievement> oldData = appDbContext.Achievements
|
||||
.Where(a => a.ArchiveId == archiveId)
|
||||
.OrderBy(a => a.Id);
|
||||
|
||||
@@ -142,13 +143,13 @@ public class AchievementDbOperation
|
||||
|
||||
if (oldEntity == null && newEntity != null)
|
||||
{
|
||||
AddEntity(newEntity);
|
||||
appDbContext.Achievements.AddAndSave(newEntity);
|
||||
add++;
|
||||
continue;
|
||||
}
|
||||
else if (oldEntity != null && newEntity == null)
|
||||
{
|
||||
RemoveEntity(oldEntity);
|
||||
appDbContext.Achievements.RemoveAndSave(oldEntity);
|
||||
remove++;
|
||||
continue;
|
||||
}
|
||||
@@ -157,7 +158,7 @@ public class AchievementDbOperation
|
||||
{
|
||||
moveOld = true;
|
||||
moveNew = false;
|
||||
RemoveEntity(oldEntity);
|
||||
appDbContext.Achievements.RemoveAndSave(oldEntity);
|
||||
remove++;
|
||||
}
|
||||
else if (oldEntity.Id == newEntity.Id)
|
||||
@@ -172,8 +173,8 @@ public class AchievementDbOperation
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveEntity(oldEntity);
|
||||
AddEntity(newEntity);
|
||||
appDbContext.Achievements.RemoveAndSave(oldEntity);
|
||||
appDbContext.Achievements.AddAndSave(newEntity);
|
||||
update++;
|
||||
}
|
||||
}
|
||||
@@ -182,7 +183,7 @@ public class AchievementDbOperation
|
||||
// entity.Id > uiaf.Id
|
||||
moveOld = false;
|
||||
moveNew = true;
|
||||
AddEntity(newEntity);
|
||||
appDbContext.Achievements.AddAndSave(newEntity);
|
||||
add++;
|
||||
}
|
||||
}
|
||||
@@ -196,16 +197,4 @@ public class AchievementDbOperation
|
||||
|
||||
return new(add, update, remove);
|
||||
}
|
||||
|
||||
private void AddEntity(EntityAchievement entity)
|
||||
{
|
||||
appDbContext.Achievements.Add(entity);
|
||||
appDbContext.SaveChanges();
|
||||
}
|
||||
|
||||
private void RemoveEntity(EntityAchievement entity)
|
||||
{
|
||||
appDbContext.Achievements.Remove(entity);
|
||||
appDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Snap.Hutao.Service.Achievement;
|
||||
/// <summary>
|
||||
/// 成就服务
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IAchievementService))]
|
||||
[Injection(InjectAs.Scoped, typeof(IAchievementService))]
|
||||
internal class AchievementService : IAchievementService
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
@@ -40,7 +40,7 @@ internal class AchievementService : IAchievementService
|
||||
this.appDbContext = appDbContext;
|
||||
this.logger = logger;
|
||||
|
||||
dbCurrent = new(appDbContext, appDbContext.AchievementArchives, messenger);
|
||||
dbCurrent = new(appDbContext.AchievementArchives, messenger);
|
||||
achievementDbOperation = new(appDbContext);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
@@ -21,7 +23,7 @@ public static class LogHelper
|
||||
{
|
||||
Type = exception.GetType().ToString(),
|
||||
Message = exception.Message,
|
||||
StackTrace = exception.ToString(),
|
||||
StackTrace = exception.StackTrace,
|
||||
};
|
||||
|
||||
if (exception is AggregateException aggregateException)
|
||||
@@ -35,12 +37,20 @@ public static class LogHelper
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (exception.InnerException != null)
|
||||
|
||||
if (exception.InnerException != null)
|
||||
{
|
||||
current.InnerExceptions ??= new();
|
||||
current.InnerExceptions.Add(Create(exception.InnerException));
|
||||
}
|
||||
|
||||
StackTrace stackTrace = new(exception, true);
|
||||
StackFrame[] frames = stackTrace.GetFrames();
|
||||
|
||||
if (frames.Length > 0 && frames[0].HasNativeImage())
|
||||
{
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace Snap.Hutao.Service.AvatarInfo;
|
||||
/// <summary>
|
||||
/// 角色信息服务
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IAvatarInfoService))]
|
||||
[Injection(InjectAs.Scoped, typeof(IAvatarInfoService))]
|
||||
internal class AvatarInfoService : IAvatarInfoService
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Binding.Gacha;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
@@ -15,18 +17,21 @@ namespace Snap.Hutao.Service.GachaLog.Factory;
|
||||
/// <summary>
|
||||
/// 祈愿统计工厂
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IGachaStatisticsFactory))]
|
||||
[Injection(InjectAs.Scoped, typeof(IGachaStatisticsFactory))]
|
||||
internal class GachaStatisticsFactory : IGachaStatisticsFactory
|
||||
{
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly AppDbContext appDbContext;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的祈愿统计工厂
|
||||
/// </summary>
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
public GachaStatisticsFactory(IMetadataService metadataService)
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
public GachaStatisticsFactory(IMetadataService metadataService, AppDbContext appDbContext)
|
||||
{
|
||||
this.metadataService = metadataService;
|
||||
this.appDbContext = appDbContext;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -41,15 +46,29 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
|
||||
List<GachaEvent> gachaevents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false);
|
||||
List<HistoryWishBuilder> historyWishBuilders = gachaevents.Select(g => new HistoryWishBuilder(g, nameAvatarMap, nameWeaponMap)).ToList();
|
||||
|
||||
SettingEntry? entry = await appDbContext.Settings
|
||||
.SingleOrDefaultAsync(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
entry = new(SettingEntry.IsEmptyHistoryWishVisible, true.ToString());
|
||||
appDbContext.Settings.Add(entry);
|
||||
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
bool isEmptyHistoryWishVisible = bool.Parse(entry.Value!);
|
||||
|
||||
IOrderedEnumerable<GachaItem> orderedItems = items.OrderBy(i => i.Id);
|
||||
return await Task.Run(() => CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap)).ConfigureAwait(false);
|
||||
return await Task.Run(() => CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap, isEmptyHistoryWishVisible)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static GachaStatistics CreateCore(
|
||||
IOrderedEnumerable<GachaItem> items,
|
||||
List<HistoryWishBuilder> historyWishBuilders,
|
||||
Dictionary<int, Avatar> avatarMap,
|
||||
Dictionary<int, Weapon> weaponMap)
|
||||
Dictionary<int, Weapon> weaponMap,
|
||||
bool isEmptyHistoryWishVisible)
|
||||
{
|
||||
TypedWishSummaryBuilder permanentWishBuilder = new("奔行世间", TypedWishSummaryBuilder.PermanentWish, 90, 10);
|
||||
TypedWishSummaryBuilder avatarWishBuilder = new("角色活动", TypedWishSummaryBuilder.AvatarEventWish, 90, 10);
|
||||
@@ -131,6 +150,7 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
|
||||
{
|
||||
// history
|
||||
HistoryWishes = historyWishBuilders
|
||||
.Where(b => isEmptyHistoryWishVisible || (!b.IsEmpty))
|
||||
.OrderByDescending(builder => builder.From)
|
||||
.ThenBy(builder => builder.ConfigType, new GachaConfigTypeComparar())
|
||||
.Select(builder => builder.ToHistoryWish()).ToList(),
|
||||
|
||||
@@ -67,6 +67,14 @@ internal class HistoryWishBuilder
|
||||
get => gachaEvent.To;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 卡池是否为空
|
||||
/// </summary>
|
||||
public bool IsEmpty
|
||||
{
|
||||
get => totalCountTracker <= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计数五星角色
|
||||
/// </summary>
|
||||
@@ -139,6 +147,8 @@ internal class HistoryWishBuilder
|
||||
TotalCount = totalCountTracker,
|
||||
|
||||
// fill
|
||||
Version = gachaEvent.Version,
|
||||
BannerImage = gachaEvent.Banner,
|
||||
OrangeUpList = orangeUpCounter.ToStatisticsList(),
|
||||
PurpleUpList = purpleUpCounter.ToStatisticsList(),
|
||||
OrangeList = orangeCounter.ToStatisticsList(),
|
||||
|
||||
@@ -14,7 +14,6 @@ using Snap.Hutao.Model.Binding.Gacha.Abstraction;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.InterChange.GachaLog;
|
||||
using Snap.Hutao.Model.Metadata.Abstraction;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.GachaLog.Factory;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
@@ -27,7 +26,7 @@ namespace Snap.Hutao.Service.GachaLog;
|
||||
/// <summary>
|
||||
/// 祈愿记录服务
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IGachaLogService))]
|
||||
[Injection(InjectAs.Scoped, typeof(IGachaLogService))]
|
||||
internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
{
|
||||
/// <summary>
|
||||
@@ -45,7 +44,6 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
private readonly IEnumerable<IGachaLogUrlProvider> urlProviders;
|
||||
private readonly GachaInfoClient gachaInfoClient;
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly IGachaStatisticsFactory gachaStatisticsFactory;
|
||||
private readonly ILogger<GachaLogService> logger;
|
||||
private readonly DbCurrent<GachaArchive, Message.GachaArchiveChangedMessage> dbCurrent;
|
||||
@@ -66,7 +64,6 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
/// <param name="urlProviders">Url提供器集合</param>
|
||||
/// <param name="gachaInfoClient">祈愿记录客户端</param>
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
/// <param name="infoBarService">信息条服务</param>
|
||||
/// <param name="gachaStatisticsFactory">祈愿统计工厂</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
@@ -75,7 +72,6 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
IEnumerable<IGachaLogUrlProvider> urlProviders,
|
||||
GachaInfoClient gachaInfoClient,
|
||||
IMetadataService metadataService,
|
||||
IInfoBarService infoBarService,
|
||||
IGachaStatisticsFactory gachaStatisticsFactory,
|
||||
ILogger<GachaLogService> logger,
|
||||
IMessenger messenger)
|
||||
@@ -84,11 +80,10 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
this.urlProviders = urlProviders;
|
||||
this.gachaInfoClient = gachaInfoClient;
|
||||
this.metadataService = metadataService;
|
||||
this.infoBarService = infoBarService;
|
||||
this.logger = logger;
|
||||
this.gachaStatisticsFactory = gachaStatisticsFactory;
|
||||
|
||||
dbCurrent = new(appDbContext, appDbContext.GachaArchives, messenger);
|
||||
dbCurrent = new(appDbContext.GachaArchives, messenger);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -135,8 +130,8 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
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;
|
||||
}
|
||||
@@ -214,8 +209,8 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
archiveCollection.Remove(archive);
|
||||
|
||||
// Sync database
|
||||
appDbContext.GachaArchives.Remove(archive);
|
||||
return appDbContext.SaveChangesAsync();
|
||||
appDbContext.GachaArchives.RemoveAndSave(archive);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static Task RandomDelayAsync(CancellationToken token)
|
||||
@@ -232,7 +227,6 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
long trimId = appDbContext.GachaItems
|
||||
.Where(i => i.ArchiveId == archiveId)
|
||||
.OrderBy(i => i.Id)
|
||||
.Take(1)
|
||||
.FirstOrDefault()?.Id ?? long.MaxValue;
|
||||
|
||||
IEnumerable<GachaItem> toAdd = list
|
||||
@@ -326,8 +320,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
if (archive == null)
|
||||
{
|
||||
GachaArchive created = GachaArchive.Create(uid);
|
||||
appDbContext.GachaArchives.Add(created);
|
||||
appDbContext.SaveChanges();
|
||||
appDbContext.GachaArchives.AddAndSave(created);
|
||||
|
||||
archive = appDbContext.GachaArchives.Single(a => a.Uid == uid);
|
||||
GachaArchive temp = archive;
|
||||
@@ -346,10 +339,9 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
.Where(i => i.ArchiveId == archive.InnerId)
|
||||
.Where(i => i.QueryType == configType)
|
||||
.OrderByDescending(i => i.Id)
|
||||
.Take(1)
|
||||
.FirstOrDefault();
|
||||
|
||||
// MaxBy should be supported by .NET 7
|
||||
// TODO MaxBy should be supported by .NET 7
|
||||
// .MaxBy(i => i.Id);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,59 +54,34 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
|
||||
{
|
||||
using (FileStream fileStream = new(tempFile.Path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
using (BinaryReader reader = new(fileStream))
|
||||
using (MemoryStream memoryStream = new())
|
||||
{
|
||||
string url = string.Empty;
|
||||
while (!reader.EndOfStream())
|
||||
{
|
||||
uint test = reader.ReadUInt32();
|
||||
|
||||
if (test == 0x2F302F31)
|
||||
{
|
||||
byte[] chars = ReadBytesUntilZero(reader);
|
||||
string result = Encoding.UTF8.GetString(chars.AsSpan());
|
||||
|
||||
if (result.Contains("&auth_appid=webview_gacha"))
|
||||
{
|
||||
url = result;
|
||||
}
|
||||
|
||||
// align up
|
||||
long offset = reader.BaseStream.Position % 128;
|
||||
reader.BaseStream.Position += 128 - offset;
|
||||
}
|
||||
}
|
||||
|
||||
return new(!string.IsNullOrEmpty(url), url);
|
||||
await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
string? result = Match(memoryStream);
|
||||
return new(!string.IsNullOrEmpty(result), result!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(false, $"未正确提供原神路径,或当前设置的路径不正确");
|
||||
return new(false, "未正确提供原神路径,或当前设置的路径不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ReadBytesUntilZero(BinaryReader binaryReader)
|
||||
private static string? Match(MemoryStream stream)
|
||||
{
|
||||
return ReadByteEnumerableUntilZero(binaryReader).ToArray();
|
||||
}
|
||||
ReadOnlySpan<byte> span = stream.ToArray();
|
||||
ReadOnlySpan<byte> match = Encoding.UTF8.GetBytes("https://webstatic.mihoyo.com/hk4e/event/e20190909gacha-v2/index.html");
|
||||
ReadOnlySpan<byte> zero = Encoding.UTF8.GetBytes("\0");
|
||||
|
||||
private static IEnumerable<byte> ReadByteEnumerableUntilZero(BinaryReader binaryReader)
|
||||
{
|
||||
while (binaryReader.BaseStream.Position < binaryReader.BaseStream.Length)
|
||||
int index = span.LastIndexOf(match);
|
||||
if (index >= 0)
|
||||
{
|
||||
byte b = binaryReader.ReadByte();
|
||||
|
||||
if (b == 0x00)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return b;
|
||||
}
|
||||
int length = span[index..].IndexOf(zero);
|
||||
return Encoding.UTF8.GetString(span.Slice(index, length));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Win32;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
|
||||
/// <summary>
|
||||
/// 定义了对注册表的操作
|
||||
/// </summary>
|
||||
internal static class GameAccountRegistryInterop
|
||||
{
|
||||
private const string GenshinKey = @"HKEY_CURRENT_USER\Software\miHoYo\原神";
|
||||
private const string SdkKey = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
|
||||
|
||||
/// <summary>
|
||||
/// 设置键值
|
||||
/// </summary>
|
||||
/// <param name="account">账户</param>
|
||||
/// <returns>账号是否设置</returns>
|
||||
public static bool Set(GameAccount? account)
|
||||
{
|
||||
if (account != null)
|
||||
{
|
||||
Registry.SetValue(GenshinKey, SdkKey, Encoding.UTF8.GetBytes(account.MihoyoSDK));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在注册表中获取账号信息
|
||||
/// </summary>
|
||||
/// <returns>当前注册表中的信息</returns>
|
||||
public static string? Get()
|
||||
{
|
||||
object? sdk = Registry.GetValue(GenshinKey, SdkKey, Array.Empty<byte>());
|
||||
|
||||
if (sdk is byte[] bytes)
|
||||
{
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,88 +1,364 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.IO.Ini;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Model.Binding.LaunchGame;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.Game.Locator;
|
||||
using Snap.Hutao.Service.Game.Unlocker;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏服务
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient, typeof(IGameService))]
|
||||
[Injection(InjectAs.Singleton, typeof(IGameService))]
|
||||
internal class GameService : IGameService
|
||||
{
|
||||
private const string GamePath = "GamePath";
|
||||
private const string GamePathKey = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}";
|
||||
private const string ConfigFile = "config.ini";
|
||||
|
||||
private readonly AppDbContext appDbContext;
|
||||
private readonly IServiceScopeFactory scopeFactory;
|
||||
private readonly IMemoryCache memoryCache;
|
||||
private readonly IEnumerable<IGameLocator> gameLocators;
|
||||
private readonly SemaphoreSlim gameSemaphore = new(1);
|
||||
|
||||
private ObservableCollection<GameAccount>? gameAccounts;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的游戏服务
|
||||
/// </summary>
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
/// <param name="scopeFactory">范围工厂</param>
|
||||
/// <param name="memoryCache">内存缓存</param>
|
||||
/// <param name="gameLocators">游戏定位器集合</param>
|
||||
public GameService(AppDbContext appDbContext, IMemoryCache memoryCache, IEnumerable<IGameLocator> gameLocators)
|
||||
public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
this.scopeFactory = scopeFactory;
|
||||
this.memoryCache = memoryCache;
|
||||
this.gameLocators = gameLocators;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<ValueResult<bool, string>> GetGamePathAsync()
|
||||
{
|
||||
string key = $"{nameof(GameService)}.Cache.{GamePath}";
|
||||
|
||||
if (memoryCache.TryGetValue(key, out object? value))
|
||||
if (memoryCache.TryGetValue(GamePathKey, out object? value))
|
||||
{
|
||||
return new(true, Must.NotNull((value as string)!));
|
||||
}
|
||||
else
|
||||
{
|
||||
SettingEntry? entry = await appDbContext.Settings
|
||||
.SingleOrDefaultAsync(e => e.Key == GamePath)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Cannot find in setting
|
||||
if (entry == null)
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
// Create new setting
|
||||
entry = new(GamePath, null);
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
// Try locate by registry
|
||||
IGameLocator locator = gameLocators.Single(l => l.Name == nameof(RegistryLauncherLocator));
|
||||
ValueResult<bool, string> result = await locator.LocateGamePathAsync().ConfigureAwait(false);
|
||||
SettingEntry entry = appDbContext.Settings.SingleOrAdd(e => e.Key == SettingEntry.GamePath, () => new(SettingEntry.GamePath, null), out bool added);
|
||||
|
||||
if (!result.IsOk)
|
||||
// Cannot find in setting
|
||||
if (added)
|
||||
{
|
||||
// Try locate manually
|
||||
locator = gameLocators.Single(l => l.Name == nameof(ManualGameLocator));
|
||||
result = await locator.LocateGamePathAsync().ConfigureAwait(false);
|
||||
IEnumerable<IGameLocator> gameLocators = scope.ServiceProvider.GetRequiredService<IEnumerable<IGameLocator>>();
|
||||
|
||||
// Try locate by registry
|
||||
IGameLocator locator = gameLocators.Single(l => l.Name == nameof(RegistryLauncherLocator));
|
||||
ValueResult<bool, string> result = await locator.LocateGamePathAsync().ConfigureAwait(false);
|
||||
|
||||
if (!result.IsOk)
|
||||
{
|
||||
// Try locate manually
|
||||
locator = gameLocators.Single(l => l.Name == nameof(ManualGameLocator));
|
||||
result = await locator.LocateGamePathAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (result.IsOk)
|
||||
{
|
||||
// Save result.
|
||||
entry.Value = result.Value;
|
||||
appDbContext.Settings.UpdateAndSave(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(false, null!);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.IsOk)
|
||||
// Set cache and return.
|
||||
string path = memoryCache.Set(GamePathKey, Must.NotNull(entry.Value!));
|
||||
return new(true, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetGamePathSkipLocator()
|
||||
{
|
||||
if (memoryCache.TryGetValue(GamePathKey, out object? value))
|
||||
{
|
||||
return (value as string)!;
|
||||
}
|
||||
else
|
||||
{
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
SettingEntry entry = appDbContext.Settings.SingleOrAdd(e => e.Key == SettingEntry.GamePath, () => new(SettingEntry.GamePath, null), out bool added);
|
||||
|
||||
entry.Value ??= string.Empty;
|
||||
appDbContext.Settings.UpdateAndSave(entry);
|
||||
|
||||
// Set cache and return.
|
||||
return memoryCache.Set(GamePathKey, entry.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OverwriteGamePath(string path)
|
||||
{
|
||||
// sync cache
|
||||
memoryCache.Set(GamePathKey, path);
|
||||
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
SettingEntry entry = appDbContext.Settings.SingleOrAdd(e => e.Key == SettingEntry.GamePath, () => new(SettingEntry.GamePath, null), out _);
|
||||
entry.Value = path;
|
||||
appDbContext.Settings.UpdateAndSave(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public MultiChannel GetMultiChannel()
|
||||
{
|
||||
string gamePath = GetGamePathSkipLocator();
|
||||
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile);
|
||||
|
||||
using (FileStream stream = File.OpenRead(configPath))
|
||||
{
|
||||
List<IniElement> elements = IniSerializer.Deserialize(stream).ToList();
|
||||
string? channel = elements.OfType<IniParameter>().FirstOrDefault(p => p.Key == "channel")?.Value;
|
||||
string? subChannel = elements.OfType<IniParameter>().FirstOrDefault(p => p.Key == "sub_channel")?.Value;
|
||||
|
||||
return new(channel, subChannel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetMultiChannel(LaunchScheme scheme)
|
||||
{
|
||||
string gamePath = GetGamePathSkipLocator();
|
||||
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile);
|
||||
|
||||
List<IniElement> elements;
|
||||
using (FileStream readStream = File.OpenRead(configPath))
|
||||
{
|
||||
elements = IniSerializer.Deserialize(readStream).ToList();
|
||||
}
|
||||
|
||||
foreach (IniElement element in elements)
|
||||
{
|
||||
if (element is IniParameter parameter)
|
||||
{
|
||||
if (parameter.Key == "channel")
|
||||
{
|
||||
// Save result.
|
||||
entry.Value = result.Value;
|
||||
await appDbContext.Settings.AddAsync(entry).ConfigureAwait(false);
|
||||
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
parameter.Value = scheme.Channel;
|
||||
}
|
||||
|
||||
if (parameter.Key == "sub_channel")
|
||||
{
|
||||
parameter.Value = scheme.SubChannel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (FileStream writeStream = File.Create(configPath))
|
||||
{
|
||||
IniSerializer.Serialize(writeStream, elements);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGameRunning()
|
||||
{
|
||||
if (gameSemaphore.CurrentCount == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Process.GetProcessesByName("YuanShen.exe").Any();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<GameAccount> GetGameAccountCollection()
|
||||
{
|
||||
if (gameAccounts == null)
|
||||
{
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
gameAccounts = new(appDbContext.GameAccounts.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
return gameAccounts;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask LaunchAsync(LaunchConfiguration configuration)
|
||||
{
|
||||
if (IsGameRunning())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string gamePath = GetGamePathSkipLocator();
|
||||
|
||||
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
|
||||
string commandLine = new CommandLineBuilder()
|
||||
.AppendIf("-popupwindow", configuration.IsBorderless)
|
||||
.Append("-screen-fullscreen", configuration.IsFullScreen ? 1 : 0)
|
||||
.Append("-screen-width", configuration.ScreenWidth)
|
||||
.Append("-screen-height", configuration.ScreenHeight)
|
||||
.Build();
|
||||
|
||||
Process game = new()
|
||||
{
|
||||
StartInfo = new()
|
||||
{
|
||||
Arguments = commandLine,
|
||||
FileName = gamePath,
|
||||
UseShellExecute = true,
|
||||
Verb = "runas",
|
||||
WorkingDirectory = Path.GetDirectoryName(gamePath),
|
||||
},
|
||||
};
|
||||
|
||||
using (await gameSemaphore.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (configuration.UnlockFPS)
|
||||
{
|
||||
IGameFpsUnlocker unlocker = new GameFpsUnlocker(game, configuration.TargetFPS);
|
||||
|
||||
TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100);
|
||||
TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000);
|
||||
TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000);
|
||||
if (game.Start())
|
||||
{
|
||||
await unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(false, null!);
|
||||
if (game.Start())
|
||||
{
|
||||
await game.WaitForExitAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Win32Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set cache and return.
|
||||
string path = memoryCache.Set(key, Must.NotNull(entry.Value!));
|
||||
return new(true, path);
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DetectGameAccountAsync()
|
||||
{
|
||||
Must.NotNull(gameAccounts!);
|
||||
|
||||
string? registrySdk = GameAccountRegistryInterop.Get();
|
||||
if (!string.IsNullOrEmpty(registrySdk))
|
||||
{
|
||||
GameAccount? account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
(bool isOk, string name) = await new GameAccountNameDialog(mainWindow).GetInputNameAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
account = GameAccount.Create(name, registrySdk);
|
||||
|
||||
// sync cache
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
gameAccounts.Add(GameAccount.Create(name, registrySdk));
|
||||
|
||||
// sync database
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
appDbContext.GameAccounts.AddAndSave(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetGameAccount(GameAccount account)
|
||||
{
|
||||
return GameAccountRegistryInterop.Set(account);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
|
||||
{
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
IQueryable<GameAccount> oldAccounts = appDbContext.GameAccounts.Where(a => a.AttachUid == uid);
|
||||
|
||||
foreach (GameAccount account in oldAccounts)
|
||||
{
|
||||
account.UpdateAttachUid(null);
|
||||
appDbContext.GameAccounts.UpdateAndSave(account);
|
||||
}
|
||||
|
||||
gameAccount.UpdateAttachUid(uid);
|
||||
appDbContext.GameAccounts.UpdateAndSave(gameAccount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
(bool isOk, string name) = await new GameAccountNameDialog(mainWindow).GetInputNameAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
gameAccount.UpdateName(name);
|
||||
|
||||
// sync database
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
appDbContext.GameAccounts.UpdateAndSave(gameAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
Must.NotNull(gameAccounts!).Remove(gameAccount);
|
||||
|
||||
await Task.Yield();
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
appDbContext.GameAccounts.RemoveAndSave(gameAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Model.Binding.LaunchGame;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
|
||||
@@ -10,9 +13,87 @@ namespace Snap.Hutao.Service.Game;
|
||||
/// </summary>
|
||||
internal interface IGameService
|
||||
{
|
||||
/// <summary>
|
||||
/// 将账号绑定到对应的Uid
|
||||
/// 清除老账号的绑定状态
|
||||
/// </summary>
|
||||
/// <param name="gameAccount">游戏内账号</param>
|
||||
/// <param name="uid">uid</param>
|
||||
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
|
||||
|
||||
/// <summary>
|
||||
/// 检测并尝试添加游戏内账户
|
||||
/// </summary>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask DetectGameAccountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取游戏内账号集合
|
||||
/// </summary>
|
||||
/// <returns>游戏内账号集合</returns>
|
||||
ObservableCollection<GameAccount> GetGameAccountCollection();
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取游戏路径
|
||||
/// </summary>
|
||||
/// <returns>结果</returns>
|
||||
ValueTask<ValueResult<bool, string>> GetGamePathAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取游戏路径,跳过异步定位器
|
||||
/// </summary>
|
||||
/// <returns>游戏路径,当路径无效时会设置并返回 <see cref="string.Empty"/></returns>
|
||||
string GetGamePathSkipLocator();
|
||||
|
||||
/// <summary>
|
||||
/// 获取多通道值
|
||||
/// </summary>
|
||||
/// <returns>多通道值</returns>
|
||||
MultiChannel GetMultiChannel();
|
||||
|
||||
/// <summary>
|
||||
/// 游戏是否正在运行
|
||||
/// </summary>
|
||||
/// <returns>是否正在运行</returns>
|
||||
bool IsGameRunning();
|
||||
|
||||
/// <summary>
|
||||
/// 异步启动
|
||||
/// </summary>
|
||||
/// <param name="configuration">启动配置</param>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask LaunchAsync(LaunchConfiguration configuration);
|
||||
|
||||
/// <summary>
|
||||
/// 异步修改游戏账号名称
|
||||
/// </summary>
|
||||
/// <param name="gameAccount">游戏账号</param>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
/// <summary>
|
||||
/// 重写游戏路径
|
||||
/// </summary>
|
||||
/// <param name="path">路径</param>
|
||||
void OverwriteGamePath(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 异步尝试移除账号
|
||||
/// </summary>
|
||||
/// <param name="gameAccount">账号</param>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
/// <summary>
|
||||
/// 修改注册表中的账号信息
|
||||
/// </summary>
|
||||
/// <param name="account">账号</param>
|
||||
/// <returns>是否设置成功</returns>
|
||||
bool SetGameAccount(GameAccount account);
|
||||
|
||||
/// <summary>
|
||||
/// 设置多通道值
|
||||
/// </summary>
|
||||
/// <param name="scheme">方案</param>
|
||||
void SetMultiChannel(LaunchScheme scheme);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏配置
|
||||
/// </summary>
|
||||
internal readonly struct LaunchConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否全屏,全屏时无边框设置将被覆盖
|
||||
/// </summary>
|
||||
public readonly bool IsFullScreen;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为无边框窗口
|
||||
/// </summary>
|
||||
public readonly bool IsBorderless;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口宽度
|
||||
/// </summary>
|
||||
public readonly int ScreenWidth;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口高度
|
||||
/// </summary>
|
||||
public readonly int ScreenHeight;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用解锁帧率
|
||||
/// </summary>
|
||||
public readonly bool UnlockFPS;
|
||||
|
||||
/// <summary>
|
||||
/// 目标帧率
|
||||
/// </summary>
|
||||
public readonly int TargetFPS;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的启动配置
|
||||
/// </summary>
|
||||
/// <param name="isFullScreen">全屏</param>
|
||||
/// <param name="isBorderless">无边框</param>
|
||||
/// <param name="screenWidth">宽度</param>
|
||||
/// <param name="screenHeight">高度</param>
|
||||
/// <param name="unlockFps">解锁帧率</param>
|
||||
/// <param name="targetFps">目标帧率</param>
|
||||
public LaunchConfiguration(bool isFullScreen, bool isBorderless, int screenWidth, int screenHeight, bool unlockFps, int targetFps)
|
||||
{
|
||||
IsFullScreen = isFullScreen;
|
||||
IsBorderless = isBorderless;
|
||||
ScreenHeight = screenHeight;
|
||||
ScreenWidth = screenWidth;
|
||||
ScreenHeight = screenHeight;
|
||||
UnlockFPS = unlockFps;
|
||||
TargetFPS = targetFps;
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,8 @@ internal class RegistryLauncherLocator : IGameLocator
|
||||
}
|
||||
else
|
||||
{
|
||||
string path = result.Value;
|
||||
string configPath = Path.Combine(path, "config.ini");
|
||||
string? path = Path.GetDirectoryName(result.Value);
|
||||
string configPath = Path.Combine(path!, "config.ini");
|
||||
string? escapedPath = null;
|
||||
using (FileStream stream = File.OpenRead(configPath))
|
||||
{
|
||||
@@ -40,7 +40,8 @@ internal class RegistryLauncherLocator : IGameLocator
|
||||
|
||||
if (escapedPath != null)
|
||||
{
|
||||
return Task.FromResult<ValueResult<bool, string>>(new(true, Unescape(escapedPath)));
|
||||
string gamePath = Path.Combine(Unescape(escapedPath), "YuanShen.exe");
|
||||
return Task.FromResult<ValueResult<bool, string>>(new(true, gamePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
31
src/Snap.Hutao/Snap.Hutao/Service/Game/MultiChannel.cs
Normal file
31
src/Snap.Hutao/Snap.Hutao/Service/Game/MultiChannel.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
|
||||
/// <summary>
|
||||
/// 多通道
|
||||
/// </summary>
|
||||
public struct MultiChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// 通道
|
||||
/// </summary>
|
||||
public string Channel;
|
||||
|
||||
/// <summary>
|
||||
/// 子通道
|
||||
/// </summary>
|
||||
public string SubChannel;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的多通道
|
||||
/// </summary>
|
||||
/// <param name="channel">通道</param>
|
||||
/// <param name="subChannel">子通道</param>
|
||||
public MultiChannel(string? channel, string? subChannel)
|
||||
{
|
||||
Channel = channel ?? string.Empty;
|
||||
SubChannel = subChannel ?? string.Empty;
|
||||
}
|
||||
}
|
||||
161
src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs
Normal file
161
src/Snap.Hutao/Snap.Hutao/Service/Hutao/HutaoCache.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Hutao;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Weapon;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
namespace Snap.Hutao.Service.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃 API 缓存
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton, typeof(IHutaoCache))]
|
||||
internal class HutaoCache : IHutaoCache
|
||||
{
|
||||
private readonly IHutaoService hutaoService;
|
||||
private readonly IMetadataService metadataService;
|
||||
|
||||
private Dictionary<int, Avatar>? idAvatarExtendedMap;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃 API 缓存
|
||||
/// </summary>
|
||||
/// <param name="hutaoService">胡桃服务</param>
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
public HutaoCache(IHutaoService hutaoService, IMetadataService metadataService)
|
||||
{
|
||||
this.hutaoService = hutaoService;
|
||||
this.metadataService = metadataService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<ComplexAvatarRank>? AvatarUsageRanks { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<ComplexAvatarRank>? AvatarAppearanceRanks { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<ComplexAvatarConstellationInfo>? AvatarConstellationInfos { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<ComplexTeamRank>? TeamAppearances { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Overview? Overview { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<ComplexAvatarCollocation>? AvatarCollocations { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeForDatabaseViewModelAsync()
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
Dictionary<int, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
|
||||
|
||||
Task avatarAppearanceRankTask = AvatarAppearanceRankAsync(idAvatarMap);
|
||||
Task avatarUsageRank = AvatarUsageRanksAsync(idAvatarMap);
|
||||
Task avatarConstellationInfoTask = AvatarConstellationInfosAsync(idAvatarMap);
|
||||
Task teamAppearanceTask = TeamAppearancesAsync(idAvatarMap);
|
||||
Task ovewviewTask = OverviewAsync();
|
||||
|
||||
await Task.WhenAll(
|
||||
avatarAppearanceRankTask,
|
||||
avatarUsageRank,
|
||||
avatarConstellationInfoTask,
|
||||
teamAppearanceTask,
|
||||
ovewviewTask)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeForWikiAvatarViewModelAsync()
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
Dictionary<int, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
|
||||
Dictionary<int, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, Model.Metadata.Reliquary.ReliquarySet> idReliquarySetMap = await metadataService.GetEquipAffixIdToReliquarySetMapAsync().ConfigureAwait(false);
|
||||
|
||||
// AvatarCollocation
|
||||
List<AvatarCollocation> avatarCollocationsRaw = await hutaoService.GetAvatarCollocationsAsync().ConfigureAwait(false);
|
||||
AvatarCollocations = avatarCollocationsRaw.Select(co =>
|
||||
{
|
||||
return new ComplexAvatarCollocation(idAvatarMap[co.AvatarId])
|
||||
{
|
||||
AvatarId = co.AvatarId,
|
||||
Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
|
||||
Weapons = co.Weapons.Select(w => new ComplexWeapon(idWeaponMap[w.Item], w.Rate)).ToList(),
|
||||
ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async ValueTask<Dictionary<int, Avatar>> GetIdAvatarMapExtendedAsync()
|
||||
{
|
||||
if (idAvatarExtendedMap == null)
|
||||
{
|
||||
Dictionary<int, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
idAvatarExtendedMap = new(idAvatarMap)
|
||||
{
|
||||
[10000005] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
|
||||
[10000007] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
|
||||
};
|
||||
}
|
||||
|
||||
return idAvatarExtendedMap;
|
||||
}
|
||||
|
||||
private async Task AvatarAppearanceRankAsync(Dictionary<int, Avatar> idAvatarMap)
|
||||
{
|
||||
List<AvatarAppearanceRank> avatarAppearanceRanksRaw = await hutaoService.GetAvatarAppearanceRanksAsync().ConfigureAwait(false);
|
||||
AvatarAppearanceRanks = avatarAppearanceRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
|
||||
{
|
||||
Floor = $"第 {rank.Floor} 层",
|
||||
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private async Task AvatarUsageRanksAsync(Dictionary<int, Avatar> idAvatarMap)
|
||||
{
|
||||
List<AvatarUsageRank> avatarUsageRanksRaw = await hutaoService.GetAvatarUsageRanksAsync().ConfigureAwait(false);
|
||||
AvatarUsageRanks = avatarUsageRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
|
||||
{
|
||||
Floor = $"第 {rank.Floor} 层",
|
||||
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private async Task AvatarConstellationInfosAsync(Dictionary<int, Avatar> idAvatarMap)
|
||||
{
|
||||
List<AvatarConstellationInfo> avatarConstellationInfosRaw = await hutaoService.GetAvatarConstellationInfosAsync().ConfigureAwait(false);
|
||||
AvatarConstellationInfos = avatarConstellationInfosRaw.OrderBy(i => i.HoldingRate).Select(info =>
|
||||
{
|
||||
return new ComplexAvatarConstellationInfo(idAvatarMap[info.AvatarId], info.HoldingRate, info.Constellations.Select(x => x.Rate));
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private async Task TeamAppearancesAsync(Dictionary<int, Avatar> idAvatarMap)
|
||||
{
|
||||
List<TeamAppearance> teamAppearancesRaw = await hutaoService.GetTeamAppearancesAsync().ConfigureAwait(false);
|
||||
TeamAppearances = teamAppearancesRaw.OrderByDescending(t => t.Floor).Select(team => new ComplexTeamRank(team, idAvatarMap)).ToList();
|
||||
}
|
||||
|
||||
private async Task OverviewAsync()
|
||||
{
|
||||
Overview = await hutaoService.GetOverviewAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,10 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
using Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
namespace Snap.Hutao.Service;
|
||||
namespace Snap.Hutao.Service.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃 API 服务
|
||||
@@ -75,4 +74,4 @@ internal class HutaoService : IHutaoService
|
||||
T web = await taskFunc(default).ConfigureAwait(false);
|
||||
return memoryCache.Set(key, web, TimeSpan.FromMinutes(30));
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/Snap.Hutao/Snap.Hutao/Service/Hutao/IHutaoCache.cs
Normal file
55
src/Snap.Hutao/Snap.Hutao/Service/Hutao/IHutaoCache.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Hutao;
|
||||
using Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
namespace Snap.Hutao.Service.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃 API 缓存
|
||||
/// </summary>
|
||||
internal interface IHutaoCache
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色使用率
|
||||
/// </summary>
|
||||
List<ComplexAvatarRank>? AvatarUsageRanks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色上场率
|
||||
/// </summary>
|
||||
List<ComplexAvatarRank>? AvatarAppearanceRanks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色命座信息
|
||||
/// </summary>
|
||||
List<ComplexAvatarConstellationInfo>? AvatarConstellationInfos { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 队伍出场
|
||||
/// </summary>
|
||||
List<ComplexTeamRank>? TeamAppearances { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总览数据
|
||||
/// </summary>
|
||||
Overview? Overview { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色搭配
|
||||
/// </summary>
|
||||
List<ComplexAvatarCollocation>? AvatarCollocations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 为数据库视图模型初始化
|
||||
/// </summary>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask<bool> InitializeForDatabaseViewModelAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 为Wiki角色视图模型初始化
|
||||
/// </summary>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask<bool> InitializeForWikiAvatarViewModelAsync();
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
namespace Snap.Hutao.Service.Abstraction;
|
||||
namespace Snap.Hutao.Service.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃 API 服务
|
||||
@@ -72,4 +72,9 @@ public interface INavigationService
|
||||
/// <param name="pageType">同步的页面类型</param>
|
||||
/// <returns>是否同步成功</returns>
|
||||
bool SyncSelectedNavigationViewItemWith(Type pageType);
|
||||
|
||||
/// <summary>
|
||||
/// 尽可能尝试返回
|
||||
/// </summary>
|
||||
void GoBack();
|
||||
}
|
||||
|
||||
@@ -178,6 +178,21 @@ internal class NavigationService : INavigationService
|
||||
NavigationView.IsPaneOpen = LocalSetting.Get(SettingKeys.IsNavPaneOpen, true);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void GoBack()
|
||||
{
|
||||
Program.DispatcherQueue!.TryEnqueue(() =>
|
||||
{
|
||||
bool canGoBack = Frame?.CanGoBack ?? false;
|
||||
|
||||
if (canGoBack)
|
||||
{
|
||||
Frame!.GoBack();
|
||||
SyncSelectedNavigationViewItemWith(Frame.Content.GetType());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 遍历所有子菜单项
|
||||
/// </summary>
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
@@ -20,10 +22,7 @@ namespace Snap.Hutao.Service.User;
|
||||
[Injection(InjectAs.Singleton, typeof(IUserService))]
|
||||
internal class UserService : IUserService
|
||||
{
|
||||
private readonly AppDbContext appDbContext;
|
||||
private readonly UserClient userClient;
|
||||
private readonly BindingClient bindingClient;
|
||||
private readonly AuthClient authClient;
|
||||
private readonly IServiceScopeFactory scopeFactory;
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
private BindingUser? currentUser;
|
||||
@@ -32,22 +31,11 @@ internal class UserService : IUserService
|
||||
/// <summary>
|
||||
/// 构造一个新的用户服务
|
||||
/// </summary>
|
||||
/// <param name="appDbContext">应用程序数据库上下文</param>
|
||||
/// <param name="userClient">用户客户端</param>
|
||||
/// <param name="bindingClient">角色客户端</param>
|
||||
/// <param name="authClient">验证客户端</param>
|
||||
/// <param name="scopeFactory">范围工厂</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
public UserService(
|
||||
AppDbContext appDbContext,
|
||||
UserClient userClient,
|
||||
BindingClient bindingClient,
|
||||
AuthClient authClient,
|
||||
IMessenger messenger)
|
||||
public UserService(IServiceScopeFactory scopeFactory, IMessenger messenger)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
this.userClient = userClient;
|
||||
this.bindingClient = bindingClient;
|
||||
this.authClient = authClient;
|
||||
this.scopeFactory = scopeFactory;
|
||||
this.messenger = messenger;
|
||||
}
|
||||
|
||||
@@ -62,44 +50,53 @@ internal class UserService : IUserService
|
||||
return;
|
||||
}
|
||||
|
||||
// only update when not processing a deletion
|
||||
if (value != null)
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
// only update when not processing a deletion
|
||||
if (value != null)
|
||||
{
|
||||
if (currentUser != null)
|
||||
{
|
||||
currentUser.IsSelected = false;
|
||||
appDbContext.Users.Update(currentUser.Entity);
|
||||
appDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
Message.UserChangedMessage message = new() { OldValue = currentUser, NewValue = value };
|
||||
|
||||
// 当删除到无用户时也能正常反应状态
|
||||
currentUser = value;
|
||||
|
||||
if (currentUser != null)
|
||||
{
|
||||
currentUser.IsSelected = false;
|
||||
currentUser.IsSelected = true;
|
||||
appDbContext.Users.Update(currentUser.Entity);
|
||||
appDbContext.SaveChanges();
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
}
|
||||
|
||||
Message.UserChangedMessage message = new(currentUser, value);
|
||||
|
||||
// 当删除到无用户时也能正常反应状态
|
||||
currentUser = value;
|
||||
|
||||
if (currentUser != null)
|
||||
{
|
||||
currentUser.IsSelected = true;
|
||||
appDbContext.Users.Update(currentUser.Entity);
|
||||
appDbContext.SaveChanges();
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task RemoveUserAsync(BindingUser user)
|
||||
public async Task RemoveUserAsync(BindingUser user)
|
||||
{
|
||||
await Task.Yield();
|
||||
Must.NotNull(userCollection!);
|
||||
|
||||
// Sync cache
|
||||
userCollection.Remove(user);
|
||||
|
||||
// Sync database
|
||||
appDbContext.Users.Remove(user.Entity);
|
||||
return appDbContext.SaveChangesAsync();
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
appDbContext.Users.RemoveAndSave(user.Entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -109,21 +106,27 @@ internal class UserService : IUserService
|
||||
{
|
||||
List<BindingUser> users = new();
|
||||
|
||||
foreach (Model.Entity.User entity in appDbContext.Users)
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
BindingUser? initialized = await BindingUser
|
||||
.ResumeAsync(entity, userClient, bindingClient)
|
||||
.ConfigureAwait(false);
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
UserClient userClient = scope.ServiceProvider.GetRequiredService<UserClient>();
|
||||
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService<BindingClient>();
|
||||
|
||||
if (initialized != null)
|
||||
foreach (Model.Entity.User entity in appDbContext.Users)
|
||||
{
|
||||
users.Add(initialized);
|
||||
}
|
||||
else
|
||||
{
|
||||
// User is unable to be initialized, remove it.
|
||||
appDbContext.Users.Remove(entity);
|
||||
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
BindingUser? initialized = await BindingUser
|
||||
.ResumeAsync(entity, userClient, bindingClient)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (initialized != null)
|
||||
{
|
||||
users.Add(initialized);
|
||||
}
|
||||
else
|
||||
{
|
||||
// User is unable to be initialized, remove it.
|
||||
appDbContext.Users.RemoveAndSave(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,24 +153,27 @@ internal class UserService : IUserService
|
||||
// 检查 uid 对应用户是否存在
|
||||
if (UserHelper.TryGetUserByUid(userCollection, uid, out BindingUser? userWithSameUid))
|
||||
{
|
||||
// 检查 stoken 是否存在
|
||||
if (cookie.ContainsSToken())
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
// insert stoken directly
|
||||
userWithSameUid.Cookie.InsertSToken(uid, cookie);
|
||||
appDbContext.Users.Update(userWithSameUid.Entity);
|
||||
appDbContext.SaveChanges();
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
return new(UserOptionResult.Upgraded, uid);
|
||||
}
|
||||
// 检查 stoken 是否存在
|
||||
if (cookie.ContainsSToken())
|
||||
{
|
||||
// insert stoken
|
||||
userWithSameUid.UpdateSToken(uid, cookie);
|
||||
appDbContext.Users.UpdateAndSave(userWithSameUid.Entity);
|
||||
|
||||
if (cookie.ContainsLTokenAndCookieToken())
|
||||
{
|
||||
userWithSameUid.Cookie = cookie;
|
||||
appDbContext.Users.Update(userWithSameUid.Entity);
|
||||
appDbContext.SaveChanges();
|
||||
return new(UserOptionResult.Upgraded, uid);
|
||||
}
|
||||
|
||||
return new(UserOptionResult.Updated, uid);
|
||||
if (cookie.ContainsLTokenAndCookieToken())
|
||||
{
|
||||
userWithSameUid.Cookie = cookie;
|
||||
appDbContext.Users.UpdateAndSave(userWithSameUid.Entity);
|
||||
|
||||
return new(UserOptionResult.Updated, uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cookie.ContainsLTokenAndCookieToken())
|
||||
@@ -184,7 +190,8 @@ internal class UserService : IUserService
|
||||
if (cookie.TryGetLoginTicket(out string? loginTicket))
|
||||
{
|
||||
// get multitoken
|
||||
Dictionary<string, string> multiToken = await authClient
|
||||
Dictionary<string, string> multiToken = await Ioc.Default
|
||||
.GetRequiredService<AuthClient>()
|
||||
.GetMultiTokenByLoginTicketAsync(loginTicket, uid, default)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -198,22 +205,27 @@ internal class UserService : IUserService
|
||||
|
||||
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(ObservableCollection<BindingUser> users, Cookie cookie)
|
||||
{
|
||||
BindingUser? newUser = await BindingUser.CreateAsync(cookie, userClient, bindingClient).ConfigureAwait(false);
|
||||
if (newUser != null)
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
// Sync cache
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
users.Add(newUser);
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
UserClient userClient = scope.ServiceProvider.GetRequiredService<UserClient>();
|
||||
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService<BindingClient>();
|
||||
|
||||
// Sync database
|
||||
appDbContext.Users.Add(newUser.Entity);
|
||||
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
BindingUser? newUser = await BindingUser.CreateAsync(cookie, userClient, bindingClient).ConfigureAwait(false);
|
||||
if (newUser != null)
|
||||
{
|
||||
// Sync cache
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
users.Add(newUser);
|
||||
|
||||
return new(UserOptionResult.Added, newUser.UserInfo!.Uid);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(UserOptionResult.Invalid, null!);
|
||||
// Sync database
|
||||
appDbContext.Users.AddAndSave(newUser.Entity);
|
||||
return new(UserOptionResult.Added, newUser.UserInfo!.Uid);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(UserOptionResult.Invalid, null!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,16 +28,13 @@
|
||||
<StartupObject>Snap.Hutao.Program</StartupObject>
|
||||
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT</DefineConstants>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Control\Panel\PanelSelector.xaml" />
|
||||
<None Remove="LaunchGameWindow.xaml" />
|
||||
<None Remove="NativeMethods.json" />
|
||||
<None Remove="NativeMethods.txt" />
|
||||
<None Remove="Resource\Icon\UI_BagTabIcon_Avatar.png" />
|
||||
@@ -45,6 +42,7 @@
|
||||
<None Remove="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
|
||||
<None Remove="Resource\Icon\UI_BtnIcon_Gacha.png" />
|
||||
<None Remove="Resource\Icon\UI_ChapterIcon_Hutao.png" />
|
||||
<None Remove="Resource\Icon\UI_GuideIcon_PlayMethod.png" />
|
||||
<None Remove="Resource\Icon\UI_Icon_Achievement.png" />
|
||||
<None Remove="Resource\Icon\UI_Icon_BoostUp.png" />
|
||||
<None Remove="Resource\Icon\UI_Icon_Locked.png" />
|
||||
@@ -62,7 +60,7 @@
|
||||
<None Remove="View\Dialog\GachaLogImportDialog.xaml" />
|
||||
<None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" />
|
||||
<None Remove="View\Dialog\GachaLogUrlDialog.xaml" />
|
||||
<None Remove="View\Dialog\UserAutoCookieDialog.xaml" />
|
||||
<None Remove="View\Dialog\GameAccountNameDialog.xaml" />
|
||||
<None Remove="View\Dialog\UserDialog.xaml" />
|
||||
<None Remove="View\MainView.xaml" />
|
||||
<None Remove="View\Page\AchievementPage.xaml" />
|
||||
@@ -71,6 +69,9 @@
|
||||
<None Remove="View\Page\AvatarPropertyPage.xaml" />
|
||||
<None Remove="View\Page\GachaLogPage.xaml" />
|
||||
<None Remove="View\Page\HutaoDatabasePage.xaml" />
|
||||
<None Remove="View\Page\LaunchGamePage.xaml" />
|
||||
<None Remove="View\Page\LoginMihoyoBBSPage.xaml" />
|
||||
<None Remove="View\Page\LoginMihoyoUserPage.xaml" />
|
||||
<None Remove="View\Page\SettingPage.xaml" />
|
||||
<None Remove="View\Page\WikiAvatarPage.xaml" />
|
||||
<None Remove="View\TitleView.xaml" />
|
||||
@@ -97,6 +98,7 @@
|
||||
<Content Include="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
|
||||
<Content Include="Resource\Icon\UI_BtnIcon_Gacha.png" />
|
||||
<Content Include="Resource\Icon\UI_ChapterIcon_Hutao.png" />
|
||||
<Content Include="Resource\Icon\UI_GuideIcon_PlayMethod.png" />
|
||||
<Content Include="Resource\Icon\UI_Icon_Achievement.png" />
|
||||
<Content Include="Resource\Icon\UI_Icon_BoostUp.png" />
|
||||
<Content Include="Resource\Icon\UI_Icon_Locked.png" />
|
||||
@@ -148,6 +150,31 @@
|
||||
<ItemGroup>
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Dialog\GameAccountNameDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Page\LoginMihoyoBBSPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Page\LoginMihoyoUserPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="LaunchGameWindow.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Page\LaunchGamePage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Page\HutaoDatabasePage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -168,11 +195,6 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Dialog\UserAutoCookieDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Dialog\AchievementArchiveCreateDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -138,7 +138,8 @@
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<TextBlock
|
||||
Margin="0,4,12,4"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,4,12,2"
|
||||
FontFamily="Consolas"
|
||||
Text="{Binding TotalCount}"
|
||||
Visibility="{Binding ElementName=DetailExpander,Path=IsExpanded,Converter={StaticResource BoolToVisibilityRevertConverter}}"
|
||||
@@ -318,7 +319,6 @@
|
||||
</ItemsControl>
|
||||
</cwucont:Case>
|
||||
</cwucont:SwitchPresenter>
|
||||
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<ContentDialog
|
||||
x:Class="Snap.Hutao.View.Dialog.GameAccountNameDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="为账号命名"
|
||||
DefaultButton="Primary"
|
||||
PrimaryButtonText="确认"
|
||||
CloseButtonText="取消"
|
||||
Style="{StaticResource DefaultContentDialogStyle}">
|
||||
|
||||
<Grid>
|
||||
<TextBox
|
||||
Margin="0,0,0,0"
|
||||
x:Name="InputText"
|
||||
PlaceholderText="在此处输入"
|
||||
VerticalAlignment="Top"/>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
|
||||
namespace Snap.Hutao.View.Dialog;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏账号命名对话框
|
||||
/// </summary>
|
||||
public sealed partial class GameAccountNameDialog : ContentDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的游戏账号命名对话框
|
||||
/// </summary>
|
||||
/// <param name="window">窗体</param>
|
||||
public GameAccountNameDialog(Window window)
|
||||
{
|
||||
InitializeComponent();
|
||||
XamlRoot = window.Content.XamlRoot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取输入的Cookie
|
||||
/// </summary>
|
||||
/// <returns>输入的结果</returns>
|
||||
public async Task<ValueResult<bool, string>> GetInputNameAsync()
|
||||
{
|
||||
ContentDialogResult result = await ShowAsync();
|
||||
string text = InputText.Text;
|
||||
return new(result == ContentDialogResult.Primary && (!string.IsNullOrEmpty(text)), text);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<ContentDialog
|
||||
x:Class="Snap.Hutao.View.Dialog.UserAutoCookieDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="登录米哈游通行证"
|
||||
DefaultButton="Primary"
|
||||
PrimaryButtonText="继续"
|
||||
CloseButtonText="取消"
|
||||
Style="{StaticResource DefaultContentDialogStyle}">
|
||||
<ContentDialog.Resources>
|
||||
<x:Double x:Key="ContentDialogMaxWidth">1600</x:Double>
|
||||
<x:Double x:Key="ContentDialogMinHeight">200</x:Double>
|
||||
<x:Double x:Key="ContentDialogMaxHeight">1200</x:Double>
|
||||
</ContentDialog.Resources>
|
||||
<Grid Loaded="OnRootLoaded">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Text="在下方登录"
|
||||
Grid.Row="0"/>
|
||||
<WebView2
|
||||
Grid.Row="2"
|
||||
Margin="0,12,0,0"
|
||||
Width="640"
|
||||
Height="400"
|
||||
x:Name="WebView"/>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user