add wiki weapon page

This commit is contained in:
DismissedLight
2022-11-29 18:16:59 +08:00
parent 8f273e69b5
commit e051787584
63 changed files with 1547 additions and 260 deletions

View File

@@ -1,4 +1,6 @@
[*.cs]
charset = utf-8-bom
[*.cs]
# SA1101: Prefix local calls with this
dotnet_diagnostic.SA1101.severity = none
@@ -76,31 +78,31 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.non_field_members.required_modifiers =
# 命名样式
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_diagnostic.SA1629.severity = none
dotnet_diagnostic.SA1642.severity = none
@@ -185,29 +187,29 @@ dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.style = 帕斯
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.类型.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.类型.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.类型.required_modifiers =
dotnet_naming_symbols.类型.required_modifiers =
dotnet_naming_symbols.非字段成员.applicable_kinds = property, event, method
dotnet_naming_symbols.非字段成员.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.非字段成员.required_modifiers =
dotnet_naming_symbols.非字段成员.required_modifiers =
# 命名样式
dotnet_naming_style.以_i_开始.required_prefix = I
dotnet_naming_style.以_i_开始.required_suffix =
dotnet_naming_style.以_i_开始.word_separator =
dotnet_naming_style.以_i_开始.required_suffix =
dotnet_naming_style.以_i_开始.word_separator =
dotnet_naming_style.以_i_开始.capitalization = pascal_case
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.required_prefix =
dotnet_naming_style.帕斯卡拼写法.required_suffix =
dotnet_naming_style.帕斯卡拼写法.word_separator =
dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case

View File

@@ -26,7 +26,6 @@ public class HttpClientGenerator : ISourceGenerator
private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute";
private const string DynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
private const string IgnoreSetCookieAttributeName = "Snap.Hutao.Web.Hoyolab.Annotation.IgnoreSetCookieAttribute";
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
@@ -54,7 +53,6 @@ public class HttpClientGenerator : ISourceGenerator
// This class is generated by Snap.Hutao.SourceGeneration
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Net.Http;
@@ -83,10 +81,7 @@ internal static partial class IocHttpClientConfiguration
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
{
lineBuilder
.Clear()
.Append("\r\n");
lineBuilder.Clear().Append(Environment.NewLine);
lineBuilder.Append(@" services.AddHttpClient<");
lineBuilder.Append($"{classSymbol.ToDisplayString()}>(");
@@ -139,11 +134,6 @@ internal static partial class IocHttpClientConfiguration
lineBuilder.Append(".AddHttpMessageHandler<DynamicSecretHandler>()");
}
if (classSymbol.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == IgnoreSetCookieAttributeName))
{
lineBuilder.Append(".AddHttpMessageHandler<IgnoreSetCookieHandler>()");
}
lineBuilder.Append(";");
lines.Add(lineBuilder.ToString());

View File

@@ -41,8 +41,10 @@
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
<shmmc:DescParamDescriptor x:Key="DescParamDescriptor"/>
<shmmc:ElementNameIconConverter x:Key="ElementNameIconConverter"/>
<shmmc:EquipIconConverter x:Key="EquipIconConverter"/>
<shmmc:GachaAvatarImgConverter x:Key="GachaAvatarImgConverter"/>
<shmmc:GachaAvatarIconConverter x:Key="GachaAvatarIconConverter"/>
<shmmc:GachaEquipIconConverter x:Key="GachaEquipIconConverter"/>
<shmmc:ItemIconConverter x:Key="ItemIconConverter"/>
<shmmc:PropertyInfoDescriptor x:Key="PropertyDescriptor"/>
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>

View File

@@ -10,8 +10,11 @@ namespace Snap.Hutao.Context.Database;
/// <summary>
/// 应用程序数据库上下文
/// </summary>
public class AppDbContext : DbContext
public sealed class AppDbContext : DbContext
{
private readonly Guid contextId;
private readonly ILogger<AppDbContext>? logger;
/// <summary>
/// 构造一个新的应用程序数据库上下文
/// </summary>
@@ -21,6 +24,19 @@ public class AppDbContext : DbContext
{
}
/// <summary>
/// 构造一个新的应用程序数据库上下文
/// </summary>
/// <param name="options">选项</param>
/// <param name="logger">日志器</param>
public AppDbContext(DbContextOptions<AppDbContext> options, ILogger<AppDbContext> logger)
: this(options)
{
contextId = Guid.NewGuid();
this.logger = logger;
logger.LogInformation("AppDbContext[{id}] created.", contextId);
}
/// <summary>
/// 设置
/// </summary>
@@ -66,6 +82,11 @@ public class AppDbContext : DbContext
/// </summary>
public DbSet<DailyNoteEntry> DailyNotes { get; set; } = default!;
/// <summary>
/// 对象缓存
/// </summary>
public DbSet<ObjectCacheEntry> ObjectCache { get; set; } = default!;
/// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>
@@ -76,6 +97,13 @@ public class AppDbContext : DbContext
return new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options);
}
/// <inheritdoc/>
public override void Dispose()
{
base.Dispose();
logger?.LogInformation("AppDbContext[{id}] disposed.", contextId);
}
/// <inheritdoc/>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{

View File

@@ -5,6 +5,7 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Logging;
using System.Collections.Immutable;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
@@ -236,10 +237,11 @@ public class ImageCache : IImageCache
}
}
}
else
else if (message.StatusCode == HttpStatusCode.TooManyRequests)
{
retryCount++;
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? RetryCountToDelay[retryCount];
logger.LogInformation("Retry after {delay}.", delay);
await Task.Delay(delay).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Extension;
/// <summary>
/// 日志器扩展
/// </summary>
[SuppressMessage("", "CA2254")]
public static class LoggerExtension
{
/// <inheritdoc cref="LoggerExtensions.LogInformation(ILogger, string?, object?[])"/>
public static T LogInformation<T>(this ILogger logger, string message, params object?[] param)
{
logger.LogInformation(message, param);
return default!;
}
/// <inheritdoc cref="LoggerExtensions.LogWarning(ILogger, string?, object?[])"/>
public static T LogWarning<T>(this ILogger logger, string message, params object?[] param)
{
logger.LogWarning(message, param);
return default!;
}
}

View File

@@ -46,4 +46,4 @@ public static class StringBuilderExtensions
{
return condition ? sb.Append(trueValue) : sb.Append(falseValue);
}
}
}

View File

@@ -0,0 +1,311 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Context.Database;
#nullable disable
namespace Snap.Hutao.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20221128115346_ObjectCache")]
partial class ObjectCache
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.0");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("Current")
.HasColumnType("INTEGER");
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DailyNote")
.HasColumnType("TEXT");
b.Property<bool>("DailyTaskNotify")
.HasColumnType("INTEGER");
b.Property<bool>("DailyTaskNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotify")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("HomeCoinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("HomeCoinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ResinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("ResinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ShowInHomeWidget")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotify")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("UserId");
b.ToTable("daily_notes");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("gacha_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("GachaType")
.HasColumnType("INTEGER");
b.Property<long>("Id")
.HasColumnType("INTEGER");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("QueryType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpireTime")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("object_cache");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Aid")
.HasColumnType("TEXT");
b.Property<string>("CookieToken")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Ltoken")
.HasColumnType("TEXT");
b.Property<string>("Mid")
.HasColumnType("TEXT");
b.Property<string>("Stoken")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("users");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,36 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
/// <inheritdoc />
public partial class ObjectCache : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "object_cache",
columns: table => new
{
Key = table.Column<string>(type: "TEXT", nullable: false),
ExpireTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_object_cache", x => x.Key);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "object_cache");
}
}
}

View File

@@ -212,6 +212,22 @@ namespace Snap.Hutao.Migrations
b.ToTable("game_accounts");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpireTime")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("object_cache");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")

View File

@@ -41,6 +41,14 @@ public class TypedWishSummary : WishBase
/// </summary>
public int LastOrangePull { get; set; }
/// <summary>
/// 据上个五星抽数格式化
/// </summary>
public string LastOrangePullFormatted
{
get => $"已垫 {LastOrangePull} 抽";
}
/// <summary>
/// 五星保底阈值
/// </summary>
@@ -51,6 +59,14 @@ public class TypedWishSummary : WishBase
/// </summary>
public int LastPurplePull { get; set; }
/// <summary>
/// 据上个四星抽数格式化
/// </summary>
public string LastPurplePullFormatted
{
get => $"已垫 {LastPurplePull} 抽";
}
/// <summary>
/// 四星保底阈值
/// </summary>

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Binding.Hutao;
@@ -9,18 +8,8 @@ namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 角色搭配
/// </summary>
public class ComplexAvatarCollocation : ComplexAvatar
public class ComplexAvatarCollocation
{
/// <summary>
/// 构造一个新的角色搭配
/// </summary>
/// <param name="avatar">角色</param>
/// <param name="rate">比率</param>
public ComplexAvatarCollocation(Avatar avatar)
: base(avatar, 0)
{
}
/// <summary>
/// 角色Id
/// </summary>

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 武器搭配
/// </summary>
public class ComplexWeaponCollocation
{
/// <summary>
/// 武器Id
/// </summary>
public WeaponId WeaponId { get; set; }
/// <summary>
/// 角色
/// </summary>
public List<ComplexAvatar> Avatars { get; set; } = default!;
}

View File

@@ -76,10 +76,7 @@ public class User : ObservableObject
public Cookie? Stoken
{
get => inner.Stoken;
set
{
inner.Stoken = value;
}
set => inner.Stoken = value;
}
/// <summary>
@@ -96,8 +93,14 @@ public class User : ObservableObject
internal static async Task<User?> ResumeAsync(EntityUser inner, CancellationToken token = default)
{
User user = new(inner);
bool successful = await user.InitializeCoreAsync(token).ConfigureAwait(false);
return successful ? user : null;
bool isOk = await user.InitializeCoreAsync(token).ConfigureAwait(false);
if (!isOk)
{
user.UserInfo = new UserInfo() { Nickname = "网络异常" };
}
return user;
}
/// <summary>

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Snap.Hutao.Model.Entity;
/// <summary>
/// 数据库对象缓存
/// </summary>
[Table("object_cache")]
public class ObjectCacheEntry
{
/// <summary>
/// 主键
/// </summary>
[Key]
public string Key { get; set; } = default!;
/// <summary>
/// 过期时间
/// </summary>
public DateTimeOffset ExpireTime { get; set; }
/// <summary>
/// 值字符串
/// </summary>
public string? Value { get; set; }
}

View File

@@ -92,4 +92,4 @@ public class SettingEntry
/// 值
/// </summary>
public string? Value { get; set; }
}
}

View File

@@ -27,4 +27,4 @@ internal class EquipIconConverter : ValueConverterBase<string, Uri>
{
return IconNameToUri(from);
}
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Control;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 武器祈愿图片转换器
/// </summary>
internal class GachaEquipIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/GachaEquipIcon/UI_Gacha_{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
/// <param name="name">名称</param>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
name = name["UI_".Length..];
return new Uri(string.Format(BaseUrl, name));
}
/// <inheritdoc/>
public override Uri Convert(string from)
{
return IconNameToUri(from);
}
}

View File

@@ -17,5 +17,5 @@ public class AffixInfo
/// 各个等级的描述
/// 0-4
/// </summary>
public List<LevelDescription<int>> Descriptions { get; set; } = default!;
public List<LevelDescription> Descriptions { get; set; } = default!;
}

View File

@@ -7,12 +7,18 @@ namespace Snap.Hutao.Model.Metadata.Weapon;
/// 等级与描述
/// </summary>
/// <typeparam name="TLevel">等级的类型</typeparam>
public class LevelDescription<TLevel>
public class LevelDescription
{
/// <summary>
/// 等级
/// </summary>
public TLevel Level { get; set; } = default!;
public int Level { get; set; } = default!;
/// <summary>
/// 格式化的等级
/// </summary>
[JsonIgnore]
public string LevelFormatted { get => $"精炼 {Level + 1} 阶"; }
/// <summary>
/// 描述

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Model.Binding.Gacha;
using Snap.Hutao.Model.Binding.Gacha.Abstraction;
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Model.Metadata.Converter;
@@ -60,6 +61,12 @@ public class Weapon : IStatisticsItemSource, ISummaryItemSource, INameQuality
/// </summary>
public AffixInfo? Affix { get; set; } = default!;
/// <summary>
/// [非元数据] 搭配数据
/// </summary>
[JsonIgnore]
public ComplexWeaponCollocation? Collocation { get; set; }
/// <inheritdoc/>
[JsonIgnore]
public ItemQuality Quality

View File

@@ -21,7 +21,6 @@ internal class IdentityConverter<TWrapper> : JsonConverter<TWrapper>
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, TWrapper value, JsonSerializerOptions options)
{
writer.WriteNumberValue(CastTo<int>.From(value));
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Avatar;
@@ -17,20 +18,24 @@ namespace Snap.Hutao.Service.Hutao;
[Injection(InjectAs.Singleton, typeof(IHutaoCache))]
internal class HutaoCache : IHutaoCache
{
private readonly IHutaoService hutaoService;
private readonly IMetadataService metadataService;
private readonly IServiceScopeFactory scopeFactory;
private Dictionary<AvatarId, Avatar>? idAvatarExtendedMap;
private bool isDatabaseViewModelInitialized;
private bool isWikiAvatarViewModelInitiaized;
private bool isWikiWeaponViewModelInitiaized;
/// <summary>
/// 构造一个新的胡桃 API 缓存
/// </summary>
/// <param name="hutaoService">胡桃服务</param>
/// <param name="metadataService">元数据服务</param>
public HutaoCache(IHutaoService hutaoService, IMetadataService metadataService)
/// <param name="scopeFactory">范围工厂</param>
public HutaoCache(IMetadataService metadataService, IServiceScopeFactory scopeFactory)
{
this.hutaoService = hutaoService;
this.metadataService = metadataService;
this.scopeFactory = scopeFactory;
}
/// <inheritdoc/>
@@ -51,9 +56,17 @@ internal class HutaoCache : IHutaoCache
/// <inheritdoc/>
public List<ComplexAvatarCollocation>? AvatarCollocations { get; set; }
/// <inheritdoc/>
public List<ComplexWeaponCollocation>? WeaponCollocations { get; set; }
/// <inheritdoc/>
public async ValueTask<bool> InitializeForDatabaseViewModelAsync()
{
if (isDatabaseViewModelInitialized)
{
return true;
}
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
@@ -72,6 +85,7 @@ internal class HutaoCache : IHutaoCache
ovewviewTask)
.ConfigureAwait(false);
isDatabaseViewModelInitialized = true;
return true;
}
@@ -81,25 +95,65 @@ internal class HutaoCache : IHutaoCache
/// <inheritdoc/>
public async ValueTask<bool> InitializeForWikiAvatarViewModelAsync()
{
if (isWikiAvatarViewModelInitiaized)
{
return true;
}
if (await metadataService.InitializeAsync().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);
AvatarCollocations = avatarCollocationsRaw.Select(co =>
List<AvatarCollocation> avatarCollocationsRaw;
using (IServiceScope scope = scopeFactory.CreateScope())
{
return new ComplexAvatarCollocation(idAvatarMap[co.AvatarId])
{
AvatarId = co.AvatarId,
Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
Weapons = co.Weapons.Select(w => new ComplexWeapon(idWeaponMap[w.Item], w.Rate)).ToList(),
ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
};
IHutaoService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoService>();
avatarCollocationsRaw = await hutaoService.GetAvatarCollocationsAsync().ConfigureAwait(false);
}
AvatarCollocations = avatarCollocationsRaw.Select(co => new ComplexAvatarCollocation()
{
AvatarId = co.AvatarId,
Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
Weapons = co.Weapons.Select(w => new ComplexWeapon(idWeaponMap[w.Item], w.Rate)).ToList(),
ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
}).ToList();
isWikiAvatarViewModelInitiaized = true;
return true;
}
return false;
}
/// <inheritdoc/>
public async ValueTask<bool> InitializeForWikiWeaponViewModelAsync()
{
if (isWikiWeaponViewModelInitiaized)
{
return true;
}
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<AvatarId, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
List<WeaponCollocation> weaponCollocationsRaw;
using (IServiceScope scope = scopeFactory.CreateScope())
{
IHutaoService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoService>();
weaponCollocationsRaw = await hutaoService.GetWeaponCollocationsAsync().ConfigureAwait(false);
}
WeaponCollocations = weaponCollocationsRaw.Select(co => new ComplexWeaponCollocation()
{
WeaponId = co.WeaponId,
Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
}).ToList();
isWikiWeaponViewModelInitiaized = true;
return true;
}
@@ -123,7 +177,13 @@ internal class HutaoCache : IHutaoCache
private async Task AvatarAppearanceRankAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<AvatarAppearanceRank> avatarAppearanceRanksRaw = await hutaoService.GetAvatarAppearanceRanksAsync().ConfigureAwait(false);
List<AvatarAppearanceRank> avatarAppearanceRanksRaw;
using (IServiceScope scope = scopeFactory.CreateScope())
{
IHutaoService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoService>();
avatarAppearanceRanksRaw = await hutaoService.GetAvatarAppearanceRanksAsync().ConfigureAwait(false);
}
AvatarAppearanceRanks = avatarAppearanceRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
{
Floor = $"第 {rank.Floor} 层",
@@ -133,7 +193,13 @@ internal class HutaoCache : IHutaoCache
private async Task AvatarUsageRanksAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<AvatarUsageRank> avatarUsageRanksRaw = await hutaoService.GetAvatarUsageRanksAsync().ConfigureAwait(false);
List<AvatarUsageRank> avatarUsageRanksRaw;
using (IServiceScope scope = scopeFactory.CreateScope())
{
IHutaoService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoService>();
avatarUsageRanksRaw = await hutaoService.GetAvatarUsageRanksAsync().ConfigureAwait(false);
}
AvatarUsageRanks = avatarUsageRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
{
Floor = $"第 {rank.Floor} 层",
@@ -143,7 +209,13 @@ internal class HutaoCache : IHutaoCache
private async Task AvatarConstellationInfosAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<AvatarConstellationInfo> avatarConstellationInfosRaw = await hutaoService.GetAvatarConstellationInfosAsync().ConfigureAwait(false);
List<AvatarConstellationInfo> avatarConstellationInfosRaw;
using (IServiceScope scope = scopeFactory.CreateScope())
{
IHutaoService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoService>();
avatarConstellationInfosRaw = await hutaoService.GetAvatarConstellationInfosAsync().ConfigureAwait(false);
}
AvatarConstellationInfos = avatarConstellationInfosRaw.OrderBy(i => i.HoldingRate).Select(info =>
{
return new ComplexAvatarConstellationInfo(idAvatarMap[info.AvatarId], info.HoldingRate, info.Constellations.Select(x => x.Rate));
@@ -152,12 +224,22 @@ internal class HutaoCache : IHutaoCache
private async Task TeamAppearancesAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<TeamAppearance> teamAppearancesRaw = await hutaoService.GetTeamAppearancesAsync().ConfigureAwait(false);
List<TeamAppearance> teamAppearancesRaw;
using (IServiceScope scope = scopeFactory.CreateScope())
{
IHutaoService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoService>();
teamAppearancesRaw = await hutaoService.GetTeamAppearancesAsync().ConfigureAwait(false);
}
TeamAppearances = teamAppearancesRaw.OrderByDescending(t => t.Floor).Select(team => new ComplexTeamRank(team, idAvatarMap)).ToList();
}
private async Task OverviewAsync()
{
Overview = await hutaoService.GetOverviewAsync().ConfigureAwait(false);
using (IServiceScope scope = scopeFactory.CreateScope())
{
IHutaoService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoService>();
Overview = await hutaoService.GetOverviewAsync().ConfigureAwait(false);
}
}
}

View File

@@ -2,6 +2,9 @@
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Model;
@@ -10,21 +13,27 @@ namespace Snap.Hutao.Service.Hutao;
/// <summary>
/// 胡桃 API 服务
/// </summary>
[Injection(InjectAs.Transient, typeof(IHutaoService))]
[Injection(InjectAs.Scoped, typeof(IHutaoService))]
internal class HutaoService : IHutaoService
{
private readonly HomaClient homaClient;
private readonly IMemoryCache memoryCache;
private readonly AppDbContext appDbContext;
private readonly JsonSerializerOptions options;
/// <summary>
/// 构造一个新的胡桃 API 服务
/// </summary>
/// <param name="homaClient">胡桃 API 客户端</param>
/// <param name="memoryCache">内存缓存</param>
public HutaoService(HomaClient homaClient, IMemoryCache memoryCache)
/// <param name="appDbContext">数据库上下文</param>
/// <param name="options">Json序列化选项</param>
public HutaoService(HomaClient homaClient, IMemoryCache memoryCache, AppDbContext appDbContext, JsonSerializerOptions options)
{
this.homaClient = homaClient;
this.memoryCache = memoryCache;
this.appDbContext = appDbContext;
this.options = options;
}
/// <inheritdoc/>
@@ -57,6 +66,12 @@ internal class HutaoService : IHutaoService
return FromCacheOrWebAsync(nameof(AvatarCollocation), homaClient.GetAvatarCollocationsAsync);
}
/// <inheritdoc/>
public ValueTask<List<WeaponCollocation>> GetWeaponCollocationsAsync()
{
return FromCacheOrWebAsync(nameof(WeaponCollocation), homaClient.GetWeaponCollocationsAsync);
}
/// <inheritdoc/>
public ValueTask<List<TeamAppearance>> GetTeamAppearancesAsync()
{
@@ -71,7 +86,27 @@ internal class HutaoService : IHutaoService
return (T)cache!;
}
if (appDbContext.ObjectCache.SingleOrDefault(e => e.Key == key) is ObjectCacheEntry entry)
{
if (entry.ExpireTime > DateTimeOffset.Now)
{
T value = JsonSerializer.Deserialize<T>(entry.Value!, options)!;
return memoryCache.Set(key, value, TimeSpan.FromMinutes(30));
}
else
{
appDbContext.ObjectCache.RemoveAndSave(entry);
}
}
T web = await taskFunc(default).ConfigureAwait(false);
appDbContext.ObjectCache.AddAndSave(new()
{
Key = key,
ExpireTime = DateTimeOffset.Now.AddHours(4),
Value = JsonSerializer.Serialize(web, options),
});
return memoryCache.Set(key, web, TimeSpan.FromMinutes(30));
}
}

View File

@@ -41,15 +41,26 @@ internal interface IHutaoCache
/// </summary>
List<ComplexAvatarCollocation>? AvatarCollocations { get; set; }
/// <summary>
/// 武器搭配
/// </summary>
List<ComplexWeaponCollocation>? WeaponCollocations { get; set; }
/// <summary>
/// 为数据库视图模型初始化
/// </summary>
/// <returns>任务</returns>
/// <returns>是否初始化完成</returns>
ValueTask<bool> InitializeForDatabaseViewModelAsync();
/// <summary>
/// 为Wiki角色视图模型初始化
/// </summary>
/// <returns>任务</returns>
/// <returns>是否初始化完成</returns>
ValueTask<bool> InitializeForWikiAvatarViewModelAsync();
/// <summary>
/// 为Wiki武器视图模型初始化
/// </summary>
/// <returns>是否初始化完成</returns>
ValueTask<bool> InitializeForWikiWeaponViewModelAsync();
}

View File

@@ -45,4 +45,10 @@ internal interface IHutaoService
/// </summary>
/// <returns>队伍上场</returns>
ValueTask<List<TeamAppearance>> GetTeamAppearancesAsync();
/// <summary>
/// 异步获取武器搭配
/// </summary>
/// <returns>武器搭配</returns>
ValueTask<List<WeaponCollocation>> GetWeaponCollocationsAsync();
}

View File

@@ -3,8 +3,6 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.Xaml.Interactivity;
using Snap.Hutao.Control.Behavior;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service;

View File

@@ -43,6 +43,7 @@
<None Remove="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
<None Remove="Resource\Icon\UI_BtnIcon_Gacha.png" />
<None Remove="Resource\Icon\UI_ChapterIcon_Hutao.png" />
<None Remove="Resource\Icon\UI_GachaShowPanel_Bg_Weapon.png" />
<None Remove="Resource\Icon\UI_GuideIcon_PlayMethod.png" />
<None Remove="Resource\Icon\UI_Icon_Achievement.png" />
<None Remove="Resource\Icon\UI_Icon_BoostUp.png" />
@@ -66,6 +67,7 @@
<None Remove="View\Dialog\AchievementImportDialog.xaml" />
<None Remove="View\Dialog\AvatarInfoQueryDialog.xaml" />
<None Remove="View\Dialog\DailyNoteNotificationDialog.xaml" />
<None Remove="View\Dialog\DailyNoteVerificationDialog.xaml" />
<None Remove="View\Dialog\GachaLogImportDialog.xaml" />
<None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" />
<None Remove="View\Dialog\GachaLogUrlDialog.xaml" />
@@ -86,6 +88,7 @@
<None Remove="View\Page\LoginMihoyoUserPage.xaml" />
<None Remove="View\Page\SettingPage.xaml" />
<None Remove="View\Page\WikiAvatarPage.xaml" />
<None Remove="View\Page\WikiWeaponPage.xaml" />
<None Remove="View\TitleView.xaml" />
<None Remove="View\UserView.xaml" />
</ItemGroup>
@@ -110,6 +113,7 @@
<Content Include="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
<Content Include="Resource\Icon\UI_BtnIcon_Gacha.png" />
<Content Include="Resource\Icon\UI_ChapterIcon_Hutao.png" />
<Content Include="Resource\Icon\UI_GachaShowPanel_Bg_Weapon.png" />
<Content Include="Resource\Icon\UI_GuideIcon_PlayMethod.png" />
<Content Include="Resource\Icon\UI_Icon_Achievement.png" />
<Content Include="Resource\Icon\UI_Icon_BoostUp.png" />
@@ -170,6 +174,16 @@
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\WikiWeaponPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\DailyNoteVerificationDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\SignInWebViewDialog.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -11,7 +11,7 @@
CornerRadius="{StaticResource CompatCornerRadius}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
Background="{StaticResource CardBackgroundFillColorDefault}">
<StackPanel>
<StackPanel Name="BackgroundStack">
<ContentPresenter
Name="ContentHost"/>
<TextBlock

View File

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control;
namespace Snap.Hutao.View.Control;
@@ -15,7 +16,8 @@ namespace Snap.Hutao.View.Control;
public sealed partial class BottomTextControl : ContentControl
{
private static readonly DependencyProperty TextProperty = Property<BottomTextControl>.Depend(nameof(Text), string.Empty, OnTextChanged);
private static readonly DependencyProperty TopContentProperty = Property<BottomTextControl>.Depend<UIElement>(nameof(TopContent), default!, OnContentChanged2);
private static readonly DependencyProperty TopContentProperty = Property<BottomTextControl>.Depend<UIElement>(nameof(TopContent), default!, OnContentChanged);
private static readonly DependencyProperty FillProperty = Property<BottomTextControl>.Depend(nameof(Fill), default(Brush), OnFillChanged);
/// <summary>
/// 构造一个新的底部带有文本的控件
@@ -43,13 +45,27 @@ public sealed partial class BottomTextControl : ContentControl
set => SetValue(TextProperty, value);
}
private static void OnTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs dp)
/// <summary>
/// 填充
/// </summary>
public Brush Fill
{
((BottomTextControl)sender).TextHost.Text = (string)dp.NewValue;
get => (Brush)GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
private static void OnContentChanged2(DependencyObject sender, DependencyPropertyChangedEventArgs dp)
private static void OnTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
((BottomTextControl)sender).ContentHost.Content = dp.NewValue;
((BottomTextControl)sender).TextHost.Text = (string)args.NewValue;
}
private static void OnContentChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
((BottomTextControl)sender).ContentHost.Content = args.NewValue;
}
private static void OnFillChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
((BottomTextControl)sender).BackgroundStack.Background = (Brush)args.NewValue;
}
}

View File

@@ -9,7 +9,7 @@
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shcp="using:Snap.Hutao.Control.Panel"
xmlns:shmbg="using:Snap.Hutao.Model.Binding.Gacha"
xmlns:shvc="using:Snap.Hutao.View.Converter"
xmlns:shvc="using:Snap.Hutao.View.Control"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance shmbg:TypedWishSummary}">
@@ -18,8 +18,6 @@
<SolidColorBrush x:Key="PurpleBrush" Color="#FFA156E0"/>
<SolidColorBrush x:Key="OrangeBrush" Color="#FFBC6932"/>
<cwuconv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
<DataTemplate x:Key="OrangeListTemplate" x:DataType="shmbg:SummaryItem">
<Grid Margin="0,4,4,0" Background="Transparent" >
<ToolTipService.ToolTip>
@@ -67,47 +65,31 @@
<DataTemplate x:Key="OrangeGridTemplate" x:DataType="shmbg:SummaryItem">
<Grid Width="40" Margin="0,4,4,0">
<ToolTipService.ToolTip>
<TextBlock Text="{Binding TimeFormatted}"/>
</ToolTipService.ToolTip>
<StackPanel>
<shci:CachedImage
<Border
BorderThickness="1"
CornerRadius="{StaticResource CompatCornerRadius}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
Background="{StaticResource CardBackgroundFillColorDefault}"
ToolTipService.ToolTip="{Binding TimeFormatted}">
<StackPanel>
<shvc:ItemIcon
Icon="{Binding Icon}"
Quality="QUALITY_ORANGE"
Height="40" Width="40"/>
<!--<shci:CachedImage
Source="{Binding Icon}"
Height="40" Width="40"/>
<TextBlock
Text="{Binding LastPull}"
Style="{StaticResource CaptionTextBlockStyle}"
HorizontalAlignment="Center"
TextWrapping="NoWrap">
<TextBlock.Foreground>
<SolidColorBrush Color="{Binding Color}"/>
</TextBlock.Foreground>
</TextBlock>
</StackPanel>
<!--<StackPanel
HorizontalAlignment="Right"
Orientation="Horizontal">
<TextBlock
Margin="0,0,8,0"
Foreground="#FF0063FF"
Text="保底"
VerticalAlignment="Center"
Visibility="{Binding IsGuarentee,Converter={StaticResource BoolToVisibilityConverter}}"/>
<TextBlock
Margin="0,0,8,0"
Text="UP"
Foreground="#FFFFA400"
VerticalAlignment="Center"
Visibility="{Binding IsUp,Converter={StaticResource BoolToVisibilityConverter}}"/>
<TextBlock
Width="20"
TextAlignment="Center"
Text="{Binding LastPull}"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"/>
</StackPanel>-->
Height="40" Width="40"/>-->
<TextBlock
Text="{Binding LastPull}"
Style="{StaticResource CaptionTextBlockStyle}"
HorizontalAlignment="Center"
TextWrapping="NoWrap">
<TextBlock.Foreground>
<SolidColorBrush Color="{Binding Color}"/>
</TextBlock.Foreground>
</TextBlock>
</StackPanel>
</Border>
</Grid>
</DataTemplate>
@@ -157,61 +139,110 @@
<TextBlock Margin="12,0,0,12" Text="抽" VerticalAlignment="Bottom"/>
</StackPanel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ProgressBar
<Border
Grid.Column="0"
Grid.Row="0"
Value="{Binding LastOrangePull}"
Maximum="{Binding GuarenteeOrangeThreshold}"
Foreground="{StaticResource OrangeBrush}"/>
<TextBlock
Width="20"
TextAlignment="Center"
BorderThickness="1"
CornerRadius="{StaticResource CompatCornerRadius}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
Background="{StaticResource CardBackgroundFillColorDefault}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ProgressRing
Margin="4"
Height="40"
Width="40"
Grid.Column="0"
IsIndeterminate="False"
Maximum="{Binding GuarenteeOrangeThreshold}"
Value="{Binding LastOrangePull}"
Foreground="{StaticResource OrangeBrush}"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"/>
<TextBlock
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Column="0"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding LastOrangePull}"
Foreground="{StaticResource OrangeBrush}"/>
<TextBlock
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Column="1"
Style="{StaticResource BaseTextBlockStyle}"
Text="五星"
Foreground="{StaticResource OrangeBrush}"/>
</Grid>
</Border>
<Border
Grid.Column="1"
Grid.Row="0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding LastOrangePull}"
Foreground="{StaticResource OrangeBrush}"/>
<ProgressBar
Grid.Column="0"
Grid.Row="1"
Value="{Binding LastPurplePull}"
Maximum="{Binding GuarenteePurpleThreshold}"
Foreground="{StaticResource PurpleBrush}"/>
<TextBlock
Width="20"
TextAlignment="Center"
Grid.Column="1"
Grid.Row="1"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding LastPurplePull}"
Foreground="{StaticResource PurpleBrush}"/>
BorderThickness="1"
CornerRadius="{StaticResource CompatCornerRadius}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
Background="{StaticResource CardBackgroundFillColorDefault}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ProgressRing
Margin="4"
Height="40"
Width="40"
Grid.Column="0"
IsIndeterminate="False"
Maximum="{Binding GuarenteePurpleThreshold}"
Value="{Binding LastPurplePull}"
Foreground="{StaticResource PurpleBrush}"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"/>
<TextBlock
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Column="0"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding LastPurplePull}"
Foreground="{StaticResource PurpleBrush}"/>
<TextBlock
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Column="1"
Style="{StaticResource BaseTextBlockStyle}"
Text="四星"
Foreground="{StaticResource PurpleBrush}"/>
</Grid>
</Border>
</Grid>
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<TextBlock
Opacity="0.6"
FontFamily="Consolas"
HorizontalAlignment="Left"
Style="{StaticResource BodyTextBlockStyle}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding FromFormatted}"/>
<TextBlock
Opacity="0.6"
FontFamily="Consolas"
Text="-"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Style="{StaticResource BodyTextBlockStyle}"/>
Style="{StaticResource CaptionTextBlockStyle}"/>
<TextBlock
Opacity="0.6"
FontFamily="Consolas"
HorizontalAlignment="Left"
Style="{StaticResource BodyTextBlockStyle}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding ToFormatted}"/>
</StackPanel>
</StackPanel>

View File

@@ -1,5 +1,5 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

View File

@@ -0,0 +1,17 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.DailyNoteVerificationDialog"
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"
Closed="OnContentDialogClosed"
Style="{StaticResource DefaultContentDialogStyle}"
Title="米游社实时便笺"
PrimaryButtonText="完成"
DefaultButton="Primary">
<Grid Loaded="OnGridLoaded">
<WebView2 Name="WebView" Height="448" Width="380"/>
</Grid>
</ContentDialog>

View File

@@ -0,0 +1,65 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Bridge;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.View.Dialog;
/// <summary>
/// ʵʱ<CAB5><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>
/// </summary>
public sealed partial class DailyNoteVerificationDialog : ContentDialog
{
private readonly IServiceScope scope;
private readonly User user;
private readonly PlayerUid uid;
[SuppressMessage("", "IDE0052")]
private DailyNoteJsInterface? dailyNoteJsInterface;
/// <summary>
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µ<EFBFBD>ʵʱ<CAB5><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>
/// </summary>
/// <param name="window"><3E><><EFBFBD><EFBFBD></param>
/// <param name="user"><3E>û<EFBFBD></param>
/// <param name="uid">uid</param>
public DailyNoteVerificationDialog(Window window, User user, PlayerUid uid)
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
this.user = user;
this.uid = uid;
scope = Ioc.Default.CreateScope();
}
private void OnGridLoaded(object sender, RoutedEventArgs e)
{
InitializeAsync().SafeForget();
}
private async Task InitializeAsync()
{
await WebView.EnsureCoreWebView2Async();
CoreWebView2 coreWebView2 = WebView.CoreWebView2;
coreWebView2.SetCookie(user.CookieToken, user.Ltoken, null).SetMobileUserAgent();
dailyNoteJsInterface = new(coreWebView2, scope.ServiceProvider);
#if DEBUG
coreWebView2.OpenDevToolsWindow();
#endif
string query = $"?role_id={uid.Value}&server={uid.Region}";
coreWebView2.Navigate($"https://webstatic.mihoyo.com/app/community-game-records/index.html?bbs_presentation_style=fullscreen#/ys/daily/{query}");
}
private void OnContentDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args)
{
dailyNoteJsInterface = null;
scope.Dispose();
}
}

View File

@@ -65,6 +65,10 @@
shvh:NavHelper.NavigateTo="shvp:WikiAvatarPage"
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BagTabIcon_Avatar.png}"/>
<NavigationViewItem
Content="武器资料"
shvh:NavHelper.NavigateTo="shvp:WikiWeaponPage"
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BagTabIcon_Weapon.png}"/>
</NavigationView.MenuItems>
<NavigationView.PaneFooter>

View File

@@ -178,7 +178,7 @@
Text="对当前选中的账号进行签到"/>
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="Yellow"
Foreground="{StaticResource SystemFillColorCautionBrush}"
Text="可能需要重启应用以对其他账号签到"/>
</StackPanel>
</sc:Setting.Description>
@@ -199,12 +199,12 @@
</sc:Setting>
</sc:SettingsGroup>
<sc:SettingsGroup Header="危险功能" Foreground="#FF800000">
<sc:SettingsGroup Header="危险功能" Foreground="{StaticResource SystemFillColorCriticalBrush}">
<sc:Setting
Icon="&#xE756;"
Header="删除所有用户"
Description="直接删除用户表的所有记录,用于修复特定的账号冲突问题"
Background="#80800000">
Background="{StaticResource SystemFillColorCriticalBackgroundBrush}">
<sc:Setting.ActionContent>
<Button
Content="执行"

View File

@@ -0,0 +1,285 @@
<shc:ScopedPage
x:Class="Snap.Hutao.View.Page.WikiWeaponPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shcp="using:Snap.Hutao.Control.Panel"
xmlns:shct="using:Snap.Hutao.Control.Text"
xmlns:shv="using:Snap.Hutao.ViewModel"
xmlns:shvc="using:Snap.Hutao.View.Control"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=shv:WikiWeaponViewModel}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>
<DataTemplate x:Key="PropertyDataTemplate">
<shvc:DescParamComboBox
HorizontalAlignment="Stretch"
Source="{Binding Converter={StaticResource PropertyDescriptor}}"/>
</DataTemplate>
</Page.Resources>
<SplitView
IsPaneOpen="True"
DisplayMode="Inline"
OpenPaneLength="200"
PaneBackground="{StaticResource CardBackgroundFillColorSecondary}">
<SplitView.Pane>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<CommandBar DefaultLabelPosition="Right">
<CommandBar.Content>
<shcp:PanelSelector Margin="6,8,0,0" x:Name="ItemsPanelSelector"/>
</CommandBar.Content>
<!--<AppBarButton Label="筛选" Icon="{shcm:FontIcon Glyph=&#xE71C;}">
<AppBarButton.Flyout>
<Flyout Placement="RightEdgeAlignedTop" LightDismissOverlayMode="On">
<cwuc:UniformGrid Columns="3" RowSpacing="16">
<cwuc:HeaderedItemsControl
Header="元素"
Padding="0,12,0,0"
ItemsSource="{Binding FilterElementInfos}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Value}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</cwuc:HeaderedItemsControl>
<cwuc:HeaderedItemsControl
Header="所属"
Padding="0,12,0,0"
ItemsSource="{Binding FilterAssociationInfos}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Value.Key}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</cwuc:HeaderedItemsControl>
<cwuc:HeaderedItemsControl
Header="武器"
Padding="0,12,0,0"
ItemsSource="{Binding FilterWeaponTypeInfos}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Value.Key}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</cwuc:HeaderedItemsControl>
<cwuc:HeaderedItemsControl
Header="星级"
Padding="0,12,0,0"
ItemsSource="{Binding FilterQualityInfos}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Value.Key}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</cwuc:HeaderedItemsControl>
<cwuc:HeaderedItemsControl
Header="体型"
Padding="0,12,0,0"
ItemsSource="{Binding FilterBodyInfos}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Value.Key}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</cwuc:HeaderedItemsControl>
</cwuc:UniformGrid>
</Flyout>
</AppBarButton.Flyout>
</AppBarButton>-->
</CommandBar>
<cwuc:SwitchPresenter Grid.Row="1" Value="{Binding ElementName=ItemsPanelSelector,Path=Current}">
<cwuc:Case Value="List">
<ListView
Grid.Row="1"
SelectionMode="Single"
ItemsSource="{Binding Weapons}"
SelectedItem="{Binding Selected,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shci:CachedImage
Grid.Column="0"
Width="48"
Height="48"
Margin="0,0,12,6"
Source="{Binding Icon,Converter={StaticResource EquipIconConverter},Mode=OneWay}"/>
<TextBlock
VerticalAlignment="Center"
Grid.Column="1"
Margin="12,0,0,0"
Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</cwuc:Case>
<cwuc:Case Value="Grid">
<GridView
HorizontalAlignment="Left"
HorizontalContentAlignment="Left"
Margin="6,6,0,0"
SelectionMode="Single"
ItemsSource="{Binding Weapons}"
SelectedItem="{Binding Selected,Mode=TwoWay}">
<GridView.ItemTemplate>
<DataTemplate>
<shci:CachedImage
Grid.Column="0"
Width="40"
Height="40"
Margin="0"
Source="{Binding Icon,Converter={StaticResource EquipIconConverter},Mode=OneWay}"/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</cwuc:Case>
</cwuc:SwitchPresenter>
</Grid>
</SplitView.Pane>
<SplitView.Content>
<ScrollViewer>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="800"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<StackPanel Margin="0,0,20,0">
<Border
Margin="16,16,0,0"
BorderThickness="1"
BorderBrush="{StaticResource CardStrokeColorDefault}"
CornerRadius="{StaticResource CompatCornerRadius}">
<Border.Background>
<ImageBrush ImageSource="ms-appx:///Resource/Icon/UI_GachaShowPanel_Bg_Weapon.png"/>
</Border.Background>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<mxi:Interaction.Behaviors>
<shcb:AutoHeightBehavior TargetWidth="2048" TargetHeight="1024"/>
</mxi:Interaction.Behaviors>
<ScrollViewer
Margin="16"
Grid.Column="0">
<StackPanel>
<shvc:BottomTextControl
RequestedTheme="Light"
Text="突破前">
<shvc:ItemIcon
Icon="{Binding Selected.Icon,Converter={StaticResource EquipIconConverter}}"
Quality="{Binding Selected.RankLevel}"/>
</shvc:BottomTextControl>
<shvc:BottomTextControl
RequestedTheme="Light"
Text="突破后"
Margin="0,16,0,0">
<shvc:ItemIcon
Icon="{Binding Selected.AwakenIcon,Converter={StaticResource EquipIconConverter}}"
Quality="{Binding Selected.RankLevel}"/>
</shvc:BottomTextControl>
</StackPanel>
</ScrollViewer>
<Grid Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="176*"/>
<ColumnDefinition Width="848*"/>
</Grid.ColumnDefinitions>
<shci:CachedImage
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Source="{Binding Selected.Icon,Converter={StaticResource GachaEquipIconConverter}}"/>
</Grid>
<TextBlock
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Margin="16"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding Selected.Name}"/>
</Grid>
</Border>
<TextBlock
Margin="16,16,0,0"
TextWrapping="Wrap"
Text="{Binding Selected.Description}"/>
<ContentControl
Margin="16,16,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Content="{Binding Selected.Property,Mode=OneWay}"
ContentTemplate="{StaticResource PropertyDataTemplate}"/>
<TextBlock Text="{Binding Selected.Affix.Name}" Style="{StaticResource BaseTextBlockStyle}" Margin="16,32,0,0"/>
<Pivot ItemsSource="{Binding Selected.Affix.Descriptions}">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding LevelFormatted}" Style="{StaticResource CaptionTextBlockStyle}"/>
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate>
<shct:DescriptionTextBlock
Margin="16,16,0,0"
Description="{Binding Description}">
<shct:DescriptionTextBlock.Resources>
<Style TargetType="TextBlock" BasedOn="{StaticResource BodyTextBlockStyle}">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</shct:DescriptionTextBlock.Resources>
</shct:DescriptionTextBlock>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
<TextBlock Text="搭配角色" Style="{StaticResource BaseTextBlockStyle}" Margin="16,32,0,0"/>
<GridView
Margin="16,16,0,0"
SelectionMode="None"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding Selected.Collocation.Avatars}">
<GridView.ItemTemplate>
<DataTemplate>
<shvc:BottomTextControl
ToolTipService.ToolTip="{Binding Name}"
Text="{Binding Rate}">
<shvc:ItemIcon
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
</shvc:BottomTextControl>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</StackPanel>
</Grid>
</ScrollViewer>
</SplitView.Content>
</SplitView>
</shc:ScopedPage>

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Control;
using Snap.Hutao.ViewModel;
namespace Snap.Hutao.View.Page;
/// <summary>
/// <20><><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3>
/// </summary>
public sealed partial class WikiWeaponPage : ScopedPage
{
/// <summary>
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µ<EFBFBD><C2B5><EFBFBD><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3>
/// </summary>
public WikiWeaponPage()
{
InitializeWith<WikiWeaponViewModel>();
InitializeComponent();
}
}

View File

@@ -0,0 +1,94 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.WinUI.UI;
using Snap.Hutao.Control;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Metadata;
namespace Snap.Hutao.ViewModel;
/// <summary>
/// 武器资料视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal class WikiWeaponViewModel : ObservableObject, ISupportCancellation
{
private readonly List<WeaponId> skippedWeapons = new()
{
11419, 11420, 11421, // 「一心传」名刀
12304, 14306, 15306, 13304, // 石英大剑, 琥珀玥, 黑檀弓, 「旗杆」
};
private readonly IMetadataService metadataService;
private readonly IHutaoCache hutaoCache;
private AdvancedCollectionView? weapons;
private Weapon? selected;
/// <summary>
/// 构造一个新的武器资料视图模型
/// </summary>
/// <param name="metadataService">元数据服务</param>
/// <param name="hutaoCache">胡桃缓存</param>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public WikiWeaponViewModel(IMetadataService metadataService, IHutaoCache hutaoCache, IAsyncRelayCommandFactory asyncRelayCommandFactory)
{
this.metadataService = metadataService;
this.hutaoCache = hutaoCache;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 角色列表
/// </summary>
public AdvancedCollectionView? Weapons { get => weapons; set => SetProperty(ref weapons, value); }
/// <summary>
/// 选中的角色
/// </summary>
public Weapon? Selected { get => selected; set => SetProperty(ref selected, value); }
/// <summary>
/// 打开界面命令
/// </summary>
public ICommand OpenUICommand { get; }
private async Task OpenUIAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
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)
.ToList();
await CombineWithWeaponCollocationsAsync(sorted).ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
Weapons = new AdvancedCollectionView(sorted, true);
Selected = Weapons.Cast<Weapon>().FirstOrDefault();
}
}
private async Task CombineWithWeaponCollocationsAsync(List<Weapon> weapons)
{
if (await hutaoCache.InitializeForWikiWeaponViewModelAsync().ConfigureAwait(false))
{
Dictionary<WeaponId, ComplexWeaponCollocation> idCollocations = hutaoCache.WeaponCollocations!.ToDictionary(a => a.WeaponId);
weapons.ForEach(w => w.Collocation = idCollocations.GetValueOrDefault(w.Id));
}
}
}

View File

@@ -6,7 +6,7 @@ using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Web;
/// <summary>
/// 米哈游Url端点
/// API端点
/// </summary>
[SuppressMessage("", "SA1201")]
[SuppressMessage("", "SA1124")]

View File

@@ -3,7 +3,6 @@
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Web.Hoyolab;
using WinRT;
namespace Snap.Hutao.Web.Bridge;

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Web.WebView2.Core;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// 实时便笺页面调用桥
/// </summary>
public class DailyNoteJsInterface : MiHoYoJSInterface
{
/// <summary>
/// 构造一个新的实时便笺页面调用桥
/// </summary>
/// <param name="webView">webview</param>
/// <param name="serviceProvider">服务提供器</param>
public DailyNoteJsInterface(CoreWebView2 webView, IServiceProvider serviceProvider)
: base(webView, serviceProvider)
{
}
}

View File

@@ -83,7 +83,15 @@ public class MiHoYoJSInterface
[JsMethod("getHTTPRequestHeaders")]
public virtual JsResult<Dictionary<string, string>> GetHttpRequestHeader(JsParam param)
{
throw new NotImplementedException();
return new()
{
Data = new Dictionary<string, string>()
{
{ "x-rpc-client_type", "5" },
{ "x-rpc-device_id", Core.CoreEnvironment.HoyolabDeviceId },
{ "x-rpc-app_version", Core.CoreEnvironment.HoyolabXrpcVersion },
},
};
}
/// <summary>
@@ -138,6 +146,30 @@ public class MiHoYoJSInterface
}
}
/// <summary>
/// 获取2代动态密钥
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
[JsMethod("getDS2")]
public virtual JsResult<Dictionary<string, string>> GetDynamicSecrectV2(JsParam<DynamicSecrect2Playload> param)
{
string salt = DynamicSecretHandler.DynamicSecrets[nameof(SaltType.X4)];
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
int r = GetRandom();
string b = param.Payload.Body;
string q = string.Join('&', param.Payload.Query.OrderBy(x => x.Key).Select(x => $"{x.Key}={x.Value}"));
string check = Md5Convert.ToHexString($"salt={salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant();
return new() { Data = new() { ["DS"] = $"{t},{r},{check}", }, };
static int GetRandom()
{
int rand = Random.Shared.Next(100000, 200000);
return rand == 100000 ? 642367 : rand;
}
}
/// <summary>
/// 获取用户基本信息
/// </summary>
@@ -162,6 +194,11 @@ public class MiHoYoJSInterface
};
}
/// <summary>
/// 获取CookieToken
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
[JsMethod("getCookieToken")]
public virtual async Task<JsResult<Dictionary<string, string>>> GetCookieTokenAsync(JsParam<CookieTokenPayload> param)
{
@@ -185,10 +222,26 @@ public class MiHoYoJSInterface
return new() { Data = new() { [Cookie.COOKIE_TOKEN] = cookieToken! } };
}
[JsMethod("configure_share")]
public virtual Task<IJsResult?> ConfigureShare(JsParam param)
/// <summary>
/// 关闭
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
[JsMethod("closePage")]
public virtual IJsResult? ClosePage(JsParam param)
{
throw new NotImplementedException();
return null;
}
/// <summary>
/// 调整分享设置
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
[JsMethod("configure_share")]
public virtual IJsResult? ConfigureShare(JsParam param)
{
return null;
}
[JsMethod("showAlertDialog")]
@@ -197,12 +250,6 @@ public class MiHoYoJSInterface
return Task.FromException<IJsResult?>(new NotImplementedException());
}
[JsMethod("closePage")]
public virtual IJsResult? ClosePage(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("startRealPersonValidation")]
public virtual IJsResult? StartRealPersonValidation(JsParam param)
{
@@ -215,12 +262,6 @@ public class MiHoYoJSInterface
throw new NotImplementedException();
}
[JsMethod("getDS2")]
public virtual IJsResult? GetDynamicSecrectV2(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("genAuthKey")]
public virtual IJsResult? GenAuthKey(JsParam param)
{
@@ -264,7 +305,7 @@ public class MiHoYoJSInterface
}
[JsMethod("showToast")]
public virtual Task<IJsResult?> ShowToast(JsParam param)
public virtual IJsResult? ShowToast(JsParam param)
{
throw new NotImplementedException();
}
@@ -287,7 +328,7 @@ public class MiHoYoJSInterface
.Append(')')
.ToString();
logger?.LogInformation("[ExecuteScript] {js}", js);
logger?.LogInformation("[ExecuteScript: {callback}]\n{payload}", callback, payload);
await ThreadHelper.SwitchToMainThreadAsync();
return await webView.ExecuteScriptAsync(js);
@@ -297,24 +338,27 @@ public class MiHoYoJSInterface
private async void OnWebMessageReceived(CoreWebView2 webView2, CoreWebView2WebMessageReceivedEventArgs args)
{
string message = args.TryGetWebMessageAsString();
logger?.LogInformation("[OnMessage] {message}", message);
JsParam param = JsonSerializer.Deserialize<JsParam>(message)!;
logger.LogInformation("[OnMessage]\nMethod : {method}\nPayload : {payload}\nCallback: {callback}", param.Method, param.Payload, param.Callback);
IJsResult? result = param.Method switch
{
"closePage" => ClosePage(param),
"configure_share" => ConfigureShare(param),
"eventTrack" => null,
"getActionTicket" => await GetActionTicketAsync(param).ConfigureAwait(false),
"getHTTPRequestHeaders" => GetHttpRequestHeader(param),
"getCookieInfo" => GetCookieInfo(param),
"getDS" => GetDynamicSecrectV1(param),
"getUserInfo" => GetUserInfo(param),
"getCookieToken" => await GetCookieTokenAsync(param).ConfigureAwait(false),
"configure_share" => null,
"getDS" => GetDynamicSecrectV1(param),
"getDS2" => GetDynamicSecrectV2(param),
"getHTTPRequestHeaders" => GetHttpRequestHeader(param),
"getUserInfo" => GetUserInfo(param),
"login" => null,
_ => null,
_ => logger.LogWarning<IJsResult>("Unhandled Message Type: {method}", param.Method),
};
if (result != null)
if (result != null && param.Callback != null)
{
await ExecuteCallbackScriptAsync(param.Callback, result.ToString(options)).ConfigureAwait(false);
}
@@ -329,6 +373,8 @@ public class MiHoYoJSInterface
{
if (new Uri(args.Uri).Host.EndsWith("mihoyo.com"))
{
// Execute this solve issue: When open same site second time,there might be no bridge init.
coreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(InitializeJsInterfaceScript2).AsTask().SafeForget(logger);
coreWebView2.ExecuteScriptAsync(InitializeJsInterfaceScript2).AsTask().SafeForget(logger);
}
}

View File

@@ -13,4 +13,4 @@ public class CookieTokenPayload
/// </summary>
[JsonPropertyName("forceRefresh")]
public bool ForceRefresh { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Bridge.Model;
/// <summary>
/// DS2请求
/// </summary>
public class DynamicSecrect2Playload
{
/// <summary>
/// q
/// </summary>
[JsonPropertyName("query")]
public Dictionary<string, string> Query { get; set; } = default!;
/// <summary>
/// b
/// </summary>
[JsonPropertyName("body")]
public string Body { get; set; } = default!;
}

View File

@@ -25,7 +25,7 @@ public class JsParam
/// 回调的名称,调用 JavaScript:mhyWebBridge 时作为首个参数传入
/// </summary>
[JsonPropertyName("callback")]
public string Callback { get; set; } = default!;
public string? Callback { get; set; }
}
/// <summary>
@@ -52,14 +52,14 @@ public class JsParam<TPayload>
/// 回调的名称,调用 JavaScript:mhyWebBridge 时作为首个参数传入
/// </summary>
[JsonPropertyName("callback")]
public string Callback { get; set; } = string.Empty;
public string? Callback { get; set; }
public static implicit operator JsParam<TPayload>(JsParam jsParam)
{
return new JsParam<TPayload>()
{
Method = jsParam.Method,
Payload = jsParam.Payload.HasValue ? jsParam.Payload.Value.Deserialize<TPayload>() : default,
Payload = jsParam.Payload.HasValue ? jsParam.Payload.Value.Deserialize<TPayload>()! : default!,
Callback = jsParam.Callback,
};
}

View File

@@ -30,4 +30,4 @@ public class SignInJsInterface : MiHoYoJSInterface
},
};
}
}
}

View File

@@ -4,7 +4,6 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
using System.Net.Http;
using System.Text.RegularExpressions;
namespace Snap.Hutao.Web.Geetest;

View File

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Hoyolab.Annotation;
/// <summary>
/// 指示相关的类忽略Http请求的Set-Cookie头
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple =false, Inherited =false)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal class IgnoreSetCookieAttribute : Attribute
{
}

View File

@@ -1,21 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab;
/// <summary>
/// 忽略 Set-Cookie 头
/// </summary>
[Injection(InjectAs.Transient)]
internal class IgnoreSetCookieHandler : DelegatingHandler
{
/// <inheritdoc/>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage message = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
message.Headers.Remove("Set-Cookie");
return message;
}
}

View File

@@ -4,7 +4,6 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Request;
using Snap.Hutao.Web.Response;
using System.Net.Http;

View File

@@ -1,14 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Auth;
/// <summary>

View File

@@ -1,12 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Geetest;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;

View File

@@ -1,14 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Widget;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>

View File

@@ -1,14 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Widget;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>

View File

@@ -137,6 +137,21 @@ internal class HomaClient
return EnumerableExtension.EmptyIfNull(resp?.Data);
}
/// <summary>
/// 异步获取武器搭配
/// GET /Statistics/Avatar/AvatarCollocation
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色/武器/圣遗物搭配</returns>
public async Task<List<WeaponCollocation>> GetWeaponCollocationsAsync(CancellationToken token = default)
{
Response<List<WeaponCollocation>>? resp = await httpClient
.GetFromJsonAsync<Response<List<WeaponCollocation>>>($"{HutaoAPI}/Statistics/Weapon/WeaponCollocation", token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data);
}
/// <summary>
/// 异步获取角色命座信息
/// GET /Statistics/Avatar/HoldingRate

View File

@@ -6,7 +6,7 @@ using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 角色相关
/// 角色相关构
/// </summary>
public abstract class AvatarBuild
{

View File

@@ -35,6 +35,6 @@ public class ReliquarySet
/// <inheritdoc/>
public override string ToString()
{
return $"{EquipAffixId}-{Count}";
return $"{(int)EquipAffixId}-{Count}";
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 武器相关构筑
/// </summary>
public abstract class WeaponBuild
{
/// <summary>
/// 角色Id
/// </summary>
public WeaponId WeaponId { get; set; }
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 武器搭配
/// </summary>
public class WeaponCollocation : WeaponBuild
{
/// <summary>
/// 其他角色
/// </summary>
public List<ItemRate<int, double>> Avatars { get; set; } = default!;
}

View File

@@ -43,6 +43,11 @@ public enum KnownReturnCode : int
/// </summary>
RET_NEED_AIGIS = -3101,
/// <summary>
/// 参数不合法
/// </summary>
InvalidParameter = -3001,
/// <summary>
/// 请在米游社App内打开~
/// </summary>