Compare commits

..

3 Commits

Author SHA1 Message Date
qhy040404
676fe87f04 Update Snap.Hutao.csproj 2024-04-21 23:49:53 +08:00
qhy040404
2056fa8f4d extract launcher 2024-04-20 15:28:07 +08:00
qhy040404
47debe42ec allow to open elevated app from taskbar 2024-04-20 15:16:22 +08:00
106 changed files with 840 additions and 1475 deletions

View File

@@ -104,7 +104,6 @@ internal class ScopedPage : Page
// Dispose the scope
pageScope.Dispose();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
}
}
}

View File

@@ -33,17 +33,17 @@ internal static class IocConfiguration
return services
.AddTransient(typeof(Database.ScopedDbCurrent<,>))
.AddTransient(typeof(Database.ScopedDbCurrent<,,>))
.AddDbContextPool<AppDbContext>(AddDbContextCore);
.AddDbContext<AppDbContext>(AddDbContextCore);
}
private static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
{
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
// Temporarily create a context
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString))
using (AppDbContext context = AppDbContext.Create(sqlConnectionString))
{
if (context.Database.GetPendingMigrations().Any())
{

View File

@@ -1,7 +1,5 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Logging;
namespace Snap.Hutao.Core.Diagnostics;
internal readonly struct MeasureExecutionToken : IDisposable
@@ -19,6 +17,6 @@ internal readonly struct MeasureExecutionToken : IDisposable
public void Dispose()
{
logger.LogColorizedDebug(("{Caller} toke {Time} ms", ConsoleColor.Gray), (callerName, ConsoleColor.Yellow), (stopwatch.GetElapsedTime().TotalMilliseconds, ConsoleColor.DarkGreen));
logger.LogDebug("{Caller} toke {Time} ms", callerName, stopwatch.GetElapsedTime().TotalMilliseconds);
}
}

View File

@@ -1,13 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.System.Com;
using Snap.Hutao.Win32.UI.Shell;
using System.IO;
using static Snap.Hutao.Win32.Macros;
using static Snap.Hutao.Win32.Ole32;
using static Snap.Hutao.Win32.Shell32;
namespace Snap.Hutao.Core.IO;
@@ -45,57 +39,4 @@ internal static class FileOperation
File.Move(sourceFileName, destFileName, false);
return true;
}
public static unsafe bool UnsafeMove(string sourceFileName, string destFileName)
{
bool result = false;
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(sourceFileName, default, in IShellItem.IID, out IShellItem* pSourceShellItem)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(destFileName, default, in IShellItem.IID, out IShellItem* pDestShellItem)))
{
pFileOperation->MoveItem(pSourceShellItem, pDestShellItem, default, default);
if (SUCCEEDED(pFileOperation->PerformOperations()))
{
result = true;
}
pDestShellItem->Release();
}
pSourceShellItem->Release();
}
pFileOperation->Release();
}
return result;
}
public static unsafe bool UnsafeDelete(string path)
{
bool result = false;
if (SUCCEEDED(CoCreateInstance(in Win32.UI.Shell.FileOperation.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IFileOperation.IID, out IFileOperation* pFileOperation)))
{
if (SUCCEEDED(SHCreateItemFromParsingName(path, default, in IShellItem.IID, out IShellItem* pShellItem)))
{
pFileOperation->DeleteItem(pShellItem, default);
if (SUCCEEDED(pFileOperation->PerformOperations()))
{
result = true;
}
pShellItem->Release();
}
pFileOperation->Release();
}
return result;
}
}

View File

@@ -10,7 +10,7 @@ internal static class Hash
{
public static string SHA1HexString(string input)
{
return HashCore(System.Convert.ToHexString, SHA1.HashData, Encoding.UTF8.GetBytes, input);
return HashCore(BitConverter.ToString, SHA1.HashData, Encoding.UTF8.GetBytes, input);
}
private static TResult HashCore<TInput, TResult>(Func<byte[], TResult> resultConverter, Func<byte[], byte[]> hashMethod, Func<TInput, byte[]> bytesConverter, TInput input)

View File

@@ -1,40 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO;
internal sealed class StreamReaderWriter : IDisposable
{
private readonly StreamReader reader;
private readonly StreamWriter writer;
public StreamReaderWriter(StreamReader reader, StreamWriter writer)
{
this.reader = reader;
this.writer = writer;
}
public StreamReader Reader { get => reader; }
public StreamWriter Writer { get => writer; }
/// <inheritdoc cref="StreamReader.ReadLineAsync(CancellationToken)"/>
public ValueTask<string?> ReadLineAsync(CancellationToken token)
{
return reader.ReadLineAsync(token);
}
/// <inheritdoc cref="StreamWriter.WriteAsync(string?)"/>
public Task WriteAsync(string value)
{
return writer.WriteAsync(value);
}
public void Dispose()
{
writer.Dispose();
reader.Dispose();
}
}

View File

@@ -21,11 +21,6 @@ internal readonly struct LogArgument
return new(argument);
}
public static implicit operator LogArgument(double argument)
{
return new(argument);
}
public static implicit operator LogArgument((object? Argument, ConsoleColor Foreground) tuple)
{
return new(tuple.Argument, tuple.Foreground);

View File

@@ -20,15 +20,10 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
{
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
string elevatedLauncherPath = Path.Combine(runtimeOptions.DataFolder, "Snap.Hutao.Elevated.Launcher.exe");
try
{
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri);
await iconFile.OverwriteCopyAsync(targetLogoPath).ConfigureAwait(false);
Uri elevatedLauncherUri = "ms-appx:///Snap.Hutao.Elevated.Launcher.exe".ToUri();
StorageFile launcherFile = await StorageFile.GetFileFromApplicationUriAsync(elevatedLauncherUri);
await launcherFile.OverwriteCopyAsync(elevatedLauncherPath).ConfigureAwait(false);
@@ -38,10 +33,10 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
return false;
}
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath, elevatedLauncherPath);
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(elevatedLauncherPath);
}
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath, string elevatedLauncherPath)
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string elevatedLauncherPath)
{
bool result = false;
@@ -52,7 +47,6 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
pShellLink->SetPath(elevatedLauncherPath);
pShellLink->SetArguments(runtimeOptions.FamilyName);
pShellLink->SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
pShellLink->SetIconLocation(targetLogoPath, 0);
if (SUCCEEDED(pShellLink->QueryInterface(in IPersistFile.IID, out IPersistFile* pPersistFile)))
{

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.Model.Entity;
/// </summary>
[HighQuality]
[Table("cultivate_entries")]
internal sealed class CultivateEntry : IDbMappingForeignKeyFrom<CultivateEntry, CultivateType, uint>, IAppDbEntity
internal sealed class CultivateEntry : IDbMappingForeignKeyFrom<CultivateEntry, CultivateType, uint>
{
/// <summary>
/// 内部Id

View File

@@ -3,7 +3,6 @@
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Abstraction;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -14,7 +13,7 @@ namespace Snap.Hutao.Model.Entity;
/// </summary>
[HighQuality]
[Table("cultivate_projects")]
internal sealed class CultivateProject : ISelectable, IMappingFrom<CultivateProject, string, string>, IAppDbEntity
internal sealed class CultivateProject : ISelectable, IMappingFrom<CultivateProject, string, string>
{
/// <summary>
/// 内部Id

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Model.Entity.Configuration;
using System.Diagnostics;
@@ -25,8 +24,18 @@ internal sealed class AppDbContext : DbContext
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
logger = this.GetService<ILogger<AppDbContext>>();
logger?.LogColorizedInformation("{Name}[{Id}] {Action}", nameof(AppDbContext), (ContextId, ConsoleColor.DarkCyan), ("created", ConsoleColor.Green));
}
/// <summary>
/// 构造一个新的应用程序数据库上下文
/// </summary>
/// <param name="options">选项</param>
/// <param name="logger">日志器</param>
public AppDbContext(DbContextOptions<AppDbContext> options, ILogger<AppDbContext> logger)
: this(options)
{
this.logger = logger;
logger.LogColorizedInformation("{Name}[{Id}] {Action}", nameof(AppDbContext), (ContextId, ConsoleColor.DarkCyan), ("created", ConsoleColor.Green));
}
public DbSet<SettingEntry> Settings { get; set; } = default!;
@@ -65,14 +74,14 @@ internal sealed class AppDbContext : DbContext
public DbSet<SpiralAbyssEntry> SpiralAbysses { get; set; } = default!;
public static AppDbContext Create(IServiceProvider serviceProvider, string sqlConnectionString)
/// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>
/// <param name="sqlConnectionString">连接字符串</param>
/// <returns>应用程序数据库上下文</returns>
public static AppDbContext Create(string sqlConnectionString)
{
DbContextOptions<AppDbContext> options = new DbContextOptionsBuilder<AppDbContext>()
.UseApplicationServiceProvider(serviceProvider)
.UseSqlite(sqlConnectionString)
.Options;
return new(options);
return new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options);
}
/// <inheritdoc/>

View File

@@ -18,7 +18,7 @@ internal sealed class AppDbContextDesignTimeFactory : IDesignTimeDbContextFactor
#if DEBUG
// TODO: replace with your own database file path.
string userdataDbName = @"D:\Hutao\Userdata.db";
return AppDbContext.Create(default!, $"Data Source={userdataDbName}");
return AppDbContext.Create($"Data Source={userdataDbName}");
#else
throw Must.NeverHappen();
#endif

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Intrinsic;
internal enum QuestType
{
AQ,
FQ,
LQ,
EQ,
DQ,
IQ,
VQ,
WQ,
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using WinRT;

View File

@@ -2922,19 +2922,13 @@
<value>Weapon WIKI</value>
</data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓Event Duration〓|〓Quest Start Time〓).*?Permanently.*?Version.*?(\d\.\d).*?update</value>
<value>(?:〓Event Duration〓|〓Quest Start Time〓).*?\d\.\dthe Version update(?:after|)Permanently available</value>
</data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓Event Duration〓.*?Available throughout the entirety of Version (\d\.\d)</value>
<value>〓Event Duration〓.*?\d\.\d Available throughout the entirety of Version</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓Event Duration〓|Event Wish Duration|【Availability Duration】|〓Discount Period〓).*?After.*?(\d\.\d).*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>Dear.*?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>Version \d\.\d Update Maintenance Preview</value>
<value>(?:〓Event Duration〓|Event Wish Duration|【Availability Duration】|〓Discount Period〓).*?(\d\.\dAfter the Version update).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓Update Maintenance Duration〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -2922,19 +2922,13 @@
<value>Senjata WIKI</value>
</data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓Durasi Event〓|〓Waktu Mulai Misi〓).*?(\d\.\d)the Version update(?:after|)Selamanya Tersedia</value>
<value>(?:〓Durasi Event〓|〓Waktu Mulai Misi〓).*?\d\.\dthe Version update(?:after|)Selamanya Tersedia</value>
</data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓Durasi Event〓.*?(\d\.\d) Tersedia selama versi ini</value>
<value>〓Durasi Event〓.*?\d\.\d Tersedia selama versi ini</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓Waktu Acara〓|Waktu Menginginkan|【Waktu Peluncuran】|〓Waktu Diskon〓).*?(\d\.\d) Setelah Pembaruan Versi.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>将于&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;进行版本更新维护</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新维护预告</value>
<value>(?:〓Waktu Acara〓|Waktu Menginginkan|【Waktu Peluncuran】|〓Waktu Diskon〓).*?(\d\.\d Setelah Pembaruan Versi).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓Durasi Pemeliharaan Pembaruan.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -2922,25 +2922,19 @@
<value>武器一覧</value>
</data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓イベント期間〓|〓任務開(?:始|放)時間〓).*?(\d\.\d).*?開放</value>
<value>(?:〓イベント期間〓|〓任務開時間〓).*?\d\.\dバージョンアップ(?:完了|)後常設オープン</value>
</data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓イベント期間〓.*?(\d\.\d)バージョン</value>
<value>〓イベント期間〓.*?\d\.\dバージョン期間オープン</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓イベント期間〓|祈願期間|【開始日時】).*?(\d\.\d)バージョンアップ.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>親愛.*?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>Ver\.\d\.\dバージョンアップのお知らせ</value>
<value>(?:〓イベント期間〓|祈願期間|【開始日時】).*?(\d\.\dバージョンアップ完了後).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓メンテナンス時間〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTitle" xml:space="preserve">
<value>Ver\.\d\.\d.*?正式リリース</value>
<value>Ver.\d\.\d.+正式リリース</value>
</data>
<data name="WebAnnouncementTimeDaysBeginFormat" xml:space="preserve">
<value>{0} 日後に開始</value>

View File

@@ -2922,19 +2922,13 @@
<value>무기 자료</value>
</data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)后永久开放</value>
<value>(?:〓活动时间〓|〓任务开放时间〓).*?\d\.\d版本更新(?:完成|)后永久开放</value>
</data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓活动时间〓.*?(\d\.\d)版本期间持续开放</value>
<value>〓活动时间〓.*?\d\.\d版本期间持续开放</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>将于&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;进行版本更新维护</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新维护预告</value>
<value>(?:〓活动时间〓|祈愿时间|【上架时间】).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓更新时间〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -2922,19 +2922,13 @@
<value>Wiki de armas</value>
</data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓Duração do evento〓|〓Hora de início da missão〓).*?(\d\.\d)a atualização da versão(?:after|)Disponível permanentemente</value>
<value>(?:〓Duração do evento〓|〓Hora de início da missão〓).*?\d\.\da atualização da versão(?:after|)Disponível permanentemente</value>
</data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓Duração do evento〓.*?(\d\.\d) Disponível em toda as versões</value>
<value>〓Duração do evento〓.*?\d\.\d Disponível em toda as versões</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓Duração do evento〓|Duração da oração do evento|【Duração da disponibilidade】).*?(\d\.\d)Após a atualização da versão.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>将于&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;进行版本更新维护</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新维护预告</value>
<value>(?:〓Duração do evento〓|Duração da oração do evento|【Duração da disponibilidade】).*?(\d\.\dApós a atualização da versão).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓Duração da manutenção da atualização.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -1361,21 +1361,6 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>是否永久删除用户数据</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<value>前往登录</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>当前未登录胡桃账号,上传深渊数据无法获赠胡桃云时长</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>继续上传</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">
<value>上传深渊数据</value>
</data>
<data name="ViewDialogUpdatePackageDownloadUpdatelogLinkContent" xml:space="preserve">
<value>查看更新日志</value>
</data>
<data name="ViewDialogUserDocumentAction" xml:space="preserve">
<value>立即前往</value>
</data>
@@ -1736,9 +1721,6 @@
<data name="ViewModelSettingGeetestCustomUrlSucceed" xml:space="preserve">
<value>无感验证复合 Url 配置成功</value>
</data>
<data name="ViewModelSettingResetStaticResourceProgress" xml:space="preserve">
<value>正在重置图片资源</value>
</data>
<data name="ViewModelSettingSetDataFolderSuccess" xml:space="preserve">
<value>设置数据目录成功,重启以应用更改</value>
</data>
@@ -2907,7 +2889,7 @@
<value>上传数据</value>
</data>
<data name="ViewTitileUpdatePackageDownloadContent" xml:space="preserve">
<value>是否立即下载</value>
<value>是否立即下载</value>
</data>
<data name="ViewTitileUpdatePackageDownloadFailedMessage" xml:space="preserve">
<value>下载更新失败</value>
@@ -3006,19 +2988,13 @@
<value>武器资料</value>
</data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)后永久开放</value>
<value>(?:〓活动时间〓|〓任务开放时间〓).*?\d\.\d版本更新(?:完成|)后永久开放</value>
</data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓活动时间〓.*?(\d\.\d)版本期间持续开放</value>
<value>〓活动时间〓.*?\d\.\d版本期间持续开放</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>将于&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;进行版本更新维护</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新维护预告</value>
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓更新时间〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -2922,19 +2922,13 @@
<value>武器资料</value>
</data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|〓任务开放时间〓).*?(\d\.\d)版本更新(?:完成|)后永久开放</value>
<value>(?:〓活动时间〓|〓任务开放时间〓).*?\d\.\d版本更新(?:完成|)后永久开放</value>
</data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓活动时间〓.*?(\d\.\d)版本期间持续开放</value>
<value>〓活动时间〓.*?\d\.\d版本期间持续开放</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活动时间〓|祈愿时间|【上架时间】|〓折扣时间〓).*?(\d\.\d)版本更新后.*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>将于&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;进行版本更新维护</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新维护预告</value>
<value>(?:〓活动时间〓|祈愿时间|【上架时间】).*?(\d\.\d版本更新后).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓更新时间〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -2922,19 +2922,13 @@
<value>武器資料</value>
</data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓活動時間〓|〓任務開放時間〓).*?(\d\.\d)版本更新(?:完成|)後永久開放</value>
<value>(?:〓活動時間〓|〓任務開放時間〓).*?\d\.\d版本更新(?:完成|)後永久開放</value>
</data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓活動時間〓.*?(\d\.\d)版本期間持續開放</value>
<value>〓活動時間〓.*?\d\.\d版本期間持續開放</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<value>(?:〓活動時間〓|祈願時間|【上架時間】|〓折扣時間〓).*?(\d\.\d).*?版本更新後.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTime" xml:space="preserve">
<value>親愛.*?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdatePreviewTitle" xml:space="preserve">
<value>\d\.\d版本更新維護預告</value>
<value>(?:〓活動時間〓|祈願時間|【上架時間】|〓折扣時間〓).*?(\d\.\d版本更新後).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
</data>
<data name="WebAnnouncementMatchVersionUpdateTime" xml:space="preserve">
<value>〓更新時間〓.+?&amp;lt;t class=\"t_(?:gl|lc)\".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Abstraction;
namespace Snap.Hutao.Service.Abstraction;
@@ -11,25 +12,13 @@ internal static class AppDbServiceAppDbEntityExtension
public static int DeleteByInnerId<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class, IAppDbEntity
{
return service.DeleteByInnerId(entity.InnerId);
}
public static int DeleteByInnerId<TEntity>(this IAppDbService<TEntity> service, Guid innerId)
where TEntity : class, IAppDbEntity
{
return service.Delete(e => e.InnerId == innerId);
return service.Execute(dbset => dbset.ExecuteDeleteWhere(e => e.InnerId == entity.InnerId));
}
public static ValueTask<int> DeleteByInnerIdAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class, IAppDbEntity
{
return service.DeleteByInnerIdAsync(entity.InnerId, token);
}
public static ValueTask<int> DeleteByInnerIdAsync<TEntity>(this IAppDbService<TEntity> service, Guid innerId, CancellationToken token = default)
where TEntity : class, IAppDbEntity
{
return service.DeleteAsync(e => e.InnerId == innerId, token);
return service.ExecuteAsync((dbset, token) => dbset.ExecuteDeleteWhereAsync(e => e.InnerId == entity.InnerId, token), token);
}
public static List<TEntity> ListByArchiveId<TEntity>(this IAppDbService<TEntity> service, Guid archiveId)

View File

@@ -30,13 +30,7 @@ internal static class AppDbServiceCollectionExtension
public static ValueTask<List<TEntity>> ListAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.ListAsync(query => query.Where(predicate), token);
}
public static ValueTask<List<TResult>> ListAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, IQueryable<TResult>> query, CancellationToken token = default)
where TEntity : class
{
return service.QueryAsync((query1, token) => query(query1).ToListAsync(token), token);
return service.QueryAsync((query, token) => query.Where(predicate).ToListAsync(token), token);
}
public static ObservableCollection<TEntity> ObservableCollection<TEntity>(this IAppDbService<TEntity> service)

View File

@@ -126,18 +126,6 @@ internal static class AppDbServiceExtension
return service.QueryAsync((query, token) => query.SingleAsync(predicate, token), token);
}
public static TEntity? SingleOrDefault<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
where TEntity : class
{
return service.Query(query => query.SingleOrDefault(predicate));
}
public static ValueTask<TEntity?> SingleOrDefaultAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.QueryAsync((query, token) => query.SingleOrDefaultAsync(predicate, token), token);
}
public static int Update<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class
{
@@ -156,21 +144,9 @@ internal static class AppDbServiceExtension
return service.Execute(dbset => dbset.RemoveAndSave(entity));
}
public static int Delete<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
where TEntity : class
{
return service.Execute(dbset => dbset.Where(predicate).ExecuteDelete());
}
public static ValueTask<int> DeleteAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.RemoveAndSaveAsync(entity, token), token);
}
public static ValueTask<int> DeleteAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.Where(predicate).ExecuteDeleteAsync(token), token);
}
}

View File

@@ -46,12 +46,13 @@ internal sealed partial class AchievementDbService : IAchievementDbService
[SuppressMessage("", "CA1305")]
public ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take, CancellationToken token = default)
{
return this.ListAsync<EntityAchievement, EntityAchievement>(
query => query
return this.QueryAsync<EntityAchievement, List<EntityAchievement>>(
(query, token) => query
.Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.OrderByDescending(a => a.Time.ToString())
.Take(take),
.Take(take)
.ToListAsync(token),
token);
}

View File

@@ -7,7 +7,6 @@ using Snap.Hutao.Service.Announcement;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using Snap.Hutao.Web.Response;
using System.Collections.Specialized;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
@@ -111,68 +110,39 @@ internal sealed partial class AnnouncementService : IAnnouncementService
.Single(wrapper => wrapper.TypeId == 1)
.List;
// 游戏公告
List<WebAnnouncement> announcements = announcementListWrappers
.Single(wrapper => wrapper.TypeId == 2)
.List;
Dictionary<string, DateTimeOffset> versionStartTimes = [];
// 更新公告
if (announcements.SingleOrDefault(ann => AnnouncementRegex.VersionUpdateTitleRegex.IsMatch(ann.Title)) is { } versionUpdate)
{
if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is not { Success: true } versionUpdateMatch)
{
return;
}
WebAnnouncement versionUpdate = announcementListWrappers
.Single(wrapper => wrapper.TypeId == 2)
.List
.Single(ann => AnnouncementRegex.VersionUpdateTitleRegex.IsMatch(ann.Title));
DateTimeOffset versionUpdateTime = UnsafeDateTimeOffset.ParseDateTime(versionUpdateMatch.Groups[1].ValueSpan, offset);
versionStartTimes.TryAdd(VersionRegex().Match(versionUpdate.Title).Groups[1].Value, versionUpdateTime);
if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is not { Success: true } versionMatch)
{
return;
}
// 更新预告
if (announcements.SingleOrDefault(ann => AnnouncementRegex.VersionUpdatePreviewTitleRegex.IsMatch(ann.Title)) is { } versionUpdatePreview)
{
if (AnnouncementRegex.VersionUpdatePreviewTimeRegex.Match(versionUpdatePreview.Content) is not { Success: true } versionUpdatePreviewMatch)
{
return;
}
DateTimeOffset versionUpdatePreviewTime = UnsafeDateTimeOffset.ParseDateTime(versionUpdatePreviewMatch.Groups[1].ValueSpan, offset);
versionStartTimes.TryAdd(VersionRegex().Match(versionUpdatePreview.Title).Groups[1].Value, versionUpdatePreviewTime);
}
DateTimeOffset versionUpdateTime = UnsafeDateTimeOffset.ParseDateTime(versionMatch.Groups[1].ValueSpan, offset);
foreach (ref readonly WebAnnouncement announcement in CollectionsMarshal.AsSpan(activities))
{
DateTimeOffset versionStartTime;
if (AnnouncementRegex.PermanentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } permanent)
{
if (versionStartTimes.TryGetValue(permanent.Groups[1].Value, out versionStartTime))
{
announcement.StartTime = versionStartTime;
continue;
}
announcement.StartTime = versionUpdateTime;
continue;
}
if (AnnouncementRegex.PersistentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } persistent)
{
if (versionStartTimes.TryGetValue(persistent.Groups[1].Value, out versionStartTime))
{
announcement.StartTime = versionStartTime;
announcement.EndTime = versionStartTime + TimeSpan.FromDays(42);
continue;
}
announcement.StartTime = versionUpdateTime;
announcement.EndTime = versionUpdateTime + TimeSpan.FromDays(42);
continue;
}
if (AnnouncementRegex.TransientActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } transient)
{
if (versionStartTimes.TryGetValue(transient.Groups[1].Value, out versionStartTime))
{
announcement.StartTime = versionStartTime;
announcement.EndTime = UnsafeDateTimeOffset.ParseDateTime(transient.Groups[2].ValueSpan, offset);
continue;
}
announcement.StartTime = versionUpdateTime;
announcement.EndTime = UnsafeDateTimeOffset.ParseDateTime(transient.Groups[2].ValueSpan, offset);
continue;
}
MatchCollection matches = AnnouncementRegex.XmlTimeTagRegex().Matches(announcement.Content);
@@ -207,7 +177,4 @@ internal sealed partial class AnnouncementService : IAnnouncementService
announcement.EndTime = max;
}
}
[GeneratedRegex("(\\d\\.\\d)")]
private static partial Regex VersionRegex();
}

View File

@@ -12,5 +12,12 @@ namespace Snap.Hutao.Service.Announcement;
[HighQuality]
internal interface IAnnouncementService
{
ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(string languageCode, Region region, CancellationToken token = default);
/// <summary>
/// 异步获取游戏公告与活动,通常会进行缓存
/// </summary>
/// <param name="languageCode">语言代码</param>
/// <param name="region">服务器</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>公告包装器</returns>
ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(string languageCode, Region region, CancellationToken cancellationToken = default);
}

View File

@@ -12,9 +12,7 @@ using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Response;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using CalculateAvatar = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Avatar;
using EnkaAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
@@ -23,6 +21,9 @@ using RecordPlayerInfo = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.PlayerInfo;
namespace Snap.Hutao.Service.AvatarInfo;
/// <summary>
/// 角色信息数据库操作
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Singleton)]
@@ -31,10 +32,18 @@ internal sealed partial class AvatarInfoDbBulkOperation
private readonly IServiceProvider serviceProvider;
private readonly IAvatarInfoDbService avatarInfoDbService;
public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByShowcaseAsync(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token)
/// <summary>
/// 更新数据库角色信息
/// </summary>
/// <param name="uid">uid</param>
/// <param name="webInfos">Enka信息</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public List<EntityAvatarInfo> UpdateDbAvatarInfosByShowcase(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token)
{
List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap);
token.ThrowIfCancellationRequested();
List<EntityAvatarInfo> dbInfos = avatarInfoDbService.GetAvatarInfoListByUid(uid);
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
using (IServiceScope scope = serviceProvider.CreateScope())
{
@@ -47,19 +56,28 @@ internal sealed partial class AvatarInfoDbBulkOperation
continue;
}
EntityAvatarInfo? entity = dbInfoMap.GetValueOrDefault(webInfo.AvatarId);
token.ThrowIfCancellationRequested();
EntityAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId);
AddOrUpdateAvatarInfo(entity, uid, appDbContext, webInfo);
}
return await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
return avatarInfoDbService.GetAvatarInfoListByUid(uid);
}
}
/// <summary>
/// 米游社我的角色方式 更新数据库角色信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByGameRecordCharacterAsync(UserAndUid userAndUid, CancellationToken token)
{
token.ThrowIfCancellationRequested();
string uid = userAndUid.Uid.Value;
List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap);
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
using (IServiceScope scope = serviceProvider.CreateScope())
{
@@ -72,47 +90,51 @@ internal sealed partial class AvatarInfoDbBulkOperation
.GetPlayerInfoAsync(userAndUid, token)
.ConfigureAwait(false);
if (!playerInfoResponse.IsOk())
if (playerInfoResponse.IsOk())
{
goto Return;
}
Response<CharacterWrapper> charactersResponse = await gameRecordClient
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false);
Response<CharacterWrapper> charactersResponse = await gameRecordClient
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false);
token.ThrowIfCancellationRequested();
if (!charactersResponse.IsOk())
{
goto Return;
}
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
GameRecordCharacterAvatarInfoTransformer transformer = serviceProvider
.GetRequiredService<GameRecordCharacterAvatarInfoTransformer>();
foreach (RecordCharacter character in characters)
{
if (AvatarIds.IsPlayer(character.Id))
if (charactersResponse.IsOk())
{
continue;
}
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
EntityAvatarInfo? entity = dbInfoMap.GetValueOrDefault(character.Id);
AddOrUpdateAvatarInfo(entity, character.Id, uid, appDbContext, transformer, character);
GameRecordCharacterAvatarInfoTransformer transformer = serviceProvider
.GetRequiredService<GameRecordCharacterAvatarInfoTransformer>();
foreach (RecordCharacter character in characters)
{
if (AvatarIds.IsPlayer(character.Id))
{
continue;
}
token.ThrowIfCancellationRequested();
EntityAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id);
AddOrUpdateAvatarInfo(entity, character.Id, uid, appDbContext, transformer, character);
}
}
}
}
Return:
return await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false);
}
/// <summary>
/// 米游社养成计算方式 更新数据库角色信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByCalculateAvatarDetailAsync(UserAndUid userAndUid, CancellationToken token)
{
token.ThrowIfCancellationRequested();
string uid = userAndUid.Uid.Value;
List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap);
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
using (IServiceScope scope = serviceProvider.CreateScope())
{
@@ -133,6 +155,8 @@ internal sealed partial class AvatarInfoDbBulkOperation
continue;
}
token.ThrowIfCancellationRequested();
Response<AvatarDetail> detailAvatarResponse = await calculateClient
.GetAvatarDetailAsync(userAndUid, avatar, token)
.ConfigureAwait(false);
@@ -142,7 +166,8 @@ internal sealed partial class AvatarInfoDbBulkOperation
continue;
}
EntityAvatarInfo? entity = dbInfoMap.GetValueOrDefault(avatar.Id);
token.ThrowIfCancellationRequested();
EntityAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == avatar.Id);
AddOrUpdateAvatarInfo(entity, avatar.Id, uid, appDbContext, transformer, detailAvatarResponse.Data);
}
}
@@ -156,14 +181,15 @@ internal sealed partial class AvatarInfoDbBulkOperation
if (entity is null)
{
entity = EntityAvatarInfo.From(uid, webInfo);
entity.ShowcaseRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
{
entity.Info = webInfo;
entity.ShowcaseRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
entity.ShowcaseRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -174,16 +200,17 @@ internal sealed partial class AvatarInfoDbBulkOperation
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatarId };
transformer.Transform(ref avatarInfo, source);
entity = EntityAvatarInfo.From(uid, avatarInfo);
entity.CalculatorRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
{
EnkaAvatarInfo avatarInfo = entity.Info;
transformer.Transform(ref avatarInfo, source);
entity.Info = avatarInfo;
entity.CalculatorRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
entity.CalculatorRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -194,29 +221,29 @@ internal sealed partial class AvatarInfoDbBulkOperation
EnkaAvatarInfo avatarInfo = new() { AvatarId = avatarId };
transformer.Transform(ref avatarInfo, source);
entity = EntityAvatarInfo.From(uid, avatarInfo);
entity.GameRecordRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
{
EnkaAvatarInfo avatarInfo = entity.Info;
transformer.Transform(ref avatarInfo, source);
entity.Info = avatarInfo;
entity.GameRecordRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
entity.GameRecordRefreshTime = DateTimeOffset.UtcNow;
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
private void EnsureItemsAvatarIdUnique(ref List<EntityAvatarInfo> dbInfos, string uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap)
private void EnsureItemsAvatarIdDistinct(ref List<EntityAvatarInfo> dbInfos, string uid)
{
dbInfoMap = [];
foreach (ref readonly EntityAvatarInfo info in CollectionsMarshal.AsSpan(dbInfos))
int distinctCount = dbInfos.Select(info => info.Info.AvatarId).ToHashSet().Count;
// Avatars are actually less than the list told us.
// This means that there are duplicate items.
if (distinctCount < dbInfos.Count)
{
if (!dbInfoMap.TryAdd(info.Info.AvatarId, info))
{
avatarInfoDbService.RemoveAvatarInfoRangeByUid(uid);
dbInfoMap.Clear();
dbInfos.Clear();
}
avatarInfoDbService.RemoveAvatarInfoRangeByUid(uid);
dbInfos = [];
}
}
}

View File

@@ -4,7 +4,6 @@
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Abstraction;
using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
namespace Snap.Hutao.Service.AvatarInfo;
@@ -15,25 +14,44 @@ internal sealed partial class AvatarInfoDbService : IAvatarInfoDbService
{
private readonly IServiceProvider serviceProvider;
public IServiceProvider ServiceProvider { get => serviceProvider; }
public List<EntityAvatarInfo> GetAvatarInfoListByUid(string uid)
{
return this.List(i => i.Uid == uid);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IQueryable<EntityAvatarInfo> result = appDbContext.AvatarInfos.AsNoTracking().Where(i => i.Uid == uid);
return [.. result];
}
}
public ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid, CancellationToken token = default)
public async ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid)
{
return this.ListAsync(i => i.Uid == uid, token);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.AvatarInfos
.AsNoTracking()
.Where(i => i.Uid == uid)
.ToListAsync()
.ConfigureAwait(false);
}
}
public void RemoveAvatarInfoRangeByUid(string uid)
{
this.Delete(i => i.Uid == uid);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.AvatarInfos.ExecuteDeleteWhere(i => i.Uid == uid);
}
}
public async ValueTask RemoveAvatarInfoRangeByUidAsync(string uid, CancellationToken token = default)
public async ValueTask RemoveAvatarInfoRangeByUidAsync(string uid)
{
await this.DeleteAsync(i => i.Uid == uid, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.AvatarInfos.ExecuteDeleteWhereAsync(i => i.Uid == uid).ConfigureAwait(false);
}
}
}

View File

@@ -25,59 +25,64 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService
private readonly IMetadataService metadataService;
private readonly ISummaryFactory summaryFactory;
public async ValueTask<ValueResult<RefreshResultKind, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default)
public async ValueTask<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default)
{
if (!await metadataService.InitializeAsync().ConfigureAwait(false))
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
return new(RefreshResultKind.MetadataNotInitialized, null);
token.ThrowIfCancellationRequested();
switch (refreshOption)
{
case RefreshOption.RequestFromEnkaAPI:
{
EnkaResponse? resp = await GetEnkaResponseAsync(userAndUid.Uid, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
if (resp is null)
{
return new(RefreshResult.APIUnavailable, default);
}
if (!string.IsNullOrEmpty(resp.Message))
{
return new(RefreshResult.StatusCodeNotSucceed, new Summary { Message = resp.Message });
}
if (!resp.IsValid)
{
return new(RefreshResult.ShowcaseNotOpen, default);
}
List<EntityAvatarInfo> list = avatarInfoDbBulkOperation.UpdateDbAvatarInfosByShowcase(userAndUid.Uid.Value, resp.AvatarInfoList, token);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResult.Ok, summary);
}
case RefreshOption.RequestFromHoyolabGameRecord:
{
List<EntityAvatarInfo> list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResult.Ok, summary);
}
case RefreshOption.RequestFromHoyolabCalculate:
{
List<EntityAvatarInfo> list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResult.Ok, summary);
}
default:
{
List<EntityAvatarInfo> list = await avatarInfoDbService.GetAvatarInfoListByUidAsync(userAndUid.Uid.Value).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
return new(RefreshResult.Ok, summary.Avatars.Count == 0 ? null : summary);
}
}
}
switch (refreshOption)
else
{
case RefreshOption.RequestFromEnkaAPI:
{
EnkaResponse? resp = await GetEnkaResponseAsync(userAndUid.Uid, token).ConfigureAwait(false);
if (resp is null)
{
return new(RefreshResultKind.APIUnavailable, default);
}
if (!string.IsNullOrEmpty(resp.Message))
{
return new(RefreshResultKind.StatusCodeNotSucceed, new Summary { Message = resp.Message });
}
if (!resp.IsValid)
{
return new(RefreshResultKind.ShowcaseNotOpen, default);
}
List<EntityAvatarInfo> list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByShowcaseAsync(userAndUid.Uid.Value, resp.AvatarInfoList, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResultKind.Ok, summary);
}
case RefreshOption.RequestFromHoyolabGameRecord:
{
List<EntityAvatarInfo> list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByGameRecordCharacterAsync(userAndUid, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResultKind.Ok, summary);
}
case RefreshOption.RequestFromHoyolabCalculate:
{
List<EntityAvatarInfo> list = await avatarInfoDbBulkOperation.UpdateDbAvatarInfosByCalculateAvatarDetailAsync(userAndUid, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResultKind.Ok, summary);
}
default:
{
List<EntityAvatarInfo> list = await avatarInfoDbService.GetAvatarInfoListByUidAsync(userAndUid.Uid.Value, token).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(list, token).ConfigureAwait(false);
return new(RefreshResultKind.Ok, summary.Avatars.Count == 0 ? null : summary);
}
return new(RefreshResult.MetadataNotInitialized, null);
}
}

View File

@@ -1,13 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal sealed class AvatarViewBuilder : IAvatarViewBuilder
{
public AvatarView View { get; } = new();
public float Score { get => View.Score; set => View.Score = value; }
public ViewModel.AvatarProperty.AvatarView AvatarView { get; } = new();
}

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class AvatarViewBuilderExtension
{
public static TBuilder SetCostumeIconOrDefault<TBuilder>(this TBuilder builder, Web.Enka.Model.AvatarInfo avatarInfo, Avatar avatar)
public static TBuilder ApplyCostumeIconOrDefault<TBuilder>(this TBuilder builder, Web.Enka.Model.AvatarInfo avatarInfo, Avatar avatar)
where TBuilder : IAvatarViewBuilder
{
if (avatarInfo.CostumeId.TryGetValue(out CostumeId id))
@@ -20,13 +20,13 @@ internal static class AvatarViewBuilderExtension
Costume costume = avatar.Costumes.Single(c => c.Id == id);
// Set to costume icon
builder.View.Icon = AvatarIconConverter.IconNameToUri(costume.FrontIcon);
builder.View.SideIcon = AvatarIconConverter.IconNameToUri(costume.SideIcon);
builder.AvatarView.Icon = AvatarIconConverter.IconNameToUri(costume.FrontIcon);
builder.AvatarView.SideIcon = AvatarIconConverter.IconNameToUri(costume.SideIcon);
}
else
{
builder.View.Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
builder.View.SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon);
builder.AvatarView.Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
builder.AvatarView.SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon);
}
return builder;
@@ -41,7 +41,7 @@ internal static class AvatarViewBuilderExtension
public static TBuilder SetCalculatorRefreshTimeFormat<TBuilder>(this TBuilder builder, string calculatorRefreshTimeFormat)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.CalculatorRefreshTimeFormat = calculatorRefreshTimeFormat);
return builder.Configure(b => b.AvatarView.CalculatorRefreshTimeFormat = calculatorRefreshTimeFormat);
}
public static TBuilder SetConstellations<TBuilder>(this TBuilder builder, List<Skill> talents, List<SkillId>? talentIds)
@@ -65,7 +65,7 @@ internal static class AvatarViewBuilderExtension
public static TBuilder SetConstellations<TBuilder>(this TBuilder builder, List<ConstellationView> constellations)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Constellations = constellations);
return builder.Configure(b => b.AvatarView.Constellations = constellations);
}
public static TBuilder SetCritScore<TBuilder>(this TBuilder builder, Dictionary<FightProperty, float>? fightPropMap)
@@ -90,13 +90,19 @@ internal static class AvatarViewBuilderExtension
public static TBuilder SetCritScore<TBuilder>(this TBuilder builder, float critScore)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.CritScore = critScore);
return builder.SetCritScore($"{critScore:F2}");
}
public static TBuilder SetCritScore<TBuilder>(this TBuilder builder, string critScore)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.CritScore = critScore);
}
public static TBuilder SetElement<TBuilder>(this TBuilder builder, ElementType element)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Element = element);
return builder.Configure(b => b.AvatarView.Element = element);
}
public static TBuilder SetFetterLevel<TBuilder>(this TBuilder builder, FetterLevel? level)
@@ -104,7 +110,7 @@ internal static class AvatarViewBuilderExtension
{
if (level.TryGetValue(out FetterLevel value))
{
return builder.Configure(b => b.View.FetterLevel = value);
return builder.Configure(b => b.AvatarView.FetterLevel = value);
}
return builder;
@@ -113,7 +119,7 @@ internal static class AvatarViewBuilderExtension
public static TBuilder SetFetterLevel<TBuilder>(this TBuilder builder, uint level)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.FetterLevel = level);
return builder.Configure(b => b.AvatarView.FetterLevel = level);
}
public static TBuilder SetGameRecordRefreshTimeFormat<TBuilder>(this TBuilder builder, DateTimeOffset refreshTime, Func<object?, string> format, string defaultValue)
@@ -125,13 +131,13 @@ internal static class AvatarViewBuilderExtension
public static TBuilder SetGameRecordRefreshTimeFormat<TBuilder>(this TBuilder builder, string gameRecordRefreshTimeFormat)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.GameRecordRefreshTimeFormat = gameRecordRefreshTimeFormat);
return builder.Configure(b => b.AvatarView.GameRecordRefreshTimeFormat = gameRecordRefreshTimeFormat);
}
public static TBuilder SetId<TBuilder>(this TBuilder builder, AvatarId id)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Id = id);
return builder.Configure(b => b.AvatarView.Id = id);
}
public static TBuilder SetLevelNumber<TBuilder>(this TBuilder builder, uint? levelNumber)
@@ -139,7 +145,7 @@ internal static class AvatarViewBuilderExtension
{
if (levelNumber.TryGetValue(out uint value))
{
return builder.Configure(b => b.View.LevelNumber = value);
return builder.Configure(b => b.AvatarView.LevelNumber = value);
}
return builder;
@@ -148,31 +154,43 @@ internal static class AvatarViewBuilderExtension
public static TBuilder SetName<TBuilder>(this TBuilder builder, string name)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Name = name);
return builder.Configure(b => b.AvatarView.Name = name);
}
public static TBuilder SetNameCard<TBuilder>(this TBuilder builder, Uri nameCard)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.NameCard = nameCard);
return builder.Configure(b => b.AvatarView.NameCard = nameCard);
}
public static TBuilder SetProperties<TBuilder>(this TBuilder builder, List<AvatarProperty> properties)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Properties = properties);
return builder.Configure(b => b.AvatarView.Properties = properties);
}
public static TBuilder SetQuality<TBuilder>(this TBuilder builder, QualityType quality)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Quality = quality);
return builder.Configure(b => b.AvatarView.Quality = quality);
}
public static TBuilder SetReliquaries<TBuilder>(this TBuilder builder, List<ReliquaryView> reliquaries)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Reliquaries = reliquaries);
return builder.Configure(b => b.AvatarView.Reliquaries = reliquaries);
}
public static TBuilder SetScore<TBuilder>(this TBuilder builder, float score)
where TBuilder : IAvatarViewBuilder
{
return builder.SetScore($"{score:F2}");
}
public static TBuilder SetScore<TBuilder>(this TBuilder builder, string score)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.AvatarView.Score = score);
}
public static TBuilder SetShowcaseRefreshTimeFormat<TBuilder>(this TBuilder builder, DateTimeOffset refreshTime, Func<object?, string> format, string defaultValue)
@@ -184,7 +202,7 @@ internal static class AvatarViewBuilderExtension
public static TBuilder SetShowcaseRefreshTimeFormat<TBuilder>(this TBuilder builder, string showcaseRefreshTimeFormat)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.ShowcaseRefreshTimeFormat = showcaseRefreshTimeFormat);
return builder.Configure(b => b.AvatarView.ShowcaseRefreshTimeFormat = showcaseRefreshTimeFormat);
}
public static TBuilder SetSkills<TBuilder>(this TBuilder builder, Dictionary<SkillId, SkillLevel>? skillLevelMap, Dictionary<SkillGroupId, SkillLevel>? proudSkillExtraLevelMap, List<ProudableSkill> proudSkills)
@@ -231,12 +249,12 @@ internal static class AvatarViewBuilderExtension
public static TBuilder SetSkills<TBuilder>(this TBuilder builder, List<SkillView> skills)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Skills = skills);
return builder.Configure(b => b.AvatarView.Skills = skills);
}
public static TBuilder SetWeapon<TBuilder>(this TBuilder builder, WeaponView? weapon)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Weapon = weapon);
return builder.Configure(b => b.AvatarView.Weapon = weapon);
}
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class EquipViewBuilderExtension
{
public static TBuilder SetLevel<TBuilder, T>(this TBuilder builder, string level)
where TBuilder : IEquipViewBuilder<T>
where T : EquipView
{
return builder.Configure(b => b.View.Level = level);
}
public static TBuilder SetQuality<TBuilder, T>(this TBuilder builder, QualityType quality)
where TBuilder : IEquipViewBuilder<T>
where T : EquipView
{
return builder.Configure(b => b.View.Quality = quality);
}
public static TBuilder SetMainProperty<TBuilder, T>(this TBuilder builder, NameValue<string> mainProperty)
where TBuilder : IEquipViewBuilder<T>
where T : EquipView
{
return builder.Configure(b => b.View.MainProperty = mainProperty);
}
}

View File

@@ -2,11 +2,10 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal interface IAvatarViewBuilder : IBuilder, IScoreAccess
internal interface IAvatarViewBuilder : IBuilder
{
AvatarView View { get; }
ViewModel.AvatarProperty.AvatarView AvatarView { get; }
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal interface IEquipViewBuilder<T> : INameIconDescriptionBuilder<T>
where T : EquipView
{
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal interface INameIconDescriptionBuilder<T> : IBuilder
where T : NameIconDescription
{
T View { get; }
}

View File

@@ -1,10 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal interface IReliquaryViewBuilder : IEquipViewBuilder<ReliquaryView>, IScoreAccess
{
}

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal interface IScoreAccess
{
float Score { get; set; }
}

View File

@@ -1,10 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal interface IWeaponViewBuilder : IEquipViewBuilder<WeaponView>
{
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class NameIconDescriptionBuilderExtension
{
public static TBuilder SetDescription<TBuilder, T>(this TBuilder builder, string description)
where TBuilder : INameIconDescriptionBuilder<T>
where T : NameIconDescription
{
return builder.Configure(b => b.View.Description = description);
}
public static TBuilder SetIcon<TBuilder, T>(this TBuilder builder, Uri icon)
where TBuilder : INameIconDescriptionBuilder<T>
where T : NameIconDescription
{
return builder.Configure(b => b.View.Icon = icon);
}
public static TBuilder SetName<TBuilder, T>(this TBuilder builder, string name)
where TBuilder : INameIconDescriptionBuilder<T>
where T : NameIconDescription
{
return builder.Configure(b => b.View.Name = name);
}
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal sealed class ReliquaryViewBuilder : IReliquaryViewBuilder
{
public ReliquaryView View { get; } = new();
public float Score { get => View.Score; set => View.Score = value; }
}

View File

@@ -1,66 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class ReliquaryViewBuilderExtension
{
public static TBuilder SetComposedSubProperties<TBuilder>(this TBuilder builder, List<ReliquaryComposedSubProperty> composedSubProperties)
where TBuilder : IReliquaryViewBuilder
{
return builder.Configure(b => b.View.ComposedSubProperties = composedSubProperties);
}
public static TBuilder SetDescription<TBuilder>(this TBuilder builder, string description)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetDescription<TBuilder, ReliquaryView>(description);
}
public static TBuilder SetIcon<TBuilder>(this TBuilder builder, Uri icon)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetIcon<TBuilder, ReliquaryView>(icon);
}
public static TBuilder SetLevel<TBuilder>(this TBuilder builder, string level)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetLevel<TBuilder, ReliquaryView>(level);
}
public static TBuilder SetMainProperty<TBuilder>(this TBuilder builder, NameValue<string> mainProperty)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetMainProperty<TBuilder, ReliquaryView>(mainProperty);
}
public static TBuilder SetName<TBuilder>(this TBuilder builder, string name)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetName<TBuilder, ReliquaryView>(name);
}
public static TBuilder SetPrimarySubProperties<TBuilder>(this TBuilder builder, List<ReliquarySubProperty> primarySubProperties)
where TBuilder : IReliquaryViewBuilder
{
return builder.Configure(b => b.View.PrimarySubProperties = primarySubProperties);
}
public static TBuilder SetQuality<TBuilder>(this TBuilder builder, QualityType quality)
where TBuilder : IReliquaryViewBuilder
{
return builder.SetQuality<TBuilder, ReliquaryView>(quality);
}
public static TBuilder SetSecondarySubProperties<TBuilder>(this TBuilder builder, List<ReliquarySubProperty> secondarySubProperties)
where TBuilder : IReliquaryViewBuilder
{
return builder.Configure(b => b.View.SecondarySubProperties = secondarySubProperties);
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Core.Abstraction.Extension;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class ScoreAccessExtension
{
public static TBuilder SetScore<TBuilder>(this TBuilder builder, float score)
where TBuilder : IBuilder, IScoreAccess
{
return builder.Configure(b => b.Score = score);
}
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal sealed class WeaponViewBuilder : IWeaponViewBuilder
{
public WeaponView View { get; } = new();
}

View File

@@ -1,99 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.AvatarProperty;
using Snap.Hutao.Web.Enka.Model;
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
internal static class WeaponViewBuilderExtension
{
public static TBuilder SetAffixLevelNumber<TBuilder>(this TBuilder builder, uint affixLevelNumber)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.AffixLevelNumber = affixLevelNumber);
}
public static TBuilder SetAffixDescription<TBuilder>(this TBuilder builder, string? affixDescription)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.AffixDescription = affixDescription ?? string.Empty);
}
public static TBuilder SetAffixName<TBuilder>(this TBuilder builder, string? affixName)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.AffixName = affixName ?? string.Empty);
}
public static TBuilder SetDescription<TBuilder>(this TBuilder builder, string description)
where TBuilder : IWeaponViewBuilder
{
return builder.SetDescription<TBuilder, WeaponView>(description);
}
public static TBuilder SetIcon<TBuilder>(this TBuilder builder, Uri icon)
where TBuilder : IWeaponViewBuilder
{
return builder.SetIcon<TBuilder, WeaponView>(icon);
}
public static TBuilder SetId<TBuilder>(this TBuilder builder, WeaponId id)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.Id = id);
}
public static TBuilder SetLevel<TBuilder>(this TBuilder builder, string level)
where TBuilder : IWeaponViewBuilder
{
return builder.SetLevel<TBuilder, WeaponView>(level);
}
public static TBuilder SetLevelNumber<TBuilder>(this TBuilder builder, uint levelNumber)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.LevelNumber = levelNumber);
}
public static TBuilder SetMainProperty<TBuilder>(this TBuilder builder, WeaponStat? mainStat)
where TBuilder : IWeaponViewBuilder
{
return builder.SetMainProperty(mainStat is not null ? FightPropertyFormat.ToNameValue(mainStat.AppendPropId, mainStat.StatValue) : NameValueDefaults.String);
}
public static TBuilder SetMainProperty<TBuilder>(this TBuilder builder, NameValue<string> mainProperty)
where TBuilder : IWeaponViewBuilder
{
return builder.SetMainProperty<TBuilder, WeaponView>(mainProperty);
}
public static TBuilder SetName<TBuilder>(this TBuilder builder, string name)
where TBuilder : IWeaponViewBuilder
{
return builder.SetName<TBuilder, WeaponView>(name);
}
public static TBuilder SetQuality<TBuilder>(this TBuilder builder, QualityType quality)
where TBuilder : IWeaponViewBuilder
{
return builder.SetQuality<TBuilder, WeaponView>(quality);
}
public static TBuilder SetSubProperty<TBuilder>(this TBuilder builder, NameDescription subProperty)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.SubProperty = subProperty);
}
public static TBuilder SetWeaponType<TBuilder>(this TBuilder builder, WeaponType weaponType)
where TBuilder : IWeaponViewBuilder
{
return builder.Configure(b => b.View.WeaponType = weaponType);
}
}

View File

@@ -6,13 +6,15 @@ using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Intrinsic.Format;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Service.AvatarInfo.Factory.Builder;
using Snap.Hutao.ViewModel.AvatarProperty;
using Snap.Hutao.Web.Enka.Model;
using System.Runtime.InteropServices;
using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon;
using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
using PropertyAvatar = Snap.Hutao.ViewModel.AvatarProperty.AvatarView;
using PropertyReliquary = Snap.Hutao.ViewModel.AvatarProperty.ReliquaryView;
using PropertyWeapon = Snap.Hutao.ViewModel.AvatarProperty.WeaponView;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
@@ -35,17 +37,17 @@ internal sealed class SummaryAvatarFactory
calculatorRefreshTime = avatarInfo.CalculatorRefreshTime;
}
public static AvatarView Create(SummaryFactoryMetadataContext context, EntityAvatarInfo avatarInfo)
public static PropertyAvatar Create(SummaryFactoryMetadataContext context, EntityAvatarInfo avatarInfo)
{
return new SummaryAvatarFactory(context, avatarInfo).Create();
}
public AvatarView Create()
public PropertyAvatar Create()
{
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
MetadataAvatar avatar = context.IdAvatarMap[avatarInfo.AvatarId];
AvatarView propertyAvatar = new AvatarViewBuilder()
PropertyAvatar propertyAvatar = new AvatarViewBuilder()
.SetId(avatar.Id)
.SetName(avatar.Name)
.SetQuality(avatar.Quality)
@@ -63,16 +65,16 @@ internal sealed class SummaryAvatarFactory
.SetShowcaseRefreshTimeFormat(showcaseRefreshTime, SH.FormatServiceAvatarInfoSummaryShowcaseRefreshTimeFormat, SH.ServiceAvatarInfoSummaryShowcaseNotRefreshed)
.SetGameRecordRefreshTimeFormat(gameRecordRefreshTime, SH.FormatServiceAvatarInfoSummaryGameRecordRefreshTimeFormat, SH.ServiceAvatarInfoSummaryGameRecordNotRefreshed)
.SetCalculatorRefreshTimeFormat(calculatorRefreshTime, SH.FormatServiceAvatarInfoSummaryCalculatorRefreshTimeFormat, SH.ServiceAvatarInfoSummaryCalculatorNotRefreshed)
.SetCostumeIconOrDefault(avatarInfo, avatar)
.View;
.ApplyCostumeIconOrDefault(avatarInfo, avatar)
.AvatarView;
return propertyAvatar;
}
private ReliquaryAndWeapon ProcessEquip(List<Equip> equipments)
{
List<ReliquaryView> reliquaryList = [];
WeaponView? weapon = null;
List<PropertyReliquary> reliquaryList = [];
PropertyWeapon? weapon = null;
foreach (ref readonly Equip equip in CollectionsMarshal.AsSpan(equipments))
{
@@ -90,7 +92,7 @@ internal sealed class SummaryAvatarFactory
return new(reliquaryList, weapon);
}
private WeaponView CreateWeapon(Equip equip)
private PropertyWeapon CreateWeapon(Equip equip)
{
MetadataWeapon weapon = context.IdWeaponMap[equip.ItemId];
@@ -116,29 +118,36 @@ internal sealed class SummaryAvatarFactory
ArgumentNullException.ThrowIfNull(equip.Weapon);
return new WeaponViewBuilder()
.SetName(weapon.Name)
.SetIcon(EquipIconConverter.IconNameToUri(weapon.Icon))
.SetDescription(weapon.Description)
.SetLevel($"Lv.{equip.Weapon.Level.Value}")
.SetQuality(weapon.Quality)
.SetMainProperty(mainStat)
.SetId(weapon.Id)
.SetLevelNumber(equip.Weapon.Level)
.SetSubProperty(subProperty)
.SetAffixLevelNumber(affixLevel + 1)
.SetAffixName(weapon.Affix?.Name)
.SetAffixDescription(weapon.Affix?.Descriptions.Single(a => a.Level == affixLevel).Description)
.SetWeaponType(weapon.WeaponType)
.View;
return new()
{
// NameIconDescription
Name = weapon.Name,
Icon = EquipIconConverter.IconNameToUri(weapon.Icon),
Description = weapon.Description,
// EquipBase
Level = $"Lv.{equip.Weapon.Level.Value}",
Quality = weapon.Quality,
MainProperty = mainStat is not null
? FightPropertyFormat.ToNameValue(mainStat.AppendPropId, mainStat.StatValue)
: NameValueDefaults.String,
// Weapon
Id = weapon.Id,
LevelNumber = equip.Weapon.Level,
SubProperty = subProperty,
AffixLevelNumber = affixLevel + 1,
AffixName = weapon.Affix?.Name ?? string.Empty,
AffixDescription = weapon.Affix?.Descriptions.Single(a => a.Level == affixLevel).Description ?? string.Empty,
};
}
private readonly struct ReliquaryAndWeapon
{
public readonly List<ReliquaryView> Reliquaries;
public readonly WeaponView? Weapon;
public readonly List<PropertyReliquary> Reliquaries;
public readonly PropertyWeapon? Weapon;
public ReliquaryAndWeapon(List<ReliquaryView> reliquaries, WeaponView? weapon)
public ReliquaryAndWeapon(List<PropertyReliquary> reliquaries, PropertyWeapon? weapon)
{
Reliquaries = reliquaries;
Weapon = weapon;

View File

@@ -13,6 +13,11 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
[HighQuality]
internal static class SummaryAvatarProperties
{
/// <summary>
/// 创建角色属性
/// </summary>
/// <param name="fightPropMap">属性映射</param>
/// <returns>列表</returns>
public static List<AvatarProperty> Create(Dictionary<FightProperty, float>? fightPropMap)
{
if (fightPropMap is null)
@@ -20,27 +25,32 @@ internal static class SummaryAvatarProperties
return [];
}
List<AvatarProperty> properties =
[
ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_HP, fightPropMap),
ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, fightPropMap),
ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, fightPropMap),
FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_ELEMENT_MASTERY, fightPropMap),
FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CRITICAL, fightPropMap),
FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CRITICAL_HURT, fightPropMap),
FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, fightPropMap)
];
AvatarProperty hpProp = ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_HP, fightPropMap);
AvatarProperty atkProp = ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, fightPropMap);
AvatarProperty defProp = ToAvatarProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, fightPropMap);
AvatarProperty emProp = FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_ELEMENT_MASTERY, fightPropMap);
AvatarProperty critRateProp = FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CRITICAL, fightPropMap);
AvatarProperty critDMGProp = FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CRITICAL_HURT, fightPropMap);
AvatarProperty chargeEffProp = FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY, fightPropMap);
List<AvatarProperty> properties = new(9) { hpProp, atkProp, defProp, emProp, critRateProp, critDMGProp, chargeEffProp };
// 元素伤害
if (TryGetBonusFightProperty(fightPropMap, out FightProperty bonusProperty, out float value) && value > 0)
if (TryGetBonusFightProperty(fightPropMap, out FightProperty bonusProperty, out float value))
{
properties.Add(FightPropertyFormat.ToAvatarProperty(bonusProperty, value));
if (value > 0)
{
properties.Add(FightPropertyFormat.ToAvatarProperty(bonusProperty, value));
}
}
// 物伤 可以和其他元素伤害并存,所以分别判断
if (fightPropMap.TryGetValue(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, out float addValue) && addValue > 0)
if (fightPropMap.TryGetValue(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, out float addValue))
{
properties.Add(FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, addValue));
if (addValue > 0)
{
properties.Add(FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, addValue));
}
}
return properties;

View File

@@ -28,12 +28,11 @@ internal sealed partial class SummaryFactory : ISummaryFactory
IOrderedEnumerable<AvatarView> avatars = avatarInfos
.Where(a => !AvatarIds.IsPlayer(a.Info.AvatarId))
.Select(a => SummaryAvatarFactory.Create(context, a))
.OrderByDescending(a => a.Quality)
.ThenByDescending(a => a.LevelNumber)
.ThenBy(a => a.Element)
.ThenBy(a => a.Weapon?.WeaponType)
.ThenByDescending(a => a.FetterLevel);
.OrderByDescending(a => a.LevelNumber)
.ThenByDescending(a => a.FetterLevel)
.ThenBy(a => a.Element);
// TODO: thenby weapon type
return new()
{
Avatars = [.. avatars],

View File

@@ -18,7 +18,6 @@ internal sealed class SummaryFactoryMetadataContext : IMetadataContext,
IMetadataDictionaryIdReliquaryAffixWeightSource,
IMetadataDictionaryIdReliquaryMainPropertySource,
IMetadataDictionaryIdReliquarySubAffixSource,
IMetadataDictionaryIdReliquarySource,
IMetadataListReliquaryMainAffixLevelSource
{
public Dictionary<AvatarId, MetadataAvatar> IdAvatarMap { get; set; } = default!;
@@ -33,5 +32,5 @@ internal sealed class SummaryFactoryMetadataContext : IMetadataContext,
public List<ReliquaryMainAffixLevel> ReliquaryMainAffixLevels { get; set; } = default!;
public Dictionary<ReliquaryId, MetadataReliquary> IdReliquaryMap { get; set; } = default!;
public List<MetadataReliquary> Reliquaries { get; set; } = default!;
}

View File

@@ -1,14 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 简述帮助类
/// </summary>
[HighQuality]
internal static class SummaryHelper
{
/// <summary>
/// 获取副属性对应的最大属性的Id
/// </summary>
/// <param name="appendId">属性Id</param>
/// <returns>最大属性Id</returns>
public static ReliquarySubAffixId GetAffixMaxId(in ReliquarySubAffixId appendId)
{
// axxxxx -> a
@@ -20,13 +27,18 @@ internal static class SummaryHelper
1 => 2,
2 => 3,
3 or 4 or 5 => 4,
_ => throw HutaoException.Throw($"不支持的 ReliquarySubAffixId: {appendId}"),
_ => throw Must.NeverHappen(),
};
// axxxxb -> axxxx -> axxxx0 -> axxxxm
return ((appendId / 10) * 10) + max;
}
/// <summary>
/// 获取百分比属性副词条分数
/// </summary>
/// <param name="appendId">id</param>
/// <returns>分数</returns>
public static float GetPercentSubAffixScore(in ReliquarySubAffixId appendId)
{
// 圣遗物相同类型副词条强化档位一共为 4/3/2 档
@@ -53,7 +65,7 @@ internal static class SummaryHelper
(1, 0) => 100F,
(1, 1) => 80F,
_ => throw HutaoException.Throw($"Unexpected AppendId: {appendId} Delta: {delta}"),
_ => throw Must.NeverHappen($"Unexpected AppendId: {appendId.Value} Delta: {delta}"),
};
}
}

View File

@@ -6,7 +6,6 @@ using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.AvatarInfo.Factory.Builder;
using Snap.Hutao.ViewModel.AvatarProperty;
using System.Runtime.InteropServices;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
@@ -14,6 +13,9 @@ using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 圣遗物工厂
/// </summary>
[HighQuality]
internal sealed class SummaryReliquaryFactory
{
@@ -35,42 +37,47 @@ internal sealed class SummaryReliquaryFactory
public ReliquaryView Create()
{
MetadataReliquary reliquary = metadataContext.IdReliquaryMap[equip.ItemId];
MetadataReliquary reliquary = metadataContext.Reliquaries.Single(r => r.Ids.Contains(equip.ItemId));
ArgumentNullException.ThrowIfNull(equip.Reliquary);
List<ReliquarySubProperty> subProperties = equip.Reliquary.AppendPropIdList.EmptyIfNull().SelectList(CreateSubProperty);
ReliquaryViewBuilder reliquaryViewBuilder = new ReliquaryViewBuilder()
.SetName(reliquary.Name)
.SetIcon(RelicIconConverter.IconNameToUri(reliquary.Icon))
.SetDescription(reliquary.Description)
.SetLevel($"+{equip.Reliquary.Level - 1U}")
.SetQuality(reliquary.RankLevel);
List<ReliquarySubProperty> subProperty = equip.Reliquary.AppendPropIdList.EmptyIfNull().SelectList(CreateSubProperty);
int affixCount = GetSecondaryAffixCount(reliquary, equip.Reliquary);
if (subProperties.Count > 0)
ReliquaryView result = new()
{
reliquaryViewBuilder
.SetPrimarySubProperties(subProperties.GetRange(..^affixCount))
.SetSecondarySubProperties(subProperties.GetRange(^affixCount..))
.SetComposedSubProperties(CreateComposedSubProperties(equip.Reliquary.AppendPropIdList));
// NameIconDescription
Name = reliquary.Name,
Icon = RelicIconConverter.IconNameToUri(reliquary.Icon),
Description = reliquary.Description,
// EquipBase
Level = $"+{equip.Reliquary.Level - 1U}",
Quality = reliquary.RankLevel,
};
if (subProperty.Count > 0)
{
result.PrimarySubProperties = subProperty.GetRange(..^affixCount);
result.SecondarySubProperties = subProperty.GetRange(^affixCount..);
ArgumentNullException.ThrowIfNull(equip.Flat.ReliquarySubstats);
result.ComposedSubProperties = CreateComposedSubProperties(equip.Reliquary.AppendPropIdList);
ReliquaryMainAffixLevel relicLevel = metadataContext.ReliquaryMainAffixLevels.Single(r => r.Level == equip.Reliquary.Level && r.Rank == reliquary.RankLevel);
FightProperty property = metadataContext.IdReliquaryMainPropertyMap[equip.Reliquary.MainPropId];
reliquaryViewBuilder
.SetMainProperty(FightPropertyFormat.ToNameValue(property, relicLevel.PropertyMap[property]))
.SetScore(ScoreReliquary(property, reliquary, relicLevel, subProperties));
result.MainProperty = FightPropertyFormat.ToNameValue(property, relicLevel.PropertyMap[property]);
result.Score = ScoreReliquary(property, reliquary, relicLevel, subProperty);
}
return reliquaryViewBuilder.View;
return result;
}
private static int GetSecondaryAffixCount(MetadataReliquary metaReliquary, Web.Enka.Model.Reliquary enkaReliquary)
private static int GetSecondaryAffixCount(MetadataReliquary reliquary, Web.Enka.Model.Reliquary enkaReliquary)
{
// 强化词条个数
return (metaReliquary.RankLevel, enkaReliquary.Level.Value) switch
return (reliquary.RankLevel, enkaReliquary.Level.Value) switch
{
(QualityType.QUALITY_ORANGE, > 20U) => 5,
(QualityType.QUALITY_ORANGE, > 16U) => 4,
@@ -111,8 +118,18 @@ internal sealed class SummaryReliquaryFactory
info.Value += subAffix.Value;
}
HutaoException.ThrowIf(infos.Count > 4, "无效的圣遗物数据");
return infos.SelectList(info => info.ToReliquaryComposedSubProperty());
if (infos.Count > 4)
{
ThrowHelper.InvalidOperation("无效的圣遗物数据");
}
List<ReliquaryComposedSubProperty> results = [];
foreach (ref readonly SummaryReliquarySubPropertyCompositionInfo info in CollectionsMarshal.AsSpan(infos))
{
results.Add(info.ToReliquaryComposedSubProperty());
}
return results;
}
private float ScoreReliquary(FightProperty property, MetadataReliquary reliquary, ReliquaryMainAffixLevel relicLevel, List<ReliquarySubProperty> subProperties)
@@ -148,42 +165,42 @@ internal sealed class SummaryReliquaryFactory
FightProperty property = affix.Type;
return new(property, FightPropertyFormat.FormatValue(property, affix.Value), ScoreSubAffix(appendPropId));
}
float ScoreSubAffix(in ReliquarySubAffixId appendId)
private float ScoreSubAffix(in ReliquarySubAffixId appendId)
{
ReliquarySubAffix affix = metadataContext.IdReliquarySubAffixMap[appendId];
ReliquaryAffixWeight affixWeight = metadataContext.IdReliquaryAffixWeightMap.GetValueOrDefault(avatarInfo.AvatarId, ReliquaryAffixWeight.Default);
float weight = affixWeight[affix.Type] / 100F;
// 数字词条,转换到等效百分比计算
if (affix.Type is FightProperty.FIGHT_PROP_HP or FightProperty.FIGHT_PROP_ATTACK or FightProperty.FIGHT_PROP_DEFENSE)
{
ReliquarySubAffix affix = metadataContext.IdReliquarySubAffixMap[appendId];
// 等效百分比 [ 当前小字词条 / 角色基本属性 ]
float equalPercent = affix.Value / avatarInfo.FightPropMap[affix.Type - 1];
ReliquaryAffixWeight affixWeight = metadataContext.IdReliquaryAffixWeightMap.GetValueOrDefault(avatarInfo.AvatarId, ReliquaryAffixWeight.Default);
float weight = affixWeight[affix.Type] / 100F;
// 获取对应百分比词条权重
weight = affixWeight[affix.Type + 1] / 100F;
// 数字词条,转换到等效百分比计算
if (affix.Type is FightProperty.FIGHT_PROP_HP or FightProperty.FIGHT_PROP_ATTACK or FightProperty.FIGHT_PROP_DEFENSE)
{
// 等效百分比 [ 当前小字词条 / 角色基本属性 ]
float equalPercent = affix.Value / avatarInfo.FightPropMap[affix.Type - 1];
// 最大同属性百分比Id
// 第四五位是战斗属性位
// 小字的加成词条在十位加一后即变换为百分比词条
ReliquarySubAffixId maxPercentAffixId = SummaryHelper.GetAffixMaxId(appendId + 10U);
// 获取对应百分比词条权重
weight = affixWeight[affix.Type + 1] / 100F;
// 最大同属性百分比数值
ReliquarySubAffix maxPercentAffix = metadataContext.IdReliquarySubAffixMap[maxPercentAffixId];
Must.Argument(
maxPercentAffix.Type
is FightProperty.FIGHT_PROP_HP_PERCENT
or FightProperty.FIGHT_PROP_ATTACK_PERCENT
or FightProperty.FIGHT_PROP_DEFENSE_PERCENT,
"ReliquarySubAffix transform failed");
float equalScore = equalPercent / maxPercentAffix.Value;
// 最大同属性百分比Id
// 第四五位是战斗属性位
// 小字的加成词条在十位加一后即变换为百分比词条
ReliquarySubAffixId maxPercentAffixId = SummaryHelper.GetAffixMaxId(appendId + 10U);
// 最大同属性百分比数值
ReliquarySubAffix maxPercentAffix = metadataContext.IdReliquarySubAffixMap[maxPercentAffixId];
HutaoException.ThrowIfNot(
maxPercentAffix.Type
is FightProperty.FIGHT_PROP_HP_PERCENT
or FightProperty.FIGHT_PROP_ATTACK_PERCENT
or FightProperty.FIGHT_PROP_DEFENSE_PERCENT,
"ReliquarySubAffix transform failed");
float equalScore = equalPercent / maxPercentAffix.Value;
return weight * equalScore * 100;
}
return weight * SummaryHelper.GetPercentSubAffixScore(appendId);
return weight * equalScore * 100;
}
return weight * SummaryHelper.GetPercentSubAffixScore(appendId);
}
}

View File

@@ -1,18 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Abstraction;
using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
namespace Snap.Hutao.Service.AvatarInfo;
internal interface IAvatarInfoDbService : IAppDbService<EntityAvatarInfo>
internal interface IAvatarInfoDbService
{
void RemoveAvatarInfoRangeByUid(string uid);
List<EntityAvatarInfo> GetAvatarInfoListByUid(string uid);
ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid, CancellationToken token = default);
ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid);
ValueTask RemoveAvatarInfoRangeByUidAsync(string uid, CancellationToken token = default);
ValueTask RemoveAvatarInfoRangeByUidAsync(string uid);
}

View File

@@ -19,5 +19,5 @@ internal interface IAvatarInfoService
/// <param name="refreshOption">刷新选项</param>
/// <param name="token">取消令牌</param>
/// <returns>总览数据</returns>
ValueTask<ValueResult<RefreshResultKind, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default);
ValueTask<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default);
}

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.AvatarInfo;
/// 刷新结果
/// </summary>
[HighQuality]
internal enum RefreshResultKind
internal enum RefreshResult
{
/// <summary>
/// 正常

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.Service.AvatarInfo.Transformer;
internal sealed class CalculateAvatarDetailAvatarInfoTransformer : IAvatarInfoTransformer<AvatarDetail>
{
/// <inheritdoc/>
public void Transform(ref readonly Web.Enka.Model.AvatarInfo avatarInfo, AvatarDetail source)
public void Transform(ref Web.Enka.Model.AvatarInfo avatarInfo, AvatarDetail source)
{
// update skills
avatarInfo.SkillLevelMap = source.SkillList.ToDictionary(s => (SkillId)s.Id, s => (SkillLevel)s.LevelCurrent);

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Service.AvatarInfo.Transformer;
internal sealed class GameRecordCharacterAvatarInfoTransformer : IAvatarInfoTransformer<Character>
{
/// <inheritdoc/>
public void Transform(ref readonly Web.Enka.Model.AvatarInfo avatarInfo, Character source)
public void Transform(ref Web.Enka.Model.AvatarInfo avatarInfo, Character source)
{
// update fetter
avatarInfo.FetterInfo ??= new();

View File

@@ -15,5 +15,5 @@ internal interface IAvatarInfoTransformer<in TSource>
/// </summary>
/// <param name="avatarInfo">基底角色Id必定存在</param>
/// <param name="source">源</param>
void Transform(ref readonly Web.Enka.Model.AvatarInfo avatarInfo, TSource source);
void Transform(ref Web.Enka.Model.AvatarInfo avatarInfo, TSource source);
}

View File

@@ -26,11 +26,11 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
private HashSet<string>? currentBackgroundPathSet;
private HashSet<string> currentBackgroundPathSet;
public async ValueTask<ValueResult<bool, BackgroundImage?>> GetNextBackgroundImageAsync(BackgroundImage? previous, CancellationToken token = default)
public async ValueTask<ValueResult<bool, BackgroundImage?>> GetNextBackgroundImageAsync(BackgroundImage? previous)
{
HashSet<string> backgroundSet = await SkipOrInitBackgroundAsync(token).ConfigureAwait(false);
HashSet<string> backgroundSet = await SkipOrInitBackgroundAsync().ConfigureAwait(false);
if (backgroundSet.Count <= 0)
{
@@ -79,7 +79,7 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
}
}
private async ValueTask<HashSet<string>> SkipOrInitBackgroundAsync(CancellationToken token = default)
private async ValueTask<HashSet<string>> SkipOrInitBackgroundAsync()
{
switch (appOptions.BackgroundImageType)
{
@@ -90,7 +90,7 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
string backgroundFolder = runtimeOptions.GetDataFolderBackgroundFolder();
currentBackgroundPathSet = Directory
.EnumerateFiles(backgroundFolder, "*", SearchOption.AllDirectories)
.GetFiles(backgroundFolder, "*.*", SearchOption.AllDirectories)
.Where(path => AllowedFormats.Contains(Path.GetExtension(path)))
.ToHashSet();
}
@@ -100,13 +100,13 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
}
case BackgroundImageType.HutaoBing:
await SetCurrentBackgroundPathSetAsync((client, token) => client.GetBingWallpaperAsync(token), token).ConfigureAwait(false);
await SetCurrentBackgroundPathSetAsync(client => client.GetBingWallpaperAsync()).ConfigureAwait(false);
break;
case BackgroundImageType.HutaoDaily:
await SetCurrentBackgroundPathSetAsync((client, token) => client.GetTodayWallpaperAsync(token), token).ConfigureAwait(false);
await SetCurrentBackgroundPathSetAsync(client => client.GetTodayWallpaperAsync()).ConfigureAwait(false);
break;
case BackgroundImageType.HutaoOfficialLauncher:
await SetCurrentBackgroundPathSetAsync((client, token) => client.GetLauncherWallpaperAsync(token), token).ConfigureAwait(false);
await SetCurrentBackgroundPathSetAsync(client => client.GetLauncherWallpaperAsync()).ConfigureAwait(false);
break;
default:
currentBackgroundPathSet = [];
@@ -116,10 +116,10 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
currentBackgroundPathSet ??= [];
return currentBackgroundPathSet;
async Task SetCurrentBackgroundPathSetAsync(Func<HutaoWallpaperClient, CancellationToken, ValueTask<Response<Wallpaper>>> responseFactory, CancellationToken token = default)
async Task SetCurrentBackgroundPathSetAsync(Func<HutaoWallpaperClient, ValueTask<Response<Wallpaper>>> responseFactory)
{
HutaoWallpaperClient wallpaperClient = serviceProvider.GetRequiredService<HutaoWallpaperClient>();
Response<Wallpaper> response = await responseFactory(wallpaperClient, token).ConfigureAwait(false);
Response<Wallpaper> response = await responseFactory(wallpaperClient).ConfigureAwait(false);
if (response is { Data: Wallpaper wallpaper })
{
await taskContext.SwitchToMainThreadAsync();

View File

@@ -5,5 +5,5 @@ namespace Snap.Hutao.Service.BackgroundImage;
internal interface IBackgroundImageService
{
ValueTask<ValueResult<bool, BackgroundImage?>> GetNextBackgroundImageAsync(BackgroundImage? previous, CancellationToken token = default);
ValueTask<ValueResult<bool, BackgroundImage?>> GetNextBackgroundImageAsync(BackgroundImage? previous);
}

View File

@@ -2,8 +2,9 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Cultivation;
@@ -14,90 +15,182 @@ internal sealed partial class CultivationDbService : ICultivationDbService
{
private readonly IServiceProvider serviceProvider;
public IServiceProvider ServiceProvider { get => serviceProvider; }
public List<InventoryItem> GetInventoryItemListByProjectId(Guid projectId)
{
return this.List<InventoryItem>(i => i.ProjectId == projectId);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IQueryable<InventoryItem> result = appDbContext.InventoryItems.AsNoTracking().Where(a => a.ProjectId == projectId);
return [.. result];
}
}
public ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId, CancellationToken token = default)
public async ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId)
{
return this.ListAsync<InventoryItem>(i => i.ProjectId == projectId, token);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.InventoryItems
.AsNoTracking()
.Where(a => a.ProjectId == projectId)
.ToListAsync()
.ConfigureAwait(false);
}
}
public ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId, CancellationToken token = default)
public async ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId)
{
return this.ListAsync<CultivateEntry>(e => e.ProjectId == projectId, token);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.CultivateEntries
.AsNoTracking()
.Where(e => e.ProjectId == projectId)
.ToListAsync()
.ConfigureAwait(false);
}
}
public ValueTask<List<CultivateEntry>> GetCultivateEntryListIncludingLevelInformationByProjectIdAsync(Guid projectId, CancellationToken token = default)
public async ValueTask<List<CultivateEntry>> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId)
{
return this.ListAsync<CultivateEntry, CultivateEntry>(query => query.Where(e => e.ProjectId == projectId).Include(e => e.LevelInformation), token);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.CultivateEntries
.AsNoTracking()
.Where(e => e.ProjectId == projectId)
.Include(e => e.LevelInformation)
.ToListAsync()
.ConfigureAwait(false);
}
}
public ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId, CancellationToken token = default)
public async ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId)
{
return this.ListAsync<CultivateItem, CultivateItem>(query => query.Where(i => i.EntryId == entryId).OrderBy(i => i.ItemId), token);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.CultivateItems
.Where(i => i.EntryId == entryId)
.OrderBy(i => i.ItemId)
.ToListAsync()
.ConfigureAwait(false);
}
}
public async ValueTask RemoveCultivateEntryByIdAsync(Guid entryId, CancellationToken token = default)
public async ValueTask RemoveCultivateEntryByIdAsync(Guid entryId)
{
await this.DeleteByInnerIdAsync<CultivateEntry>(entryId, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateEntries
.ExecuteDeleteWhereAsync(i => i.InnerId == entryId)
.ConfigureAwait(false);
}
}
public void UpdateCultivateItem(CultivateItem item)
{
this.Update(item);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.CultivateItems.UpdateAndSave(item);
}
}
public async ValueTask UpdateCultivateItemAsync(CultivateItem item, CancellationToken token = default)
public async ValueTask UpdateCultivateItemAsync(CultivateItem item)
{
await this.UpdateAsync(item, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateItems.UpdateAndSaveAsync(item).ConfigureAwait(false);
}
}
public async ValueTask<CultivateEntry?> GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId, CancellationToken token = default)
public async ValueTask<CultivateEntry?> GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId)
{
return await this.SingleOrDefaultAsync<CultivateEntry>(e => e.ProjectId == projectId && e.Id == itemId, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.CultivateEntries
.SingleOrDefaultAsync(e => e.ProjectId == projectId && e.Id == itemId)
.ConfigureAwait(false);
}
}
public async ValueTask AddCultivateEntryAsync(CultivateEntry entry, CancellationToken token = default)
public async ValueTask AddCultivateEntryAsync(CultivateEntry entry)
{
await this.AddAsync(entry, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateEntries.AddAndSaveAsync(entry).ConfigureAwait(false);
}
}
public async ValueTask RemoveCultivateItemRangeByEntryIdAsync(Guid entryId, CancellationToken token = default)
public async ValueTask RemoveCultivateItemRangeByEntryIdAsync(Guid entryId)
{
await this.DeleteAsync<CultivateItem>(i => i.EntryId == entryId, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateItems
.ExecuteDeleteWhereAsync(i => i.EntryId == entryId)
.ConfigureAwait(false);
}
}
public async ValueTask AddCultivateItemRangeAsync(IEnumerable<CultivateItem> toAdd, CancellationToken token = default)
public async ValueTask AddCultivateItemRangeAsync(IEnumerable<CultivateItem> toAdd)
{
await this.AddRangeAsync(toAdd, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateItems.AddRangeAndSaveAsync(toAdd).ConfigureAwait(false);
}
}
public async ValueTask AddCultivateProjectAsync(CultivateProject project, CancellationToken token = default)
public async ValueTask AddCultivateProjectAsync(CultivateProject project)
{
await this.AddAsync(project, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateProjects.AddAndSaveAsync(project).ConfigureAwait(false);
}
}
public async ValueTask RemoveCultivateProjectByIdAsync(Guid projectId, CancellationToken token = default)
public async ValueTask RemoveCultivateProjectByIdAsync(Guid projectId)
{
await this.DeleteByInnerIdAsync<CultivateProject>(projectId, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateProjects
.ExecuteDeleteWhereAsync(p => p.InnerId == projectId)
.ConfigureAwait(false);
}
}
public ObservableCollection<CultivateProject> GetCultivateProjectCollection()
{
return this.ObservableCollection<CultivateProject>();
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.CultivateProjects.AsNoTracking().ToObservableCollection();
}
}
public async ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId, CancellationToken token = default)
public async ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId)
{
await this.DeleteAsync<CultivateEntryLevelInformation>(l => l.EntryId == entryId, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.LevelInformations.ExecuteDeleteWhereAsync(l => l.EntryId == entryId).ConfigureAwait(false);
}
}
public async ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation, CancellationToken token = default)
public async ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation)
{
await this.AddAsync(levelInformation, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.LevelInformations.AddAndSaveAsync(levelInformation).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Cultivation;
/// <summary>
/// 集合部分
/// </summary>
internal sealed partial class CultivationService
{
private ObservableCollection<CultivateProject>? projects;
/// <inheritdoc/>
public CultivateProject? Current
{
get => dbCurrent.Current;
set => dbCurrent.Current = value;
}
/// <inheritdoc/>
public ObservableCollection<CultivateProject> ProjectCollection
{
get
{
if (projects is null)
{
projects = cultivationDbService.GetCultivateProjectCollection();
Current ??= projects.SelectedOrDefault();
}
return projects;
}
}
/// <inheritdoc/>
public async ValueTask<ProjectAddResult> TryAddProjectAsync(CultivateProject project)
{
if (string.IsNullOrWhiteSpace(project.Name))
{
return ProjectAddResult.InvalidName;
}
ArgumentNullException.ThrowIfNull(projects);
if (projects.Any(a => a.Name == project.Name))
{
return ProjectAddResult.AlreadyExists;
}
// Sync cache
await taskContext.SwitchToMainThreadAsync();
projects.Add(project);
// Sync database
await taskContext.SwitchToBackgroundAsync();
await cultivationDbService.AddCultivateProjectAsync(project).ConfigureAwait(false);
return ProjectAddResult.Added;
}
/// <inheritdoc/>
public async ValueTask RemoveProjectAsync(CultivateProject project)
{
ArgumentNullException.ThrowIfNull(projects);
// Sync cache
// Keep this on main thread.
await taskContext.SwitchToMainThreadAsync();
projects.Remove(project);
// Sync database
await taskContext.SwitchToBackgroundAsync();
await cultivationDbService.RemoveCultivateProjectByIdAsync(project.InnerId).ConfigureAwait(false);
}
}

View File

@@ -4,13 +4,13 @@
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Model.Metadata.Item;
using Snap.Hutao.Service.Inventory;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.ViewModel.Cultivation;
using System.Collections.ObjectModel;
using CalculateItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
namespace Snap.Hutao.Service.Cultivation;
@@ -25,46 +25,28 @@ internal sealed partial class CultivationService : ICultivationService
private readonly ScopedDbCurrent<CultivateProject, Message.CultivateProjectChangedMessage> dbCurrent;
private readonly ICultivationDbService cultivationDbService;
private readonly IInventoryDbService inventoryDbService;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private ObservableCollection<CultivateProject>? projects;
/// <inheritdoc/>
public CultivateProject? Current
{
get => dbCurrent.Current;
set => dbCurrent.Current = value;
}
/// <inheritdoc/>
public ObservableCollection<CultivateProject> ProjectCollection
{
get
{
if (projects is null)
{
projects = cultivationDbService.GetCultivateProjectCollection();
Current ??= projects.SelectedOrDefault();
}
return projects;
}
}
/// <inheritdoc/>
public List<InventoryItemView> GetInventoryItemViews(CultivateProject cultivateProject, ICultivationMetadataContext context, ICommand saveCommand)
{
Guid projectId = cultivateProject.InnerId;
List<InventoryItem> entities = cultivationDbService.GetInventoryItemListByProjectId(projectId);
List<InventoryItemView> results = [];
foreach (Material meta in context.EnumerateInventoryMaterial())
using (IServiceScope scope = serviceProvider.CreateScope())
{
InventoryItem entity = entities.SingleOrDefault(e => e.ItemId == meta.Id) ?? InventoryItem.From(projectId, meta.Id);
results.Add(new(entity, meta, saveCommand));
}
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return results;
Guid projectId = cultivateProject.InnerId;
List<InventoryItem> entities = cultivationDbService.GetInventoryItemListByProjectId(projectId);
List<InventoryItemView> results = [];
foreach (Material meta in context.EnumerateInventoryMaterial())
{
InventoryItem entity = entities.SingleOrDefault(e => e.ItemId == meta.Id) ?? InventoryItem.From(projectId, meta.Id);
results.Add(new(entity, meta, saveCommand));
}
return results;
}
}
/// <inheritdoc/>
@@ -72,15 +54,13 @@ internal sealed partial class CultivationService : ICultivationService
{
await taskContext.SwitchToBackgroundAsync();
List<CultivateEntry> entries = await cultivationDbService
.GetCultivateEntryListIncludingLevelInformationByProjectIdAsync(cultivateProject.InnerId)
.GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(cultivateProject.InnerId)
.ConfigureAwait(false);
List<CultivateEntryView> resultEntries = new(entries.Count);
foreach (CultivateEntry entry in entries)
{
List<CultivateItemView> entryItems = [];
// Async operation here, thus we can't use the Span trick.
foreach (CultivateItem cultivateItem in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false))
{
entryItems.Add(new(cultivateItem, context.GetMaterial(cultivateItem.ItemId)));
@@ -98,7 +78,9 @@ internal sealed partial class CultivationService : ICultivationService
resultEntries.Add(new(entry, item, entryItems));
}
return resultEntries.SortByDescending(e => e.IsToday).ToObservableCollection();
return resultEntries
.OrderByDescending(e => e.IsToday)
.ToObservableCollection();
}
/// <inheritdoc/>
@@ -110,9 +92,11 @@ internal sealed partial class CultivationService : ICultivationService
Guid projectId = cultivateProject.InnerId;
foreach (CultivateEntry entry in await cultivationDbService.GetCultivateEntryListByProjectIdAsync(projectId, token).ConfigureAwait(false))
token.ThrowIfCancellationRequested();
foreach (CultivateEntry entry in await cultivationDbService.GetCultivateEntryListByProjectIdAsync(projectId).ConfigureAwait(false))
{
foreach (CultivateItem item in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId, token).ConfigureAwait(false))
foreach (CultivateItem item in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false))
{
if (item.IsFinished)
{
@@ -130,7 +114,9 @@ internal sealed partial class CultivationService : ICultivationService
}
}
foreach (InventoryItem inventoryItem in await cultivationDbService.GetInventoryItemListByProjectIdAsync(projectId, token).ConfigureAwait(false))
token.ThrowIfCancellationRequested();
foreach (InventoryItem inventoryItem in await cultivationDbService.GetInventoryItemListByProjectIdAsync(projectId).ConfigureAwait(false))
{
if (resultItems.SingleOrDefault(i => i.Inner.Id == inventoryItem.ItemId) is { } existedItem)
{
@@ -138,12 +124,15 @@ internal sealed partial class CultivationService : ICultivationService
}
}
token.ThrowIfCancellationRequested();
return resultItems.SortBy(item => item.Inner.Id, MaterialIdComparer.Shared).ToObservableCollection();
}
/// <inheritdoc/>
public async ValueTask RemoveCultivateEntryAsync(Guid entryId)
{
await taskContext.SwitchToBackgroundAsync();
await cultivationDbService.RemoveCultivateEntryByIdAsync(entryId).ConfigureAwait(false);
}
@@ -160,7 +149,7 @@ internal sealed partial class CultivationService : ICultivationService
}
/// <inheritdoc/>
public async ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<CalculateItem> items, LevelInformation levelInformation)
public async ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Web.Hoyolab.Takumi.Event.Calculate.Item> items, LevelInformation levelInformation)
{
if (items.Count == 0)
{
@@ -201,45 +190,4 @@ internal sealed partial class CultivationService : ICultivationService
return true;
}
/// <inheritdoc/>
public async ValueTask<ProjectAddResultKind> TryAddProjectAsync(CultivateProject project)
{
if (string.IsNullOrWhiteSpace(project.Name))
{
return ProjectAddResultKind.InvalidName;
}
ArgumentNullException.ThrowIfNull(projects);
if (projects.Any(a => a.Name == project.Name))
{
return ProjectAddResultKind.AlreadyExists;
}
// Sync cache
await taskContext.SwitchToMainThreadAsync();
projects.Add(project);
// Sync database
await taskContext.SwitchToBackgroundAsync();
await cultivationDbService.AddCultivateProjectAsync(project).ConfigureAwait(false);
return ProjectAddResultKind.Added;
}
/// <inheritdoc/>
public async ValueTask RemoveProjectAsync(CultivateProject project)
{
ArgumentNullException.ThrowIfNull(projects);
// Sync cache
// Keep this on main thread.
await taskContext.SwitchToMainThreadAsync();
projects.Remove(project);
// Sync database
await taskContext.SwitchToBackgroundAsync();
await cultivationDbService.RemoveCultivateProjectByIdAsync(project.InnerId).ConfigureAwait(false);
}
}

View File

@@ -2,48 +2,43 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Cultivation;
internal interface ICultivationDbService : IAppDbService<InventoryItem>,
IAppDbService<CultivateEntryLevelInformation>,
IAppDbService<CultivateProject>,
IAppDbService<CultivateEntry>,
IAppDbService<CultivateItem>
internal interface ICultivationDbService
{
ValueTask AddCultivateProjectAsync(CultivateProject project, CancellationToken token = default);
ValueTask AddCultivateProjectAsync(CultivateProject project);
ValueTask RemoveCultivateEntryByIdAsync(Guid entryId, CancellationToken token = default);
ValueTask RemoveCultivateEntryByIdAsync(Guid entryId);
ValueTask RemoveCultivateItemRangeByEntryIdAsync(Guid entryId, CancellationToken token = default);
ValueTask RemoveCultivateItemRangeByEntryIdAsync(Guid entryId);
ValueTask RemoveCultivateProjectByIdAsync(Guid projectId, CancellationToken token = default);
ValueTask RemoveCultivateProjectByIdAsync(Guid projectId);
ValueTask<CultivateEntry?> GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId, CancellationToken token = default);
ValueTask<CultivateEntry?> GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId);
ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId, CancellationToken token = default);
ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId);
ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId, CancellationToken token = default);
ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId);
ObservableCollection<CultivateProject> GetCultivateProjectCollection();
List<InventoryItem> GetInventoryItemListByProjectId(Guid projectId);
ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId, CancellationToken token = default);
ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId);
ValueTask AddCultivateEntryAsync(CultivateEntry entry, CancellationToken token = default);
ValueTask AddCultivateEntryAsync(CultivateEntry entry);
ValueTask AddCultivateItemRangeAsync(IEnumerable<CultivateItem> toAdd, CancellationToken token = default);
ValueTask AddCultivateItemRangeAsync(IEnumerable<CultivateItem> toAdd);
void UpdateCultivateItem(CultivateItem item);
ValueTask UpdateCultivateItemAsync(CultivateItem item, CancellationToken token = default);
ValueTask UpdateCultivateItemAsync(CultivateItem item);
ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId, CancellationToken token = default);
ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId);
ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation, CancellationToken token = default);
ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation);
ValueTask<List<CultivateEntry>> GetCultivateEntryListIncludingLevelInformationByProjectIdAsync(Guid projectId, CancellationToken token = default);
ValueTask<List<CultivateEntry>> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId);
}

View File

@@ -65,5 +65,5 @@ internal interface ICultivationService
/// </summary>
/// <param name="project">项目</param>
/// <returns>添加操作的结果</returns>
ValueTask<ProjectAddResultKind> TryAddProjectAsync(CultivateProject project);
ValueTask<ProjectAddResult> TryAddProjectAsync(CultivateProject project);
}

View File

@@ -8,10 +8,9 @@ namespace Snap.Hutao.Service.Cultivation;
internal sealed class MaterialIdComparer : IComparer<MaterialId>
{
private static MaterialIdComparer? shared;
private static object? syncRoot;
private static readonly Lazy<MaterialIdComparer> LazyShared = new(() => new());
public static MaterialIdComparer Shared { get => LazyInitializer.EnsureInitialized(ref shared, ref syncRoot, () => new()); }
public static MaterialIdComparer Shared { get => LazyShared.Value; }
public int Compare(MaterialId x, MaterialId y)
{

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.Cultivation;
/// 项目添加结果
/// </summary>
[HighQuality]
internal enum ProjectAddResultKind
internal enum ProjectAddResult
{
/// <summary>
/// 添加成功

View File

@@ -25,7 +25,7 @@ internal sealed partial class DailyNoteNotificationOperation
private readonly ITaskContext taskContext;
private readonly IGameServiceFacade gameService;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly BindingClient bindingClient;
private readonly DailyNoteOptions options;
private readonly RuntimeOptions runtimeOptions;
@@ -58,19 +58,14 @@ internal sealed partial class DailyNoteNotificationOperation
}
else
{
using (IServiceScope scope = serviceScopeFactory.CreateScope())
Response<ListWrapper<UserGameRole>> rolesResponse = await bindingClient
.GetUserGameRolesOverseaAwareAsync(entry.User)
.ConfigureAwait(false);
if (rolesResponse.IsOk())
{
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService<BindingClient>();
Response<ListWrapper<UserGameRole>> rolesResponse = await bindingClient
.GetUserGameRolesOverseaAwareAsync(entry.User)
.ConfigureAwait(false);
if (rolesResponse.IsOk())
{
List<UserGameRole> roles = rolesResponse.Data.List;
attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? ToastAttributionUnknown;
}
List<UserGameRole> roles = rolesResponse.Data.List;
attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? ToastAttributionUnknown;
}
}

View File

@@ -17,7 +17,7 @@ namespace Snap.Hutao.Service.Hutao;
internal sealed partial class HutaoAsAService : IHutaoAsAService
{
private const int AnnouncementDuration = 30;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly HutaoAsAServiceClient hutaoAsServiceClient;
private ObservableCollection<HutaoAnnouncement>? announcements;
@@ -29,13 +29,7 @@ internal sealed partial class HutaoAsAService : IHutaoAsAService
ApplicationDataCompositeValue excludedIds = LocalSetting.Get(SettingKeys.ExcludedAnnouncementIds, new ApplicationDataCompositeValue());
List<long> data = excludedIds.Select(kvp => long.Parse(kvp.Key, CultureInfo.InvariantCulture)).ToList();
Response<List<HutaoAnnouncement>> response;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoAsAServiceClient hutaoAsAServiceClient = scope.ServiceProvider.GetRequiredService<HutaoAsAServiceClient>();
response = await hutaoAsAServiceClient.GetAnnouncementListAsync(data, token).ConfigureAwait(false);
}
Response<List<HutaoAnnouncement>> response = await hutaoAsServiceClient.GetAnnouncementListAsync(data, token).ConfigureAwait(false);
if (response.IsOk())
{

View File

@@ -15,7 +15,7 @@ internal sealed partial class HutaoUserService : IHutaoUserService, IHutaoUserSe
{
private readonly TaskCompletionSource initializeCompletionSource = new();
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly HutaoPassportClient passportClient;
private readonly ITaskContext taskContext;
private readonly HutaoUserOptions options;
@@ -40,25 +40,20 @@ internal sealed partial class HutaoUserService : IHutaoUserService, IHutaoUserSe
}
else
{
using (IServiceScope scope = serviceScopeFactory.CreateScope())
Web.Response.Response<string> response = await passportClient.LoginAsync(userName, password, token).ConfigureAwait(false);
if (response.IsOk())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
Web.Response.Response<string> response = await hutaoPassportClient.LoginAsync(userName, password, token).ConfigureAwait(false);
if (response.IsOk())
if (await options.PostLoginSucceedAsync(passportClient, taskContext, userName, password, response.Data).ConfigureAwait(false))
{
if (await options.PostLoginSucceedAsync(hutaoPassportClient, taskContext, userName, password, response.Data).ConfigureAwait(false))
{
isInitialized = true;
}
}
else
{
await taskContext.SwitchToMainThreadAsync();
options.PostLoginFailed();
isInitialized = true;
}
}
else
{
await taskContext.SwitchToMainThreadAsync();
options.PostLoginFailed();
}
}
initializeCompletionSource.TrySetResult();

View File

@@ -1,12 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataDictionaryIdReliquarySource
{
public Dictionary<ReliquaryId, Reliquary> IdReliquaryMap { get; set; }
}

View File

@@ -62,11 +62,6 @@ internal static class MetadataServiceContextExtension
dictionaryIdMaterialSource.IdMaterialMap = await metadataService.GetIdToMaterialMapAsync(token).ConfigureAwait(false);
}
if (context is IMetadataDictionaryIdReliquarySource dictionaryIdReliquarySource)
{
dictionaryIdReliquarySource.IdReliquaryMap = await metadataService.GetIdToReliquaryMapAsync(token).ConfigureAwait(false);
}
if (context is IMetadataDictionaryIdReliquaryAffixWeightSource dictionaryIdReliquaryAffixWeightSource)
{
dictionaryIdReliquaryAffixWeightSource.IdReliquaryAffixWeightMap = await metadataService.GetIdToReliquaryAffixWeightMapAsync(token).ConfigureAwait(false);

View File

@@ -6,7 +6,6 @@ using Snap.Hutao.Core;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service.Notification;
@@ -30,12 +29,12 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi
private readonly TaskCompletionSource initializeCompletionSource = new();
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly ILogger<MetadataService> logger;
private readonly MetadataOptions metadataOptions;
private readonly IInfoBarService infoBarService;
private readonly JsonSerializerOptions options;
private readonly IMemoryCache memoryCache;
private readonly HttpClient httpClient;
private bool isInitialized;
@@ -86,7 +85,7 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi
else
{
FileNotFoundException exception = new(SH.ServiceMetadataFileNotFound, fileName);
throw HutaoException.Throw(SH.ServiceMetadataFileNotFound, exception);
throw ThrowHelper.UserdataCorrupted(SH.ServiceMetadataFileNotFound, exception);
}
}
@@ -120,17 +119,10 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi
Dictionary<string, string>? metadataFileHashs;
try
{
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
IHttpClientFactory httpClientFactory = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>();
using (HttpClient httpClient = httpClientFactory.CreateClient(nameof(MetadataService)))
{
// Download meta check file
metadataFileHashs = await httpClient
.GetFromJsonAsync<Dictionary<string, string>>(metadataOptions.GetLocalizedRemoteFile(MetaFileName), options, token)
.ConfigureAwait(false);
}
}
// download meta check file
metadataFileHashs = await httpClient
.GetFromJsonAsync<Dictionary<string, string>>(metadataOptions.GetLocalizedRemoteFile(MetaFileName), options, token)
.ConfigureAwait(false);
if (metadataFileHashs is null)
{
@@ -184,28 +176,23 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi
private async ValueTask DownloadMetadataSourceFilesAsync(string fileFullName, CancellationToken token)
{
Stream sourceStream;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
IHttpClientFactory httpClientFactory = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>();
using (HttpClient httpClient = httpClientFactory.CreateClient(nameof(MetadataService)))
{
sourceStream = await httpClient
.GetStreamAsync(metadataOptions.GetLocalizedRemoteFile(fileFullName), token)
.ConfigureAwait(false);
}
}
Stream sourceStream = await httpClient
.GetStreamAsync(metadataOptions.GetLocalizedRemoteFile(fileFullName), token)
.ConfigureAwait(false);
// Write stream while convert LF to CRLF
using (StreamReaderWriter readerWriter = new(new(sourceStream), File.CreateText(metadataOptions.GetLocalizedLocalFile(fileFullName))))
using (StreamReader streamReader = new(sourceStream))
{
while (await readerWriter.ReadLineAsync(token).ConfigureAwait(false) is { } line)
using (StreamWriter streamWriter = File.CreateText(metadataOptions.GetLocalizedLocalFile(fileFullName)))
{
await readerWriter.WriteAsync(line).ConfigureAwait(false);
if (!readerWriter.Reader.EndOfStream)
while (await streamReader.ReadLineAsync(token).ConfigureAwait(false) is { } line)
{
await readerWriter.WriteAsync(StringLiterals.CRLF).ConfigureAwait(false);
await streamWriter.WriteAsync(line).ConfigureAwait(false);
if (!streamReader.EndOfStream)
{
await streamWriter.WriteAsync(StringLiterals.CRLF).ConfigureAwait(false);
}
}
}
}

View File

@@ -25,12 +25,22 @@ internal static class MetadataServiceDictionaryExtension
public static ValueTask<Dictionary<ExtendedEquipAffixId, ReliquarySet>> GetExtendedEquipAffixIdToReliquarySetMapAsync(this IMetadataService metadataService, CancellationToken token = default)
{
return metadataService.FromCacheAsDictionaryAsync(FileNameReliquarySet, (List<ReliquarySet> list) => list.SelectMany(set => set.EquipAffixIds, (set, id) => (Id: id, Set: set)), token);
return metadataService.FromCacheAsDictionaryAsync<ReliquarySet, (ExtendedEquipAffixId Id, ReliquarySet Set), ExtendedEquipAffixId, ReliquarySet>(
FileNameReliquarySet,
list => list.SelectMany(set => set.EquipAffixIds.Select(id => (Id: id, Set: set))),
tuple => tuple.Id,
tuple => tuple.Set,
token);
}
public static ValueTask<Dictionary<TowerLevelGroupId, List<TowerLevel>>> GetGroupIdToTowerLevelGroupMapAsync(this IMetadataService metadataService, CancellationToken token = default)
{
return metadataService.FromCacheAsDictionaryAsync(FileNameTowerLevel, (List<TowerLevel> list) => list.GroupBy(l => l.GroupId), g => g.Key, g => g.ToList(), token);
return metadataService.FromCacheAsDictionaryAsync<TowerLevel, IGrouping<TowerLevelGroupId, TowerLevel>, TowerLevelGroupId, List<TowerLevel>>(
FileNameTowerLevel,
list => list.GroupBy(l => l.GroupId),
g => g.Key,
g => g.ToList(),
token);
}
public static ValueTask<Dictionary<AchievementId, Model.Metadata.Achievement.Achievement>> GetIdToAchievementMapAsync(this IMetadataService metadataService, CancellationToken token = default)
@@ -71,11 +81,6 @@ internal static class MetadataServiceDictionaryExtension
return metadataService.FromCacheAsDictionaryAsync<MaterialId, Material>(FileNameMaterial, a => a.Id, token);
}
public static ValueTask<Dictionary<ReliquaryId, Reliquary>> GetIdToReliquaryMapAsync(this IMetadataService metadataService, CancellationToken token = default)
{
return metadataService.FromCacheAsDictionaryAsync(FileNameReliquary, (List<Reliquary> list) => list.SelectMany(r => r.Ids, (r, i) => (Index: i, Reliquary: r)), token);
}
public static ValueTask<Dictionary<AvatarId, ReliquaryAffixWeight>> GetIdToReliquaryAffixWeightMapAsync(this IMetadataService metadataService, CancellationToken token = default)
{
return metadataService.FromCacheAsDictionaryAsync<AvatarId, ReliquaryAffixWeight>(FileNameReliquaryAffixWeight, r => r.AvatarId, token);
@@ -172,12 +177,6 @@ internal static class MetadataServiceDictionaryExtension
return metadataService.MemoryCache.Set(cacheKey, dict);
}
private static ValueTask<Dictionary<TKey, TValue>> FromCacheAsDictionaryAsync<TData, TKey, TValue>(this IMetadataService metadataService, string fileName, Func<List<TData>, IEnumerable<(TKey Key, TValue Value)>> listSelector, CancellationToken token)
where TKey : notnull
{
return FromCacheAsDictionaryAsync(metadataService, fileName, listSelector, kvp => kvp.Key, kvp => kvp.Value, token);
}
private static async ValueTask<Dictionary<TKey, TValue>> FromCacheAsDictionaryAsync<TData, TMiddle, TKey, TValue>(this IMetadataService metadataService, string fileName, Func<List<TData>, IEnumerable<TMiddle>> listSelector, Func<TMiddle, TKey> keySelector, Func<TMiddle, TValue> valueSelector, CancellationToken token)
where TKey : notnull
{

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Service.User;
[Injection(InjectAs.Singleton, typeof(IUserFingerprintService))]
internal sealed partial class UserFingerprintService : IUserFingerprintService
{
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly DeviceFpClient deviceFpClient;
public async ValueTask TryInitializeAsync(ViewModel.User.User user, CancellationToken token = default)
{
@@ -101,13 +101,7 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService
DeviceFp = string.IsNullOrEmpty(user.Fingerprint) ? Core.Random.GetLowerHexString(13) : user.Fingerprint,
};
Response<DeviceFpWrapper> response;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
DeviceFpClient deviceFpClient = scope.ServiceProvider.GetRequiredService<DeviceFpClient>();
response = await deviceFpClient.GetFingerprintAsync(data, token).ConfigureAwait(false);
}
Response<DeviceFpWrapper> response = await deviceFpClient.GetFingerprintAsync(data, token).ConfigureAwait(false);
user.TryUpdateFingerprint(response.IsOk() ? response.Data.DeviceFp : string.Empty);
user.NeedDbUpdateAfterResume = true;

View File

@@ -181,8 +181,6 @@
<None Remove="View\Dialog\LaunchGameConfigurationFixDialog.xaml" />
<None Remove="View\Dialog\LaunchGamePackageConvertDialog.xaml" />
<None Remove="View\Dialog\ReconfirmDialog.xaml" />
<None Remove="View\Dialog\SpiralAbyssUploadRecordHomaNotLoginDialog.xaml" />
<None Remove="View\Dialog\UpdatePackageDownloadConfirmDialog.xaml" />
<None Remove="View\Dialog\UserDialog.xaml" />
<None Remove="View\Dialog\UserQRCodeDialog.xaml" />
<None Remove="View\Guide\GuideView.xaml" />
@@ -330,7 +328,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Snap.Hutao.Elevated.Launcher.Runtime" Version="1.1.1">
<PackageReference Include="Snap.Hutao.Elevated.Launcher.Runtime" Version="1.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
@@ -355,16 +353,6 @@
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnablePreviewMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\SpiralAbyssUploadRecordHomaNotLoginDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\UpdatePackageDownloadConfirmDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\LaunchGameConfigurationFixDialog.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -28,8 +28,8 @@
<cwc:Segmented
x:Name="SkillSelectorSegmented"
HorizontalAlignment="Stretch"
ItemTemplate="{StaticResource SkillHeaderTemplate}"
SelectionChanged="OnSkillSelectorSegmentedSelectionChanged"/>
SelectionChanged="OnSkillSelectorSegmentedSelectionChanged"
ItemTemplate="{StaticResource SkillHeaderTemplate}"/>
<ContentPresenter Content="{x:Bind Selected, Mode=OneWay}" ContentTemplate="{x:Bind ItemTemplate}"/>
</StackPanel>
</UserControl>

View File

@@ -19,8 +19,6 @@
PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportUserNameHint}"
Text="{x:Bind UserName, Mode=TwoWay}"/>
<PasswordBox
Width="360"
MaxWidth="360"
Margin="0,16,0,0"
Password="{x:Bind Password, Mode=TwoWay}"
PlaceholderText="{shcm:ResourceString Name=ViewPageHutaoPassportPasswordHint}"/>

View File

@@ -31,8 +31,6 @@
Content="{shcm:ResourceString Name=ViewPageHutaoPassportVerifyCodeAction}"/>
</Grid>
<PasswordBox
Width="360"
MaxWidth="360"
Margin="0,16,0,0"
IsEnabled="{x:Bind VerifyCode, Converter={StaticResource StringBoolConverter}, Mode=OneWay}"
Password="{x:Bind Password, Mode=TwoWay}"

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.View.Dialog;
[DependencyProperty("VerifyCode", typeof(string))]
internal sealed partial class HutaoPassportRegisterDialog : ContentDialog
{
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly HutaoPassportClient homaPassportClient;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
@@ -23,7 +23,7 @@ internal sealed partial class HutaoPassportRegisterDialog : ContentDialog
InitializeComponent();
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
serviceScopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
homaPassportClient = serviceProvider.GetRequiredService<HutaoPassportClient>();
infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
}
@@ -49,12 +49,7 @@ internal sealed partial class HutaoPassportRegisterDialog : ContentDialog
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse response = await hutaoPassportClient.RequestVerifyAsync(UserName, VerifyCodeRequestType.Registration).ConfigureAwait(false);
infoBarService.Information(response.GetLocalizationMessage());
}
HutaoResponse response = await homaPassportClient.RequestVerifyAsync(UserName, VerifyCodeRequestType.Registration).ConfigureAwait(false);
infoBarService.Information(response.GetLocalizationMessage());
}
}

View File

@@ -31,8 +31,6 @@
Content="{shcm:ResourceString Name=ViewPageHutaoPassportVerifyCodeAction}"/>
</Grid>
<PasswordBox
Width="360"
MaxWidth="360"
Margin="0,16,0,0"
IsEnabled="{x:Bind VerifyCode, Converter={StaticResource StringBoolConverter}, Mode=OneWay}"
Password="{x:Bind Password, Mode=TwoWay}"

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.View.Dialog;
[DependencyProperty("VerifyCode", typeof(string))]
internal sealed partial class HutaoPassportResetPasswordDialog : ContentDialog
{
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly HutaoPassportClient homaPassportClient;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
@@ -23,7 +23,7 @@ internal sealed partial class HutaoPassportResetPasswordDialog : ContentDialog
InitializeComponent();
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
serviceScopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
homaPassportClient = serviceProvider.GetRequiredService<HutaoPassportClient>();
infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
}
@@ -49,12 +49,7 @@ internal sealed partial class HutaoPassportResetPasswordDialog : ContentDialog
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse response = await hutaoPassportClient.RequestVerifyAsync(UserName, VerifyCodeRequestType.ResetPassword).ConfigureAwait(false);
infoBarService.Information(response.GetLocalizationMessage());
}
HutaoResponse response = await homaPassportClient.RequestVerifyAsync(UserName, VerifyCodeRequestType.ResetPassword).ConfigureAwait(false);
infoBarService.Information(response.GetLocalizationMessage());
}
}

View File

@@ -37,8 +37,6 @@
Content="{shcm:ResourceString Name=ViewPageHutaoPassportVerifyCodeAction}"/>
</Grid>
<PasswordBox
Width="360"
MaxWidth="360"
Margin="0,16,0,0"
IsEnabled="{x:Bind VerifyCode, Converter={StaticResource StringBoolConverter}, Mode=OneWay}"
Password="{x:Bind Password, Mode=TwoWay}"

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.View.Dialog;
[DependencyProperty("VerifyCode", typeof(string))]
internal sealed partial class HutaoPassportUnregisterDialog : ContentDialog
{
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly HutaoPassportClient homaPassportClient;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
@@ -23,7 +23,7 @@ internal sealed partial class HutaoPassportUnregisterDialog : ContentDialog
InitializeComponent();
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
serviceScopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
homaPassportClient = serviceProvider.GetRequiredService<HutaoPassportClient>();
infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
}
@@ -49,12 +49,7 @@ internal sealed partial class HutaoPassportUnregisterDialog : ContentDialog
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse response = await hutaoPassportClient.RequestVerifyAsync(UserName, VerifyCodeRequestType.CancelRegistration).ConfigureAwait(false);
infoBarService.Information(response.GetLocalizationMessage());
}
HutaoResponse response = await homaPassportClient.RequestVerifyAsync(UserName, VerifyCodeRequestType.CancelRegistration).ConfigureAwait(false);
infoBarService.Information(response.GetLocalizationMessage());
}
}

View File

@@ -1,16 +0,0 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.SpiralAbyssUploadRecordHomaNotLoginDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
Title="{shcm:ResourceString Name=ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle}"
CloseButtonText="{shcm:ResourceString Name=ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText}"
DefaultButton="Close"
PrimaryButtonText="{shcm:ResourceString Name=ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText}"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<TextBlock Text="{shcm:ResourceString Name=ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint}"/>
</ContentDialog>

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.View.Dialog;
internal sealed partial class SpiralAbyssUploadRecordHomaNotLoginDialog : ContentDialog
{
private readonly ITaskContext taskContext;
public SpiralAbyssUploadRecordHomaNotLoginDialog(IServiceProvider serviceProvider)
{
InitializeComponent();
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
}
public async ValueTask<bool> ConfirmAsync()
{
await taskContext.SwitchToMainThreadAsync();
return await ShowAsync() is ContentDialogResult.Primary;
}
}

View File

@@ -1,24 +0,0 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.UpdatePackageDownloadConfirmDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
CloseButtonText="{shcm:ResourceString Name=ContentDialogCancelCloseButtonText}"
DefaultButton="Primary"
PrimaryButtonText="{shcm:ResourceString Name=ContentDialogConfirmPrimaryButtonText}"
Style="{ThemeResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<StackPanel Spacing="16">
<TextBlock Text="">
<Run Text="{shcm:ResourceString Name=ViewTitileUpdatePackageDownloadContent}"/>
<Hyperlink NavigateUri="https://hut.ao/statements/update-log.html">
<Run Text="{shcm:ResourceString Name=ViewDialogUpdatePackageDownloadUpdatelogLinkContent}"/>
</Hyperlink>
<!-- We leave a Run here to prevent the Hyperlink Stretch -->
<Run/>
</TextBlock>
</StackPanel>
</ContentDialog>

View File

@@ -1,14 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.View.Dialog;
internal sealed partial class UpdatePackageDownloadConfirmDialog : ContentDialog
{
public UpdatePackageDownloadConfirmDialog()
{
InitializeComponent();
}
}

View File

@@ -229,7 +229,7 @@
DisplayMemberPath="Name"
ItemsSource="{Binding StaticResourceOptions.ImageArchives}"
SelectedItem="{Binding StaticResourceOptions.ImageArchive, Mode=TwoWay}"/>
<TextBlock Margin="0,16,0,0" Text="{Binding StaticResourceOptions.SizeInformationText, Mode=OneWay}"/>
</StackPanel>

View File

@@ -735,7 +735,7 @@
HorizontalAlignment="Right"
Foreground="#FFFFFFFF"
Style="{StaticResource TitleTextBlockStyle}"
Text="{Binding SelectedAvatar.ScoreFormatted}"/>
Text="{Binding SelectedAvatar.Score}"/>
<TextBlock
HorizontalAlignment="Right"
Foreground="#FFFFFFFF"
@@ -747,7 +747,7 @@
HorizontalAlignment="Right"
Foreground="#FFFFFFFF"
Style="{StaticResource TitleTextBlockStyle}"
Text="{Binding SelectedAvatar.CritScoreFormatted}"/>
Text="{Binding SelectedAvatar.CritScore}"/>
<TextBlock
HorizontalAlignment="Right"
Foreground="#FFFFFFFF"

View File

@@ -120,7 +120,7 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
{
try
{
ValueResult<RefreshResultKind, Summary?> summaryResult;
ValueResult<RefreshResult, Summary?> summaryResult;
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
{
ContentDialog dialog = await contentDialogFactory
@@ -135,8 +135,8 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
}
}
(RefreshResultKind result, Summary? summary) = summaryResult;
if (result == RefreshResultKind.Ok)
(RefreshResult result, Summary? summary) = summaryResult;
if (result == RefreshResult.Ok)
{
await taskContext.SwitchToMainThreadAsync();
Summary = summary;
@@ -146,16 +146,16 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
{
switch (result)
{
case RefreshResultKind.APIUnavailable:
case RefreshResult.APIUnavailable:
infoBarService.Warning(SH.ViewModelAvatarPropertyEnkaApiUnavailable);
break;
case RefreshResultKind.StatusCodeNotSucceed:
case RefreshResult.StatusCodeNotSucceed:
ArgumentNullException.ThrowIfNull(summary);
infoBarService.Warning(summary.Message);
break;
case RefreshResultKind.ShowcaseNotOpen:
case RefreshResult.ShowcaseNotOpen:
infoBarService.Warning(SH.ViewModelAvatarPropertyShowcaseNotOpen);
break;
}

View File

@@ -5,7 +5,6 @@ using Snap.Hutao.Model;
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.AvatarInfo.Factory.Builder;
namespace Snap.Hutao.ViewModel.AvatarProperty;
@@ -82,12 +81,12 @@ internal sealed class AvatarView : INameIconSide, ICalculableSource<ICalculableA
/// <summary>
/// 评分
/// </summary>
public string ScoreFormatted { get => $"{Score:F2}"; }
public string Score { get; set; } = default!;
/// <summary>
/// 双爆评分
/// </summary>
public string CritScoreFormatted { get => $"{CritScore:F2}"; }
public string CritScore { get; set; } = default!;
/// <summary>
/// 好感度等级
@@ -110,10 +109,6 @@ internal sealed class AvatarView : INameIconSide, ICalculableSource<ICalculableA
/// </summary>
internal uint LevelNumber { get; set; }
internal float Score { get; set; }
internal float CritScore { get; set; }
/// <inheritdoc/>
public ICalculableAvatar ToCalculable()
{

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.ViewModel.AvatarProperty;
/// 装备基类
/// </summary>
[HighQuality]
internal abstract class EquipView : NameIconDescription
internal abstract class Equip : NameIconDescription
{
/// <summary>
/// 等级

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.ViewModel.AvatarProperty;
/// 圣遗物
/// </summary>
[HighQuality]
internal sealed class ReliquaryView : EquipView
internal sealed class ReliquaryView : Equip
{
/// <summary>
/// 初始词条

View File

@@ -3,7 +3,6 @@
using Snap.Hutao.Model;
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.ViewModel.AvatarProperty;
@@ -12,7 +11,7 @@ namespace Snap.Hutao.ViewModel.AvatarProperty;
/// 武器
/// </summary>
[HighQuality]
internal sealed class WeaponView : EquipView, ICalculableSource<ICalculableWeapon>
internal sealed class WeaponView : Equip, ICalculableSource<ICalculableWeapon>
{
/// <summary>
/// 副属性
@@ -54,8 +53,6 @@ internal sealed class WeaponView : EquipView, ICalculableSource<ICalculableWeapo
/// </summary>
internal uint MaxLevel { get => Model.Metadata.Weapon.Weapon.GetMaxLevelByQuality(Quality); }
internal WeaponType WeaponType { get; set; }
/// <inheritdoc/>
public ICalculableWeapon ToCalculable()
{

View File

@@ -86,15 +86,15 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
switch (await cultivationService.TryAddProjectAsync(project).ConfigureAwait(false))
{
case ProjectAddResultKind.Added:
case ProjectAddResult.Added:
infoBarService.Success(SH.ViewModelCultivationProjectAdded);
await taskContext.SwitchToMainThreadAsync();
SelectedProject = project;
break;
case ProjectAddResultKind.InvalidName:
case ProjectAddResult.InvalidName:
infoBarService.Information(SH.ViewModelCultivationProjectInvalidName);
break;
case ProjectAddResultKind.AlreadyExists:
case ProjectAddResult.AlreadyExists:
infoBarService.Information(SH.ViewModelCultivationProjectAlreadyExists);
break;
default:

View File

@@ -20,7 +20,7 @@ namespace Snap.Hutao.ViewModel.Setting;
internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly HutaoPassportClient homaPassportClient;
private readonly HutaoUserOptions hutaoUserOptions;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
@@ -46,17 +46,12 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
HutaoResponse<string> response = await homaPassportClient.RegisterAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse<string> response = await hutaoPassportClient.RegisterAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(hutaoPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(homaPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
}
}
@@ -76,19 +71,14 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
HutaoResponse response = await homaPassportClient.UnregisterAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
infoBarService.Information(response.GetLocalizationMessageOrMessage());
HutaoResponse response = await hutaoPassportClient.UnregisterAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await taskContext.SwitchToMainThreadAsync();
hutaoUserOptions.PostLogoutOrUnregister();
}
await taskContext.SwitchToMainThreadAsync();
hutaoUserOptions.PostLogoutOrUnregister();
}
}
}
@@ -108,17 +98,12 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
HutaoResponse<string> response = await homaPassportClient.LoginAsync(username, password).ConfigureAwait(false);
if (response.IsOk())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse<string> response = await hutaoPassportClient.LoginAsync(username, password).ConfigureAwait(false);
if (response.IsOk())
{
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(hutaoPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(homaPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
}
}
@@ -144,17 +129,12 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
return;
}
using (IServiceScope scope = serviceScopeFactory.CreateScope())
HutaoResponse<string> response = await homaPassportClient.ResetPasswordAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
HutaoResponse<string> response = await hutaoPassportClient.ResetPasswordAsync(username, password, verifyCode).ConfigureAwait(false);
if (response.IsOk())
{
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(hutaoPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await hutaoUserOptions.PostLoginSucceedAsync(homaPassportClient, taskContext, username, password, response.Data).ConfigureAwait(false);
}
}
}

View File

@@ -5,9 +5,7 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Shell;
using Snap.Hutao.Core.Windowing;
@@ -222,6 +220,14 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
return ValueTask.FromResult(true);
}
[Command("ResetStaticResourceCommand")]
private static void ResetStaticResource()
{
StaticResource.FailAll();
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
AppInstance.Restart(string.Empty);
}
[Command("StoreReviewCommand")]
private static async Task StoreReviewAsync()
{
@@ -234,24 +240,6 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
await Launcher.LaunchUriAsync(new("ms-windows-store://pdp/?productid=9PH4NXJ2JN52"));
}
[Command("ResetStaticResourceCommand")]
private async Task ResetStaticResource()
{
ContentDialog dialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelSettingResetStaticResourceProgress)
.ConfigureAwait(false);
await using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{
await taskContext.SwitchToBackgroundAsync();
StaticResource.FailAll();
Directory.Delete(Path.Combine(runtimeOptions.LocalCache, nameof(ImageCache)), true);
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
}
AppInstance.Restart(string.Empty);
}
[Command("DeleteGameWebCacheCommand")]
private void DeleteGameWebCache()
{

View File

@@ -2,16 +2,10 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Message;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.SpiralAbyss;
using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.View.Page;
using Snap.Hutao.ViewModel.Complex;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hutao.Response;
@@ -30,14 +24,11 @@ namespace Snap.Hutao.ViewModel.SpiralAbyss;
internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecipient<UserChangedMessage>
{
private readonly ISpiralAbyssRecordService spiralAbyssRecordService;
private readonly IContentDialogFactory contentDialogFactory;
private readonly HutaoSpiralAbyssClient spiralAbyssClient;
private readonly INavigationService navigationService;
private readonly IInfoBarService infoBarService;
private readonly ITaskContext taskContext;
private readonly IUserService userService;
private readonly HutaoDatabaseViewModel hutaoDatabaseViewModel;
private readonly HutaoUserOptions hutaoUserOptions;
private ObservableCollection<SpiralAbyssView>? spiralAbyssEntries;
private SpiralAbyssView? selectedView;
@@ -137,20 +128,6 @@ internal sealed partial class SpiralAbyssRecordViewModel : Abstraction.ViewModel
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{
if (!hutaoUserOptions.IsLoggedIn)
{
SpiralAbyssUploadRecordHomaNotLoginDialog dialog = await contentDialogFactory
.CreateInstanceAsync<SpiralAbyssUploadRecordHomaNotLoginDialog>()
.ConfigureAwait(false);
if (!await dialog.ConfirmAsync().ConfigureAwait(false))
{
await taskContext.SwitchToMainThreadAsync();
await navigationService.NavigateAsync<SettingPage>(INavigationAwaiter.Default, true).ConfigureAwait(false);
return;
}
}
SimpleRecord? record = await spiralAbyssClient.GetPlayerRecordAsync(userAndUid).ConfigureAwait(false);
if (record is not null)
{

View File

@@ -10,7 +10,6 @@ using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.Update;
using Snap.Hutao.View.Dialog;
using System.Diagnostics;
using System.Globalization;
using System.Text;
@@ -70,15 +69,14 @@ internal sealed partial class TitleViewModel : Abstraction.ViewModel
if (checkUpdateResult.Kind is CheckUpdateResultKind.NeedDownload)
{
UpdatePackageDownloadConfirmDialog dialog = await contentDialogFactory
.CreateInstanceAsync<UpdatePackageDownloadConfirmDialog>()
ContentDialogResult downloadUpdateUserConsentResult = await contentDialogFactory
.CreateForConfirmCancelAsync(
SH.FormatViewTitileUpdatePackageDownloadTitle(UpdateStatus?.Version),
SH.ViewTitileUpdatePackageDownloadContent,
ContentDialogButton.Primary)
.ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
dialog.Title = SH.FormatViewTitileUpdatePackageDownloadTitle(UpdateStatus?.Version);
if (await dialog.ShowAsync() is ContentDialogResult.Primary)
if (downloadUpdateUserConsentResult is ContentDialogResult.Primary)
{
// This method will set CheckUpdateResult.Kind to NeedInstall if download success
if (!await DownloadPackageAsync(progress, checkUpdateResult).ConfigureAwait(false))

Some files were not shown because too many files have changed in this diff Show More