refactor wiki viewmodel

This commit is contained in:
Lightczx
2023-08-10 17:25:37 +08:00
parent 420bf6ff41
commit d9169df3b8
18 changed files with 536 additions and 490 deletions

View File

@@ -1,10 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Json.Annotation;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.Wiki;
namespace Snap.Hutao.Model.Metadata.Avatar;
@@ -13,4 +10,13 @@ namespace Snap.Hutao.Model.Metadata.Avatar;
/// </summary>
internal sealed class AvatarBaseValue : BaseValue
{
public override float GetValue(FightProperty fightProperty)
{
return fightProperty switch
{
FightProperty.FIGHT_PROP_CRITICAL => 0.05F,
FightProperty.FIGHT_PROP_CRITICAL_HURT => 0.5F,
_ => base.GetValue(fightProperty),
};
}
}

View File

@@ -12,6 +12,8 @@ namespace Snap.Hutao.Model.Metadata.Monster;
/// </summary>
internal sealed class Monster
{
internal const uint MaxLevel = 100;
/// <summary>
/// Id
/// </summary>

View File

@@ -29,11 +29,9 @@ internal sealed class Promote
/// </summary>
public List<TypeValue<FightProperty, float>> AddProperties { get; set; } = default!;
/// <summary>
/// 属性映射
/// </summary>
public Dictionary<FightProperty, float> AddPropertyMap
public float GetValue(FightProperty property)
{
get => addPropertyMap ??= AddProperties.ToDictionary(a => a.Type, a => a.Value);
addPropertyMap ??= AddProperties.ToDictionary(a => a.Type, a => a.Value);
return addPropertyMap.GetValueOrDefault(property);
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Model.Primitive;
internal static class LevelFormat
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Format(uint value)
{
return $"Lv.{value}";
}
}

View File

@@ -45,10 +45,10 @@ internal sealed partial class HutaoCache : IHutaoCache
public Overview? Overview { get; set; }
/// <inheritdoc/>
public List<AvatarCollocationView>? AvatarCollocations { get; set; }
public Dictionary<AvatarId, AvatarCollocationView>? AvatarCollocations { get; set; }
/// <inheritdoc/>
public List<WeaponCollocationView>? WeaponCollocations { get; set; }
public Dictionary<WeaponId, WeaponCollocationView>? WeaponCollocations { get; set; }
/// <inheritdoc/>
public async ValueTask<bool> InitializeForDatabaseViewModelAsync()
@@ -153,7 +153,7 @@ internal sealed partial class HutaoCache : IHutaoCache
Avatars = co.Avatars.SelectList(a => new AvatarView(idAvatarMap[a.Item], a.Rate)),
Weapons = co.Weapons.SelectList(w => new WeaponView(idWeaponMap[w.Item], w.Rate)),
ReliquarySets = co.Reliquaries.SelectList(r => new ReliquarySetView(r, idReliquarySetMap)),
});
}).ToDictionary(a => a.AvatarId);
}
private async ValueTask WeaponCollocationsAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
@@ -169,7 +169,7 @@ internal sealed partial class HutaoCache : IHutaoCache
{
WeaponId = co.WeaponId,
Avatars = co.Avatars.SelectList(a => new AvatarView(idAvatarMap[a.Item], a.Rate)),
});
}).ToDictionary(w => w.WeaponId);
}
[SuppressMessage("", "SH003")]

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.Complex;
using Snap.Hutao.Web.Hutao.Model;
@@ -41,12 +42,12 @@ internal interface IHutaoCache
/// <summary>
/// 角色搭配
/// </summary>
List<AvatarCollocationView>? AvatarCollocations { get; set; }
Dictionary<AvatarId, AvatarCollocationView>? AvatarCollocations { get; set; }
/// <summary>
/// 武器搭配
/// </summary>
List<WeaponCollocationView>? WeaponCollocations { get; set; }
Dictionary<WeaponId, WeaponCollocationView>? WeaponCollocations { get; set; }
/// <summary>
/// 为数据库视图模型初始化

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Service.User;
internal interface IUserInitializationService
{
ValueTask<ViewModel.User.User?> CreateOrDefaultUserFromCookieAsync(Cookie cookie, bool isOversea, CancellationToken token = default(CancellationToken));
ValueTask<ViewModel.User.User> ResumeUserAsync(Model.Entity.User inner, CancellationToken token = default(CancellationToken));
}

View File

@@ -0,0 +1,185 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Service.User;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IUserInitializationService))]
internal sealed partial class UserInitializationService : IUserInitializationService
{
private readonly IServiceProvider serviceProvider;
public async ValueTask<ViewModel.User.User> ResumeUserAsync(Model.Entity.User inner, CancellationToken token = default)
{
ViewModel.User.User user = ViewModel.User.User.From(inner, serviceProvider);
if (!await InitializeUserAsync(user, token).ConfigureAwait(false))
{
user.UserInfo = new() { Nickname = SH.ModelBindingUserInitializationFailed };
user.UserGameRoles = new();
}
return user;
}
public async ValueTask<ViewModel.User.User?> CreateOrDefaultUserFromCookieAsync(Cookie cookie, bool isOversea, CancellationToken token = default)
{
// 这里只负责创建实体用户,稍后在用户服务中保存到数据库
Model.Entity.User entity = Model.Entity.User.From(cookie, isOversea);
entity.Aid = cookie.GetValueOrDefault(Cookie.STUID);
entity.Mid = isOversea ? entity.Aid : cookie.GetValueOrDefault(Cookie.MID);
entity.IsOversea = isOversea;
if (entity.Aid is not null && entity.Mid is not null)
{
ViewModel.User.User user = ViewModel.User.User.From(entity, serviceProvider);
bool initialized = await InitializeUserAsync(user, token).ConfigureAwait(false);
return initialized ? user : null;
}
else
{
return null;
}
}
private async ValueTask<bool> InitializeUserAsync(ViewModel.User.User user, CancellationToken token = default)
{
if (user.IsInitialized)
{
// Prevent multiple initialization.
return true;
}
if (user.SToken is null)
{
return false;
}
bool isOversea = user.Entity.IsOversea;
if (!await TrySetUserLTokenAsync(user, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetUserCookieTokenAsync(user, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetUserUserInfoAsync(user, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetUserUserGameRolesAsync(user, token).ConfigureAwait(false))
{
return false;
}
user.SelectedUserGameRole = user.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen);
return user.IsInitialized = true;
}
private async ValueTask<bool> TrySetUserLTokenAsync(ViewModel.User.User user, CancellationToken token)
{
if (user.LToken is not null)
{
return true;
}
Response<LTokenWrapper> lTokenResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea)
.GetLTokenBySTokenAsync(user.Entity, token)
.ConfigureAwait(false);
if (lTokenResponse.IsOk())
{
user.LToken = new()
{
[Cookie.LTUID] = user.Entity.Aid ?? string.Empty,
[Cookie.LTOKEN] = lTokenResponse.Data.LToken,
};
return true;
}
else
{
return false;
}
}
private async ValueTask<bool> TrySetUserCookieTokenAsync(ViewModel.User.User user, CancellationToken token)
{
if (user.CookieToken is not null)
{
return true;
}
Response<UidCookieToken> cookieTokenResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea)
.GetCookieAccountInfoBySTokenAsync(user.Entity, token)
.ConfigureAwait(false);
if (cookieTokenResponse.IsOk())
{
user.CookieToken = new()
{
[Cookie.ACCOUNT_ID] = user.Entity.Aid ?? string.Empty,
[Cookie.COOKIE_TOKEN] = cookieTokenResponse.Data.CookieToken,
};
return true;
}
else
{
return false;
}
}
private async ValueTask<bool> TrySetUserUserInfoAsync(ViewModel.User.User user, CancellationToken token)
{
Response<UserFullInfoWrapper> response = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IUserClient>>()
.Create(user.IsOversea)
.GetUserFullInfoAsync(user.Entity, token)
.ConfigureAwait(false);
if (response.IsOk())
{
user.UserInfo = response.Data.UserInfo;
return true;
}
else
{
return false;
}
}
private async ValueTask<bool> TrySetUserUserGameRolesAsync(ViewModel.User.User user, CancellationToken token)
{
Response<ListWrapper<UserGameRole>> userGameRolesResponse = await serviceProvider
.GetRequiredService<BindingClient>()
.GetUserGameRolesOverseaAwareAsync(user.Entity, token)
.ConfigureAwait(false);
if (userGameRolesResponse.IsOk())
{
user.UserGameRoles = userGameRolesResponse.Data.List;
return user.UserGameRoles.Any();
}
else
{
return false;
}
}
}

View File

@@ -25,11 +25,12 @@ namespace Snap.Hutao.Service.User;
[Injection(InjectAs.Singleton, typeof(IUserService))]
internal sealed partial class UserService : IUserService
{
private readonly ITaskContext taskContext;
private readonly IUserDbService userDbService;
private readonly IServiceProvider serviceProvider;
private readonly IMessenger messenger;
private readonly ScopedDbCurrent<BindingUser, Model.Entity.User, UserChangedMessage> dbCurrent;
private readonly IUserInitializationService userInitializationService;
private readonly IServiceProvider serviceProvider;
private readonly IUserDbService userDbService;
private readonly ITaskContext taskContext;
private readonly IMessenger messenger;
private ObservableCollection<BindingUser>? userCollection;
private ObservableCollection<UserAndUid>? userAndUidCollection;
@@ -63,7 +64,7 @@ internal sealed partial class UserService : IUserService
if (userCollection is null)
{
List<Model.Entity.User> entities = await userDbService.GetUserListAsync().ConfigureAwait(false);
List<BindingUser> users = await entities.SelectListAsync(BindingUser.ResumeAsync, default).ConfigureAwait(false);
List<BindingUser> users = await entities.SelectListAsync(userInitializationService.ResumeUserAsync, default).ConfigureAwait(false);
userCollection = users.ToObservableCollection();
try
@@ -72,7 +73,7 @@ internal sealed partial class UserService : IUserService
}
catch (InvalidOperationException ex)
{
throw new UserdataCorruptedException(SH.ServiceUserCurrentMultiMatched, ex);
ThrowHelper.UserdataCorrupted(SH.ServiceUserCurrentMultiMatched, ex);
}
}
@@ -184,7 +185,7 @@ internal sealed partial class UserService : IUserService
private async ValueTask<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea)
{
await taskContext.SwitchToBackgroundAsync();
BindingUser? newUser = await BindingUser.CreateAsync(cookie, isOversea).ConfigureAwait(false);
BindingUser? newUser = await userInitializationService.CreateOrDefaultUserFromCookieAsync(cookie, isOversea).ConfigureAwait(false);
if (newUser is not null)
{

View File

@@ -3,6 +3,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Model;
@@ -17,34 +18,36 @@ namespace Snap.Hutao.ViewModel.User;
/// <summary>
/// 用于视图绑定的用户
/// TODO: move initializaion part to service
/// </summary>
[HighQuality]
internal sealed class User : ObservableObject, IEntityOnly<EntityUser>, ISelectable
internal sealed class User : ObservableObject, IEntityOnly<EntityUser>, IMappingFrom<User, EntityUser, IServiceProvider>, ISelectable
{
private readonly EntityUser inner;
private readonly IServiceProvider serviceProvider;
private UserGameRole? selectedUserGameRole;
private bool isInitialized;
/// <summary>
/// 构造一个新的绑定视图用户
/// </summary>
/// <param name="user">用户实体</param>
private User(EntityUser user)
private User(EntityUser user, IServiceProvider serviceProvider)
{
inner = user;
this.serviceProvider = serviceProvider;
}
/// <summary>
/// 用户信息
/// </summary>
public UserInfo? UserInfo { get; private set; }
public bool IsInitialized { get; set; }
/// <summary>
/// 用户信息
/// </summary>
public List<UserGameRole> UserGameRoles { get; private set; } = default!;
public UserInfo? UserInfo { get; set; }
/// <summary>
/// 用户信息
/// </summary>
public List<UserGameRole> UserGameRoles { get; set; } = default!;
/// <summary>
/// 用户信息, 请勿访问set
@@ -56,9 +59,8 @@ internal sealed class User : ObservableObject, IEntityOnly<EntityUser>, ISelecta
{
if (SetProperty(ref selectedUserGameRole, value))
{
Ioc.Default
.GetRequiredService<IMessenger>()
.Send(new Message.UserChangedMessage() { OldValue = this, NewValue = this });
IMessenger messenger = serviceProvider.GetRequiredService<IMessenger>();
messenger.Send(new Message.UserChangedMessage() { OldValue = this, NewValue = this });
}
}
}
@@ -103,178 +105,8 @@ internal sealed class User : ObservableObject, IEntityOnly<EntityUser>, ISelecta
/// </summary>
public EntityUser Entity { get => inner; }
/// <summary>
/// 从数据库恢复用户
/// </summary>
/// <param name="inner">数据库实体</param>
/// <param name="token">取消令牌</param>
/// <returns>用户</returns>
internal static async ValueTask<User> ResumeAsync(EntityUser inner, CancellationToken token = default)
public static User From(EntityUser user, IServiceProvider provider)
{
User user = new(inner);
if (!await user.InitializeCoreAsync(token).ConfigureAwait(false))
{
user.UserInfo = new() { Nickname = SH.ModelBindingUserInitializationFailed };
user.UserGameRoles = new();
}
return user;
}
/// <summary>
/// 创建并初始化用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <param name="isOversea">是否为国际服</param>
/// <param name="token">取消令牌</param>
/// <returns>用户</returns>
internal static async Task<User?> CreateAsync(Cookie cookie, bool isOversea, CancellationToken token = default)
{
// 这里只负责创建实体用户,稍后在用户服务中保存到数据库
EntityUser entity = EntityUser.From(cookie, isOversea);
entity.Aid = cookie.GetValueOrDefault(Cookie.STUID);
entity.Mid = isOversea ? entity.Aid : cookie.GetValueOrDefault(Cookie.MID);
entity.IsOversea = isOversea;
if (entity.Aid != null && entity.Mid != null)
{
User user = new(entity);
bool initialized = await user.InitializeCoreAsync(token).ConfigureAwait(false);
return initialized ? user : null;
}
else
{
return null;
}
}
private async Task<bool> InitializeCoreAsync(CancellationToken token = default)
{
if (isInitialized)
{
// Prevent multiple initialization.
return true;
}
if (SToken == null)
{
return false;
}
using (IServiceScope scope = Ioc.Default.CreateScope())
{
bool isOversea = Entity.IsOversea;
if (!await TrySetLTokenAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetCookieTokenAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetUserInfoAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetUserGameRolesAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
}
SelectedUserGameRole = UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen);
return isInitialized = true;
}
private async Task<bool> TrySetLTokenAsync(IServiceProvider provider, CancellationToken token)
{
if (LToken != null)
{
return true;
}
Response<LTokenWrapper> lTokenResponse = await provider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(Entity.IsOversea)
.GetLTokenBySTokenAsync(Entity, token)
.ConfigureAwait(false);
if (lTokenResponse.IsOk())
{
LToken = Cookie.Parse($"{Cookie.LTUID}={Entity.Aid};{Cookie.LTOKEN}={lTokenResponse.Data.LToken}");
return true;
}
else
{
return false;
}
}
private async Task<bool> TrySetCookieTokenAsync(IServiceProvider provider, CancellationToken token)
{
if (CookieToken != null)
{
return true;
}
Response<UidCookieToken> cookieTokenResponse = await provider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(Entity.IsOversea)
.GetCookieAccountInfoBySTokenAsync(Entity, token)
.ConfigureAwait(false);
if (cookieTokenResponse.IsOk())
{
CookieToken = Cookie.Parse($"{Cookie.ACCOUNT_ID}={Entity.Aid};{Cookie.COOKIE_TOKEN}={cookieTokenResponse.Data.CookieToken}");
return true;
}
else
{
return false;
}
}
private async Task<bool> TrySetUserInfoAsync(IServiceProvider provider, CancellationToken token)
{
Response<UserFullInfoWrapper> response = await provider
.GetRequiredService<IOverseaSupportFactory<IUserClient>>()
.Create(Entity.IsOversea)
.GetUserFullInfoAsync(Entity, token)
.ConfigureAwait(false);
if (response.IsOk())
{
UserInfo = response.Data.UserInfo;
return true;
}
else
{
return false;
}
}
private async Task<bool> TrySetUserGameRolesAsync(IServiceProvider provider, CancellationToken token)
{
Response<ListWrapper<UserGameRole>> userGameRolesResponse = await provider
.GetRequiredService<BindingClient>()
.GetUserGameRolesOverseaAwareAsync(Entity, token)
.ConfigureAwait(false);
if (userGameRolesResponse.IsOk())
{
UserGameRoles = userGameRolesResponse.Data.List;
return UserGameRoles.Any();
}
else
{
return false;
}
return new(user, provider);
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Service.Navigation;
@@ -23,9 +24,10 @@ namespace Snap.Hutao.ViewModel.User;
[Injection(InjectAs.Singleton)]
internal sealed partial class UserViewModel : ObservableObject
{
private readonly INavigationService navigationService;
private readonly IServiceProvider serviceProvider;
private readonly IInfoBarService infoBarService;
private readonly Core.RuntimeOptions hutaoOptions;
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
private readonly IUserService userService;
@@ -44,7 +46,7 @@ internal sealed partial class UserViewModel : ObservableObject
{
userService.Current = value;
if (value != null)
if (value is not null)
{
value.SelectedUserGameRole = value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen);
}
@@ -63,12 +65,13 @@ internal sealed partial class UserViewModel : ObservableObject
/// <param name="optionResult">操作结果</param>
/// <param name="uid">uid</param>
/// <returns>任务</returns>
public async Task HandleUserOptionResultAsync(UserOptionResult optionResult, string uid)
internal async ValueTask HandleUserOptionResultAsync(UserOptionResult optionResult, string uid)
{
switch (optionResult)
{
case UserOptionResult.Added:
if (Users!.Count == 1)
ArgumentNullException.ThrowIfNull(Users);
if (Users.Count == 1)
{
await taskContext.SwitchToMainThreadAsync();
SelectedUser = Users.Single();
@@ -107,16 +110,16 @@ internal sealed partial class UserViewModel : ObservableObject
[Command("AddUserCommand")]
private Task AddUserAsync()
{
return AddUserCoreAsync(false);
return AddUserCoreAsync(false).AsTask();
}
[Command("AddOverseaUserCommand")]
private Task AddOverseaUserAsync()
{
return AddUserCoreAsync(true);
return AddUserCoreAsync(true).AsTask();
}
private async Task AddUserCoreAsync(bool isOversea)
private async ValueTask AddUserCoreAsync(bool isOversea)
{
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
@@ -126,9 +129,9 @@ internal sealed partial class UserViewModel : ObservableObject
ValueResult<bool, string> result = await dialog.GetInputCookieAsync().ConfigureAwait(false);
// User confirms the input
if (result.IsOk)
if (result.TryGetValue(out string rawCookie))
{
Cookie cookie = Cookie.Parse(result.Value);
Cookie cookie = Cookie.Parse(rawCookie);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(cookie, isOversea).ConfigureAwait(false);
@@ -139,11 +142,9 @@ internal sealed partial class UserViewModel : ObservableObject
[Command("LoginMihoyoUserCommand")]
private void LoginMihoyoUser()
{
if (hutaoOptions.IsWebView2Supported)
if (runtimeOptions.IsWebView2Supported)
{
serviceProvider
.GetRequiredService<INavigationService>()
.Navigate<LoginMihoyoUserPage>(INavigationAwaiter.Default);
navigationService.Navigate<LoginMihoyoUserPage>(INavigationAwaiter.Default);
}
else
{
@@ -154,11 +155,9 @@ internal sealed partial class UserViewModel : ObservableObject
[Command("LoginHoyoverseUserCommand")]
private void LoginHoyoverseUser()
{
if (hutaoOptions.IsWebView2Supported)
if (runtimeOptions.IsWebView2Supported)
{
serviceProvider
.GetRequiredService<INavigationService>()
.Navigate<LoginHoyoverseUserPage>(INavigationAwaiter.Default);
navigationService.Navigate<LoginHoyoverseUserPage>(INavigationAwaiter.Default);
}
else
{
@@ -169,11 +168,11 @@ internal sealed partial class UserViewModel : ObservableObject
[Command("RemoveUserCommand")]
private async Task RemoveUserAsync(User? user)
{
if (user != null)
if (user is not null)
{
try
{
await userService.RemoveUserAsync(user!).ConfigureAwait(false);
await userService.RemoveUserAsync(user).ConfigureAwait(false);
infoBarService.Success(string.Format(SH.ViewModelUserRemoved, user.UserInfo?.Nickname));
}
catch (UserdataCorruptedException ex)
@@ -188,26 +187,29 @@ internal sealed partial class UserViewModel : ObservableObject
{
try
{
ArgumentNullException.ThrowIfNull(user);
string cookieString = new StringBuilder()
.Append(user!.SToken)
.AppendIf(user.SToken != null, ';')
.Append(user.SToken)
.AppendIf(user.SToken is not null, ';')
.Append(user.LToken)
.AppendIf(user.LToken != null, ';')
.AppendIf(user.LToken is not null, ';')
.Append(user.CookieToken)
.ToString();
serviceProvider.GetRequiredService<IClipboardInterop>().SetText(cookieString);
infoBarService.Success(string.Format(SH.ViewModelUserCookieCopied, user.UserInfo!.Nickname));
ArgumentNullException.ThrowIfNull(user.UserInfo);
infoBarService.Success(string.Format(SH.ViewModelUserCookieCopied, user.UserInfo.Nickname));
}
catch (Exception e)
catch (Exception ex)
{
infoBarService.Error(e);
infoBarService.Error(ex);
}
}
[Command("RefreshCookieTokenCommand")]
private async Task RefreshCookieTokenAsync()
{
if (SelectedUser != null)
if (SelectedUser is not null)
{
if (await userService.RefreshCookieTokenAsync(SelectedUser).ConfigureAwait(false))
{

View File

@@ -26,6 +26,7 @@ internal static class AvatarFilter
{
List<bool> matches = new();
// TODO: use Collection Literals
foreach (StringSegment segment in new StringTokenizer(input, new char[] { ' ' }))
{
string value = segment.ToString();

View File

@@ -71,7 +71,7 @@ internal sealed class BaseValueInfo : ObservableObject
/// </summary>
public string CurrentLevelFormatted
{
get => $"Lv.{CurrentLevel}";
get => LevelFormat.Format(CurrentLevel);
}
/// <summary>
@@ -95,10 +95,10 @@ internal sealed class BaseValueInfo : ObservableObject
Values = propValues.SelectList(propValue =>
{
float value = propValue.Value * growCurveMap[level].GetValueOrDefault(propValue.Type);
if (promoteMap != null)
if (promoteMap is not null)
{
PromoteLevel promoteLevel = GetPromoteLevel(level, promoted);
float addValue = promoteMap[promoteLevel].AddPropertyMap.GetValueOrDefault(propValue.Property, 0F);
float addValue = promoteMap[promoteLevel].GetValue(propValue.Property);
value += addValue;
}
@@ -109,33 +109,11 @@ internal sealed class BaseValueInfo : ObservableObject
private PromoteLevel GetPromoteLevel(in Level level, bool promoted)
{
if (MaxLevel <= 70)
if (MaxLevel <= 70 && level.Value == 70U)
{
if (promoted)
{
return level.Value switch
{
>= 60U => 4,
>= 50U => 3,
>= 40U => 2,
>= 20U => 1,
_ => 0,
};
return 4;
}
else
{
return level.Value switch
{
> 60U => 4,
> 50U => 3,
> 40U => 2,
> 20U => 1,
_ => 0,
};
}
}
else
{
if (promoted)
{
return level.Value switch
@@ -164,4 +142,3 @@ internal sealed class BaseValueInfo : ObservableObject
}
}
}
}

View File

@@ -31,7 +31,7 @@ internal sealed class CookBonusView
/// <returns>新的料理奖励视图</returns>
public static CookBonusView? Create(CookBonus? cookBonus, Dictionary<MaterialId, Material> idMaterialMap)
{
if (cookBonus == null)
if (cookBonus is null)
{
return null;
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
namespace Snap.Hutao.ViewModel.Wiki;
@@ -23,6 +24,11 @@ internal sealed class PropertyCurveValue
Value = value;
}
public PropertyCurveValue(FightProperty property, Dictionary<FightProperty, GrowCurveType> growCurve, BaseValue baseValue)
: this(property, growCurve.GetValueOrDefault(property), baseValue.GetValue(property))
{
}
/// <summary>
/// 战斗属性值
/// </summary>

View File

@@ -2,11 +2,9 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI;
using Microsoft.Extensions.Primitives;
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Intrinsic.Immutable;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Item;
@@ -17,15 +15,14 @@ using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.ViewModel.Complex;
using Snap.Hutao.Web.Response;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using CalcAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
using CalcClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
using CalcConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
using CalcItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
using CalcItemHelper = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.ItemHelper;
using CalculateAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
using CalculateClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
using CalculateConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
using CalculateItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
using CalculateItemHelper = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.ItemHelper;
namespace Snap.Hutao.ViewModel.Wiki;
@@ -41,6 +38,8 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
private readonly IMetadataService metadataService;
private readonly ITaskContext taskContext;
private readonly IHutaoCache hutaoCache;
private readonly IInfoBarService infoBarService;
private readonly IUserService userService;
private AdvancedCollectionView? avatars;
private Avatar? selected;
@@ -78,11 +77,13 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
/// </summary>
public string? FilterText { get => filterText; set => SetProperty(ref filterText, value); }
/// <inheritdoc/>
protected override async Task OpenUIAsync()
protected override async ValueTask<bool> InitializeUIAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
if (!await metadataService.InitializeAsync().ConfigureAwait(false))
{
return false;
}
levelAvatarCurveMap = await metadataService.GetLevelToAvatarCurveMapAsync().ConfigureAwait(false);
promotes = await metadataService.GetAvatarPromotesAsync().ConfigureAwait(false);
@@ -98,52 +99,63 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
await taskContext.SwitchToMainThreadAsync();
Avatars = new AdvancedCollectionView(sorted, true);
Selected = Avatars.Cast<Avatar>().FirstOrDefault();
}
return true;
}
private async Task CombineComplexDataAsync(List<Avatar> avatars, Dictionary<MaterialId, Material> idMaterialMap)
private async ValueTask CombineComplexDataAsync(List<Avatar> avatars, Dictionary<MaterialId, Material> idMaterialMap)
{
if (await hutaoCache.InitializeForWikiAvatarViewModelAsync().ConfigureAwait(false))
if (!await hutaoCache.InitializeForWikiAvatarViewModelAsync().ConfigureAwait(false))
{
Dictionary<AvatarId, AvatarCollocationView> idCollocations = hutaoCache.AvatarCollocations!.ToDictionary(a => a.AvatarId);
return;
}
ArgumentNullException.ThrowIfNull(hutaoCache.AvatarCollocations);
foreach (Avatar avatar in avatars)
{
avatar.Collocation = idCollocations.GetValueOrDefault(avatar.Id);
avatar.Collocation = hutaoCache.AvatarCollocations.GetValueOrDefault(avatar.Id);
avatar.CookBonusView ??= CookBonusView.Create(avatar.FetterInfo.CookBonus, idMaterialMap);
avatar.CultivationItemsView ??= avatar.CultivationItems.SelectList(i => idMaterialMap.GetValueOrDefault(i, Material.Default)!);
}
avatar.CultivationItemsView ??= avatar.CultivationItems.SelectList(i => idMaterialMap.GetValueOrDefault(i, Material.Default));
}
}
[Command("CultivateCommand")]
private async Task CultivateAsync(Avatar? avatar)
{
if (avatar != null)
if (avatar is null)
{
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
IUserService userService = serviceProvider.GetRequiredService<IUserService>();
return;
}
if (userService.Current != null)
if (userService.Current is null)
{
infoBarService.Warning(SH.MustSelectUserAndUid);
return;
}
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
CalculableOptions options = new(avatar.ToCalculable(), null);
CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance<CultivatePromotionDeltaDialog>(options);
(bool isOk, CalcAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
(bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
if (isOk)
if (!isOk)
{
Response<CalcConsumption> consumptionResponse = await serviceProvider
.GetRequiredService<CalcClient>()
return;
}
Response<CalculateConsumption> consumptionResponse = await serviceProvider
.GetRequiredService<CalculateClient>()
.ComputeAsync(userService.Current.Entity, delta)
.ConfigureAwait(false);
if (consumptionResponse.IsOk())
if (!consumptionResponse.IsOk())
{
CalcConsumption consumption = consumptionResponse.Data;
return;
}
List<CalcItem> items = CalcItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume);
CalculateConsumption consumption = consumptionResponse.Data;
List<CalculateItem> items = CalculateItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume);
try
{
bool saved = await serviceProvider
@@ -165,50 +177,53 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
infoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
}
}
}
}
else
{
infoBarService.Warning(SH.MustSelectUserAndUid);
}
}
}
private void UpdateBaseValueInfo(Avatar? avatar)
{
if (avatar == null)
if (avatar is null)
{
BaseValueInfo = null;
return;
}
else
{
Dictionary<PromoteLevel, Promote> avatarPromoteMap = promotes!.Where(p => p.Id == avatar.PromoteId).ToDictionary(p => p.Level);
ArgumentNullException.ThrowIfNull(promotes);
Dictionary<PromoteLevel, Promote> avatarPromoteMap = promotes.Where(p => p.Id == avatar.PromoteId).ToDictionary(p => p.Level);
Dictionary<FightProperty, GrowCurveType> avatarGrowCurve = avatar.GrowCurves.ToDictionary(g => g.Type, g => g.Value);
FightProperty promoteProperty = avatarPromoteMap[0].AddProperties.Last().Type;
List<PropertyCurveValue> propertyCurveValues = new()
{
new(FightProperty.FIGHT_PROP_BASE_HP, avatarGrowCurve[FightProperty.FIGHT_PROP_BASE_HP], avatar.BaseValue.HpBase),
new(FightProperty.FIGHT_PROP_BASE_ATTACK, avatarGrowCurve[FightProperty.FIGHT_PROP_BASE_ATTACK], avatar.BaseValue.AttackBase),
new(FightProperty.FIGHT_PROP_BASE_DEFENSE, avatarGrowCurve[FightProperty.FIGHT_PROP_BASE_DEFENSE], avatar.BaseValue.DefenseBase),
new(promoteProperty, GrowCurveType.GROW_CURVE_NONE, 0),
new(FightProperty.FIGHT_PROP_BASE_HP, avatarGrowCurve, avatar.BaseValue),
new(FightProperty.FIGHT_PROP_BASE_ATTACK, avatarGrowCurve, avatar.BaseValue),
new(FightProperty.FIGHT_PROP_BASE_DEFENSE, avatarGrowCurve, avatar.BaseValue),
new(promoteProperty, avatarGrowCurve, avatar.BaseValue),
};
BaseValueInfo = new(avatar.MaxLevel, propertyCurveValues, levelAvatarCurveMap!, avatarPromoteMap);
}
ArgumentNullException.ThrowIfNull(levelAvatarCurveMap);
BaseValueInfo = new(avatar.MaxLevel, propertyCurveValues, levelAvatarCurveMap, avatarPromoteMap);
}
[Command("FilterCommand")]
private void ApplyFilter(string? input)
{
if (Avatars != null)
if (Avatars is null)
{
if (!string.IsNullOrWhiteSpace(input))
return;
}
if (string.IsNullOrWhiteSpace(input))
{
Avatars.Filter = default!;
return;
}
Avatars.Filter = AvatarFilter.Compile(input);
if (!Avatars.Contains(Selected))
if (Avatars.Contains(Selected))
{
return;
}
try
{
Avatars.MoveCurrentToFirst();
@@ -218,10 +233,3 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel
}
}
}
else
{
Avatars.Filter = null!;
}
}
}
}

View File

@@ -50,8 +50,7 @@ internal sealed partial class WikiMonsterViewModel : Abstraction.ViewModel
/// </summary>
public BaseValueInfo? BaseValueInfo { get => baseValueInfo; set => SetProperty(ref baseValueInfo, value); }
/// <inheritdoc/>
protected override async Task OpenUIAsync()
protected override async ValueTask<bool> InitializeUIAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
@@ -61,30 +60,33 @@ internal sealed partial class WikiMonsterViewModel : Abstraction.ViewModel
Dictionary<MaterialId, DisplayItem> idDisplayMap = await metadataService.GetIdToDisplayItemAndMaterialMapAsync().ConfigureAwait(false);
foreach (Monster monster in monsters)
{
monster.DropsView ??= monster.Drops?.SelectList(i => idDisplayMap.GetValueOrDefault(i)!);
monster.DropsView ??= monster.Drops?.SelectList(i => idDisplayMap.GetValueOrDefault(i, Material.Default));
}
List<Monster> ordered = monsters.OrderBy(m => m.Id.Value).ToList();
List<Monster> ordered = monsters.SortBy(m => m.Id.Value);
await taskContext.SwitchToMainThreadAsync();
Monsters = new AdvancedCollectionView(ordered, true);
Selected = Monsters.Cast<Monster>().FirstOrDefault();
return true;
}
return false;
}
private void UpdateBaseValueInfo(Monster? monster)
{
if (monster == null)
if (monster is null)
{
BaseValueInfo = null;
}
else
{
List<PropertyCurveValue> propertyCurveValues = monster.GrowCurves
.Select(curveInfo => new PropertyCurveValue(curveInfo.Type, curveInfo.Value, monster.BaseValue.GetValue(curveInfo.Type)))
.ToList();
.SelectList(curveInfo => new PropertyCurveValue(curveInfo.Type, curveInfo.Value, monster.BaseValue.GetValue(curveInfo.Type)));
BaseValueInfo = new(100, propertyCurveValues, levelMonsterCurveMap!);
ArgumentNullException.ThrowIfNull(levelMonsterCurveMap);
BaseValueInfo = new(Monster.MaxLevel, propertyCurveValues, levelMonsterCurveMap);
}
}
}

View File

@@ -14,13 +14,12 @@ using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.ViewModel.Complex;
using Snap.Hutao.Web.Response;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using CalcAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
using CalcClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
using CalcConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
using CalculateAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
using CalculateClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
using CalculateConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
namespace Snap.Hutao.ViewModel.Wiki;
@@ -31,16 +30,12 @@ namespace Snap.Hutao.ViewModel.Wiki;
[Injection(InjectAs.Scoped)]
internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
{
private static readonly List<WeaponId> SkippedWeapons = new()
{
12304, 14306, 15306, 13304, // 石英大剑, 琥珀玥, 黑檀弓, 「旗杆」
11419, 11420, 11421, // 「一心传」名刀
};
private readonly ITaskContext taskContext;
private readonly IServiceProvider serviceProvider;
private readonly IMetadataService metadataService;
private readonly IHutaoCache hutaoCache;
private readonly IInfoBarService infoBarService;
private readonly IUserService userService;
private AdvancedCollectionView? weapons;
private Weapon? selected;
@@ -88,7 +83,6 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
List<Weapon> weapons = await metadataService.GetWeaponsAsync().ConfigureAwait(false);
List<Weapon> sorted = weapons
.Where(weapon => !SkippedWeapons.Contains(weapon.Id))
.OrderByDescending(weapon => weapon.RankLevel)
.ThenBy(weapon => weapon.WeaponType)
.ThenByDescending(weapon => weapon.Id.Value)
@@ -103,42 +97,51 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
}
}
private async Task CombineWithWeaponCollocationsAsync(List<Weapon> weapons)
private async ValueTask CombineWithWeaponCollocationsAsync(List<Weapon> weapons)
{
if (await hutaoCache.InitializeForWikiWeaponViewModelAsync().ConfigureAwait(false))
{
Dictionary<WeaponId, WeaponCollocationView> idCollocations = hutaoCache.WeaponCollocations!.ToDictionary(a => a.WeaponId);
weapons.ForEach(w => w.Collocation = idCollocations.GetValueOrDefault(w.Id));
ArgumentNullException.ThrowIfNull(hutaoCache.WeaponCollocations);
weapons.ForEach(w => w.Collocation = hutaoCache.WeaponCollocations.GetValueOrDefault(w.Id));
}
}
[Command("CultivateCommand")]
private async Task CultivateAsync(Weapon? weapon)
{
if (weapon != null)
if (weapon is null)
{
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
IUserService userService = serviceProvider.GetRequiredService<IUserService>();
return;
}
if (userService.Current != null)
if (userService.Current is null)
{
infoBarService.Warning(SH.MustSelectUserAndUid);
return;
}
// ContentDialog must be created by main thread.
await taskContext.SwitchToMainThreadAsync();
CalculableOptions options = new(null, weapon.ToCalculable());
CultivatePromotionDeltaDialog dialog = serviceProvider.CreateInstance<CultivatePromotionDeltaDialog>(options);
(bool isOk, CalcAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
(bool isOk, CalculateAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false);
if (isOk)
if (!isOk)
{
Response<CalcConsumption> consumptionResponse = await serviceProvider
.GetRequiredService<CalcClient>()
return;
}
Response<CalculateConsumption> consumptionResponse = await serviceProvider
.GetRequiredService<CalculateClient>()
.ComputeAsync(userService.Current.Entity, delta)
.ConfigureAwait(false);
if (consumptionResponse.IsOk())
if (!consumptionResponse.IsOk())
{
CalcConsumption consumption = consumptionResponse.Data;
return;
}
CalculateConsumption consumption = consumptionResponse.Data;
try
{
bool saved = await serviceProvider
@@ -160,44 +163,45 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
infoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
}
}
}
}
else
{
infoBarService.Warning(SH.MustSelectUserAndUid);
}
}
}
private void UpdateBaseValueInfo(Weapon? weapon)
{
if (weapon == null)
if (weapon is null)
{
BaseValueInfo = null;
return;
}
else
{
Dictionary<PromoteLevel, Promote> weaponPromoteMap = promotes!.Where(p => p.Id == weapon.PromoteId).ToDictionary(p => p.Level);
ArgumentNullException.ThrowIfNull(promotes);
Dictionary<PromoteLevel, Promote> weaponPromoteMap = promotes.Where(p => p.Id == weapon.PromoteId).ToDictionary(p => p.Level);
List<PropertyCurveValue> propertyCurveValues = weapon.GrowCurves
.Select(curveInfo => new PropertyCurveValue(curveInfo.Type, curveInfo.Value, curveInfo.InitValue))
.ToList();
.SelectList(curveInfo => new PropertyCurveValue(curveInfo.Type, curveInfo.Value, curveInfo.InitValue));
BaseValueInfo = new(weapon.MaxLevel, propertyCurveValues, levelWeaponCurveMap!, weaponPromoteMap);
}
ArgumentNullException.ThrowIfNull(levelWeaponCurveMap);
BaseValueInfo = new(weapon.MaxLevel, propertyCurveValues, levelWeaponCurveMap, weaponPromoteMap);
}
[Command("FilterCommand")]
private void ApplyFilter(string? input)
{
if (Weapons != null)
if (Weapons is null)
{
if (!string.IsNullOrWhiteSpace(input))
return;
}
if (string.IsNullOrWhiteSpace(input))
{
Weapons.Filter = default!;
return;
}
Weapons.Filter = WeaponFilter.Compile(input);
if (!Weapons.Contains(Selected))
if (Weapons.Contains(Selected))
{
return;
}
try
{
Weapons.MoveCurrentToFirst();
@@ -207,10 +211,3 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel
}
}
}
else
{
Weapons.Filter = null!;
}
}
}
}