add api cache

This commit is contained in:
DismissedLight
2022-10-24 16:12:30 +08:00
parent bf5fcb70f8
commit fa19f7e817
28 changed files with 472 additions and 243 deletions

View File

@@ -3,6 +3,7 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Extension;
namespace Snap.Hutao.Core.Database;
@@ -14,7 +15,7 @@ namespace Snap.Hutao.Core.Database;
/// <typeparam name="TMessage">消息的类型</typeparam>
internal class DbCurrent<TEntity, TMessage>
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TEntity>
where TMessage : Message.ValueChangedMessage<TEntity>, new()
{
private readonly DbContext dbContext;
private readonly DbSet<TEntity> dbSet;
@@ -25,12 +26,12 @@ internal class DbCurrent<TEntity, TMessage>
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="dbSet">数据集</param>
/// <param name="messenger">消息器</param>
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, IMessenger messenger)
///
public DbCurrent(DbSet<TEntity> dbSet, IMessenger messenger)
{
this.dbContext = dbContext;
this.dbContext = dbSet.Context();
this.dbSet = dbSet;
this.messenger = messenger;
}
@@ -55,96 +56,18 @@ internal class DbCurrent<TEntity, TMessage>
if (current != null)
{
current.IsSelected = false;
dbSet.Update(current);
dbContext.SaveChanges();
dbSet.UpdateAndSave(current);
}
}
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
TMessage message = new() { OldValue = current, NewValue = value };
current = value;
if (current != null)
{
current.IsSelected = true;
dbSet.Update(current);
dbContext.SaveChanges();
}
messenger.Send(message);
}
}
}
/// <summary>
/// 数据库当前项
/// 简化对数据库中选中项的管理
/// </summary>
/// <typeparam name="TObservable">绑定类型</typeparam>
/// <typeparam name="TEntity">实体的类型</typeparam>
/// <typeparam name="TMessage">消息的类型</typeparam>
[SuppressMessage("", "SA1402")]
internal class DbCurrent<TObservable, TEntity, TMessage>
where TObservable : class
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TObservable>
{
private readonly DbContext dbContext;
private readonly DbSet<TEntity> dbSet;
private readonly IMessenger messenger;
private readonly Func<TObservable, TEntity> selector;
private TObservable? current;
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="dbSet">数据集</param>
/// <param name="selector">选择器</param>
/// <param name="messenger">消息器</param>
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, Func<TObservable, TEntity> selector, IMessenger messenger)
{
this.dbContext = dbContext;
this.dbSet = dbSet;
this.selector = selector;
this.messenger = messenger;
}
/// <summary>
/// 当前选中的项
/// </summary>
public TObservable? Current
{
get => current;
set
{
// prevent useless sets
if (current == value)
{
return;
}
// only update when not processing a deletion
if (value != null)
{
if (current != null)
{
TEntity entity = selector(current);
entity.IsSelected = false;
dbSet.Update(entity);
dbContext.SaveChanges();
}
}
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
current = value;
if (current != null)
{
TEntity entity = selector(current);
entity.IsSelected = true;
dbSet.Update(entity);
dbContext.SaveChanges();
dbSet.UpdateAndSave(current);
}
messenger.Send(message);

View File

@@ -0,0 +1,66 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Snap.Hutao.Extension;
/// <summary>
/// 数据库集合上下文
/// </summary>
public static class DbSetExtension
{
/// <summary>
/// 获取对应的数据库上下文
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <returns>对应的数据库上下文</returns>
public static DbContext Context<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class
{
return dbSet.GetService<ICurrentDbContext>().Context;
}
/// <summary>
/// 获取或添加一个对应的实体
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="predicate">谓词</param>
/// <param name="entityFactory">实体工厂</param>
/// <param name="added">是否添加</param>
/// <returns>实体</returns>
public static TEntity SingleOrAdd<TEntity>(this DbSet<TEntity> dbSet, Func<TEntity, bool> predicate, Func<TEntity> entityFactory, out bool added)
where TEntity : class
{
added = false;
TEntity? entry = dbSet.SingleOrDefault(predicate);
if (entry == null)
{
entry = entityFactory();
dbSet.Add(entry);
dbSet.Context().SaveChanges();
added = true;
}
return entry;
}
/// <summary>
/// 更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Update(entity);
return dbSet.Context().SaveChanges();
}
}

View File

@@ -8,16 +8,7 @@ namespace Snap.Hutao.Message;
/// <summary>
/// 成就存档切换消息
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
internal class AchievementArchiveChangedMessage : ValueChangedMessage<AchievementArchive>
{
/// <summary>
/// 构造一个新的用户切换消息
/// </summary>
/// <param name="oldArchive">老用户</param>
/// <param name="newArchive">新用户</param>
public AchievementArchiveChangedMessage(AchievementArchive? oldArchive, AchievementArchive? newArchive)
: base(oldArchive, newArchive)
{
}
}

View File

@@ -8,16 +8,7 @@ namespace Snap.Hutao.Message;
/// <summary>
/// 祈愿记录存档切换消息
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
internal class GachaArchiveChangedMessage : ValueChangedMessage<GachaArchive>
{
/// <summary>
/// 构造一个新的用户切换消息
/// </summary>
/// <param name="oldArchive">老用户</param>
/// <param name="newArchive">新用户</param>
public GachaArchiveChangedMessage(GachaArchive? oldArchive, GachaArchive? newArchive)
: base(oldArchive, newArchive)
{
}
}

View File

@@ -8,15 +8,7 @@ namespace Snap.Hutao.Message;
/// <summary>
/// 用户切换消息
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
internal class UserChangedMessage : ValueChangedMessage<User>
{
/// <summary>
/// 构造一个新的用户切换消息
/// </summary>
/// <param name="oldUser">老用户</param>
/// <param name="newUser">新用户</param>
public UserChangedMessage(User? oldUser, User? newUser)
: base(oldUser, newUser)
{
}
}

View File

@@ -7,9 +7,17 @@ namespace Snap.Hutao.Message;
/// 值变化消息
/// </summary>
/// <typeparam name="TValue">值的类型</typeparam>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
internal abstract class ValueChangedMessage<TValue>
where TValue : class
{
/// <summary>
/// 动态访问
/// </summary>
public ValueChangedMessage()
{
}
/// <summary>
/// 构造一个新的值变化消息
/// </summary>
@@ -24,10 +32,10 @@ internal abstract class ValueChangedMessage<TValue>
/// <summary>
/// 旧的值
/// </summary>
public TValue? OldValue { get; private set; }
public TValue? OldValue { get; set; }
/// <summary>
/// 新的值
/// </summary>
public TValue? NewValue { get; private set; }
public TValue? NewValue { get; set; }
}

View File

@@ -24,7 +24,7 @@ public class GachaStatistics
public TypedWishSummary PermanentWish { get; set; } = default!;
/// <summary>
/// 历史
/// 历史卡池
/// </summary>
public List<HistoryWish> HistoryWishes { get; set; } = default!;

View File

@@ -12,6 +12,16 @@ namespace Snap.Hutao.Model.Entity;
[Table("settings")]
public class SettingEntry
{
/// <summary>
/// 游戏路径
/// </summary>
public const string GamePath = "GamePath";
/// <summary>
/// 空的历史记录卡池是否可见
/// </summary>
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
/// <summary>
/// 构造一个新的设置入口
/// </summary>

View File

@@ -13,6 +13,8 @@ internal class SkillIconConverter : ValueConverterBase<string, Uri>
private const string SkillUrl = "https://static.snapgenshin.com/Skill/{0}.png";
private const string TalentUrl = "https://static.snapgenshin.com/Talent/{0}.png";
private static readonly Uri UIIconNone = new("https://static.snapgenshin.com/Bg/UI_Icon_None.png");
/// <summary>
/// 名称转Uri
/// </summary>
@@ -20,6 +22,11 @@ internal class SkillIconConverter : ValueConverterBase<string, Uri>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return UIIconNone;
}
if (name.StartsWith("UI_Talent_"))
{
return new Uri(string.Format(TalentUrl, name));

View File

@@ -40,7 +40,7 @@ internal class AchievementService : IAchievementService
this.appDbContext = appDbContext;
this.logger = logger;
dbCurrent = new(appDbContext, appDbContext.AchievementArchives, messenger);
dbCurrent = new(appDbContext.AchievementArchives, messenger);
achievementDbOperation = new(appDbContext);
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
@@ -21,7 +23,7 @@ public static class LogHelper
{
Type = exception.GetType().ToString(),
Message = exception.Message,
StackTrace = exception.ToString(),
StackTrace = exception.StackTrace,
};
if (exception is AggregateException aggregateException)
@@ -35,12 +37,21 @@ public static class LogHelper
}
}
}
else if (exception.InnerException != null)
if (exception.InnerException != null)
{
current.InnerExceptions ??= new();
current.InnerExceptions.Add(Create(exception.InnerException));
}
StackTrace stackTrace = new(exception, true);
StackFrame[] frames = stackTrace.GetFrames();
if (frames.Length > 0 && frames[0].HasNativeImage())
{
}
return current;
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.Gacha;
using Snap.Hutao.Model.Entity;
@@ -19,14 +21,17 @@ namespace Snap.Hutao.Service.GachaLog.Factory;
internal class GachaStatisticsFactory : IGachaStatisticsFactory
{
private readonly IMetadataService metadataService;
private readonly AppDbContext appDbContext;
/// <summary>
/// 构造一个新的祈愿统计工厂
/// </summary>
/// <param name="metadataService">元数据服务</param>
public GachaStatisticsFactory(IMetadataService metadataService)
/// <param name="appDbContext">数据库上下文</param>
public GachaStatisticsFactory(IMetadataService metadataService, AppDbContext appDbContext)
{
this.metadataService = metadataService;
this.appDbContext = appDbContext;
}
/// <inheritdoc/>
@@ -41,15 +46,29 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
List<GachaEvent> gachaevents = await metadataService.GetGachaEventsAsync().ConfigureAwait(false);
List<HistoryWishBuilder> historyWishBuilders = gachaevents.Select(g => new HistoryWishBuilder(g, nameAvatarMap, nameWeaponMap)).ToList();
SettingEntry? entry = await appDbContext.Settings
.SingleOrDefaultAsync(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible)
.ConfigureAwait(false);
if (entry == null)
{
entry = new(SettingEntry.IsEmptyHistoryWishVisible, true.ToString());
appDbContext.Settings.Add(entry);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
}
bool isEmptyHistoryWishVisible = bool.Parse(entry.Value!);
IOrderedEnumerable<GachaItem> orderedItems = items.OrderBy(i => i.Id);
return await Task.Run(() => CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap)).ConfigureAwait(false);
return await Task.Run(() => CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap, isEmptyHistoryWishVisible)).ConfigureAwait(false);
}
private static GachaStatistics CreateCore(
IOrderedEnumerable<GachaItem> items,
List<HistoryWishBuilder> historyWishBuilders,
Dictionary<int, Avatar> avatarMap,
Dictionary<int, Weapon> weaponMap)
Dictionary<int, Weapon> weaponMap,
bool isEmptyHistoryWishVisible)
{
TypedWishSummaryBuilder permanentWishBuilder = new("奔行世间", TypedWishSummaryBuilder.PermanentWish, 90, 10);
TypedWishSummaryBuilder avatarWishBuilder = new("角色活动", TypedWishSummaryBuilder.AvatarEventWish, 90, 10);
@@ -131,6 +150,7 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
{
// history
HistoryWishes = historyWishBuilders
.Where(b => isEmptyHistoryWishVisible || (!b.IsEmpty))
.OrderByDescending(builder => builder.From)
.ThenBy(builder => builder.ConfigType, new GachaConfigTypeComparar())
.Select(builder => builder.ToHistoryWish()).ToList(),

View File

@@ -67,6 +67,14 @@ internal class HistoryWishBuilder
get => gachaEvent.To;
}
/// <summary>
/// 卡池是否为空
/// </summary>
public bool IsEmpty
{
get => totalCountTracker <= 0;
}
/// <summary>
/// 计数五星角色
/// </summary>

View File

@@ -88,7 +88,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
this.logger = logger;
this.gachaStatisticsFactory = gachaStatisticsFactory;
dbCurrent = new(appDbContext, appDbContext.GachaArchives, messenger);
dbCurrent = new(appDbContext.GachaArchives, messenger);
}
/// <inheritdoc/>

View File

@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Locator;
@@ -16,8 +17,6 @@ namespace Snap.Hutao.Service.Game;
[Injection(InjectAs.Transient, typeof(IGameService))]
internal class GameService : IGameService
{
private const string GamePath = "GamePath";
private readonly AppDbContext appDbContext;
private readonly IMemoryCache memoryCache;
private readonly IEnumerable<IGameLocator> gameLocators;
@@ -38,7 +37,7 @@ internal class GameService : IGameService
/// <inheritdoc/>
public async ValueTask<ValueResult<bool, string>> GetGamePathAsync()
{
string key = $"{nameof(GameService)}.Cache.{GamePath}";
string key = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}";
if (memoryCache.TryGetValue(key, out object? value))
{
@@ -46,16 +45,11 @@ internal class GameService : IGameService
}
else
{
SettingEntry? entry = await appDbContext.Settings
.SingleOrDefaultAsync(e => e.Key == GamePath)
.ConfigureAwait(false);
SettingEntry entry = appDbContext.Settings.SingleOrAdd(e => e.Key == SettingEntry.GamePath, () => new(SettingEntry.GamePath, null), out bool added);
// Cannot find in setting
if (entry == null)
if (added)
{
// Create new setting
entry = new(GamePath, null);
// Try locate by registry
IGameLocator locator = gameLocators.Single(l => l.Name == nameof(RegistryLauncherLocator));
ValueResult<bool, string> result = await locator.LocateGamePathAsync().ConfigureAwait(false);
@@ -71,7 +65,7 @@ internal class GameService : IGameService
{
// Save result.
entry.Value = result.Value;
await appDbContext.Settings.AddAsync(entry).ConfigureAwait(false);
appDbContext.Settings.Update(entry);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
}
else

View File

@@ -29,8 +29,8 @@ internal class RegistryLauncherLocator : IGameLocator
}
else
{
string path = result.Value;
string configPath = Path.Combine(path, "config.ini");
string? path = Path.GetDirectoryName(result.Value);
string configPath = Path.Combine(path!, "config.ini");
string? escapedPath = null;
using (FileStream stream = File.OpenRead(configPath))
{
@@ -40,7 +40,8 @@ internal class RegistryLauncherLocator : IGameLocator
if (escapedPath != null)
{
return Task.FromResult<ValueResult<bool, string>>(new(true, Unescape(escapedPath)));
string gamePath = Path.Combine(Unescape(escapedPath), "YuanShen.exe");
return Task.FromResult<ValueResult<bool, string>>(new(true, gamePath));
}
}

View File

@@ -0,0 +1,160 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Service.Hutao;
/// <summary>
/// 胡桃 API 缓存
/// </summary>
[Injection(InjectAs.Singleton, typeof(IHtaoCache))]
internal class HutaoCache : IHtaoCache
{
private readonly IHutaoService hutaoService;
private readonly IMetadataService metadataService;
private Dictionary<int, Avatar>? idAvatarExtendedMap;
/// <summary>
/// 构造一个新的胡桃 API 缓存
/// </summary>
/// <param name="hutaoService">胡桃服务</param>
/// <param name="metadataService">元数据服务</param>
public HutaoCache(IHutaoService hutaoService, IMetadataService metadataService)
{
this.hutaoService = hutaoService;
this.metadataService = metadataService;
}
/// <inheritdoc/>
public List<ComplexAvatarRank>? AvatarUsageRanks { get; set; }
/// <inheritdoc/>
public List<ComplexAvatarRank>? AvatarAppearanceRanks { get; set; }
/// <inheritdoc/>
public List<ComplexAvatarConstellationInfo>? AvatarConstellationInfos { get; set; }
/// <inheritdoc/>
public List<ComplexTeamRank>? TeamAppearances { get; set; }
/// <inheritdoc/>
public Overview? Overview { get; set; }
/// <inheritdoc/>
public List<ComplexAvatarCollocation>? AvatarCollocations { get; set; }
/// <inheritdoc/>
public async ValueTask<bool> InitializeForDatabaseViewModelAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<int, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
Task avatarAppearanceRankTask = AvatarAppearanceRankAsync(idAvatarMap);
Task avatarUsageRank = AvatarUsageRanksAsync(idAvatarMap);
Task avatarConstellationInfoTask = AvatarConstellationInfosAsync(idAvatarMap);
Task teamAppearanceTask = TeamAppearancesAsync(idAvatarMap);
Task ovewviewTask = OverviewAsync();
await Task.WhenAll(
avatarAppearanceRankTask,
avatarUsageRank,
avatarConstellationInfoTask,
teamAppearanceTask,
ovewviewTask)
.ConfigureAwait(false);
return true;
}
return false;
}
/// <inheritdoc/>
public async ValueTask<bool> InitializeForWikiAvatarViewModelAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<int, Avatar> idAvatarMap = await GetIdAvatarMapExtendedAsync().ConfigureAwait(false);
Dictionary<int, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
Dictionary<int, Model.Metadata.Reliquary.ReliquarySet> idReliquarySetMap = await metadataService.GetEquipAffixIdToReliquarySetMapAsync().ConfigureAwait(false);
// AvatarCollocation
List<AvatarCollocation> avatarCollocationsRaw = await hutaoService.GetAvatarCollocationsAsync().ConfigureAwait(false);
AvatarCollocations = avatarCollocationsRaw.Select(co =>
{
return new ComplexAvatarCollocation(idAvatarMap[co.AvatarId])
{
Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
Weapons = co.Weapons.Select(w => new ComplexWeapon(idWeaponMap[w.Item], w.Rate)).ToList(),
ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
};
}).ToList();
return true;
}
return false;
}
private async ValueTask<Dictionary<int, Avatar>> GetIdAvatarMapExtendedAsync()
{
if (idAvatarExtendedMap == null)
{
Dictionary<int, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
idAvatarExtendedMap = new(idAvatarMap)
{
[10000005] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
[10000007] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
};
}
return idAvatarExtendedMap;
}
private async Task AvatarAppearanceRankAsync(Dictionary<int, Avatar> idAvatarMap)
{
List<AvatarAppearanceRank> avatarAppearanceRanksRaw = await hutaoService.GetAvatarAppearanceRanksAsync().ConfigureAwait(false);
AvatarAppearanceRanks = avatarAppearanceRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
{
Floor = $"第 {rank.Floor} 层",
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
}).ToList();
}
private async Task AvatarUsageRanksAsync(Dictionary<int, Avatar> idAvatarMap)
{
List<AvatarUsageRank> avatarUsageRanksRaw = await hutaoService.GetAvatarUsageRanksAsync().ConfigureAwait(false);
AvatarUsageRanks = avatarUsageRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
{
Floor = $"第 {rank.Floor} 层",
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
}).ToList();
}
private async Task AvatarConstellationInfosAsync(Dictionary<int, Avatar> idAvatarMap)
{
List<AvatarConstellationInfo> avatarConstellationInfosRaw = await hutaoService.GetAvatarConstellationInfosAsync().ConfigureAwait(false);
AvatarConstellationInfos = avatarConstellationInfosRaw.OrderBy(i => i.HoldingRate).Select(info =>
{
return new ComplexAvatarConstellationInfo(idAvatarMap[info.AvatarId], info.HoldingRate, info.Constellations.Select(x => x.Rate));
}).ToList();
}
private async Task TeamAppearancesAsync(Dictionary<int, Avatar> idAvatarMap)
{
List<TeamAppearance> teamAppearancesRaw = await hutaoService.GetTeamAppearancesAsync().ConfigureAwait(false);
TeamAppearances = teamAppearancesRaw.OrderByDescending(t => t.Floor).Select(team => new ComplexTeamRank(team, idAvatarMap)).ToList();
}
private async Task OverviewAsync()
{
Overview = await hutaoService.GetOverviewAsync().ConfigureAwait(false);
}
}

View File

@@ -2,11 +2,10 @@
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Service;
namespace Snap.Hutao.Service.Hutao;
/// <summary>
/// 胡桃 API 服务
@@ -75,4 +74,4 @@ internal class HutaoService : IHutaoService
T web = await taskFunc(default).ConfigureAwait(false);
return memoryCache.Set(key, web, TimeSpan.FromMinutes(30));
}
}
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Service.Hutao;
/// <summary>
/// 胡桃 API 缓存
/// </summary>
internal interface IHtaoCache
{
/// <summary>
/// 角色使用率
/// </summary>
List<ComplexAvatarRank>? AvatarUsageRanks { get; set; }
/// <summary>
/// 角色上场率
/// </summary>
List<ComplexAvatarRank>? AvatarAppearanceRanks { get; set; }
/// <summary>
/// 角色命座信息
/// </summary>
List<ComplexAvatarConstellationInfo>? AvatarConstellationInfos { get; set; }
/// <summary>
/// 队伍出场
/// </summary>
List<ComplexTeamRank>? TeamAppearances { get; set; }
/// <summary>
/// 总览数据
/// </summary>
Overview? Overview { get; set; }
/// <summary>
/// 角色搭配
/// </summary>
List<ComplexAvatarCollocation>? AvatarCollocations { get; set; }
/// <summary>
/// 为数据库视图模型初始化
/// </summary>
/// <returns>任务</returns>
ValueTask<bool> InitializeForDatabaseViewModelAsync();
/// <summary>
/// 为Wiki角色视图模型初始化
/// </summary>
/// <returns>任务</returns>
ValueTask<bool> InitializeForWikiAvatarViewModelAsync();
}

View File

@@ -3,7 +3,7 @@
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.Hutao;
/// <summary>
/// 胡桃 API 服务

View File

@@ -73,7 +73,7 @@ internal class UserService : IUserService
}
}
Message.UserChangedMessage message = new(currentUser, value);
Message.UserChangedMessage message = new() { OldValue = currentUser, NewValue = value };
// 当删除到无用户时也能正常反应状态
currentUser = value;

View File

@@ -28,11 +28,7 @@
<StartupObject>Snap.Hutao.Program</StartupObject>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT</DefineConstants>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<DebugSymbols>true</DebugSymbols>
<DebugType>embedded</DebugType>
</PropertyGroup>

View File

@@ -25,6 +25,17 @@
<Grid>
<Pivot>
<Pivot.RightHeader>
<CommandBar>
<AppBarButton>
<AppBarButton.Flyout>
<Flyout>
</Flyout>
</AppBarButton.Flyout>
</AppBarButton>
</CommandBar>
</Pivot.RightHeader>
<PivotItem Header="角色使用">
<Pivot ItemsSource="{Binding AvatarUsageRanks}">
<Pivot.HeaderTemplate>
@@ -38,7 +49,7 @@
<GridView
SelectionMode="None"
ItemsSource="{Binding Avatars}"
Margin="12,12,0,0">
Margin="12,12,0,-12">
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
<Setter Property="Margin" Value="0,0,12,12"/>
@@ -78,7 +89,7 @@
<GridView
SelectionMode="None"
ItemsSource="{Binding Avatars}"
Margin="12,12,0,0">
Margin="12,12,0,-12">
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
<Setter Property="Margin" Value="0,0,12,12"/>
@@ -208,7 +219,7 @@
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0">
<ScrollViewer Grid.Column="0" Margin="0,12,0,0">
<ItemsControl ItemsSource="{Binding Up}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@@ -219,7 +230,7 @@
<DataTemplate>
<Border
CornerRadius="{StaticResource CompatCornerRadius}"
Margin="12,12,12,0"
Margin="12,0,12,12"
Background="{StaticResource CardBackgroundFillColorDefault}">
<Grid Margin="6">
<Grid.ColumnDefinitions>
@@ -254,7 +265,7 @@
</ItemsControl>
</ScrollViewer>
<ScrollViewer Grid.Column="1">
<ScrollViewer Grid.Column="1" Margin="0,12,0,0">
<ItemsControl ItemsSource="{Binding Down}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@@ -265,7 +276,7 @@
<DataTemplate>
<Border
CornerRadius="{StaticResource CompatCornerRadius}"
Margin="12,12,12,0"
Margin="12,0,12,12"
Background="{StaticResource CardBackgroundFillColorDefault}">
<Grid Margin="6">
<Grid.ColumnDefinitions>

View File

@@ -73,10 +73,28 @@
Severity="Informational"
Message="都说了没有了"
IsOpen="True"
CornerRadius="0,0,4,4"/>
CornerRadius="0,0,4,4">
<InfoBar.ActionButton>
<Button HorizontalAlignment="Right" Width="1" Content="没用的按钮"/>
</InfoBar.ActionButton>
</InfoBar>
</sc:SettingExpander>
</sc:SettingsGroup>
<sc:SettingsGroup Header="祈愿记录">
<sc:Setting
Icon="&#xE81C;"
Header="无记录的历史祈愿活动"
Description="在祈愿记录页面显示或隐藏无记录的历史祈愿活动">
<ToggleSwitch
Style="{StaticResource ToggleSwitchSettingStyle}"
OnContent="显示"
OffContent="隐藏"
IsOn="{Binding IsEmptyHistoryWishVisible,Mode=TwoWay}"/>
</sc:Setting>
</sc:SettingsGroup>
<sc:SettingsGroup Header="测试功能">
<sc:Setting
Icon="&#xEC25;"

View File

@@ -215,7 +215,7 @@
Source="{Binding Selected,Converter={StaticResource AvatarNameCardPicConverter}}"/>
<ScrollViewer>
<StackPanel Margin="0,0,20,16">
<StackPanel Margin="0,0,20,16" MaxWidth="800" HorizontalAlignment="Left">
<!--简介-->
<Grid
Margin="16,16,0,16"

View File

@@ -3,14 +3,9 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Control;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Hutao.Model;
using Snap.Hutao.Service.Hutao;
namespace Snap.Hutao.ViewModel;
@@ -20,8 +15,7 @@ namespace Snap.Hutao.ViewModel;
[Injection(InjectAs.Transient)]
internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
{
private readonly IHutaoService hutaoService;
private readonly IMetadataService metadataService;
private readonly IHtaoCache hutaoCache;
private List<ComplexAvatarRank>? avatarUsageRanks;
private List<ComplexAvatarRank>? avatarAppearanceRanks;
@@ -31,13 +25,12 @@ internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
/// <summary>
/// 构造一个新的胡桃数据库视图模型
/// </summary>
/// <param name="hutaoService">胡桃服务</param>
/// <param name="hutaoCache">胡桃服务缓存</param>
/// <param name="metadataService">元数据服务</param>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public HutaoDatabaseViewModel(IHutaoService hutaoService, IMetadataService metadataService, IAsyncRelayCommandFactory asyncRelayCommandFactory)
public HutaoDatabaseViewModel(IHtaoCache hutaoCache, IAsyncRelayCommandFactory asyncRelayCommandFactory)
{
this.hutaoService = hutaoService;
this.metadataService = metadataService;
this.hutaoCache = hutaoCache;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
}
@@ -72,80 +65,12 @@ internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
private async Task OpenUIAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
if (await hutaoCache.InitializeForDatabaseViewModelAsync().ConfigureAwait(true))
{
Dictionary<int, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
idAvatarMap = new(idAvatarMap)
{
[10000005] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
[10000007] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
};
Dictionary<int, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
Dictionary<int, Model.Metadata.Reliquary.ReliquarySet> idReliquarySetMap = await metadataService.GetEquipAffixIdToReliquarySetMapAsync().ConfigureAwait(false);
List<ComplexAvatarRank> avatarAppearanceRanksLocal = default!;
List<ComplexAvatarRank> avatarUsageRanksLocal = default!;
List<ComplexAvatarConstellationInfo> avatarConstellationInfosLocal = default!;
List<ComplexTeamRank> teamAppearancesLocal = default!;
Task avatarAppearanceRankTask = Task.Run(async () =>
{
// AvatarAppearanceRank
List<AvatarAppearanceRank> avatarAppearanceRanksRaw = await hutaoService.GetAvatarAppearanceRanksAsync().ConfigureAwait(false);
avatarAppearanceRanksLocal = avatarAppearanceRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
{
Floor = $"第 {rank.Floor} 层",
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
}).ToList();
});
Task avatarUsageRank = Task.Run(async () =>
{
// AvatarUsageRank
List<AvatarUsageRank> avatarUsageRanksRaw = await hutaoService.GetAvatarUsageRanksAsync().ConfigureAwait(false);
avatarUsageRanksLocal = avatarUsageRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
{
Floor = $"第 {rank.Floor} 层",
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
}).ToList();
});
Task avatarConstellationInfoTask = Task.Run(async () =>
{
// AvatarConstellationInfo
List<AvatarConstellationInfo> avatarConstellationInfosRaw = await hutaoService.GetAvatarConstellationInfosAsync().ConfigureAwait(false);
avatarConstellationInfosLocal = avatarConstellationInfosRaw.OrderBy(i => i.HoldingRate).Select(info =>
{
return new ComplexAvatarConstellationInfo(idAvatarMap[info.AvatarId], info.HoldingRate, info.Constellations.Select(x => x.Rate));
}).ToList();
});
Task teamAppearanceTask = Task.Run(async () =>
{
List<TeamAppearance> teamAppearancesRaw = await hutaoService.GetTeamAppearancesAsync().ConfigureAwait(false);
teamAppearancesLocal = teamAppearancesRaw.OrderByDescending(t => t.Floor).Select(team => new ComplexTeamRank(team, idAvatarMap)).ToList();
});
await Task.WhenAll(avatarAppearanceRankTask, avatarUsageRank, avatarConstellationInfoTask, teamAppearanceTask).ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
AvatarAppearanceRanks = avatarAppearanceRanksLocal;
AvatarUsageRanks = avatarUsageRanksLocal;
AvatarConstellationInfos = avatarConstellationInfosLocal;
TeamAppearances = teamAppearancesLocal;
//// AvatarCollocation
//List<AvatarCollocation> avatarCollocationsRaw = await hutaoService.GetAvatarCollocationsAsync().ConfigureAwait(false);
//List<ComplexAvatarCollocation> avatarCollocationsLocal = avatarCollocationsRaw.Select(co =>
//{
// return new ComplexAvatarCollocation(idAvatarMap[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();
AvatarAppearanceRanks = hutaoCache.AvatarAppearanceRanks;
AvatarUsageRanks = hutaoCache.AvatarUsageRanks;
AvatarConstellationInfos = hutaoCache.AvatarConstellationInfos;
TeamAppearances = hutaoCache.TeamAppearances;
}
}
}

View File

@@ -2,6 +2,9 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity;
namespace Snap.Hutao.ViewModel;
@@ -11,13 +14,24 @@ namespace Snap.Hutao.ViewModel;
[Injection(InjectAs.Transient)]
internal class SettingViewModel : ObservableObject
{
private readonly AppDbContext appDbContext;
private readonly SettingEntry isEmptyHistoryWishVisibleEntry;
private bool isEmptyHistoryWishVisible;
/// <summary>
/// 构造一个新的测试视图模型
/// </summary>
/// <param name="appDbContext">数据库上下文</param>
/// <param name="experimental">实验性功能</param>
public SettingViewModel(ExperimentalFeaturesViewModel experimental)
public SettingViewModel(AppDbContext appDbContext, ExperimentalFeaturesViewModel experimental)
{
this.appDbContext = appDbContext;
Experimental = experimental;
isEmptyHistoryWishVisibleEntry = appDbContext.Settings
.SingleOrAdd(e => e.Key == SettingEntry.IsEmptyHistoryWishVisible, () => new(SettingEntry.IsEmptyHistoryWishVisible, true.ToString()), out _);
IsEmptyHistoryWishVisible = bool.Parse(isEmptyHistoryWishVisibleEntry.Value!);
}
/// <summary>
@@ -28,6 +42,20 @@ internal class SettingViewModel : ObservableObject
get => Core.CoreEnvironment.Version.ToString();
}
/// <summary>
/// 空的历史卡池是否可见
/// </summary>
public bool IsEmptyHistoryWishVisible
{
get => isEmptyHistoryWishVisible;
set
{
SetProperty(ref isEmptyHistoryWishVisible, value);
isEmptyHistoryWishVisibleEntry.Value = value.ToString();
appDbContext.Settings.UpdateAndSave(isEmptyHistoryWishVisibleEntry);
}
}
/// <summary>
/// 实验性功能
/// </summary>

View File

@@ -27,4 +27,19 @@ public class Overview
/// 满星数
/// </summary>
public int SpiralAbyssFullStar { get; set; }
/// <summary>
/// 统计时间
/// </summary>
public long Timestamp { get; set; }
/// <summary>
/// 总时间
/// </summary>
public double TimeTotal { get; set; }
/// <summary>
/// 平均时间
/// </summary>
public double TimeAverage { get; set; }
}