web bridge check point

This commit is contained in:
DismissedLight
2022-11-25 14:51:30 +08:00
parent ee4197a18a
commit 0dd79d4206
47 changed files with 1099 additions and 127 deletions

View File

@@ -30,8 +30,13 @@ public static class CastTo<TTo>
private static Func<TCachedFrom, TTo> Get()
{
// 参数表达式,表示 传入源类型
ParameterExpression param = Expression.Parameter(typeof(TCachedFrom));
// 一元转换 调用 相关类的显式或隐式转换运算符
UnaryExpression convert = Expression.ConvertChecked(param, typeof(TTo));
// 生成一个源类型入,目标类型出的 lamdba
return Expression.Lambda<Func<TCachedFrom, TTo>>(convert, param).Compile();
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Web.Hutao.Model;
using System.Text;
@@ -17,7 +18,7 @@ public class ComplexReliquarySet
/// </summary>
/// <param name="reliquarySetRate">圣遗物套装率</param>
/// <param name="idReliquarySetMap">圣遗物套装映射</param>
public ComplexReliquarySet(ItemRate<ReliquarySets, double> reliquarySetRate, Dictionary<int, Metadata.Reliquary.ReliquarySet> idReliquarySetMap)
public ComplexReliquarySet(ItemRate<ReliquarySets, double> reliquarySetRate, Dictionary<EquipAffixId, Metadata.Reliquary.ReliquarySet> idReliquarySetMap)
{
ReliquarySets sets = reliquarySetRate.Item;
@@ -43,7 +44,7 @@ public class ComplexReliquarySet
}
else
{
Name = "无圣遗物";
Name = "无圣遗物或散件";
}
Rate = $"{reliquarySetRate.Rate:P3}";

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Model.Binding.Hutao;
@@ -16,7 +17,7 @@ internal class ComplexTeamRank
/// </summary>
/// <param name="teamRank">队伍排行</param>
/// <param name="idAvatarMap">映射</param>
public ComplexTeamRank(TeamAppearance teamRank, Dictionary<int, Avatar> idAvatarMap)
public ComplexTeamRank(TeamAppearance teamRank, Dictionary<AvatarId, Avatar> idAvatarMap)
{
Floor = $"第 {teamRank.Floor} 层";
Up = teamRank.Up.Select(teamRate => new Team(teamRate, idAvatarMap)).ToList();

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Model.Binding.Hutao;
@@ -16,10 +17,10 @@ internal class Team : List<ComplexAvatar>
/// </summary>
/// <param name="team">队伍</param>
/// <param name="idAvatarMap">映射</param>
public Team(ItemRate<string, int> team, Dictionary<int, Avatar> idAvatarMap)
public Team(ItemRate<string, int> team, Dictionary<AvatarId, Avatar> idAvatarMap)
: base(4)
{
IEnumerable<int> ids = team.Item.Split(',').Select(i => int.Parse(i));
IEnumerable<int> ids = team.Item.Split(',').Select(int.Parse);
foreach (int id in ids)
{

View File

@@ -181,7 +181,7 @@ public class User : ObservableObject
if (cookieToken != null)
{
Cookie cookieTokenCookie = Cookie.Parse($"acount_id={Entity.Aid};cookie_token={cookieToken}");
Cookie cookieTokenCookie = Cookie.Parse($"account_id={Entity.Aid};cookie_token={cookieToken}");
Entity.CookieToken = cookieTokenCookie;
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Metadata.Reliquary;
/// <summary>
@@ -8,6 +10,11 @@ namespace Snap.Hutao.Model.Metadata.Reliquary;
/// </summary>
public class ReliquaryAffix : ReliquaryAffixBase
{
/// <summary>
/// Id
/// </summary>
public new ReliquaryAffixId Id { get; set; }
/// <summary>
/// 值
/// </summary>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Metadata.Reliquary;
@@ -13,7 +14,7 @@ public class ReliquaryAffixBase
/// <summary>
/// Id
/// </summary>
public int Id { get; set; }
public ReliquaryMainAffixId Id { get; set; }
/// <summary>
/// 战斗属性

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Metadata.Reliquary;
/// <summary>
@@ -16,7 +18,7 @@ public class ReliquarySet
/// <summary>
/// 装备被动Id
/// </summary>
public int EquipAffixId { get; set; }
public EquipAffixId EquipAffixId { get; set; }
/// <summary>
/// 套装名称

View File

@@ -6,9 +6,9 @@ using Snap.Hutao.Model.Primitive.Converter;
namespace Snap.Hutao.Model.Primitive;
/// <summary>
/// 角色Id
/// 8位 角色Id
/// </summary>
[JsonConverter(typeof(AvatarIdConverter))]
[JsonConverter(typeof(IdentityConverter<AvatarId>))]
public readonly struct AvatarId : IEquatable<AvatarId>
{
/// <summary>

View File

@@ -1,22 +0,0 @@
// 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);
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Convert;
namespace Snap.Hutao.Model.Primitive.Converter;
/// <summary>
/// Id 转换器
/// </summary>
/// <typeparam name="TWrapper">包装类型</typeparam>
internal class IdentityConverter<TWrapper> : JsonConverter<TWrapper>
where TWrapper : struct
{
/// <inheritdoc/>
public override TWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return CastTo<TWrapper>.From(reader.GetInt32());
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, TWrapper value, JsonSerializerOptions options)
{
writer.WriteNumberValue(CastTo<int>.From(value));
}
}

View File

@@ -1,22 +0,0 @@
// 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);
}
}

View 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>
/// 6位 装备属性Id
/// </summary>
[JsonConverter(typeof(IdentityConverter<EquipAffixId>))]
public readonly struct EquipAffixId : IEquatable<EquipAffixId>
{
/// <summary>
/// 值
/// </summary>
public readonly int Value;
/// <summary>
/// Initializes a new instance of the <see cref="EquipAffixId"/> struct.
/// </summary>
/// <param name="value">value</param>
public EquipAffixId(int value)
{
Value = value;
}
public static implicit operator int(EquipAffixId value)
{
return value.Value;
}
public static implicit operator EquipAffixId(int value)
{
return new(value);
}
public static bool operator ==(EquipAffixId left, EquipAffixId right)
{
return left.Value == right.Value;
}
public static bool operator !=(EquipAffixId left, EquipAffixId right)
{
return !(left == right);
}
/// <inheritdoc/>
public bool Equals(EquipAffixId other)
{
return Value == other.Value;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is EquipAffixId other && Equals(other);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return Value.GetHashCode();
}
}

View 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>
/// 7位 装备属性Id
/// </summary>
[JsonConverter(typeof(IdentityConverter<ExtendedEquipAffixId>))]
public readonly struct ExtendedEquipAffixId : IEquatable<ExtendedEquipAffixId>
{
/// <summary>
/// 值
/// </summary>
public readonly int Value;
/// <summary>
/// Initializes a new instance of the <see cref="ExtendedEquipAffixId"/> struct.
/// </summary>
/// <param name="value">value</param>
public ExtendedEquipAffixId(int value)
{
Value = value;
}
public static implicit operator int(ExtendedEquipAffixId value)
{
return value.Value;
}
public static implicit operator ExtendedEquipAffixId(int value)
{
return new(value);
}
public static bool operator ==(ExtendedEquipAffixId left, ExtendedEquipAffixId right)
{
return left.Value == right.Value;
}
public static bool operator !=(ExtendedEquipAffixId left, ExtendedEquipAffixId right)
{
return !(left == right);
}
/// <inheritdoc/>
public bool Equals(ExtendedEquipAffixId other)
{
return Value == other.Value;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is ExtendedEquipAffixId other && Equals(other);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return Value.GetHashCode();
}
}

View 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>
/// 6位 圣遗物副词条Id
/// </summary>
[JsonConverter(typeof(IdentityConverter<ReliquaryAffixId>))]
public readonly struct ReliquaryAffixId : IEquatable<ReliquaryAffixId>
{
/// <summary>
/// 值
/// </summary>
public readonly int Value;
/// <summary>
/// Initializes a new instance of the <see cref="ReliquaryAffixId"/> struct.
/// </summary>
/// <param name="value">value</param>
public ReliquaryAffixId(int value)
{
Value = value;
}
public static implicit operator int(ReliquaryAffixId value)
{
return value.Value;
}
public static implicit operator ReliquaryAffixId(int value)
{
return new(value);
}
public static bool operator ==(ReliquaryAffixId left, ReliquaryAffixId right)
{
return left.Value == right.Value;
}
public static bool operator !=(ReliquaryAffixId left, ReliquaryAffixId right)
{
return !(left == right);
}
/// <inheritdoc/>
public bool Equals(ReliquaryAffixId other)
{
return Value == other.Value;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is ReliquaryAffixId other && Equals(other);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return Value.GetHashCode();
}
}

View 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>
/// 5位 圣遗物主属性Id
/// </summary>
[JsonConverter(typeof(IdentityConverter<ReliquaryMainAffixId>))]
public readonly struct ReliquaryMainAffixId : IEquatable<ReliquaryMainAffixId>
{
/// <summary>
/// 值
/// </summary>
public readonly int Value;
/// <summary>
/// Initializes a new instance of the <see cref="ReliquaryMainAffixId"/> struct.
/// </summary>
/// <param name="value">value</param>
public ReliquaryMainAffixId(int value)
{
Value = value;
}
public static implicit operator int(ReliquaryMainAffixId value)
{
return value.Value;
}
public static implicit operator ReliquaryMainAffixId(int value)
{
return new(value);
}
public static bool operator ==(ReliquaryMainAffixId left, ReliquaryMainAffixId right)
{
return left.Value == right.Value;
}
public static bool operator !=(ReliquaryMainAffixId left, ReliquaryMainAffixId right)
{
return !(left == right);
}
/// <inheritdoc/>
public bool Equals(ReliquaryMainAffixId other)
{
return Value == other.Value;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is ReliquaryMainAffixId other && Equals(other);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return Value.GetHashCode();
}
}

View File

@@ -6,9 +6,9 @@ using Snap.Hutao.Model.Primitive.Converter;
namespace Snap.Hutao.Model.Primitive;
/// <summary>
/// 武器Id
/// 5位 武器Id
/// </summary>
[JsonConverter(typeof(WeaponIdConverter))]
[JsonConverter(typeof(IdentityConverter<WeaponId>))]
public readonly struct WeaponId : IEquatable<WeaponId>
{
/// <summary>

View File

@@ -4,6 +4,7 @@ WM_NCRBUTTONDOWN
WM_NCRBUTTONUP
// Type definition
HRESULT
MINMAXINFO
// Comctl32

View File

@@ -6,6 +6,7 @@ using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Web.Enka.Model;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
@@ -22,10 +23,10 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// </summary>
internal class SummaryAvatarFactory
{
private readonly Dictionary<int, MetadataAvatar> idAvatarMap;
private readonly Dictionary<int, FightProperty> idRelicMainPropMap;
private readonly Dictionary<int, ReliquaryAffix> idReliquaryAffixMap;
private readonly Dictionary<int, MetadataWeapon> idWeaponMap;
private readonly Dictionary<AvatarId, MetadataAvatar> idAvatarMap;
private readonly Dictionary<WeaponId, MetadataWeapon> idWeaponMap;
private readonly Dictionary<ReliquaryMainAffixId, FightProperty> idRelicMainPropMap;
private readonly Dictionary<ReliquaryAffixId, ReliquaryAffix> idReliquaryAffixMap;
private readonly List<ReliquaryLevel> reliqueryLevels;
private readonly List<MetadataReliquary> reliquaries;
@@ -42,18 +43,18 @@ internal class SummaryAvatarFactory
/// <param name="reliquaries">圣遗物</param>
/// <param name="avatarInfo">角色信息</param>
public SummaryAvatarFactory(
Dictionary<int, MetadataAvatar> idAvatarMap,
Dictionary<int, MetadataWeapon> idWeaponMap,
Dictionary<int, FightProperty> idRelicMainPropMap,
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap,
Dictionary<AvatarId, MetadataAvatar> idAvatarMap,
Dictionary<WeaponId, MetadataWeapon> idWeaponMap,
Dictionary<ReliquaryMainAffixId, FightProperty> idRelicMainPropMap,
Dictionary<ReliquaryAffixId, ReliquaryAffix> idReliquaryAffixMap,
List<ReliquaryLevel> reliqueryLevels,
List<MetadataReliquary> reliquaries,
ModelAvatarInfo avatarInfo)
{
this.idAvatarMap = idAvatarMap;
this.idWeaponMap = idWeaponMap;
this.idRelicMainPropMap = idRelicMainPropMap;
this.idReliquaryAffixMap = idReliquaryAffixMap;
this.idWeaponMap = idWeaponMap;
this.reliqueryLevels = reliqueryLevels;
this.reliquaries = reliquaries;
this.avatarInfo = avatarInfo;

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Metadata;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
@@ -33,10 +34,10 @@ internal class SummaryFactory : ISummaryFactory
/// <inheritdoc/>
public async Task<Summary> CreateAsync(ModelPlayerInfo playerInfo, IEnumerable<ModelAvatarInfo> avatarInfos, CancellationToken token)
{
Dictionary<int, MetadataAvatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
Dictionary<int, MetadataWeapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
Dictionary<int, FightProperty> idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync(token).ConfigureAwait(false);
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync(token).ConfigureAwait(false);
Dictionary<AvatarId, MetadataAvatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
Dictionary<WeaponId, MetadataWeapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
Dictionary<ReliquaryMainAffixId, FightProperty> idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync(token).ConfigureAwait(false);
Dictionary<ReliquaryAffixId, ReliquaryAffix> idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync(token).ConfigureAwait(false);
List<ReliquaryLevel> reliqueryLevels = await metadataService.GetReliquaryLevelsAsync(token).ConfigureAwait(false);
List<MetadataReliquary> reliquaries = await metadataService.GetReliquariesAsync(token).ConfigureAwait(false);

View File

@@ -5,6 +5,7 @@ using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon;
@@ -18,10 +19,10 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// </summary>
internal class SummaryFactoryImplementation
{
private readonly Dictionary<int, MetadataAvatar> idAvatarMap;
private readonly Dictionary<int, FightProperty> idRelicMainPropMap;
private readonly Dictionary<int, MetadataWeapon> idWeaponMap;
private readonly Dictionary<int, ReliquaryAffix> idReliquaryAffixMap;
private readonly Dictionary<AvatarId, MetadataAvatar> idAvatarMap;
private readonly Dictionary<WeaponId, MetadataWeapon> idWeaponMap;
private readonly Dictionary<ReliquaryMainAffixId, FightProperty> idRelicMainPropMap;
private readonly Dictionary<ReliquaryAffixId, ReliquaryAffix> idReliquaryAffixMap;
private readonly List<ReliquaryLevel> reliqueryLevels;
private readonly List<MetadataReliquary> reliquaries;
@@ -35,10 +36,10 @@ internal class SummaryFactoryImplementation
/// <param name="reliqueryLevels">圣遗物主属性等级</param>
/// <param name="reliquaries">圣遗物</param>
public SummaryFactoryImplementation(
Dictionary<int, MetadataAvatar> idAvatarMap,
Dictionary<int, MetadataWeapon> idWeaponMap,
Dictionary<int, FightProperty> idRelicMainPropMap,
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap,
Dictionary<AvatarId, MetadataAvatar> idAvatarMap,
Dictionary<WeaponId, MetadataWeapon> idWeaponMap,
Dictionary<ReliquaryMainAffixId, FightProperty> idRelicMainPropMap,
Dictionary<ReliquaryAffixId, ReliquaryAffix> idReliquaryAffixMap,
List<ReliquaryLevel> reliqueryLevels,
List<MetadataReliquary> reliquaries)
{

View File

@@ -6,6 +6,7 @@ using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Web.Enka.Model;
using System.Runtime.InteropServices;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
@@ -20,8 +21,8 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// </summary>
internal class SummaryReliquaryFactory
{
private readonly Dictionary<int, MetadataReliquaryAffix> idReliquaryAffixMap;
private readonly Dictionary<int, FightProperty> idRelicMainPropMap;
private readonly Dictionary<ReliquaryAffixId, MetadataReliquaryAffix> idReliquaryAffixMap;
private readonly Dictionary<ReliquaryMainAffixId, FightProperty> idRelicMainPropMap;
private readonly List<ReliquaryLevel> reliqueryLevels;
private readonly List<MetadataReliquary> reliquaries;
@@ -38,8 +39,8 @@ internal class SummaryReliquaryFactory
/// <param name="avatarInfo">角色信息</param>
/// <param name="equip">圣遗物</param>
public SummaryReliquaryFactory(
Dictionary<int, MetadataReliquaryAffix> idReliquaryAffixMap,
Dictionary<int, FightProperty> idRelicMainPropMap,
Dictionary<ReliquaryAffixId, MetadataReliquaryAffix> idReliquaryAffixMap,
Dictionary<ReliquaryMainAffixId, FightProperty> idRelicMainPropMap,
List<ReliquaryLevel> reliqueryLevels,
List<MetadataReliquary> reliquaries,
ModelAvatarInfo avatarInfo,

View File

@@ -108,11 +108,8 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
entry.DailyNote = dailyNote;
// cache
Guid userId = entry.UserId;
await ThreadHelper.SwitchToMainThreadAsync();
// BUG: can found multiple entries sometime
entries?.Single(e => e.UserId == userId).UpdateDailyNote(dailyNote);
entries?.Single(e => e.UserId == entry.UserId && e.Uid == entry.Uid).UpdateDailyNote(dailyNote);
if (notify)
{

View File

@@ -10,6 +10,7 @@ using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Metadata;
namespace Snap.Hutao.Service.GachaLog.Factory;
@@ -37,8 +38,8 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
/// <inheritdoc/>
public async Task<GachaStatistics> CreateAsync(IEnumerable<GachaItem> items)
{
Dictionary<int, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
Dictionary<int, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
Dictionary<AvatarId, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
Dictionary<WeaponId, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
Dictionary<string, Avatar> nameAvatarMap = await metadataService.GetNameToAvatarMapAsync().ConfigureAwait(false);
Dictionary<string, Weapon> nameWeaponMap = await metadataService.GetNameToWeaponMapAsync().ConfigureAwait(false);
@@ -66,8 +67,8 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
private static GachaStatistics CreateCore(
IOrderedEnumerable<GachaItem> items,
List<HistoryWishBuilder> historyWishBuilders,
Dictionary<int, Avatar> avatarMap,
Dictionary<int, Weapon> weaponMap,
Dictionary<AvatarId, Avatar> avatarMap,
Dictionary<WeaponId, Weapon> weaponMap,
bool isEmptyHistoryWishVisible)
{
TypedWishSummaryBuilder permanentWishBuilder = new("奔行世间", TypedWishSummaryBuilder.PermanentWish, 90, 10);

View File

@@ -13,6 +13,7 @@ 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.Model.Primitive;
using Snap.Hutao.Service.GachaLog.Factory;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
@@ -52,8 +53,8 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
private Dictionary<string, Model.Metadata.Avatar.Avatar>? nameAvatarMap;
private Dictionary<string, Model.Metadata.Weapon.Weapon>? nameWeaponMap;
private Dictionary<int, Model.Metadata.Avatar.Avatar>? idAvatarMap;
private Dictionary<int, Model.Metadata.Weapon.Weapon>? idWeaponMap;
private Dictionary<AvatarId, Model.Metadata.Avatar.Avatar>? idAvatarMap;
private Dictionary<WeaponId, Model.Metadata.Weapon.Weapon>? idWeaponMap;
private ObservableCollection<GachaArchive>? archiveCollection;
/// <summary>

View File

@@ -5,6 +5,7 @@ using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Hutao.Model;
@@ -19,7 +20,7 @@ internal class HutaoCache : IHutaoCache
private readonly IHutaoService hutaoService;
private readonly IMetadataService metadataService;
private Dictionary<int, Avatar>? idAvatarExtendedMap;
private Dictionary<AvatarId, Avatar>? idAvatarExtendedMap;
/// <summary>
/// 构造一个新的胡桃 API 缓存
@@ -55,7 +56,7 @@ internal class HutaoCache : IHutaoCache
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<int, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
Task avatarAppearanceRankTask = AvatarAppearanceRankAsync(idAvatarMap);
Task avatarUsageRank = AvatarUsageRanksAsync(idAvatarMap);
@@ -82,9 +83,9 @@ internal class HutaoCache : IHutaoCache
{
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);
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
Dictionary<WeaponId, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
Dictionary<EquipAffixId, Model.Metadata.Reliquary.ReliquarySet> idReliquarySetMap = await metadataService.GetEquipAffixIdToReliquarySetMapAsync().ConfigureAwait(false);
// AvatarCollocation
List<AvatarCollocation> avatarCollocationsRaw = await hutaoService.GetAvatarCollocationsAsync().ConfigureAwait(false);
@@ -105,11 +106,11 @@ internal class HutaoCache : IHutaoCache
return false;
}
private async ValueTask<Dictionary<int, Avatar>> GetIdAvatarMapExtendedAsync()
private async ValueTask<Dictionary<AvatarId, Avatar>> GetIdAvatarMapExtendedAsync()
{
if (idAvatarExtendedMap == null)
{
Dictionary<int, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
Dictionary<AvatarId, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
idAvatarExtendedMap = new(idAvatarMap)
{
[AvatarIds.PlayerBoy] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
@@ -120,7 +121,7 @@ internal class HutaoCache : IHutaoCache
return idAvatarExtendedMap;
}
private async Task AvatarAppearanceRankAsync(Dictionary<int, Avatar> idAvatarMap)
private async Task AvatarAppearanceRankAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<AvatarAppearanceRank> avatarAppearanceRanksRaw = await hutaoService.GetAvatarAppearanceRanksAsync().ConfigureAwait(false);
AvatarAppearanceRanks = avatarAppearanceRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
@@ -130,7 +131,7 @@ internal class HutaoCache : IHutaoCache
}).ToList();
}
private async Task AvatarUsageRanksAsync(Dictionary<int, Avatar> idAvatarMap)
private async Task AvatarUsageRanksAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<AvatarUsageRank> avatarUsageRanksRaw = await hutaoService.GetAvatarUsageRanksAsync().ConfigureAwait(false);
AvatarUsageRanks = avatarUsageRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
@@ -140,7 +141,7 @@ internal class HutaoCache : IHutaoCache
}).ToList();
}
private async Task AvatarConstellationInfosAsync(Dictionary<int, Avatar> idAvatarMap)
private async Task AvatarConstellationInfosAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<AvatarConstellationInfo> avatarConstellationInfosRaw = await hutaoService.GetAvatarConstellationInfosAsync().ConfigureAwait(false);
AvatarConstellationInfos = avatarConstellationInfosRaw.OrderBy(i => i.HoldingRate).Select(info =>
@@ -149,7 +150,7 @@ internal class HutaoCache : IHutaoCache
}).ToList();
}
private async Task TeamAppearancesAsync(Dictionary<int, Avatar> idAvatarMap)
private async Task TeamAppearancesAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<TeamAppearance> teamAppearancesRaw = await hutaoService.GetTeamAppearancesAsync().ConfigureAwait(false);
TeamAppearances = teamAppearancesRaw.OrderByDescending(t => t.Floor).Select(team => new ComplexTeamRank(team, idAvatarMap)).ToList();

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service;
@@ -117,6 +118,7 @@ internal class InfoBarService : IInfoBarService
Title = title,
Message = message,
IsOpen = true,
Transitions = new() { new AddDeleteThemeTransition() },
};
infoBar.Closed += OnInfoBarClosed;

View File

@@ -7,6 +7,7 @@ using Snap.Hutao.Model.Metadata.Achievement;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Service.Metadata;
@@ -47,7 +48,7 @@ internal interface IMetadataService
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>装备被动Id到圣遗物套装的映射</returns>
ValueTask<Dictionary<int, ReliquarySet>> GetEquipAffixIdToReliquarySetMapAsync(CancellationToken token = default);
ValueTask<Dictionary<EquipAffixId, ReliquarySet>> GetEquipAffixIdToReliquarySetMapAsync(CancellationToken token = default);
/// <summary>
/// 异步获取卡池配置列表
@@ -61,28 +62,28 @@ internal interface IMetadataService
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>Id到角色的字典</returns>
ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default);
ValueTask<Dictionary<AvatarId, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default);
/// <summary>
/// 异步获取ID到圣遗物副词条的字典
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>字典</returns>
ValueTask<Dictionary<int, ReliquaryAffix>> GetIdReliquaryAffixMapAsync(CancellationToken token = default);
ValueTask<Dictionary<ReliquaryAffixId, ReliquaryAffix>> GetIdReliquaryAffixMapAsync(CancellationToken token = default);
/// <summary>
/// 异步获取圣遗物主词条Id与属性的字典
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>字典</returns>
ValueTask<Dictionary<int, FightProperty>> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default);
ValueTask<Dictionary<ReliquaryMainAffixId, FightProperty>> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default);
/// <summary>
/// 异步获取ID到武器的字典
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>Id到武器的字典</returns>
ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default);
ValueTask<Dictionary<WeaponId, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default);
/// <summary>
/// 异步获取名称到角色的字典

View File

@@ -5,6 +5,7 @@ using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Service.Metadata;
@@ -14,33 +15,33 @@ namespace Snap.Hutao.Service.Metadata;
internal partial class MetadataService
{
/// <inheritdoc/>
public ValueTask<Dictionary<int, ReliquarySet>> GetEquipAffixIdToReliquarySetMapAsync(CancellationToken token = default)
public ValueTask<Dictionary<EquipAffixId, ReliquarySet>> GetEquipAffixIdToReliquarySetMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, ReliquarySet>("ReliquarySet", r => r.EquipAffixId, token);
return FromCacheAsDictionaryAsync<EquipAffixId, ReliquarySet>("ReliquarySet", r => r.EquipAffixId, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default)
public ValueTask<Dictionary<AvatarId, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, Avatar>("Avatar", a => a.Id, token);
return FromCacheAsDictionaryAsync<AvatarId, Avatar>("Avatar", a => a.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, ReliquaryAffix>> GetIdReliquaryAffixMapAsync(CancellationToken token = default)
public ValueTask<Dictionary<ReliquaryAffixId, ReliquaryAffix>> GetIdReliquaryAffixMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, ReliquaryAffix>("ReliquaryAffix", a => a.Id, token);
return FromCacheAsDictionaryAsync<ReliquaryAffixId, ReliquaryAffix>("ReliquaryAffix", a => a.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, FightProperty>> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default)
public ValueTask<Dictionary<ReliquaryMainAffixId, FightProperty>> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, FightProperty, ReliquaryAffixBase>("ReliquaryMainAffix", r => r.Id, r => r.Type, token);
return FromCacheAsDictionaryAsync<ReliquaryMainAffixId, FightProperty, ReliquaryAffixBase>("ReliquaryMainAffix", r => r.Id, r => r.Type, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default)
public ValueTask<Dictionary<WeaponId, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, Weapon>("Weapon", w => w.Id, token);
return FromCacheAsDictionaryAsync<WeaponId, Weapon>("Weapon", w => w.Id, token);
}
/// <inheritdoc/>

View File

@@ -71,6 +71,7 @@
<None Remove="View\Dialog\GachaLogUrlDialog.xaml" />
<None Remove="View\Dialog\GameAccountNameDialog.xaml" />
<None Remove="View\Dialog\LoginMihoyoBBSDialog.xaml" />
<None Remove="View\Dialog\SignInWebViewDialog.xaml" />
<None Remove="View\Dialog\UserDialog.xaml" />
<None Remove="View\MainView.xaml" />
<None Remove="View\Page\AchievementPage.xaml" />
@@ -145,7 +146,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25231-preview" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25247-preview" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221116.1" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.435">
<PrivateAssets>all</PrivateAssets>
@@ -169,6 +170,11 @@
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\SignInWebViewDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\LoginMihoyoBBSDialog.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -0,0 +1,13 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.SignInWebViewDialog"
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"
Style="{StaticResource DefaultContentDialogStyle}">
<Grid Loaded="OnGridLoaded" Height="600" Width="380">
<WebView2 Name="WebView"/>
</Grid>
</ContentDialog>

View File

@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Bridge;
using Snap.Hutao.Web.Bridge.Model;
using Snap.Hutao.Web.Bridge.Model.Event;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Windows.UI.Popups;
namespace Snap.Hutao.View.Dialog;
/// <summary>
/// ǩ<><C7A9><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3>ͼ<EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>
/// </summary>
public sealed partial class SignInWebViewDialog : ContentDialog
{
/// <summary>
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µ<EFBFBD>ǩ<EFBFBD><C7A9><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3>ͼ<EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>
/// </summary>
/// <param name="window"><3E><><EFBFBD><EFBFBD></param>
public SignInWebViewDialog(MainWindow window)
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
}
private void OnGridLoaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
InitializeAsync().SafeForget();
}
private async Task InitializeAsync()
{
await WebView.EnsureCoreWebView2Async();
CoreWebView2 coreWebView2 = WebView.CoreWebView2;
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
ILogger<MiHoYoJsBridge> logger = Ioc.Default.GetRequiredService<ILogger<MiHoYoJsBridge>>();
User? user = userService.Current;
coreWebView2.SetCookie(user?.CookieToken, user?.Ltoken);
coreWebView2.SetMobileUserAgent();
coreWebView2.InitializeBridge(logger, false)
.Register<JsEventClosePage>(e => Hide())
.Register<JsEventRealPersonValidation>(e => infoBarService.Information("<22>޷<EFBFBD>ʹ<EFBFBD>ô˹<C3B4><CBB9><EFBFBD>", "<22><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><CAB5><EFBFBD><EFBFBD>֤<EFBFBD><D6A4><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"))
.Register<JsEventGetStatusBarHeight>(s => s.Callback(result => result.Data["statusBarHeight"] = 0))
.Register<JsEventGetDynamicSecretV1>(s => s.Callback(result =>
{
result.Data["DS"] = DynamicSecretHandler.GetDynamicSecret(nameof(SaltType.K2), nameof(DynamicSecretVersion.Gen1), includeChars: true);
}))
.Register<JsEventGetUserInfo>(s => s.Callback(result =>
{
result.Data["id"] = "111";
result.Data["gender"] = 0;
result.Data["nickname"] = "222";
result.Data["introduce"] = "333";
result.Data["avatar_url"] = "https://img-static.mihoyo.com/communityweb/upload/52de23f1b1a060e4ccaa8b24c1305dd9.png";
}));
coreWebView2.OpenDevToolsWindow();
coreWebView2.Navigate("https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?act_id=e202009291139501");
}
}

View File

@@ -133,11 +133,6 @@
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</StackPanel.Resources>
<StackPanel.Transitions>
<TransitionCollection>
<AddDeleteThemeTransition/>
</TransitionCollection>
</StackPanel.Transitions>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -194,6 +194,13 @@
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBlock
Margin="10,6,0,6"
Style="{StaticResource BaseTextBlockStyle}"
Text="Webview"/>
<CommandBar DefaultLabelPosition="Right">
<AppBarButton Label="米游社签到" Icon="{shcm:FontIcon Glyph=&#xEB41;}" Command="{Binding ShowSignInWebViewDialogCommand}"/>
</CommandBar>
<TextBlock
Margin="10,6,0,6"
Style="{StaticResource BaseTextBlockStyle}"

View File

@@ -46,6 +46,7 @@ internal class UserViewModel : ObservableObject
LoginMihoyoUserCommand = new RelayCommand(LoginMihoyoUser);
RemoveUserCommand = asyncRelayCommandFactory.Create<User>(RemoveUserAsync);
CopyCookieCommand = new RelayCommand<User>(CopyCookie);
ShowSignInWebViewDialogCommand = asyncRelayCommandFactory.Create(ShowSignInWebViewDialogAsync);
}
/// <summary>
@@ -83,6 +84,8 @@ internal class UserViewModel : ObservableObject
/// </summary>
public ICommand LoginMihoyoUserCommand { get; }
public ICommand ShowSignInWebViewDialogCommand { get; }
/// <summary>
/// 移除用户命令
/// </summary>
@@ -165,4 +168,10 @@ internal class UserViewModel : ObservableObject
infoBarService.Error(e);
}
}
private async Task ShowSignInWebViewDialogAsync()
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
await new SignInWebViewDialog(mainWindow).ShowAsync();
}
}

View File

@@ -226,7 +226,7 @@ internal static class ApiEndpoints
/// <summary>
/// 获取V2Stoken
/// </summary>
public const string AccountGetSTokenByOldtoken = $"{PassportApi}/account/ma-cn-session/app/getTokenBySToken";
public const string AccountGetSTokenByOldToken = $"{PassportApi}/account/ma-cn-session/app/getTokenBySToken";
/// <summary>
/// 登录

View File

@@ -0,0 +1,107 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Web.Hoyolab;
using WinRT;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// Bridge 拓展
/// </summary>
public static class BridgeExtension
{
private const string InitializeJsInterfaceScript = """
let c = {};
c.postMessage = str => chrome.webview.hostObjects.MiHoYoJsBridge.OnMessage(str);
c.closePage = () => c.postMessage('{"method":"closePage"}');
window.MiHoYoJSInterface = c;
""";
private const string HideScrollBarScript = """
let st = document.createElement('style');
st.innerHTML = '::-webkit-scrollbar{display:none}';
document.querySelector('body').appendChild(st);
""";
/// <summary>
/// 设置 移动端UA
/// </summary>
/// <param name="webView">webview2</param>
public static void SetMobileUserAgent(this CoreWebView2 webView)
{
webView.Settings.UserAgent = "Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/106.0.5249.126 Mobile Safari/537.36 miHoYoBBS/2.41.0";
}
/// <summary>
/// 初始化调用桥
/// </summary>
/// <param name="webView">webview2</param>
/// <param name="logger">日志器</param>
/// <param name="checkHost">检查主机</param>
/// <returns>初始化后的调用桥</returns>
public static MiHoYoJsBridge InitializeBridge(this CoreWebView2 webView, ILogger<MiHoYoJsBridge> logger, bool checkHost = true)
{
MiHoYoJsBridge bridge = new(webView, logger);
var result = webView.As<ICoreWebView2Interop>().AddHostObjectToScript("MiHoYoJsBridge", bridge);
webView.DOMContentLoaded += OnDOMContentLoaded;
webView.NavigationStarting += (coreWebView2, args) => OnWebViewNavigationStarting(coreWebView2, args, checkHost);
return bridge;
}
/// <summary>
/// 设置WebView2的Cookie
/// </summary>
/// <param name="webView">webview2</param>
/// <param name="cookieToken">CookieToken</param>
/// <param name="ltoken">Ltoken</param>
/// <param name="stoken">Stoken</param>
/// <returns>链式调用的WebView2</returns>
public static CoreWebView2 SetCookie(this CoreWebView2 webView, Cookie? cookieToken = null, Cookie? ltoken = null, Cookie? stoken = null)
{
CoreWebView2CookieManager cookieManager = webView.CookieManager;
if (cookieToken != null)
{
cookieManager.AddMihoyoCookie("account_id", cookieToken).AddMihoyoCookie("cookie_token", cookieToken);
}
if (ltoken != null)
{
cookieManager.AddMihoyoCookie("ltuid", ltoken).AddMihoyoCookie("ltoken", ltoken);
}
if (stoken != null)
{
cookieManager.AddMihoyoCookie("stuid", stoken).AddMihoyoCookie("stoken", stoken);
}
return webView;
}
private static CoreWebView2CookieManager AddMihoyoCookie(this CoreWebView2CookieManager manager, string name, Cookie cookie)
{
manager.AddOrUpdateCookie(manager.CreateCookie(name, cookie[name], ".mihoyo.com", "/"));
return manager;
}
[SuppressMessage("", "VSTHRD100")]
private static async void OnDOMContentLoaded(CoreWebView2 coreWebView2, CoreWebView2DOMContentLoadedEventArgs args)
{
string result = await coreWebView2.ExecuteScriptAsync(HideScrollBarScript);
_ = result;
}
[SuppressMessage("", "VSTHRD100")]
private static async void OnWebViewNavigationStarting(CoreWebView2 coreWebView2, CoreWebView2NavigationStartingEventArgs args, bool checkHost)
{
if (!checkHost || new Uri(args.Uri).Host.EndsWith("mihoyo.com"))
{
string result = await coreWebView2.ExecuteScriptAsync(InitializeJsInterfaceScript);
_ = result;
}
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// ICoreWebView2Interop
/// </summary>
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("912b34a7-d10b-49c4-af18-7cb7e604e01a")]
public interface ICoreWebView2Interop
{
/// <summary>
/// Add the provided host object to script running in the WebView with the specified name.
/// </summary>
/// <param name="name">名称</param>
/// <param name="obj">对象</param>
/// <returns>结果</returns>
HRESULT AddHostObjectToScript([In] string name, [In] ref object obj);
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// 调用桥暴露的COM接口
/// </summary>
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IMiHoYoJsBridge
{
/// <summary>
/// 消息发生时调用
/// </summary>
/// <param name="str">消息</param>
void OnMessage(string str);
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Web.Bridge.Model;
using Snap.Hutao.Web.Bridge.Model.Event;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// 调用桥
/// </summary>
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
public sealed class MiHoYoJsBridge /*: IMiHoYoJsBridge*/
{
private readonly CoreWebView2 webView;
private readonly ILogger<MiHoYoJsBridge>? logger;
private readonly Dictionary<string, string> jsWebInvokeTypeCache = new();
private readonly Dictionary<string, Action<JsParam>> callbackHandlers = new();
/// <summary>
/// 构造一个新的调用桥
/// </summary>
/// <param name="webView">webview2</param>
/// <param name="logger">日志器</param>
internal MiHoYoJsBridge(CoreWebView2 webView, ILogger<MiHoYoJsBridge>? logger = null)
{
this.webView = webView;
this.logger = logger;
}
/// <summary>
/// 消息发生时调用
/// </summary>
/// <param name="message">消息</param>
public void OnMessage(string message)
{
logger?.LogInformation("[OnMessage] {message}", message);
JsParam p = JsonSerializer.Deserialize<JsParam>(message)!;
p.Bridge = this;
callbackHandlers.GetValueOrDefault(p.Method)?.Invoke(p);
}
/// <summary>
/// 调用JS回调
/// </summary>
/// <param name="callbackName">回调名称</param>
/// <param name="payload">传输的数据</param>
/// <returns>执行结果</returns>
public Task<string> InvokeJsCallbackAsync(string callbackName, string? payload = null)
{
if (string.IsNullOrEmpty(callbackName))
{
return Task.FromResult(string.Empty);
}
string dataStr = payload == null ? string.Empty : $", {payload}";
string js = $"javascript:mhyWebBridge(\"{callbackName}\"{dataStr})";
logger?.LogInformation("[InvokeJsCallback] {js}", js);
return webView.ExecuteScriptAsync(js).AsTask();
}
/// <summary>
/// 注册回调
/// </summary>
/// <typeparam name="T">回调类型</typeparam>
/// <param name="callback">回调</param>
/// <returns>桥</returns>
public MiHoYoJsBridge Register<T>(Action<JsParam> callback)
where T : notnull
{
callbackHandlers[GetCallbackName<T>()] = callback;
return this;
}
/// <summary>
/// 注册回调
/// </summary>
/// <typeparam name="T">回调类型</typeparam>
/// <param name="callback">回调</param>
/// <returns>桥</returns>
public MiHoYoJsBridge Register<T>(Action<JsParam, T> callback)
where T : notnull
{
callbackHandlers[GetCallbackName<T>()] = p => callback(p, p.Data.As<T>());
return this;
}
private string GetCallbackName<T>()
{
Type type = typeof(T);
string invokeName = type.GetCustomAttribute<WebInvokeAttribute>()?.Name
?? throw new ArgumentException("Type Callback not registered.");
return jsWebInvokeTypeCache[type.Name] = invokeName;
}
}

View File

@@ -0,0 +1,196 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Bridge.Model.Event;
/// <summary>
/// Web 调用
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class WebInvokeAttribute : Attribute
{
/// <summary>
/// 构造一个新的Web 调用特性
/// </summary>
/// <param name="name">函数名称</param>
public WebInvokeAttribute(string name)
{
Name = name;
}
/// <summary>
/// 调用函数名称
/// </summary>
public string Name { get; init; }
}
public class ButtonParam
{
[JsonPropertyName("title")]
public string Title { get; set; } = default!;
[JsonPropertyName("style")]
public string Style { get; set; } = default!;
}
public abstract class GenAuthKeyBase
{
[JsonPropertyName("game_biz")]
public string Biz { get; set; } = default!;
[JsonPropertyName("auth_appid")]
public string AppId { get; set; } = default!;
[JsonPropertyName("game_uid")]
public uint Uid { get; set; }
[JsonPropertyName("region")]
public string Region { get; set; } = default!;
}
[WebInvoke("closePage")]
public struct JsEventClosePage
{
}
[WebInvoke("configure_share")]
public class JsEventConfigureShare
{
[JsonPropertyName("enable")]
public bool Enable { get; set; }
}
[WebInvoke("genAppAuthKey")]
public class JsEventGenAppAuthKey
: GenAuthKeyBase
{
}
[WebInvoke("genAuthKey")]
public class JsEventGenAuthKey
: GenAuthKeyBase
{
}
[WebInvoke("getActionTicket")]
public class JsEventGetActionTicket
{
[JsonPropertyName("action_type")]
public string ActionType { get; set; } = default!;
}
[WebInvoke("getCookieToken")]
public class JsEventGetCookieToken
{
[JsonPropertyName("forceRefresh")]
public bool ForceRefresh { get; set; }
}
[WebInvoke("getDS")]
public struct JsEventGetDynamicSecretV1
{
}
[WebInvoke("getDS2")]
public class JsEventGetDynamicSecretV2
{
[JsonPropertyName("query")]
public Dictionary<string, string> Query { get; set; } = new();
[JsonPropertyName("body")]
public string Body { get; set; } = default!;
}
[WebInvoke("getNotificationSettings")]
public struct JsEventGetNotificationSettings
{
}
[WebInvoke("startRealnameAuth")]
public struct JsEventGetRealNameStatus
{
// guess
}
[WebInvoke("getHTTPRequestHeaders")]
public struct JsEventGetRequestHeader
{
}
[WebInvoke("getStatusBarHeight")]
public struct JsEventGetStatusBarHeight
{
// just zero
}
[WebInvoke("getUserInfo")]
public struct JsEventGetUserInfo
{
}
[WebInvoke("getCookieInfo")]
public struct JsEventGetWebLoginInfo
{
}
[WebInvoke("openSystemBrowser")]
public class JsEventOpenSystemBrowser
{
[JsonPropertyName("open_url")]
public string PageUrl { get; set; } = default!;
}
[WebInvoke("pushPage")]
public class JsEventPushPage
{
private string pageUrl = default!;
[JsonPropertyName("page")]
public string PageUrl
{
get => pageUrl;
set => SetPageUrl(value);
}
private void SetPageUrl(string value)
{
pageUrl = value.StartsWith("mihoyobbs")
? value.Replace("mihoyobbs://", "https://bbs.mihoyo.com/dby/").Replace("topic", "topicDetail")
: value;
}
}
[WebInvoke("startRealPersonValidation")]
public struct JsEventRealPersonValidation
{
}
[WebInvoke("saveLoginTicket")]
public class JsEventSaveLoginTicket
{
[JsonPropertyName("login_ticket")]
public string LoginTicket { get; set; } = default!;
}
[WebInvoke("showAlertDialog")]
public class JsEventShowAlertDialog
{
[JsonPropertyName("title")]
public string Title { get; set; } = default!;
[JsonPropertyName("message")]
public string Message { get; set; } = default!;
[JsonPropertyName("buttons")]
public List<ButtonParam> Buttons { get; set; } = new();
}
[WebInvoke("showToast")]
public class JsEventShowToast
{
[JsonPropertyName("toast")]
public string Text { get; set; } = default!;
[JsonPropertyName("type")]
public string Type { get; set; } = default!;
}

View File

@@ -0,0 +1,60 @@
namespace Snap.Hutao.Web.Bridge.Model;
/// <summary>
/// Js 参数
/// </summary>
public class JsParam
{
/// <summary>
/// 方法名称
/// </summary>
[JsonPropertyName("method")]
public string Method { get; set; } = string.Empty;
/// <summary>
/// 数据
/// </summary>
[JsonPropertyName("payload")]
public JsonElement Data { get; set; }
/// <summary>
/// 回调名称
/// </summary>
[JsonPropertyName("callback")]
public string CallbackName { get; set; } = string.Empty;
/// <summary>
/// 对应的调用桥
/// </summary>
internal MiHoYoJsBridge Bridge { get; set; } = null!;
/// <summary>
/// 执行回调
/// </summary>
/// <param name="resultFactory">结果工厂</param>
public void Callback(Func<JsResult>? resultFactory = null)
{
JsResult? result = resultFactory?.Invoke() ?? new();
Callback(result?.ToString());
}
/// <summary>
/// 执行回调
/// </summary>
/// <param name="resultModifier">结果工厂</param>
public void Callback(Action<JsResult> resultModifier)
{
JsResult result = new();
resultModifier(result);
Callback(result?.ToString());
}
/// <summary>
/// 执行回调
/// </summary>
/// <param name="result">结果</param>
public void Callback(string? result = null)
{
Bridge.InvokeJsCallbackAsync(CallbackName, result).GetAwaiter().GetResult();
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Snap.Hutao.Web.Bridge.Model;
/// <summary>
/// Js结果
/// </summary>
public class JsResult
{
/// <summary>
/// 代码
/// </summary>
[JsonPropertyName("retcode")]
public int Code { get; set; }
/// <summary>
/// 消息
/// </summary>
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
/// <summary>
/// 数据
/// </summary>
[JsonPropertyName("data")]
public Dictionary<string, object> Data { get; set; } = new();
/// <inheritdoc/>
public override string ToString()
{
return JsonSerializer.Serialize(this);
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Snap.Hutao.Web.Bridge.Model;
/// <summary>
/// JsonElement 拓展
/// </summary>
public static class JsonElementExtension
{
/// <summary>
/// 序列化到对应类型
/// </summary>
/// <typeparam name="T">对应类型</typeparam>
/// <param name="jsonElement">元素</param>
/// <returns>对应类型的实例</returns>
public static T As<T>(this JsonElement jsonElement)
where T : notnull
{
return jsonElement.Deserialize<T>()!;
}
}

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
[Injection(InjectAs.Transient)]
public class DynamicSecretHandler : DelegatingHandler
{
private const string RandomRange = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
private const string RandomRange = "abcdefghijklmnopqrstuvwxyz1234567890";
// https://github.com/UIGF-org/Hoyolab.Salt
private static readonly ImmutableDictionary<string, string> DynamicSecrets = new Dictionary<string, string>()
@@ -66,6 +66,36 @@ public class DynamicSecretHandler : DelegatingHandler
return await base.SendAsync(request, token).ConfigureAwait(false);
}
/// <summary>
/// 获取DS
/// </summary>
/// <param name="saltType">nameof <see cref="SaltType"/></param>
/// <param name="version">nameof <see cref="DynamicSecretVersion"/></param>
/// <param name="body">body</param>
/// <param name="query">query</param>
/// <param name="includeChars">是否需要字母</param>
/// <returns>DS</returns>
public static string GetDynamicSecret(string saltType, string version, string? body = null, string? query = null, bool includeChars = true)
{
string salt = DynamicSecrets[saltType];
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string r = includeChars ? GetRandomStringWithChars() : GetRandomStringNoChars();
string dsContent = $"salt={salt}&t={t}&r={r}";
if (version == nameof(DynamicSecretVersion.Gen2))
{
string[] queries = Uri.UnescapeDataString(query!).Split('?', 2);
string q = queries.Length == 2 ? string.Join('&', queries[1].Split('&').OrderBy(x => x)) : string.Empty;
dsContent = $"{dsContent}&b={body}&q={q}";
}
return Md5Convert.ToHexString(dsContent).ToLowerInvariant();
}
private static string GetRandomStringWithChars()
{
StringBuilder sb = new(6);

View File

@@ -69,7 +69,7 @@ internal class PassportClient2
HttpResponseMessage message = await httpClient
.SetHeader("Cookie", stokenV1.ToString())
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
.PostAsync(ApiEndpoints.AccountGetSTokenByOldtoken, null, token)
.PostAsync(ApiEndpoints.AccountGetSTokenByOldToken, null, token)
.ConfigureAwait(false);
Response<LoginResult>? resp = await message.Content

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
@@ -23,7 +25,7 @@ public class ReliquarySet
/// <summary>
/// Id
/// </summary>
public int EquipAffixId { get; }
public ExtendedEquipAffixId EquipAffixId { get; }
/// <summary>
/// 个数