Compare commits

..

55 Commits

Author SHA1 Message Date
qhy040404
5182fd8577 trigger gc after viewmodel dispose 2024-04-28 14:19:24 +08:00
DismissedLight
ff2521c02c Merge pull request #1581 from DGP-Studio/feat/custom_sa_homa_not_login_dialog 2024-04-28 10:25:15 +08:00
Lightczx
5954c1a0ab fix build failed 2024-04-26 14:49:50 +08:00
Lightczx
4dc753bf5a Merge branch 'develop' of https://github.com/DGP-Studio/Snap.Hutao into develop 2024-04-26 14:47:43 +08:00
Lightczx
bd3617c15a refactor cultivation 2024-04-26 14:47:40 +08:00
qhy040404
70da292f21 refine #1575 ui 2024-04-26 12:31:27 +08:00
DismissedLight
97c5e7d37f Merge pull request #1574 from DGP-Studio/fix/1485 2024-04-26 09:27:45 +08:00
DismissedLight
388f9d5657 Merge pull request #1575 from DGP-Studio/feat/1245 2024-04-26 09:24:50 +08:00
qhy040404
74e11f3823 impl #1245 2024-04-25 22:39:05 +08:00
qhy040404
c1305cda43 set passwordbox to a constant value 2024-04-25 22:13:48 +08:00
Lightczx
b0d5051957 refactor 2024-04-25 17:29:14 +08:00
Lightczx
6a42c36a76 optimization 2024-04-25 14:44:06 +08:00
Lightczx
7cf106ec50 avatar info optimization 2024-04-24 17:12:34 +08:00
DismissedLight
f12cd63c92 Merge pull request #1563 from DGP-Studio/fix/imagecache 2024-04-23 16:55:39 +08:00
Lightczx
c441fdb6b0 code style 2024-04-23 16:55:04 +08:00
DismissedLight
09a880525b Merge pull request #1567 from Natrium0521/develop 2024-04-23 15:58:19 +08:00
Lightczx
15212d8f21 code style 2024-04-23 15:55:53 +08:00
Natrium
a60c4bff08 fix empty announcement when localized regex not match 2024-04-23 11:52:44 +08:00
qhy040404
e02985926d code style 2024-04-23 11:33:54 +08:00
Natrium
09448b7137 fix empty announcement 2024-04-23 11:15:21 +08:00
Natrium
6487df776a Fix Announcement time display 2024-04-23 00:57:34 +08:00
DismissedLight
2be11c22df Merge pull request #1560 from DGP-Studio/feat/taskbar_elevated 2024-04-22 17:01:54 +08:00
Lightczx
9ecb3d5821 adjust HttpClient scope 2024-04-22 16:51:35 +08:00
Lightczx
ca64c3e0ef impl #1550 2024-04-22 15:56:51 +08:00
qhy040404
3fe726aa63 extract logo 2024-04-22 15:23:47 +08:00
qhy040404
6b7ffe9fe9 Update Snap.Hutao.csproj 2024-04-22 15:23:46 +08:00
qhy040404
5ed5729c4e extract launcher 2024-04-22 15:23:46 +08:00
qhy040404
9ba0066f40 allow to open elevated app from taskbar 2024-04-22 15:23:38 +08:00
Lightczx
9c639fbaa4 Merge branch 'develop' of https://github.com/DGP-Studio/Snap.Hutao into develop 2024-04-22 14:17:18 +08:00
Lightczx
a592816661 Add quest type 2024-04-22 14:17:15 +08:00
DismissedLight
ec9c5ebee1 Merge pull request #1557 from DGP-Studio/fix/geetest 2024-04-22 09:30:16 +08:00
DismissedLight
9475c19b64 Merge pull request #1556 from DGP-Studio/feat/patch_statistics 2024-04-22 09:28:45 +08:00
qhy040404
9bfe7f78ef add unsafe move and delete to file operation 2024-04-20 23:48:35 +08:00
qhy040404
88d7f0bcc7 fix miss image cache 2024-04-20 22:22:53 +08:00
qhy040404
4920da4ea2 fix geetest 2024-04-20 11:27:10 +08:00
qhy040404
99b2ccb33b add patch statistics 2024-04-20 11:06:16 +08:00
Lightczx
9b94a75d6f refactor 2024-04-19 16:18:57 +08:00
Lightczx
e390ad2839 AvatarViewBuilder 2024-04-19 15:21:59 +08:00
DismissedLight
3086d59674 Merge pull request #1552 from DGP-Studio/feat/unheld_statistics_items 2024-04-19 09:40:57 +08:00
Lightczx
cb00fdbda0 code style 2024-04-19 09:41:04 +08:00
DismissedLight
1702dfcdc6 Merge branch 'develop' into feat/unheld_statistics_items 2024-04-19 09:22:57 +08:00
Lightczx
4b54b343f9 summary factory update 2024-04-18 17:13:14 +08:00
qhy040404
292b21a759 fix build 2024-04-17 15:15:00 +08:00
qhy040404
29e9413022 code style 2024-04-17 15:11:27 +08:00
qhy040404
7c923aaa5e impl #1355 2024-04-17 15:11:27 +08:00
Lightczx
c6618be0fc InitializeDataContext 2024-04-17 15:05:26 +08:00
DismissedLight
45dd276b89 Merge pull request #1546 from DGP-Studio/revert/qr_login 2024-04-17 13:52:42 +08:00
DismissedLight
8a9fb38f49 Merge pull request #1543 from DGP-Studio/fix/chronicled_hutao_statistics 2024-04-17 13:51:56 +08:00
Lightczx
1249216491 Update HorizontalEqualPanel.cs 2024-04-17 13:50:27 +08:00
DismissedLight
4bf3f4151e Merge branch 'develop' into fix/chronicled_hutao_statistics 2024-04-17 13:40:35 +08:00
Lightczx
89d909b04f httpclient scope 2024-04-17 13:37:04 +08:00
qhy040404
2cec0f5e0e refactor GachaLog statistics 2024-04-17 11:03:38 +08:00
qhy040404
bbb97cd802 fix typo 2024-04-16 17:26:53 +08:00
qhy040404
67b058f126 fix hutao statistics unavailable 2024-04-16 17:26:53 +08:00
qhy040404
b98611ccd9 Revert "temporary fix qr login"
This reverts commit d4bd610fe2.
2024-04-16 17:26:30 +08:00
165 changed files with 2412 additions and 1455 deletions

View File

@@ -28,4 +28,21 @@ internal static class FrameworkElementExtension
frameworkElement.IsRightTapEnabled = false;
frameworkElement.IsTabStop = false;
}
public static void InitializeDataContext<TDataContext>(this FrameworkElement frameworkElement, IServiceProvider? serviceProvider = default)
where TDataContext : class
{
IServiceProvider service = serviceProvider ?? Ioc.Default;
try
{
frameworkElement.DataContext = service.GetRequiredService<TDataContext>();
}
catch (Exception ex)
{
ILogger? logger = service.GetRequiredService(typeof(ILogger<>).MakeGenericType([frameworkElement.GetType()])) as ILogger;
logger?.LogError(ex, "Failed to initialize DataContext");
throw;
}
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;
@@ -18,13 +19,14 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement child in Children)
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
{
// ScrollViewer will always return an Infinity Size, we should use ActualWidth for this situation.
double availableWidth = double.IsInfinity(availableSize.Width) ? ActualWidth : availableSize.Width;
double childAvailableWidth = (availableWidth + Spacing) / Children.Count;
double childAvailableWidth = (availableWidth + Spacing) / visibleChildren.Count;
double childMaxAvailableWidth = Math.Max(MinItemWidth, childAvailableWidth);
child.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
visibleChild.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
}
return base.MeasureOverride(availableSize);
@@ -32,14 +34,14 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
protected override Size ArrangeOverride(Size finalSize)
{
int itemCount = Children.Count;
double availableItemWidth = (finalSize.Width - (Spacing * (itemCount - 1))) / itemCount;
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
double availableItemWidth = (finalSize.Width - (Spacing * (visibleChildren.Count - 1))) / visibleChildren.Count;
double actualItemWidth = Math.Max(MinItemWidth, availableItemWidth);
double offset = 0;
foreach (UIElement child in Children)
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
{
child.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
visibleChild.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
offset += actualItemWidth + Spacing;
}
@@ -49,7 +51,8 @@ internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
private static void OnLoaded(object sender, RoutedEventArgs e)
{
HorizontalEqualPanel panel = (HorizontalEqualPanel)sender;
panel.MinWidth = (panel.MinItemWidth * panel.Children.Count) + (panel.Spacing * (panel.Children.Count - 1));
int vivibleChildrenCount = panel.Children.Count(child => child.Visibility is Visibility.Visible);
panel.MinWidth = (panel.MinItemWidth * vivibleChildrenCount) + (panel.Spacing * (vivibleChildrenCount - 1));
}
private static void OnSizeChanged(object sender, SizeChangedEventArgs e)

View File

@@ -15,7 +15,7 @@ internal class ScopedPage : Page
{
private readonly RoutedEventHandler unloadEventHandler;
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope currentScope;
private readonly IServiceScope pageScope;
private bool inFrame = true;
@@ -23,7 +23,7 @@ internal class ScopedPage : Page
{
unloadEventHandler = OnUnloaded;
Unloaded += unloadEventHandler;
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
pageScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
}
public async ValueTask NotifyRecipientAsync(INavigationData extra)
@@ -44,9 +44,17 @@ internal class ScopedPage : Page
protected void InitializeWith<TViewModel>()
where TViewModel : class, IViewModel
{
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
try
{
IViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
}
catch (Exception ex)
{
pageScope.ServiceProvider.GetRequiredService<ILogger<ScopedPage>>().LogError(ex, "Failed to initialize view model.");
throw;
}
}
/// <inheritdoc/>
@@ -95,7 +103,8 @@ internal class ScopedPage : Page
viewModel.IsViewDisposed = true;
// Dispose the scope
currentScope.Dispose();
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<,,>))
.AddDbContext<AppDbContext>(AddDbContextCore);
.AddDbContextPool<AppDbContext>(AddDbContextCore);
}
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
private static void AddDbContextCore(IServiceProvider serviceProvider, DbContextOptionsBuilder builder)
{
RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
RuntimeOptions runtimeOptions = serviceProvider.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(sqlConnectionString))
using (AppDbContext context = AppDbContext.Create(serviceProvider, sqlConnectionString))
{
if (context.Database.GetPendingMigrations().Any())
{

View File

@@ -1,5 +1,7 @@
// 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
@@ -17,6 +19,6 @@ internal readonly struct MeasureExecutionToken : IDisposable
public void Dispose()
{
logger.LogDebug("{Caller} toke {Time} ms", callerName, stopwatch.GetElapsedTime().TotalMilliseconds);
logger.LogColorizedDebug(("{Caller} toke {Time} ms", ConsoleColor.Gray), (callerName, ConsoleColor.Yellow), (stopwatch.GetElapsedTime().TotalMilliseconds, ConsoleColor.DarkGreen));
}
}

View File

@@ -1,7 +1,13 @@
// 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;
@@ -39,4 +45,57 @@ 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(BitConverter.ToString, SHA1.HashData, Encoding.UTF8.GetBytes, input);
return HashCore(System.Convert.ToHexString, 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

@@ -0,0 +1,40 @@
// 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,6 +21,11 @@ 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

@@ -21,22 +21,27 @@ 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);
}
catch
{
return false;
}
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath);
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath, elevatedLauncherPath);
}
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath)
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath, string elevatedLauncherPath)
{
bool result = false;
@@ -44,17 +49,11 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
HRESULT hr = CoCreateInstance(in ShellLink.CLSID, default, CLSCTX.CLSCTX_INPROC_SERVER, in IShellLinkW.IID, out IShellLinkW* pShellLink);
if (SUCCEEDED(hr))
{
pShellLink->SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
pShellLink->SetPath(elevatedLauncherPath);
pShellLink->SetArguments(runtimeOptions.FamilyName);
pShellLink->SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
pShellLink->SetIconLocation(targetLogoPath, 0);
if (SUCCEEDED(pShellLink->QueryInterface(in IShellLinkDataList.IID, out IShellLinkDataList* pShellLinkDataList)))
{
pShellLinkDataList->GetFlags(out uint flags);
pShellLinkDataList->SetFlags(flags | (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER);
pShellLinkDataList->Release();
}
if (SUCCEEDED(pShellLink->QueryInterface(in IPersistFile.IID, out IPersistFile* pPersistFile)))
{
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.ViewModel.Game;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
@@ -35,7 +36,7 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOpt
scope = serviceProvider.CreateScope();
windowOptions = new(this, DragableGrid, new(MaxWidth, MaxHeight));
this.InitializeController(serviceProvider);
RootGrid.DataContext = scope.ServiceProvider.GetRequiredService<LaunchGameViewModel>();
RootGrid.InitializeDataContext<LaunchGameViewModel>(scope.ServiceProvider);
}
/// <inheritdoc/>

View File

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

View File

@@ -3,6 +3,7 @@
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;
@@ -13,7 +14,7 @@ namespace Snap.Hutao.Model.Entity;
/// </summary>
[HighQuality]
[Table("cultivate_projects")]
internal sealed class CultivateProject : ISelectable, IMappingFrom<CultivateProject, string, string>
internal sealed class CultivateProject : ISelectable, IMappingFrom<CultivateProject, string, string>, IAppDbEntity
{
/// <summary>
/// 内部Id

View File

@@ -2,6 +2,7 @@
// 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;
@@ -24,18 +25,8 @@ internal sealed class AppDbContext : DbContext
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
/// <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));
logger = this.GetService<ILogger<AppDbContext>>();
logger?.LogColorizedInformation("{Name}[{Id}] {Action}", nameof(AppDbContext), (ContextId, ConsoleColor.DarkCyan), ("created", ConsoleColor.Green));
}
public DbSet<SettingEntry> Settings { get; set; } = default!;
@@ -74,14 +65,14 @@ internal sealed class AppDbContext : DbContext
public DbSet<SpiralAbyssEntry> SpiralAbysses { get; set; } = default!;
/// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>
/// <param name="sqlConnectionString">连接字符串</param>
/// <returns>应用程序数据库上下文</returns>
public static AppDbContext Create(string sqlConnectionString)
public static AppDbContext Create(IServiceProvider serviceProvider, string sqlConnectionString)
{
return new(new DbContextOptionsBuilder<AppDbContext>().UseSqlite(sqlConnectionString).Options);
DbContextOptions<AppDbContext> options = new DbContextOptionsBuilder<AppDbContext>()
.UseApplicationServiceProvider(serviceProvider)
.UseSqlite(sqlConnectionString)
.Options;
return new(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($"Data Source={userdataDbName}");
return AppDbContext.Create(default!, $"Data Source={userdataDbName}");
#else
throw Must.NeverHappen();
#endif

View File

@@ -19,6 +19,7 @@ internal sealed partial class SettingEntry
public const string AnnouncementRegion = "AnnouncementRegion";
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
public const string IsUnobtainedWishItemVisible = "IsUnobtainedWishItemVisible";
public const string GeetestCustomCompositeUrl = "GeetestCustomCompositeUrl";

View File

@@ -0,0 +1,16 @@
// 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

@@ -11,11 +11,6 @@ namespace Snap.Hutao.Model.Metadata.Converter;
[HighQuality]
internal sealed class AvatarNameCardPicConverter : ValueConverter<Avatar.Avatar?, Uri>
{
/// <summary>
/// 从角色转换到名片
/// </summary>
/// <param name="avatar">角色</param>
/// <returns>名片</returns>
public static Uri AvatarToUri(Avatar.Avatar? avatar)
{
if (avatar is null)

View File

@@ -2,6 +2,7 @@
// 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,13 +2922,19 @@
<value>Weapon WIKI</value>
</data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓Event Duration〓|〓Quest Start Time〓).*?\d\.\dthe Version update(?:after|)Permanently available</value>
<value>(?:〓Event Duration〓|〓Quest Start Time〓).*?Permanently.*?Version.*?(\d\.\d).*?update</value>
</data>
<data name="WebAnnouncementMatchPersistentActivityTime" xml:space="preserve">
<value>〓Event Duration〓.*?\d\.\d Available throughout the entirety of Version</value>
<value>〓Event Duration〓.*?Available throughout the entirety of Version (\d\.\d)</value>
</data>
<data name="WebAnnouncementMatchTransientActivityTime" xml:space="preserve">
<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>
<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>
</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,13 +2922,19 @@
<value>Senjata WIKI</value>
</data>
<data name="WebAnnouncementMatchPermanentActivityTime" xml:space="preserve">
<value>(?:〓Durasi Event〓|〓Waktu Mulai Misi〓).*?\d\.\dthe Version update(?:after|)Selamanya Tersedia</value>
<value>(?:〓Durasi Event〓|〓Waktu Mulai Misi〓).*?(\d\.\d)the 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>
<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>
</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,19 +2922,25 @@
<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>
<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>
</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,13 +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>
<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>
</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,13 +2922,19 @@
<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\.\da atualização da versão(?:after|)Disponível permanentemente</value>
<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>
</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\.\dApós a atualização da versão).*?~.*?&amp;lt;t class="t_(?:gl|lc)".*?&amp;gt;(.*?)&amp;lt;/t&amp;gt;</value>
<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>
</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,6 +1361,21 @@
<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>
@@ -1721,6 +1736,9 @@
<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>
@@ -2702,6 +2720,12 @@
<data name="ViewPageSettingTranslateNavigate" xml:space="preserve">
<value>贡献翻译</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleDescription" xml:space="preserve">
<value>在祈愿记录页面角色与武器页签显示未抽取到的祈愿物品</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleHeader" xml:space="preserve">
<value>未抽取到的祈愿物品</value>
</data>
<data name="ViewPageSettingUpdateCheckAction" xml:space="preserve">
<value>前往商店</value>
</data>
@@ -2883,7 +2907,7 @@
<value>上传数据</value>
</data>
<data name="ViewTitileUpdatePackageDownloadContent" xml:space="preserve">
<value>是否立即下载</value>
<value>是否立即下载</value>
</data>
<data name="ViewTitileUpdatePackageDownloadFailedMessage" xml:space="preserve">
<value>下载更新失败</value>
@@ -2982,13 +3006,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>
<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>
</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,13 +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>
<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>
</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,13 +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>
<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>
</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,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Abstraction;
namespace Snap.Hutao.Service.Abstraction;
@@ -12,13 +11,25 @@ internal static class AppDbServiceAppDbEntityExtension
public static int DeleteByInnerId<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class, IAppDbEntity
{
return service.Execute(dbset => dbset.ExecuteDeleteWhere(e => e.InnerId == entity.InnerId));
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);
}
public static ValueTask<int> DeleteByInnerIdAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class, IAppDbEntity
{
return service.ExecuteAsync((dbset, token) => dbset.ExecuteDeleteWhereAsync(e => e.InnerId == entity.InnerId, token), token);
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);
}
public static List<TEntity> ListByArchiveId<TEntity>(this IAppDbService<TEntity> service, Guid archiveId)

View File

@@ -30,7 +30,13 @@ 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.QueryAsync((query, token) => query.Where(predicate).ToListAsync(token), token);
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);
}
public static ObservableCollection<TEntity> ObservableCollection<TEntity>(this IAppDbService<TEntity> service)

View File

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

View File

@@ -7,6 +7,7 @@ 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;
@@ -17,16 +18,15 @@ namespace Snap.Hutao.Service;
/// <inheritdoc/>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IAnnouncementService))]
[Injection(InjectAs.Scoped, typeof(IAnnouncementService))]
internal sealed partial class AnnouncementService : IAnnouncementService
{
private static readonly string CacheKey = $"{nameof(AnnouncementService)}.Cache.{nameof(AnnouncementWrapper)}";
private readonly AnnouncementClient announcementClient;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly ITaskContext taskContext;
private readonly IMemoryCache memoryCache;
/// <inheritdoc/>
public async ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(string languageCode, Region region, CancellationToken cancellationToken = default)
{
// 缓存中存在记录,直接返回
@@ -36,29 +36,37 @@ internal sealed partial class AnnouncementService : IAnnouncementService
}
await taskContext.SwitchToBackgroundAsync();
Response<AnnouncementWrapper> announcementWrapperResponse = await announcementClient
.GetAnnouncementsAsync(languageCode, region, cancellationToken)
.ConfigureAwait(false);
if (!announcementWrapperResponse.IsOk())
List<AnnouncementContent>? contents;
AnnouncementWrapper wrapper;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
return default!;
AnnouncementClient announcementClient = scope.ServiceProvider.GetRequiredService<AnnouncementClient>();
Response<AnnouncementWrapper> announcementWrapperResponse = await announcementClient
.GetAnnouncementsAsync(languageCode, region, cancellationToken)
.ConfigureAwait(false);
if (!announcementWrapperResponse.IsOk())
{
return default!;
}
wrapper = announcementWrapperResponse.Data;
Response<ListWrapper<AnnouncementContent>> announcementContentResponse = await announcementClient
.GetAnnouncementContentsAsync(languageCode, region, cancellationToken)
.ConfigureAwait(false);
if (!announcementContentResponse.IsOk())
{
return default!;
}
contents = announcementContentResponse.Data.List;
}
AnnouncementWrapper wrapper = announcementWrapperResponse.Data;
Response<ListWrapper<AnnouncementContent>> announcementContentResponse = await announcementClient
.GetAnnouncementContentsAsync(languageCode, region, cancellationToken)
.ConfigureAwait(false);
if (!announcementContentResponse.IsOk())
{
return default!;
}
List<AnnouncementContent> contents = announcementContentResponse.Data.List;
Dictionary<int, string> contentMap = contents
.ToDictionary(id => id.AnnId, content => content.Content);
Dictionary<int, string> contentMap = contents.ToDictionary(id => id.AnnId, content => content.Content);
// 将活动公告置于前方
wrapper.List.Reverse();
@@ -75,8 +83,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
{
foreach (ref readonly WebAnnouncement item in CollectionsMarshal.AsSpan(listWrapper.List))
{
contentMap.TryGetValue(item.AnnId, out string? rawContent);
item.Content = rawContent ?? string.Empty;
item.Content = contentMap.GetValueOrDefault(item.AnnId, string.Empty);
}
}
@@ -104,39 +111,68 @@ internal sealed partial class AnnouncementService : IAnnouncementService
.Single(wrapper => wrapper.TypeId == 1)
.List;
// 更新公告
WebAnnouncement versionUpdate = announcementListWrappers
// 游戏公告
List<WebAnnouncement> announcements = announcementListWrappers
.Single(wrapper => wrapper.TypeId == 2)
.List
.Single(ann => AnnouncementRegex.VersionUpdateTitleRegex.IsMatch(ann.Title));
.List;
if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is not { Success: true } versionMatch)
Dictionary<string, DateTimeOffset> versionStartTimes = [];
// 更新公告
if (announcements.SingleOrDefault(ann => AnnouncementRegex.VersionUpdateTitleRegex.IsMatch(ann.Title)) is { } versionUpdate)
{
return;
if (AnnouncementRegex.VersionUpdateTimeRegex.Match(versionUpdate.Content) is not { Success: true } versionUpdateMatch)
{
return;
}
DateTimeOffset versionUpdateTime = UnsafeDateTimeOffset.ParseDateTime(versionUpdateMatch.Groups[1].ValueSpan, offset);
versionStartTimes.TryAdd(VersionRegex().Match(versionUpdate.Title).Groups[1].Value, versionUpdateTime);
}
DateTimeOffset versionUpdateTime = UnsafeDateTimeOffset.ParseDateTime(versionMatch.Groups[1].ValueSpan, offset);
// 更新预告
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);
}
foreach (ref readonly WebAnnouncement announcement in CollectionsMarshal.AsSpan(activities))
{
DateTimeOffset versionStartTime;
if (AnnouncementRegex.PermanentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } permanent)
{
announcement.StartTime = versionUpdateTime;
continue;
if (versionStartTimes.TryGetValue(permanent.Groups[1].Value, out versionStartTime))
{
announcement.StartTime = versionStartTime;
continue;
}
}
if (AnnouncementRegex.PersistentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } persistent)
{
announcement.StartTime = versionUpdateTime;
announcement.EndTime = versionUpdateTime + TimeSpan.FromDays(42);
continue;
if (versionStartTimes.TryGetValue(persistent.Groups[1].Value, out versionStartTime))
{
announcement.StartTime = versionStartTime;
announcement.EndTime = versionStartTime + TimeSpan.FromDays(42);
continue;
}
}
if (AnnouncementRegex.TransientActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } transient)
{
announcement.StartTime = versionUpdateTime;
announcement.EndTime = UnsafeDateTimeOffset.ParseDateTime(transient.Groups[2].ValueSpan, offset);
continue;
if (versionStartTimes.TryGetValue(transient.Groups[1].Value, out versionStartTime))
{
announcement.StartTime = versionStartTime;
announcement.EndTime = UnsafeDateTimeOffset.ParseDateTime(transient.Groups[2].ValueSpan, offset);
continue;
}
}
MatchCollection matches = AnnouncementRegex.XmlTimeTagRegex().Matches(announcement.Content);
@@ -171,4 +207,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
announcement.EndTime = max;
}
}
[GeneratedRegex("(\\d\\.\\d)")]
private static partial Regex VersionRegex();
}

View File

@@ -12,12 +12,5 @@ namespace Snap.Hutao.Service.Announcement;
[HighQuality]
internal interface IAnnouncementService
{
/// <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);
ValueTask<AnnouncementWrapper> GetAnnouncementWrapperAsync(string languageCode, Region region, CancellationToken token = default);
}

View File

@@ -16,6 +16,7 @@ namespace Snap.Hutao.Service;
internal sealed partial class AppOptions : DbStoreOptions
{
private bool? isEmptyHistoryWishVisible;
private bool? isUnobtainedWishItemVisible;
private BackdropType? backdropType;
private ElementTheme? elementTheme;
private BackgroundImageType? backgroundImageType;
@@ -24,10 +25,16 @@ internal sealed partial class AppOptions : DbStoreOptions
public bool IsEmptyHistoryWishVisible
{
get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible);
get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, false);
set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value);
}
public bool IsUnobtainedWishItemVisible
{
get => GetOption(ref isUnobtainedWishItemVisible, SettingEntry.IsUnobtainedWishItemVisible, false);
set => SetOption(ref isUnobtainedWishItemVisible, SettingEntry.IsUnobtainedWishItemVisible, value);
}
public List<NameValue<BackdropType>> BackdropTypes { get; } = CollectionsNameValue.FromEnum<BackdropType>(type => type >= 0);
public BackdropType BackdropType

View File

@@ -12,7 +12,9 @@ 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;
@@ -21,9 +23,6 @@ using RecordPlayerInfo = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.PlayerInfo;
namespace Snap.Hutao.Service.AvatarInfo;
/// <summary>
/// 角色信息数据库操作
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Singleton)]
@@ -32,18 +31,10 @@ internal sealed partial class AvatarInfoDbBulkOperation
private readonly IServiceProvider serviceProvider;
private readonly IAvatarInfoDbService avatarInfoDbService;
/// <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)
public async ValueTask<List<EntityAvatarInfo>> UpdateDbAvatarInfosByShowcaseAsync(string uid, IEnumerable<EnkaAvatarInfo> webInfos, CancellationToken token)
{
token.ThrowIfCancellationRequested();
List<EntityAvatarInfo> dbInfos = avatarInfoDbService.GetAvatarInfoListByUid(uid);
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
List<EntityAvatarInfo> dbInfos = await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap);
using (IServiceScope scope = serviceProvider.CreateScope())
{
@@ -56,28 +47,19 @@ internal sealed partial class AvatarInfoDbBulkOperation
continue;
}
token.ThrowIfCancellationRequested();
EntityAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId);
EntityAvatarInfo? entity = dbInfoMap.GetValueOrDefault(webInfo.AvatarId);
AddOrUpdateAvatarInfo(entity, uid, appDbContext, webInfo);
}
token.ThrowIfCancellationRequested();
return avatarInfoDbService.GetAvatarInfoListByUid(uid);
return await avatarInfoDbService.GetAvatarInfoListByUidAsync(uid).ConfigureAwait(false);
}
}
/// <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);
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap);
using (IServiceScope scope = serviceProvider.CreateScope())
{
@@ -90,51 +72,47 @@ internal sealed partial class AvatarInfoDbBulkOperation
.GetPlayerInfoAsync(userAndUid, token)
.ConfigureAwait(false);
if (playerInfoResponse.IsOk())
if (!playerInfoResponse.IsOk())
{
Response<CharacterWrapper> charactersResponse = await gameRecordClient
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false);
goto Return;
}
token.ThrowIfCancellationRequested();
Response<CharacterWrapper> charactersResponse = await gameRecordClient
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false);
if (charactersResponse.IsOk())
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))
{
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
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);
}
continue;
}
EntityAvatarInfo? entity = dbInfoMap.GetValueOrDefault(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);
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
EnsureItemsAvatarIdUnique(ref dbInfos, uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap);
using (IServiceScope scope = serviceProvider.CreateScope())
{
@@ -155,8 +133,6 @@ internal sealed partial class AvatarInfoDbBulkOperation
continue;
}
token.ThrowIfCancellationRequested();
Response<AvatarDetail> detailAvatarResponse = await calculateClient
.GetAvatarDetailAsync(userAndUid, avatar, token)
.ConfigureAwait(false);
@@ -166,8 +142,7 @@ internal sealed partial class AvatarInfoDbBulkOperation
continue;
}
token.ThrowIfCancellationRequested();
EntityAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == avatar.Id);
EntityAvatarInfo? entity = dbInfoMap.GetValueOrDefault(avatar.Id);
AddOrUpdateAvatarInfo(entity, avatar.Id, uid, appDbContext, transformer, detailAvatarResponse.Data);
}
}
@@ -181,15 +156,14 @@ 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)]
@@ -200,17 +174,16 @@ 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)]
@@ -221,29 +194,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 EnsureItemsAvatarIdDistinct(ref List<EntityAvatarInfo> dbInfos, string uid)
private void EnsureItemsAvatarIdUnique(ref List<EntityAvatarInfo> dbInfos, string uid, out Dictionary<AvatarId, EntityAvatarInfo> dbInfoMap)
{
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)
dbInfoMap = [];
foreach (ref readonly EntityAvatarInfo info in CollectionsMarshal.AsSpan(dbInfos))
{
avatarInfoDbService.RemoveAvatarInfoRangeByUid(uid);
dbInfos = [];
if (!dbInfoMap.TryAdd(info.Info.AvatarId, info))
{
avatarInfoDbService.RemoveAvatarInfoRangeByUid(uid);
dbInfoMap.Clear();
dbInfos.Clear();
}
}
}
}

View File

@@ -4,6 +4,7 @@
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;
@@ -14,44 +15,25 @@ internal sealed partial class AvatarInfoDbService : IAvatarInfoDbService
{
private readonly IServiceProvider serviceProvider;
public IServiceProvider ServiceProvider { get => serviceProvider; }
public List<EntityAvatarInfo> GetAvatarInfoListByUid(string 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];
}
return this.List(i => i.Uid == uid);
}
public async ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid)
public ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid, CancellationToken token = default)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.AvatarInfos
.AsNoTracking()
.Where(i => i.Uid == uid)
.ToListAsync()
.ConfigureAwait(false);
}
return this.ListAsync(i => i.Uid == uid, token);
}
public void RemoveAvatarInfoRangeByUid(string uid)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.AvatarInfos.ExecuteDeleteWhere(i => i.Uid == uid);
}
this.Delete(i => i.Uid == uid);
}
public async ValueTask RemoveAvatarInfoRangeByUidAsync(string uid)
public async ValueTask RemoveAvatarInfoRangeByUidAsync(string uid, CancellationToken token = default)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.AvatarInfos.ExecuteDeleteWhereAsync(i => i.Uid == uid).ConfigureAwait(false);
}
await this.DeleteAsync(i => i.Uid == uid, token).ConfigureAwait(false);
}
}

View File

@@ -13,9 +13,6 @@ using EntityAvatarInfo = Snap.Hutao.Model.Entity.AvatarInfo;
namespace Snap.Hutao.Service.AvatarInfo;
/// <summary>
/// 角色信息服务
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Scoped, typeof(IAvatarInfoService))]
@@ -23,77 +20,76 @@ internal sealed partial class AvatarInfoService : IAvatarInfoService
{
private readonly AvatarInfoDbBulkOperation avatarInfoDbBulkOperation;
private readonly IAvatarInfoDbService avatarInfoDbService;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly ILogger<AvatarInfoService> logger;
private readonly IMetadataService metadataService;
private readonly ISummaryFactory summaryFactory;
private readonly EnkaClient enkaClient;
/// <inheritdoc/>
public async ValueTask<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default)
public async ValueTask<ValueResult<RefreshResultKind, Summary?>> GetSummaryAsync(UserAndUid userAndUid, RefreshOption refreshOption, CancellationToken token = default)
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
if (!await metadataService.InitializeAsync().ConfigureAwait(false))
{
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);
}
}
return new(RefreshResultKind.MetadataNotInitialized, null);
}
else
switch (refreshOption)
{
return new(RefreshResult.MetadataNotInitialized, null);
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);
}
}
}
private async ValueTask<EnkaResponse?> GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default)
{
return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false)
?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false);
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
EnkaClient enkaClient = scope.ServiceProvider.GetRequiredService<EnkaClient>();
return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false)
?? await enkaClient.GetDataAsync(uid, token).ConfigureAwait(false);
}
}
private async ValueTask<Summary> GetSummaryCoreAsync(IEnumerable<EntityAvatarInfo> avatarInfos, CancellationToken token)

View File

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

View File

@@ -0,0 +1,242 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.AvatarProperty;
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)
where TBuilder : IAvatarViewBuilder
{
if (avatarInfo.CostumeId.TryGetValue(out CostumeId id))
{
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);
}
else
{
builder.View.Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
builder.View.SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon);
}
return builder;
}
public static TBuilder SetCalculatorRefreshTimeFormat<TBuilder>(this TBuilder builder, DateTimeOffset refreshTime, Func<object?, string> format, string defaultValue)
where TBuilder : IAvatarViewBuilder
{
return builder.SetCalculatorRefreshTimeFormat(refreshTime == DateTimeOffsetExtension.DatebaseDefaultTime ? defaultValue : format(refreshTime.ToLocalTime()));
}
public static TBuilder SetCalculatorRefreshTimeFormat<TBuilder>(this TBuilder builder, string calculatorRefreshTimeFormat)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.CalculatorRefreshTimeFormat = calculatorRefreshTimeFormat);
}
public static TBuilder SetConstellations<TBuilder>(this TBuilder builder, List<Skill> talents, List<SkillId>? talentIds)
where TBuilder : IAvatarViewBuilder
{
return builder.SetConstellations(CreateConstellations(talents, talentIds.EmptyIfNull()));
static List<ConstellationView> CreateConstellations(List<Skill> talents, List<SkillId> talentIds)
{
// TODO: use builder here
return talents.SelectList(talent => new ViewModel.AvatarProperty.ConstellationView()
{
Name = talent.Name,
Icon = SkillIconConverter.IconNameToUri(talent.Icon),
Description = talent.Description,
IsActivated = talentIds.Contains(talent.Id),
});
}
}
public static TBuilder SetConstellations<TBuilder>(this TBuilder builder, List<ConstellationView> constellations)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Constellations = constellations);
}
public static TBuilder SetCritScore<TBuilder>(this TBuilder builder, Dictionary<FightProperty, float>? fightPropMap)
where TBuilder : IAvatarViewBuilder
{
return builder.SetCritScore(ScoreCrit(fightPropMap));
static float ScoreCrit(Dictionary<FightProperty, float>? fightPropMap)
{
if (fightPropMap.IsNullOrEmpty())
{
return 0F;
}
float cr = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL];
float cd = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL_HURT];
return 100 * ((cr * 2) + cd);
}
}
public static TBuilder SetCritScore<TBuilder>(this TBuilder builder, float critScore)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.CritScore = critScore);
}
public static TBuilder SetElement<TBuilder>(this TBuilder builder, ElementType element)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Element = element);
}
public static TBuilder SetFetterLevel<TBuilder>(this TBuilder builder, FetterLevel? level)
where TBuilder : IAvatarViewBuilder
{
if (level.TryGetValue(out FetterLevel value))
{
return builder.Configure(b => b.View.FetterLevel = value);
}
return builder;
}
public static TBuilder SetFetterLevel<TBuilder>(this TBuilder builder, uint level)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.FetterLevel = level);
}
public static TBuilder SetGameRecordRefreshTimeFormat<TBuilder>(this TBuilder builder, DateTimeOffset refreshTime, Func<object?, string> format, string defaultValue)
where TBuilder : IAvatarViewBuilder
{
return builder.SetGameRecordRefreshTimeFormat(refreshTime == DateTimeOffsetExtension.DatebaseDefaultTime ? defaultValue : format(refreshTime.ToLocalTime()));
}
public static TBuilder SetGameRecordRefreshTimeFormat<TBuilder>(this TBuilder builder, string gameRecordRefreshTimeFormat)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.GameRecordRefreshTimeFormat = gameRecordRefreshTimeFormat);
}
public static TBuilder SetId<TBuilder>(this TBuilder builder, AvatarId id)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Id = id);
}
public static TBuilder SetLevelNumber<TBuilder>(this TBuilder builder, uint? levelNumber)
where TBuilder : IAvatarViewBuilder
{
if (levelNumber.TryGetValue(out uint value))
{
return builder.Configure(b => b.View.LevelNumber = value);
}
return builder;
}
public static TBuilder SetName<TBuilder>(this TBuilder builder, string name)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Name = name);
}
public static TBuilder SetNameCard<TBuilder>(this TBuilder builder, Uri nameCard)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.NameCard = nameCard);
}
public static TBuilder SetProperties<TBuilder>(this TBuilder builder, List<AvatarProperty> properties)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Properties = properties);
}
public static TBuilder SetQuality<TBuilder>(this TBuilder builder, QualityType quality)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Quality = quality);
}
public static TBuilder SetReliquaries<TBuilder>(this TBuilder builder, List<ReliquaryView> reliquaries)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Reliquaries = reliquaries);
}
public static TBuilder SetShowcaseRefreshTimeFormat<TBuilder>(this TBuilder builder, DateTimeOffset refreshTime, Func<object?, string> format, string defaultValue)
where TBuilder : IAvatarViewBuilder
{
return builder.SetShowcaseRefreshTimeFormat(refreshTime == DateTimeOffsetExtension.DatebaseDefaultTime ? defaultValue : format(refreshTime.ToLocalTime()));
}
public static TBuilder SetShowcaseRefreshTimeFormat<TBuilder>(this TBuilder builder, string showcaseRefreshTimeFormat)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.ShowcaseRefreshTimeFormat = showcaseRefreshTimeFormat);
}
public static TBuilder SetSkills<TBuilder>(this TBuilder builder, Dictionary<SkillId, SkillLevel>? skillLevelMap, Dictionary<SkillGroupId, SkillLevel>? proudSkillExtraLevelMap, List<ProudableSkill> proudSkills)
where TBuilder : IAvatarViewBuilder
{
return builder.SetSkills(CreateSkills(skillLevelMap, proudSkillExtraLevelMap, proudSkills));
static List<SkillView> CreateSkills(Dictionary<SkillId, SkillLevel>? skillLevelMap, Dictionary<SkillGroupId, SkillLevel>? proudSkillExtraLevelMap, List<ProudableSkill> proudSkills)
{
if (skillLevelMap.IsNullOrEmpty())
{
return [];
}
Dictionary<SkillId, SkillLevel> skillExtraLeveledMap = new(skillLevelMap);
if (proudSkillExtraLevelMap is not null)
{
foreach ((SkillGroupId groupId, SkillLevel extraLevel) in proudSkillExtraLevelMap)
{
skillExtraLeveledMap.IncreaseValue(proudSkills.Single(p => p.GroupId == groupId).Id, extraLevel);
}
}
return proudSkills.SelectList(proudableSkill =>
{
SkillId skillId = proudableSkill.Id;
// TODO: use builder here
return new SkillView()
{
Name = proudableSkill.Name,
Icon = SkillIconConverter.IconNameToUri(proudableSkill.Icon),
Description = proudableSkill.Description,
GroupId = proudableSkill.GroupId,
LevelNumber = skillLevelMap[skillId],
Info = DescriptionsParametersDescriptor.Convert(proudableSkill.Proud, skillExtraLeveledMap[skillId]),
};
});
}
}
public static TBuilder SetSkills<TBuilder>(this TBuilder builder, List<SkillView> skills)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Skills = skills);
}
public static TBuilder SetWeapon<TBuilder>(this TBuilder builder, WeaponView? weapon)
where TBuilder : IAvatarViewBuilder
{
return builder.Configure(b => b.View.Weapon = weapon);
}
}

View File

@@ -0,0 +1,33 @@
// 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

@@ -0,0 +1,12 @@
// 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 IAvatarViewBuilder : IBuilder, IScoreAccess
{
AvatarView View { get; }
}

View File

@@ -0,0 +1,11 @@
// 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

@@ -0,0 +1,13 @@
// 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

@@ -0,0 +1,10 @@
// 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

@@ -0,0 +1,9 @@
// 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

@@ -0,0 +1,10 @@
// 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

@@ -0,0 +1,33 @@
// 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

@@ -0,0 +1,13 @@
// 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

@@ -0,0 +1,66 @@
// 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

@@ -0,0 +1,16 @@
// 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

@@ -0,0 +1,11 @@
// 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

@@ -0,0 +1,99 @@
// 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

@@ -5,17 +5,8 @@ using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 简述工厂
/// </summary>
[HighQuality]
internal interface ISummaryFactory
{
/// <summary>
/// 异步创建简述对象
/// </summary>
/// <param name="avatarInfos">角色列表</param>
/// <param name="token">取消令牌</param>
/// <returns>简述对象</returns>
ValueTask<Summary> CreateAsync(IEnumerable<Model.Entity.AvatarInfo> avatarInfos, CancellationToken token);
}

View File

@@ -5,21 +5,17 @@ using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Intrinsic.Format;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Primitive;
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;
/// <summary>
/// 单个角色工厂
/// </summary>
[HighQuality]
internal sealed class SummaryAvatarFactory
{
@@ -27,16 +23,11 @@ internal sealed class SummaryAvatarFactory
private readonly DateTimeOffset showcaseRefreshTime;
private readonly DateTimeOffset gameRecordRefreshTime;
private readonly DateTimeOffset calculatorRefreshTime;
private readonly SummaryMetadataContext metadataContext;
private readonly SummaryFactoryMetadataContext context;
/// <summary>
/// 构造一个新的角色工厂
/// </summary>
/// <param name="metadataContext">元数据上下文</param>
/// <param name="avatarInfo">角色信息</param>
public SummaryAvatarFactory(SummaryMetadataContext metadataContext, EntityAvatarInfo avatarInfo)
public SummaryAvatarFactory(SummaryFactoryMetadataContext context, EntityAvatarInfo avatarInfo)
{
this.metadataContext = metadataContext;
this.context = context;
this.avatarInfo = avatarInfo.Info;
showcaseRefreshTime = avatarInfo.ShowcaseRefreshTime;
@@ -44,84 +35,51 @@ internal sealed class SummaryAvatarFactory
calculatorRefreshTime = avatarInfo.CalculatorRefreshTime;
}
/// <summary>
/// 创建角色
/// </summary>
/// <returns>角色</returns>
public PropertyAvatar Create()
public static AvatarView Create(SummaryFactoryMetadataContext context, EntityAvatarInfo avatarInfo)
{
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
MetadataAvatar avatar = metadataContext.IdAvatarMap[avatarInfo.AvatarId];
PropertyAvatar propertyAvatar = new()
{
// metadata part
Id = avatar.Id,
Name = avatar.Name,
Quality = avatar.Quality,
NameCard = AvatarNameCardPicConverter.AvatarToUri(avatar),
Element = ElementNameIconConverter.ElementNameToElementType(avatar.FetterInfo.VisionBefore),
// webinfo & metadata mixed part
Constellations = SummaryHelper.CreateConstellations(avatar.SkillDepot.Talents, avatarInfo.TalentIdList),
Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.CompositeSkillsNoInherents()),
// webinfo part
FetterLevel = avatarInfo.FetterInfo?.ExpLevel ?? 0U,
Properties = SummaryAvatarProperties.Create(avatarInfo.FightPropMap),
CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}",
LevelNumber = avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].Value ?? 0U,
// processed webinfo part
Weapon = reliquaryAndWeapon.Weapon,
Reliquaries = reliquaryAndWeapon.Reliquaries,
Score = $"{reliquaryAndWeapon.Reliquaries.Sum(r => r.Score):F2}",
// times
ShowcaseRefreshTimeFormat = showcaseRefreshTime == DateTimeOffsetExtension.DatebaseDefaultTime
? SH.ServiceAvatarInfoSummaryShowcaseNotRefreshed
: SH.FormatServiceAvatarInfoSummaryShowcaseRefreshTimeFormat(showcaseRefreshTime.ToLocalTime()),
GameRecordRefreshTimeFormat = gameRecordRefreshTime == DateTimeOffsetExtension.DatebaseDefaultTime
? SH.ServiceAvatarInfoSummaryGameRecordNotRefreshed
: SH.FormatServiceAvatarInfoSummaryGameRecordRefreshTimeFormat(gameRecordRefreshTime.ToLocalTime()),
CalculatorRefreshTimeFormat = calculatorRefreshTime == DateTimeOffsetExtension.DatebaseDefaultTime
? SH.ServiceAvatarInfoSummaryCalculatorNotRefreshed
: SH.FormatServiceAvatarInfoSummaryCalculatorRefreshTimeFormat(calculatorRefreshTime.ToLocalTime()),
};
ApplyCostumeIconOrDefault(ref propertyAvatar, avatar);
return propertyAvatar;
return new SummaryAvatarFactory(context, avatarInfo).Create();
}
private void ApplyCostumeIconOrDefault(ref PropertyAvatar propertyAvatar, MetadataAvatar avatar)
public AvatarView Create()
{
if (avatarInfo.CostumeId.TryGetValue(out CostumeId id))
{
Model.Metadata.Avatar.Costume costume = avatar.Costumes.Single(c => c.Id == id);
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
MetadataAvatar avatar = context.IdAvatarMap[avatarInfo.AvatarId];
// Set to costume icon
propertyAvatar.Icon = AvatarIconConverter.IconNameToUri(costume.FrontIcon);
propertyAvatar.SideIcon = AvatarIconConverter.IconNameToUri(costume.SideIcon);
}
else
{
propertyAvatar.Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
propertyAvatar.SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon);
}
AvatarView propertyAvatar = new AvatarViewBuilder()
.SetId(avatar.Id)
.SetName(avatar.Name)
.SetQuality(avatar.Quality)
.SetNameCard(AvatarNameCardPicConverter.AvatarToUri(avatar))
.SetElement(ElementNameIconConverter.ElementNameToElementType(avatar.FetterInfo.VisionBefore))
.SetConstellations(avatar.SkillDepot.Talents, avatarInfo.TalentIdList)
.SetSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.CompositeSkillsNoInherents())
.SetFetterLevel(avatarInfo.FetterInfo?.ExpLevel)
.SetProperties(SummaryAvatarProperties.Create(avatarInfo.FightPropMap))
.SetCritScore(avatarInfo.FightPropMap)
.SetLevelNumber(avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].Value)
.SetWeapon(reliquaryAndWeapon.Weapon)
.SetReliquaries(reliquaryAndWeapon.Reliquaries)
.SetScore(reliquaryAndWeapon.Reliquaries.Sum(r => r.Score))
.SetShowcaseRefreshTimeFormat(showcaseRefreshTime, SH.FormatServiceAvatarInfoSummaryShowcaseRefreshTimeFormat, SH.ServiceAvatarInfoSummaryShowcaseNotRefreshed)
.SetGameRecordRefreshTimeFormat(gameRecordRefreshTime, SH.FormatServiceAvatarInfoSummaryGameRecordRefreshTimeFormat, SH.ServiceAvatarInfoSummaryGameRecordNotRefreshed)
.SetCalculatorRefreshTimeFormat(calculatorRefreshTime, SH.FormatServiceAvatarInfoSummaryCalculatorRefreshTimeFormat, SH.ServiceAvatarInfoSummaryCalculatorNotRefreshed)
.SetCostumeIconOrDefault(avatarInfo, avatar)
.View;
return propertyAvatar;
}
private ReliquaryAndWeapon ProcessEquip(List<Equip> equipments)
{
List<PropertyReliquary> reliquaryList = [];
PropertyWeapon? weapon = null;
List<ReliquaryView> reliquaryList = [];
WeaponView? weapon = null;
foreach (Equip equip in equipments)
foreach (ref readonly Equip equip in CollectionsMarshal.AsSpan(equipments))
{
switch (equip.Flat.ItemType)
{
case ItemType.ITEM_RELIQUARY:
SummaryReliquaryFactory summaryReliquaryFactory = new(metadataContext, avatarInfo, equip);
reliquaryList.Add(summaryReliquaryFactory.CreateReliquary());
reliquaryList.Add(SummaryReliquaryFactory.Create(context, avatarInfo, equip));
break;
case ItemType.ITEM_WEAPON:
weapon = CreateWeapon(equip);
@@ -132,9 +90,9 @@ internal sealed class SummaryAvatarFactory
return new(reliquaryList, weapon);
}
private PropertyWeapon CreateWeapon(Equip equip)
private WeaponView CreateWeapon(Equip equip)
{
MetadataWeapon weapon = metadataContext.IdWeaponMap[equip.ItemId];
MetadataWeapon weapon = context.IdWeaponMap[equip.ItemId];
// AffixMap can be null when it's a white weapon.
ArgumentNullException.ThrowIfNull(equip.Weapon);
@@ -158,34 +116,29 @@ internal sealed class SummaryAvatarFactory
ArgumentNullException.ThrowIfNull(equip.Weapon);
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,
};
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;
}
private readonly struct ReliquaryAndWeapon
{
public readonly List<PropertyReliquary> Reliquaries;
public readonly PropertyWeapon? Weapon;
public readonly List<ReliquaryView> Reliquaries;
public readonly WeaponView? Weapon;
public ReliquaryAndWeapon(List<PropertyReliquary> reliquaries, PropertyWeapon? weapon)
public ReliquaryAndWeapon(List<ReliquaryView> reliquaries, WeaponView? weapon)
{
Reliquaries = reliquaries;
Weapon = weapon;

View File

@@ -13,11 +13,6 @@ 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)
@@ -25,32 +20,27 @@ internal static class SummaryAvatarProperties
return [];
}
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 };
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)
];
// 元素伤害
if (TryGetBonusFightProperty(fightPropMap, out FightProperty bonusProperty, out float value))
if (TryGetBonusFightProperty(fightPropMap, out FightProperty bonusProperty, out float value) && value > 0)
{
if (value > 0)
{
properties.Add(FightPropertyFormat.ToAvatarProperty(bonusProperty, value));
}
properties.Add(FightPropertyFormat.ToAvatarProperty(bonusProperty, value));
}
// 物伤 可以和其他元素伤害并存,所以分别判断
if (fightPropMap.TryGetValue(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, out float addValue))
if (fightPropMap.TryGetValue(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, out float addValue) && addValue > 0)
{
if (addValue > 0)
{
properties.Add(FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, addValue));
}
properties.Add(FightPropertyFormat.ToAvatarProperty(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, addValue));
}
return properties;

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
@@ -20,22 +21,18 @@ internal sealed partial class SummaryFactory : ISummaryFactory
/// <inheritdoc/>
public async ValueTask<Summary> CreateAsync(IEnumerable<Model.Entity.AvatarInfo> avatarInfos, CancellationToken token)
{
SummaryMetadataContext metadataContext = new()
{
IdAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false),
IdWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false),
IdReliquaryAffixWeightMap = await metadataService.GetIdToReliquaryAffixWeightMapAsync(token).ConfigureAwait(false),
IdReliquaryMainAffixMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync(token).ConfigureAwait(false),
IdReliquarySubAffixMap = await metadataService.GetIdToReliquarySubAffixMapAsync(token).ConfigureAwait(false),
ReliquaryLevels = await metadataService.GetReliquaryLevelListAsync(token).ConfigureAwait(false),
Reliquaries = await metadataService.GetReliquaryListAsync(token).ConfigureAwait(false),
};
SummaryFactoryMetadataContext context = await metadataService
.GetContextAsync<SummaryFactoryMetadataContext>(token)
.ConfigureAwait(false);
IOrderedEnumerable<AvatarView> avatars = avatarInfos
.Where(a => !AvatarIds.IsPlayer(a.Info.AvatarId))
.Select(a => new SummaryAvatarFactory(metadataContext, a).Create())
.OrderByDescending(a => a.LevelNumber)
.ThenBy(a => a.Name);
.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);
return new()
{

View File

@@ -4,51 +4,34 @@
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 简述元数据上下文
/// 包含了所有制造简述需要的元数据
/// </summary>
[HighQuality]
internal sealed class SummaryMetadataContext
internal sealed class SummaryFactoryMetadataContext : IMetadataContext,
IMetadataDictionaryIdAvatarSource,
IMetadataDictionaryIdWeaponSource,
IMetadataDictionaryIdReliquaryAffixWeightSource,
IMetadataDictionaryIdReliquaryMainPropertySource,
IMetadataDictionaryIdReliquarySubAffixSource,
IMetadataDictionaryIdReliquarySource,
IMetadataListReliquaryMainAffixLevelSource
{
/// <summary>
/// 角色映射
/// </summary>
public Dictionary<AvatarId, MetadataAvatar> IdAvatarMap { get; set; } = default!;
/// <summary>
/// 武器映射
/// </summary>
public Dictionary<WeaponId, MetadataWeapon> IdWeaponMap { get; set; } = default!;
/// <summary>
/// 权重映射
/// </summary>
public Dictionary<AvatarId, ReliquaryAffixWeight> IdReliquaryAffixWeightMap { get; set; } = default!;
/// <summary>
/// 圣遗物主属性映射
/// </summary>
public Dictionary<ReliquaryMainAffixId, FightProperty> IdReliquaryMainAffixMap { get; set; } = default!;
public Dictionary<ReliquaryMainAffixId, FightProperty> IdReliquaryMainPropertyMap { get; set; } = default!;
/// <summary>
/// 圣遗物副属性映射
/// </summary>
public Dictionary<ReliquarySubAffixId, ReliquarySubAffix> IdReliquarySubAffixMap { get; set; } = default!;
/// <summary>
/// 圣遗物等级
/// </summary>
public List<ReliquaryMainAffixLevel> ReliquaryLevels { get; set; } = default!;
public List<ReliquaryMainAffixLevel> ReliquaryMainAffixLevels { get; set; } = default!;
/// <summary>
/// 圣遗物
/// </summary>
public List<MetadataReliquary> Reliquaries { get; set; } = default!;
public Dictionary<ReliquaryId, MetadataReliquary> IdReliquaryMap { get; set; } = default!;
}

View File

@@ -1,85 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 简述帮助类
/// </summary>
[HighQuality]
internal static class SummaryHelper
{
/// <summary>
/// 创建命之座
/// </summary>
/// <param name="talents">全部命座</param>
/// <param name="talentIds">激活的命座列表</param>
/// <returns>命之座</returns>
public static List<ConstellationView> CreateConstellations(List<Skill> talents, List<SkillId>? talentIds)
{
talentIds ??= [];
return talents.SelectList(talent => new ConstellationView()
{
Name = talent.Name,
Icon = SkillIconConverter.IconNameToUri(talent.Icon),
Description = talent.Description,
IsActivated = talentIds.Contains(talent.Id),
});
}
/// <summary>
/// 创建技能组
/// </summary>
/// <param name="skillLevelMap">技能等级映射</param>
/// <param name="proudSkillExtraLevelMap">额外提升等级映射</param>
/// <param name="proudSkills">技能列表</param>
/// <returns>技能</returns>
public static List<SkillView> CreateSkills(Dictionary<SkillId, SkillLevel>? skillLevelMap, Dictionary<SkillGroupId, SkillLevel>? proudSkillExtraLevelMap, List<ProudableSkill> proudSkills)
{
if (skillLevelMap.IsNullOrEmpty())
{
return [];
}
Dictionary<SkillId, SkillLevel> skillExtraLeveledMap = new(skillLevelMap);
if (proudSkillExtraLevelMap is not null)
{
foreach ((SkillGroupId groupId, SkillLevel extraLevel) in proudSkillExtraLevelMap)
{
skillExtraLeveledMap.IncreaseValue(proudSkills.Single(p => p.GroupId == groupId).Id, extraLevel);
}
}
return proudSkills.SelectList(proudableSkill =>
{
SkillId skillId = proudableSkill.Id;
return new SkillView()
{
Name = proudableSkill.Name,
Icon = SkillIconConverter.IconNameToUri(proudableSkill.Icon),
Description = proudableSkill.Description,
GroupId = proudableSkill.GroupId,
LevelNumber = skillLevelMap[skillId],
Info = DescriptionsParametersDescriptor.Convert(proudableSkill.Proud, skillExtraLeveledMap[skillId]),
};
});
}
/// <summary>
/// 获取副属性对应的最大属性的Id
/// </summary>
/// <param name="appendId">属性Id</param>
/// <returns>最大属性Id</returns>
public static ReliquarySubAffixId GetAffixMaxId(in ReliquarySubAffixId appendId)
{
// axxxxx -> a
@@ -91,18 +20,13 @@ internal static class SummaryHelper
1 => 2,
2 => 3,
3 or 4 or 5 => 4,
_ => throw Must.NeverHappen(),
_ => throw HutaoException.Throw($"不支持的 ReliquarySubAffixId: {appendId}"),
};
// 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 档
@@ -129,25 +53,7 @@ internal static class SummaryHelper
(1, 0) => 100F,
(1, 1) => 80F,
_ => throw Must.NeverHappen($"Unexpected AppendId: {appendId.Value} Delta: {delta}"),
_ => throw HutaoException.Throw($"Unexpected AppendId: {appendId} Delta: {delta}"),
};
}
/// <summary>
/// 获取双爆评分
/// </summary>
/// <param name="fightPropMap">属性</param>
/// <returns>评分</returns>
public static float ScoreCrit(Dictionary<FightProperty, float>? fightPropMap)
{
if (fightPropMap.IsNullOrEmpty())
{
return 0F;
}
float cr = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL];
float cd = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL_HURT];
return 100 * ((cr * 2) + cd);
}
}

View File

@@ -6,6 +6,7 @@ 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;
@@ -13,76 +14,63 @@ using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 圣遗物工厂
/// </summary>
[HighQuality]
internal sealed class SummaryReliquaryFactory
{
private readonly SummaryMetadataContext metadataContext;
private readonly SummaryFactoryMetadataContext metadataContext;
private readonly ModelAvatarInfo avatarInfo;
private readonly Web.Enka.Model.Equip equip;
/// <summary>
/// 构造一个新的圣遗物工厂
/// </summary>
/// <param name="metadataContext">元数据上下文</param>
/// <param name="avatarInfo">角色信息</param>
/// <param name="equip">圣遗物</param>
public SummaryReliquaryFactory(SummaryMetadataContext metadataContext, ModelAvatarInfo avatarInfo, Web.Enka.Model.Equip equip)
public SummaryReliquaryFactory(SummaryFactoryMetadataContext metadataContext, ModelAvatarInfo avatarInfo, Web.Enka.Model.Equip equip)
{
this.metadataContext = metadataContext;
this.avatarInfo = avatarInfo;
this.equip = equip;
}
/// <summary>
/// 构造圣遗物
/// </summary>
/// <returns>圣遗物</returns>
public ReliquaryView CreateReliquary()
public static ReliquaryView Create(SummaryFactoryMetadataContext metadataContext, ModelAvatarInfo avatarInfo, Web.Enka.Model.Equip equip)
{
MetadataReliquary reliquary = metadataContext.Reliquaries.Single(r => r.Ids.Contains(equip.ItemId));
return new SummaryReliquaryFactory(metadataContext, avatarInfo, equip).Create();
}
public ReliquaryView Create()
{
MetadataReliquary reliquary = metadataContext.IdReliquaryMap[equip.ItemId];
ArgumentNullException.ThrowIfNull(equip.Reliquary);
List<ReliquarySubProperty> subProperty = equip.Reliquary.AppendPropIdList.EmptyIfNull().SelectList(CreateSubProperty);
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);
int affixCount = GetSecondaryAffixCount(reliquary, equip.Reliquary);
ReliquaryView result = new()
if (subProperties.Count > 0)
{
// NameIconDescription
Name = reliquary.Name,
Icon = RelicIconConverter.IconNameToUri(reliquary.Icon),
Description = reliquary.Description,
reliquaryViewBuilder
.SetPrimarySubProperties(subProperties.GetRange(..^affixCount))
.SetSecondarySubProperties(subProperties.GetRange(^affixCount..))
.SetComposedSubProperties(CreateComposedSubProperties(equip.Reliquary.AppendPropIdList));
// EquipBase
Level = $"+{equip.Reliquary.Level - 1U}",
Quality = reliquary.RankLevel,
};
ReliquaryMainAffixLevel relicLevel = metadataContext.ReliquaryMainAffixLevels.Single(r => r.Level == equip.Reliquary.Level && r.Rank == reliquary.RankLevel);
FightProperty property = metadataContext.IdReliquaryMainPropertyMap[equip.Reliquary.MainPropId];
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.ReliquaryLevels.Single(r => r.Level == equip.Reliquary.Level && r.Rank == reliquary.RankLevel);
FightProperty property = metadataContext.IdReliquaryMainAffixMap[equip.Reliquary.MainPropId];
result.MainProperty = FightPropertyFormat.ToNameValue(property, relicLevel.PropertyMap[property]);
result.Score = ScoreReliquary(property, reliquary, relicLevel, subProperty);
reliquaryViewBuilder
.SetMainProperty(FightPropertyFormat.ToNameValue(property, relicLevel.PropertyMap[property]))
.SetScore(ScoreReliquary(property, reliquary, relicLevel, subProperties));
}
return result;
return reliquaryViewBuilder.View;
}
private static int GetSecondaryAffixCount(MetadataReliquary reliquary, Web.Enka.Model.Reliquary enkaReliquary)
private static int GetSecondaryAffixCount(MetadataReliquary metaReliquary, Web.Enka.Model.Reliquary enkaReliquary)
{
// 强化词条个数
return (reliquary.RankLevel, enkaReliquary.Level.Value) switch
return (metaReliquary.RankLevel, enkaReliquary.Level.Value) switch
{
(QualityType.QUALITY_ORANGE, > 20U) => 5,
(QualityType.QUALITY_ORANGE, > 16U) => 4,
@@ -123,18 +111,8 @@ internal sealed class SummaryReliquaryFactory
info.Value += subAffix.Value;
}
if (infos.Count > 4)
{
ThrowHelper.InvalidOperation("无效的圣遗物数据");
}
List<ReliquaryComposedSubProperty> results = [];
foreach (ref readonly SummaryReliquarySubPropertyCompositionInfo info in CollectionsMarshal.AsSpan(infos))
{
results.Add(info.ToReliquaryComposedSubProperty());
}
return results;
HutaoException.ThrowIf(infos.Count > 4, "无效的圣遗物数据");
return infos.SelectList(info => info.ToReliquaryComposedSubProperty());
}
private float ScoreReliquary(FightProperty property, MetadataReliquary reliquary, ReliquaryMainAffixLevel relicLevel, List<ReliquarySubProperty> subProperties)
@@ -146,7 +124,7 @@ internal sealed class SummaryReliquaryFactory
// 从喵插件抓取的圣遗物评分权重
// 部分复杂的角色暂时使用了默认值
ReliquaryAffixWeight affixWeight = metadataContext.IdReliquaryAffixWeightMap.GetValueOrDefault(avatarInfo.AvatarId, ReliquaryAffixWeight.Default);
ReliquaryMainAffixLevel? maxRelicLevel = metadataContext.ReliquaryLevels.Where(r => r.Rank == reliquary.RankLevel).MaxBy(r => r.Level);
ReliquaryMainAffixLevel? maxRelicLevel = metadataContext.ReliquaryMainAffixLevels.Where(r => r.Rank == reliquary.RankLevel).MaxBy(r => r.Level);
ArgumentNullException.ThrowIfNull(maxRelicLevel);
float percent = relicLevel.PropertyMap[property] / maxRelicLevel.PropertyMap[property];
@@ -170,42 +148,42 @@ internal sealed class SummaryReliquaryFactory
FightProperty property = affix.Type;
return new(property, FightPropertyFormat.FormatValue(property, affix.Value), ScoreSubAffix(appendPropId));
}
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)
float ScoreSubAffix(in ReliquarySubAffixId appendId)
{
// 等效百分比 [ 当前小字词条 / 角色基本属性 ]
float equalPercent = affix.Value / avatarInfo.FightPropMap[affix.Type - 1];
ReliquarySubAffix affix = metadataContext.IdReliquarySubAffixMap[appendId];
// 获取对应百分比词条权重
weight = affixWeight[affix.Type + 1] / 100F;
ReliquaryAffixWeight affixWeight = metadataContext.IdReliquaryAffixWeightMap.GetValueOrDefault(avatarInfo.AvatarId, ReliquaryAffixWeight.Default);
float weight = affixWeight[affix.Type] / 100F;
// 最大同属性百分比Id
// 第四五位是战斗属性位
// 小字的加成词条在十位加一后即变换为百分比词条
ReliquarySubAffixId maxPercentAffixId = SummaryHelper.GetAffixMaxId(appendId + 10U);
// 数字词条,转换到等效百分比计算
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];
// 最大同属性百分比数值
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;
// 获取对应百分比词条权重
weight = affixWeight[affix.Type + 1] / 100F;
return weight * equalScore * 100;
// 最大同属性百分比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 * SummaryHelper.GetPercentSubAffixScore(appendId);
}
}

View File

@@ -1,17 +1,18 @@
// 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
internal interface IAvatarInfoDbService : IAppDbService<EntityAvatarInfo>
{
void RemoveAvatarInfoRangeByUid(string uid);
List<EntityAvatarInfo> GetAvatarInfoListByUid(string uid);
ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid);
ValueTask<List<EntityAvatarInfo>> GetAvatarInfoListByUidAsync(string uid, CancellationToken token = default);
ValueTask RemoveAvatarInfoRangeByUidAsync(string uid);
ValueTask RemoveAvatarInfoRangeByUidAsync(string uid, CancellationToken token = default);
}

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.Service.AvatarInfo.Transformer;
internal sealed class CalculateAvatarDetailAvatarInfoTransformer : IAvatarInfoTransformer<AvatarDetail>
{
/// <inheritdoc/>
public void Transform(ref Web.Enka.Model.AvatarInfo avatarInfo, AvatarDetail source)
public void Transform(ref readonly 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 Web.Enka.Model.AvatarInfo avatarInfo, Character source)
public void Transform(ref readonly 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 Web.Enka.Model.AvatarInfo avatarInfo, TSource source);
void Transform(ref readonly 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)
public async ValueTask<ValueResult<bool, BackgroundImage?>> GetNextBackgroundImageAsync(BackgroundImage? previous, CancellationToken token = default)
{
HashSet<string> backgroundSet = await SkipOrInitBackgroundAsync().ConfigureAwait(false);
HashSet<string> backgroundSet = await SkipOrInitBackgroundAsync(token).ConfigureAwait(false);
if (backgroundSet.Count <= 0)
{
@@ -79,7 +79,7 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
}
}
private async ValueTask<HashSet<string>> SkipOrInitBackgroundAsync()
private async ValueTask<HashSet<string>> SkipOrInitBackgroundAsync(CancellationToken token = default)
{
switch (appOptions.BackgroundImageType)
{
@@ -90,7 +90,7 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
string backgroundFolder = runtimeOptions.GetDataFolderBackgroundFolder();
currentBackgroundPathSet = Directory
.GetFiles(backgroundFolder, "*.*", SearchOption.AllDirectories)
.EnumerateFiles(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 => client.GetBingWallpaperAsync()).ConfigureAwait(false);
await SetCurrentBackgroundPathSetAsync((client, token) => client.GetBingWallpaperAsync(token), token).ConfigureAwait(false);
break;
case BackgroundImageType.HutaoDaily:
await SetCurrentBackgroundPathSetAsync(client => client.GetTodayWallpaperAsync()).ConfigureAwait(false);
await SetCurrentBackgroundPathSetAsync((client, token) => client.GetTodayWallpaperAsync(token), token).ConfigureAwait(false);
break;
case BackgroundImageType.HutaoOfficialLauncher:
await SetCurrentBackgroundPathSetAsync(client => client.GetLauncherWallpaperAsync()).ConfigureAwait(false);
await SetCurrentBackgroundPathSetAsync((client, token) => client.GetLauncherWallpaperAsync(token), token).ConfigureAwait(false);
break;
default:
currentBackgroundPathSet = [];
@@ -116,10 +116,10 @@ internal sealed partial class BackgroundImageService : IBackgroundImageService
currentBackgroundPathSet ??= [];
return currentBackgroundPathSet;
async Task SetCurrentBackgroundPathSetAsync(Func<HutaoWallpaperClient, ValueTask<Response<Wallpaper>>> responseFactory)
async Task SetCurrentBackgroundPathSetAsync(Func<HutaoWallpaperClient, CancellationToken, ValueTask<Response<Wallpaper>>> responseFactory, CancellationToken token = default)
{
HutaoWallpaperClient wallpaperClient = serviceProvider.GetRequiredService<HutaoWallpaperClient>();
Response<Wallpaper> response = await responseFactory(wallpaperClient).ConfigureAwait(false);
Response<Wallpaper> response = await responseFactory(wallpaperClient, token).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);
ValueTask<ValueResult<bool, BackgroundImage?>> GetNextBackgroundImageAsync(BackgroundImage? previous, CancellationToken token = default);
}

View File

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

View File

@@ -1,79 +0,0 @@
// 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,28 +25,46 @@ 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)
{
using (IServiceScope scope = serviceProvider.CreateScope())
Guid projectId = cultivateProject.InnerId;
List<InventoryItem> entities = cultivationDbService.GetInventoryItemListByProjectId(projectId);
List<InventoryItemView> results = [];
foreach (Material meta in context.EnumerateInventoryMaterial())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
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;
InventoryItem entity = entities.SingleOrDefault(e => e.ItemId == meta.Id) ?? InventoryItem.From(projectId, meta.Id);
results.Add(new(entity, meta, saveCommand));
}
return results;
}
/// <inheritdoc/>
@@ -54,13 +72,15 @@ internal sealed partial class CultivationService : ICultivationService
{
await taskContext.SwitchToBackgroundAsync();
List<CultivateEntry> entries = await cultivationDbService
.GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(cultivateProject.InnerId)
.GetCultivateEntryListIncludingLevelInformationByProjectIdAsync(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)));
@@ -78,9 +98,7 @@ internal sealed partial class CultivationService : ICultivationService
resultEntries.Add(new(entry, item, entryItems));
}
return resultEntries
.OrderByDescending(e => e.IsToday)
.ToObservableCollection();
return resultEntries.SortByDescending(e => e.IsToday).ToObservableCollection();
}
/// <inheritdoc/>
@@ -92,11 +110,9 @@ internal sealed partial class CultivationService : ICultivationService
Guid projectId = cultivateProject.InnerId;
token.ThrowIfCancellationRequested();
foreach (CultivateEntry entry in await cultivationDbService.GetCultivateEntryListByProjectIdAsync(projectId).ConfigureAwait(false))
foreach (CultivateEntry entry in await cultivationDbService.GetCultivateEntryListByProjectIdAsync(projectId, token).ConfigureAwait(false))
{
foreach (CultivateItem item in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false))
foreach (CultivateItem item in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId, token).ConfigureAwait(false))
{
if (item.IsFinished)
{
@@ -114,9 +130,7 @@ internal sealed partial class CultivationService : ICultivationService
}
}
token.ThrowIfCancellationRequested();
foreach (InventoryItem inventoryItem in await cultivationDbService.GetInventoryItemListByProjectIdAsync(projectId).ConfigureAwait(false))
foreach (InventoryItem inventoryItem in await cultivationDbService.GetInventoryItemListByProjectIdAsync(projectId, token).ConfigureAwait(false))
{
if (resultItems.SingleOrDefault(i => i.Inner.Id == inventoryItem.ItemId) is { } existedItem)
{
@@ -124,15 +138,12 @@ 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);
}
@@ -149,7 +160,7 @@ internal sealed partial class CultivationService : ICultivationService
}
/// <inheritdoc/>
public async ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Web.Hoyolab.Takumi.Event.Calculate.Item> items, LevelInformation levelInformation)
public async ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<CalculateItem> items, LevelInformation levelInformation)
{
if (items.Count == 0)
{
@@ -190,4 +201,45 @@ 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,43 +2,48 @@
// 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
internal interface ICultivationDbService : IAppDbService<InventoryItem>,
IAppDbService<CultivateEntryLevelInformation>,
IAppDbService<CultivateProject>,
IAppDbService<CultivateEntry>,
IAppDbService<CultivateItem>
{
ValueTask AddCultivateProjectAsync(CultivateProject project);
ValueTask AddCultivateProjectAsync(CultivateProject project, CancellationToken token = default);
ValueTask RemoveCultivateEntryByIdAsync(Guid entryId);
ValueTask RemoveCultivateEntryByIdAsync(Guid entryId, CancellationToken token = default);
ValueTask RemoveCultivateItemRangeByEntryIdAsync(Guid entryId);
ValueTask RemoveCultivateItemRangeByEntryIdAsync(Guid entryId, CancellationToken token = default);
ValueTask RemoveCultivateProjectByIdAsync(Guid projectId);
ValueTask RemoveCultivateProjectByIdAsync(Guid projectId, CancellationToken token = default);
ValueTask<CultivateEntry?> GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId);
ValueTask<CultivateEntry?> GetCultivateEntryByProjectIdAndItemIdAsync(Guid projectId, uint itemId, CancellationToken token = default);
ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId);
ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId, CancellationToken token = default);
ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId);
ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId, CancellationToken token = default);
ObservableCollection<CultivateProject> GetCultivateProjectCollection();
List<InventoryItem> GetInventoryItemListByProjectId(Guid projectId);
ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId);
ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId, CancellationToken token = default);
ValueTask AddCultivateEntryAsync(CultivateEntry entry);
ValueTask AddCultivateEntryAsync(CultivateEntry entry, CancellationToken token = default);
ValueTask AddCultivateItemRangeAsync(IEnumerable<CultivateItem> toAdd);
ValueTask AddCultivateItemRangeAsync(IEnumerable<CultivateItem> toAdd, CancellationToken token = default);
void UpdateCultivateItem(CultivateItem item);
ValueTask UpdateCultivateItemAsync(CultivateItem item);
ValueTask UpdateCultivateItemAsync(CultivateItem item, CancellationToken token = default);
ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId);
ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId, CancellationToken token = default);
ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation);
ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation, CancellationToken token = default);
ValueTask<List<CultivateEntry>> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId);
ValueTask<List<CultivateEntry>> GetCultivateEntryListIncludingLevelInformationByProjectIdAsync(Guid projectId, CancellationToken token = default);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,11 +46,17 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
{
DailyNoteEntry newEntry = DailyNoteEntry.From(userAndUid);
Web.Response.Response<WebDailyNote> dailyNoteResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
.Create(PlayerUid.IsOversea(roleUid))
.GetDailyNoteAsync(userAndUid)
.ConfigureAwait(false);
Web.Response.Response<WebDailyNote> dailyNoteResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IGameRecordClient gameRecordClient = scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
.Create(PlayerUid.IsOversea(roleUid));
dailyNoteResponse = await gameRecordClient
.GetDailyNoteAsync(userAndUid)
.ConfigureAwait(false);
}
if (dailyNoteResponse.IsOk())
{
@@ -117,11 +123,17 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
continue;
}
Web.Response.Response<WebDailyNote> dailyNoteResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
.Create(PlayerUid.IsOversea(entry.Uid))
.GetDailyNoteAsync(new(entry.User, entry.Uid))
.ConfigureAwait(false);
Web.Response.Response<WebDailyNote> dailyNoteResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IGameRecordClient gameRecordClient = serviceProvider
.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>()
.Create(PlayerUid.IsOversea(entry.Uid));
dailyNoteResponse = await gameRecordClient
.GetDailyNoteAsync(new(entry.User, entry.Uid))
.ConfigureAwait(false);
}
if (dailyNoteResponse.IsOk())
{

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Service.Metadata;
@@ -10,6 +11,7 @@ using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using Snap.Hutao.Web.Hutao.GachaLog;
using System.Collections.Frozen;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Service.GachaLog.Factory;
@@ -22,7 +24,24 @@ namespace Snap.Hutao.Service.GachaLog.Factory;
[Injection(InjectAs.Scoped, typeof(IGachaStatisticsFactory))]
internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
{
private readonly IMetadataService metadataService;
private static readonly FrozenSet<uint> BlueStandardWeaponIdsSet = FrozenSet.ToFrozenSet(
[
11301U, 11302U, 11306U,
12301U, 12302U, 12305U,
13303U,
14301U, 14302U, 14304U,
15301U, 15302U, 15304U
]);
private static readonly FrozenSet<uint> PurpleStandardWeaponIdsSet = FrozenSet.ToFrozenSet(
[
11401U, 11402U, 11403U, 11405U,
12401U, 12402U, 12403U, 12405U,
13401U, 13407U,
14401U, 14402U, 14403U, 14409U,
15401U, 15402U, 15403U, 15405U
]);
private readonly HomaGachaLogClient homaGachaLogClient;
private readonly ITaskContext taskContext;
private readonly AppOptions options;
@@ -33,7 +52,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
await taskContext.SwitchToBackgroundAsync();
List<HistoryWishBuilder> historyWishBuilders = context.GachaEvents.SelectList(gachaEvent => new HistoryWishBuilder(gachaEvent, context));
return CreateCore(taskContext, homaGachaLogClient, items, historyWishBuilders, context, options.IsEmptyHistoryWishVisible);
return CreateCore(taskContext, homaGachaLogClient, items, historyWishBuilders, context, options);
}
private static GachaStatistics CreateCore(
@@ -42,7 +61,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
List<Model.Entity.GachaItem> items,
List<HistoryWishBuilder> historyWishBuilders,
in GachaLogServiceMetadataContext context,
bool isEmptyHistoryWishVisible)
AppOptions appOptions)
{
TypedWishSummaryBuilderContext standardContext = TypedWishSummaryBuilderContext.StandardWish(taskContext, gachaLogClient);
TypedWishSummaryBuilder standardWishBuilder = new(standardContext);
@@ -62,6 +81,45 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
Dictionary<Weapon, int> purpleWeaponCounter = [];
Dictionary<Weapon, int> blueWeaponCounter = [];
if (appOptions.IsUnobtainedWishItemVisible)
{
orangeAvatarCounter = context.IdAvatarMap.Values
.Where(avatar => avatar.Quality == QualityType.QUALITY_ORANGE)
.ToDictionary(avatar => avatar, _ => 0);
purpleAvatarCounter = context.IdAvatarMap.Values
.Where(avatar => avatar.Quality == QualityType.QUALITY_PURPLE)
.ToDictionary(avatar => avatar, _ => 0);
orangeWeaponCounter = context.IdWeaponMap.Values
.Where(weapon => weapon.Quality == QualityType.QUALITY_ORANGE)
.ToDictionary(weapon => weapon, _ => 0);
HashSet<Weapon> purpleWeapons = [];
foreach (uint weaponId in PurpleStandardWeaponIdsSet)
{
purpleWeapons.Add(context.IdWeaponMap[weaponId]);
}
foreach (GachaEvent gachaEvent in context.GachaEvents)
{
if (gachaEvent.Type is GachaType.ActivityWeapon)
{
foreach (uint weaponId in gachaEvent.UpPurpleList)
{
purpleWeapons.Add(context.IdWeaponMap[weaponId]);
}
}
}
HashSet<Weapon> blueWeapons = [];
foreach (uint weaponId in BlueStandardWeaponIdsSet)
{
blueWeapons.Add(context.IdWeaponMap[weaponId]);
}
purpleWeaponCounter = purpleWeapons.ToDictionary(weapon => weapon, _ => 0);
blueWeaponCounter = blueWeapons.ToDictionary(weapon => weapon, _ => 0);
}
// Pre group builders
Dictionary<GachaType, List<HistoryWishBuilder>> historyWishBuilderMap = historyWishBuilders
.GroupBy(b => b.ConfigType)
@@ -148,7 +206,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
{
// history
HistoryWishes = historyWishBuilders
.Where(b => isEmptyHistoryWishVisible || (!b.IsEmpty))
.Where(b => appOptions.IsEmptyHistoryWishVisible || (!b.IsEmpty))
.OrderByDescending(builder => builder.From)
.ThenBy(builder => builder.ConfigType, GachaTypeComparer.Shared)
.Select(builder => builder.ToHistoryWish())

View File

@@ -19,7 +19,7 @@ internal sealed class HutaoStatisticsFactory
private readonly GachaEvent avatarEvent;
private readonly GachaEvent avatarEvent2;
private readonly GachaEvent weaponEvent;
private readonly GachaEvent chronicledEvent;
private readonly GachaEvent? chronicledEvent;
public HutaoStatisticsFactory(in HutaoStatisticsFactoryMetadataContext context)
{
@@ -32,7 +32,7 @@ internal sealed class HutaoStatisticsFactory
avatarEvent = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaType.ActivityAvatar);
avatarEvent2 = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaType.SpecialActivityAvatar);
weaponEvent = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaType.ActivityWeapon);
chronicledEvent = context.GachaEvents.Single(g => g.From < now && g.To > now && g.Type == GachaType.ActivityCity);
chronicledEvent = context.GachaEvents.SingleOrDefault(g => g.From < now && g.To > now && g.Type == GachaType.ActivityCity);
}
public HutaoStatistics Create(GachaEventStatistics raw)
@@ -42,7 +42,7 @@ internal sealed class HutaoStatisticsFactory
AvatarEvent = CreateWishSummary(avatarEvent, raw.AvatarEvent),
AvatarEvent2 = CreateWishSummary(avatarEvent2, raw.AvatarEvent2),
WeaponEvent = CreateWishSummary(weaponEvent, raw.WeaponEvent),
Chronicled = CreateWishSummary(chronicledEvent, raw.Chronicled),
Chronicled = chronicledEvent is null ? null : CreateWishSummary(chronicledEvent, raw.Chronicled),
};
}

View File

@@ -17,7 +17,7 @@ namespace Snap.Hutao.Service.Hutao;
internal sealed partial class HutaoAsAService : IHutaoAsAService
{
private const int AnnouncementDuration = 30;
private readonly HutaoAsAServiceClient hutaoAsServiceClient;
private readonly IServiceScopeFactory serviceScopeFactory;
private ObservableCollection<HutaoAnnouncement>? announcements;
@@ -29,7 +29,13 @@ 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 = await hutaoAsServiceClient.GetAnnouncementListAsync(data, token).ConfigureAwait(false);
Response<List<HutaoAnnouncement>> response;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoAsAServiceClient hutaoAsAServiceClient = scope.ServiceProvider.GetRequiredService<HutaoAsAServiceClient>();
response = await hutaoAsAServiceClient.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 HutaoPassportClient passportClient;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly ITaskContext taskContext;
private readonly HutaoUserOptions options;
@@ -40,19 +40,24 @@ internal sealed partial class HutaoUserService : IHutaoUserService, IHutaoUserSe
}
else
{
Web.Response.Response<string> response = await passportClient.LoginAsync(userName, password, token).ConfigureAwait(false);
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
HutaoPassportClient hutaoPassportClient = scope.ServiceProvider.GetRequiredService<HutaoPassportClient>();
if (response.IsOk())
{
if (await options.PostLoginSucceedAsync(passportClient, taskContext, userName, password, response.Data).ConfigureAwait(false))
Web.Response.Response<string> response = await hutaoPassportClient.LoginAsync(userName, password, token).ConfigureAwait(false);
if (response.IsOk())
{
isInitialized = true;
if (await options.PostLoginSucceedAsync(hutaoPassportClient, taskContext, userName, password, response.Data).ConfigureAwait(false))
{
isInitialized = true;
}
}
else
{
await taskContext.SwitchToMainThreadAsync();
options.PostLoginFailed();
}
}
else
{
await taskContext.SwitchToMainThreadAsync();
options.PostLoginFailed();
}
}

View File

@@ -0,0 +1,12 @@
// 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 IMetadataDictionaryIdReliquaryAffixWeightSource
{
public Dictionary<AvatarId, ReliquaryAffixWeight> IdReliquaryAffixWeightMap { get; set; }
}

View File

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

View File

@@ -0,0 +1,12 @@
// 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

@@ -0,0 +1,12 @@
// 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 IMetadataDictionaryIdReliquarySubAffixSource
{
public Dictionary<ReliquarySubAffixId, ReliquarySubAffix> IdReliquarySubAffixMap { get; set; }
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Reliquary;
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataListReliquaryMainAffixLevelSource
{
public List<ReliquaryMainAffixLevel> ReliquaryMainAffixLevels { get; set; }
}

View File

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

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Item;
using Snap.Hutao.Model.Metadata.Weapon;
@@ -32,10 +33,25 @@ internal static class MetadataServiceContextExtension
{
listMaterialSource.Materials = await metadataService.GetMaterialListAsync(token).ConfigureAwait(false);
}
if (context is IMetadataListReliquaryMainAffixLevelSource listReliquaryMainAffixLevelSource)
{
listReliquaryMainAffixLevelSource.ReliquaryMainAffixLevels = await metadataService.GetReliquaryMainAffixLevelListAsync(token).ConfigureAwait(false);
}
if (context is IMetadataListReliquarySource listReliquarySource)
{
listReliquarySource.Reliquaries = await metadataService.GetReliquaryListAsync(token).ConfigureAwait(false);
}
}
// Dictionary
{
if (context is IMetadataDictionaryIdAchievementSource dictionaryIdAchievementSource)
{
dictionaryIdAchievementSource.IdAchievementMap = await metadataService.GetIdToAchievementMapAsync(token).ConfigureAwait(false);
}
if (context is IMetadataDictionaryIdAvatarSource dictionaryIdAvatarSource)
{
dictionaryIdAvatarSource.IdAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
@@ -46,6 +62,26 @@ 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);
}
if (context is IMetadataDictionaryIdReliquaryMainPropertySource dictionaryIdReliquaryMainPropertySource)
{
dictionaryIdReliquaryMainPropertySource.IdReliquaryMainPropertyMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync(token).ConfigureAwait(false);
}
if (context is IMetadataDictionaryIdReliquarySubAffixSource dictionaryIdReliquarySubAffixSource)
{
dictionaryIdReliquarySubAffixSource.IdReliquarySubAffixMap = await metadataService.GetIdToReliquarySubAffixMapAsync(token).ConfigureAwait(false);
}
if (context is IMetadataDictionaryIdWeaponSource dictionaryIdWeaponSource)
{
dictionaryIdWeaponSource.IdWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);

View File

@@ -6,6 +6,7 @@ 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;
@@ -29,12 +30,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;
@@ -85,7 +86,7 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi
else
{
FileNotFoundException exception = new(SH.ServiceMetadataFileNotFound, fileName);
throw ThrowHelper.UserdataCorrupted(SH.ServiceMetadataFileNotFound, exception);
throw HutaoException.Throw(SH.ServiceMetadataFileNotFound, exception);
}
}
@@ -119,10 +120,17 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi
Dictionary<string, string>? metadataFileHashs;
try
{
// download meta check file
metadataFileHashs = await httpClient
.GetFromJsonAsync<Dictionary<string, string>>(metadataOptions.GetLocalizedRemoteFile(MetaFileName), options, token)
.ConfigureAwait(false);
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);
}
}
if (metadataFileHashs is null)
{
@@ -176,23 +184,28 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi
private async ValueTask DownloadMetadataSourceFilesAsync(string fileFullName, CancellationToken token)
{
Stream sourceStream = await httpClient
.GetStreamAsync(metadataOptions.GetLocalizedRemoteFile(fileFullName), token)
.ConfigureAwait(false);
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);
}
}
// Write stream while convert LF to CRLF
using (StreamReader streamReader = new(sourceStream))
using (StreamReaderWriter readerWriter = new(new(sourceStream), File.CreateText(metadataOptions.GetLocalizedLocalFile(fileFullName))))
{
using (StreamWriter streamWriter = File.CreateText(metadataOptions.GetLocalizedLocalFile(fileFullName)))
while (await readerWriter.ReadLineAsync(token).ConfigureAwait(false) is { } line)
{
while (await streamReader.ReadLineAsync(token).ConfigureAwait(false) is { } line)
{
await streamWriter.WriteAsync(line).ConfigureAwait(false);
await readerWriter.WriteAsync(line).ConfigureAwait(false);
if (!streamReader.EndOfStream)
{
await streamWriter.WriteAsync(StringLiterals.CRLF).ConfigureAwait(false);
}
if (!readerWriter.Reader.EndOfStream)
{
await readerWriter.WriteAsync(StringLiterals.CRLF).ConfigureAwait(false);
}
}
}

View File

@@ -25,22 +25,12 @@ internal static class MetadataServiceDictionaryExtension
public static ValueTask<Dictionary<ExtendedEquipAffixId, ReliquarySet>> GetExtendedEquipAffixIdToReliquarySetMapAsync(this IMetadataService metadataService, CancellationToken token = default)
{
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);
return metadataService.FromCacheAsDictionaryAsync(FileNameReliquarySet, (List<ReliquarySet> list) => list.SelectMany(set => set.EquipAffixIds, (set, id) => (Id: id, Set: set)), token);
}
public static ValueTask<Dictionary<TowerLevelGroupId, List<TowerLevel>>> GetGroupIdToTowerLevelGroupMapAsync(this IMetadataService metadataService, CancellationToken token = default)
{
return metadataService.FromCacheAsDictionaryAsync<TowerLevel, IGrouping<TowerLevelGroupId, TowerLevel>, TowerLevelGroupId, List<TowerLevel>>(
FileNameTowerLevel,
list => list.GroupBy(l => l.GroupId),
g => g.Key,
g => g.ToList(),
token);
return metadataService.FromCacheAsDictionaryAsync(FileNameTowerLevel, (List<TowerLevel> 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)
@@ -81,6 +71,11 @@ 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);
@@ -177,6 +172,12 @@ 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

@@ -80,7 +80,7 @@ internal static class MetadataServiceListExtension
return metadataService.FromCacheOrFileAsync<List<ReliquaryMainAffix>>(FileNameReliquaryMainAffix, token);
}
public static ValueTask<List<ReliquaryMainAffixLevel>> GetReliquaryLevelListAsync(this IMetadataService metadataService, CancellationToken token = default)
public static ValueTask<List<ReliquaryMainAffixLevel>> GetReliquaryMainAffixLevelListAsync(this IMetadataService metadataService, CancellationToken token = default)
{
return metadataService.FromCacheOrFileAsync<List<ReliquaryMainAffixLevel>>(FileNameReliquaryMainAffixLevel, token);
}

View File

@@ -16,31 +16,22 @@ internal sealed partial class SignInService : ISignInService
public async ValueTask<ValueResult<bool, string>> ClaimRewardAsync(UserAndUid userAndUid, CancellationToken token = default)
{
ISignInClient signInClient = serviceProvider
.GetRequiredService<IOverseaSupportFactory<ISignInClient>>()
.Create(userAndUid.User.IsOversea);
Response<Reward> rewardResponse = await signInClient.GetRewardAsync(userAndUid.User, token).ConfigureAwait(false);
if (rewardResponse.IsOk())
using (IServiceScope scope = serviceProvider.CreateScope())
{
ISignInClient signInClient = scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<ISignInClient>>()
.Create(userAndUid.User.IsOversea);
Response<Reward> rewardResponse = await signInClient.GetRewardAsync(userAndUid.User, token).ConfigureAwait(false);
if (!rewardResponse.IsOk())
{
return new(false, SH.ServiceSignInRewardListRequestFailed);
}
Response<SignInResult> resultResponse = await signInClient.SignAsync(userAndUid, token).ConfigureAwait(false);
if (resultResponse.IsOk(showInfoBar: false))
{
Response<SignInRewardInfo> infoResponse = await signInClient.GetInfoAsync(userAndUid, token).ConfigureAwait(false);
if (infoResponse.IsOk())
{
int index = infoResponse.Data.TotalSignDay - 1;
Award award = rewardResponse.Data.Awards[index];
return new(true, SH.FormatServiceSignInSuccessRewardFormat(award.Name, award.Count));
}
else
{
return new(false, SH.ServiceSignInInfoRequestFailed);
}
}
else
if (!resultResponse.IsOk(showInfoBar: false))
{
string message = resultResponse.Message;
@@ -56,10 +47,16 @@ internal sealed partial class SignInService : ISignInService
return new(false, SH.FormatServiceSignInClaimRewardFailedFormat(message));
}
}
else
{
return new(false, SH.ServiceSignInRewardListRequestFailed);
Response<SignInRewardInfo> infoResponse = await signInClient.GetInfoAsync(userAndUid, token).ConfigureAwait(false);
if (!infoResponse.IsOk())
{
return new(false, SH.ServiceSignInInfoRequestFailed);
}
int index = infoResponse.Data.TotalSignDay - 1;
Award award = rewardResponse.Data.Awards[index];
return new(true, SH.FormatServiceSignInSuccessRewardFormat(award.Name, award.Count));
}
}
}

View File

@@ -21,8 +21,9 @@ namespace Snap.Hutao.Service.SpiralAbyss;
[Injection(InjectAs.Scoped, typeof(ISpiralAbyssRecordService))]
internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordService
{
private readonly IOverseaSupportFactory<IGameRecordClient> gameRecordClientFactory;
//private readonly IOverseaSupportFactory<IGameRecordClient> gameRecordClientFactory;
private readonly ISpiralAbyssRecordDbService spiralAbyssRecordDbService;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly IMetadataService metadataService;
private readonly ITaskContext taskContext;
@@ -76,54 +77,69 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi
/// <inheritdoc/>
public async ValueTask RefreshSpiralAbyssAsync(UserAndUid userAndUid)
{
// request the index first
await gameRecordClientFactory
.Create(userAndUid.User.IsOversea)
.GetPlayerInfoAsync(userAndUid)
.ConfigureAwait(false);
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
IOverseaSupportFactory<IGameRecordClient> gameRecordClientFactory = scope.ServiceProvider.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>();
await RefreshSpiralAbyssCoreAsync(userAndUid, SpiralAbyssSchedule.Last).ConfigureAwait(false);
await RefreshSpiralAbyssCoreAsync(userAndUid, SpiralAbyssSchedule.Current).ConfigureAwait(false);
// request the index first
await gameRecordClientFactory
.Create(userAndUid.User.IsOversea)
.GetPlayerInfoAsync(userAndUid)
.ConfigureAwait(false);
await RefreshSpiralAbyssCoreAsync(userAndUid, SpiralAbyssSchedule.Last).ConfigureAwait(false);
await RefreshSpiralAbyssCoreAsync(userAndUid, SpiralAbyssSchedule.Current).ConfigureAwait(false);
}
}
private async ValueTask RefreshSpiralAbyssCoreAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule)
{
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response = await gameRecordClientFactory
.Create(userAndUid.User.IsOversea)
.GetSpiralAbyssAsync(userAndUid, schedule)
.ConfigureAwait(false);
if (response.IsOk())
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss webSpiralAbyss = response.Data;
IOverseaSupportFactory<IGameRecordClient> gameRecordClientFactory = scope.ServiceProvider.GetRequiredService<IOverseaSupportFactory<IGameRecordClient>>();
ArgumentNullException.ThrowIfNull(spiralAbysses);
ArgumentNullException.ThrowIfNull(metadataContext);
int index = spiralAbysses.FirstIndexOf(s => s.ScheduleId == webSpiralAbyss.ScheduleId);
if (index >= 0)
{
await taskContext.SwitchToBackgroundAsync();
SpiralAbyssView view = spiralAbysses[index];
SpiralAbyssEntry targetEntry;
if (view.Entity is not null)
{
view.Entity.SpiralAbyss = webSpiralAbyss;
await spiralAbyssRecordDbService.UpdateSpiralAbyssEntryAsync(view.Entity).ConfigureAwait(false);
targetEntry = view.Entity;
}
else
{
SpiralAbyssEntry newEntry = SpiralAbyssEntry.From(userAndUid.Uid.Value, webSpiralAbyss);
await spiralAbyssRecordDbService.AddSpiralAbyssEntryAsync(newEntry).ConfigureAwait(false);
targetEntry = newEntry;
}
await taskContext.SwitchToMainThreadAsync();
spiralAbysses.RemoveAt(index);
spiralAbysses.Insert(index, SpiralAbyssView.From(targetEntry, metadataContext));
}
response = await gameRecordClientFactory
.Create(userAndUid.User.IsOversea)
.GetSpiralAbyssAsync(userAndUid, schedule)
.ConfigureAwait(false);
}
if (!response.IsOk())
{
return;
}
Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss webSpiralAbyss = response.Data;
ArgumentNullException.ThrowIfNull(spiralAbysses);
ArgumentNullException.ThrowIfNull(metadataContext);
int index = spiralAbysses.FirstIndexOf(s => s.ScheduleId == webSpiralAbyss.ScheduleId);
if (index < 0)
{
return;
}
await taskContext.SwitchToBackgroundAsync();
SpiralAbyssView view = spiralAbysses[index];
SpiralAbyssEntry targetEntry;
if (view.Entity is not null)
{
view.Entity.SpiralAbyss = webSpiralAbyss;
await spiralAbyssRecordDbService.UpdateSpiralAbyssEntryAsync(view.Entity).ConfigureAwait(false);
targetEntry = view.Entity;
}
else
{
SpiralAbyssEntry newEntry = SpiralAbyssEntry.From(userAndUid.Uid.Value, webSpiralAbyss);
await spiralAbyssRecordDbService.AddSpiralAbyssEntryAsync(newEntry).ConfigureAwait(false);
targetEntry = newEntry;
}
await taskContext.SwitchToMainThreadAsync();
spiralAbysses.RemoveAt(index);
spiralAbysses.Insert(index, SpiralAbyssView.From(targetEntry, metadataContext));
}
}

View File

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

View File

@@ -68,6 +68,7 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
return false;
}
// TODO: sharing scope
if (!await TrySetUserLTokenAsync(user, token).ConfigureAwait(false))
{
return false;
@@ -100,11 +101,17 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
return true;
}
Response<LTokenWrapper> lTokenResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea)
.GetLTokenBySTokenAsync(user.Entity, token)
.ConfigureAwait(false);
Response<LTokenWrapper> lTokenResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IPassportClient passportClient = scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea);
lTokenResponse = await passportClient
.GetLTokenBySTokenAsync(user.Entity, token)
.ConfigureAwait(false);
}
if (lTokenResponse.IsOk())
{
@@ -131,11 +138,17 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
}
}
Response<UidCookieToken> cookieTokenResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea)
.GetCookieAccountInfoBySTokenAsync(user.Entity, token)
.ConfigureAwait(false);
Response<UidCookieToken> cookieTokenResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IPassportClient passportClient = scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea);
cookieTokenResponse = await passportClient
.GetCookieAccountInfoBySTokenAsync(user.Entity, token)
.ConfigureAwait(false);
}
if (cookieTokenResponse.IsOk())
{
@@ -157,11 +170,17 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
private async ValueTask<bool> TrySetUserUserInfoAsync(ViewModel.User.User user, CancellationToken token)
{
Response<UserFullInfoWrapper> response = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IUserClient>>()
.Create(user.IsOversea)
.GetUserFullInfoAsync(user.Entity, token)
.ConfigureAwait(false);
Response<UserFullInfoWrapper> response;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IUserClient userClient = scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<IUserClient>>()
.Create(user.IsOversea);
response = await userClient
.GetUserFullInfoAsync(user.Entity, token)
.ConfigureAwait(false);
}
if (response.IsOk())
{
@@ -176,10 +195,16 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
private async ValueTask<bool> TrySetUserUserGameRolesAsync(ViewModel.User.User user, CancellationToken token)
{
Response<ListWrapper<UserGameRole>> userGameRolesResponse = await serviceProvider
.GetRequiredService<BindingClient>()
.GetUserGameRolesOverseaAwareAsync(user.Entity, token)
.ConfigureAwait(false);
Response<ListWrapper<UserGameRole>> userGameRolesResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
BindingClient bindingClient = scope.ServiceProvider
.GetRequiredService<BindingClient>();
userGameRolesResponse = await bindingClient
.GetUserGameRolesOverseaAwareAsync(user.Entity, token)
.ConfigureAwait(false);
}
if (userGameRolesResponse.IsOk())
{

View File

@@ -93,11 +93,17 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
public async ValueTask<bool> RefreshCookieTokenAsync(Model.Entity.User user)
{
// TODO: 提醒其他组件此用户的Cookie已更改
Response<UidCookieToken> cookieTokenResponse = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea)
.GetCookieAccountInfoBySTokenAsync(user)
.ConfigureAwait(false);
Response<UidCookieToken> cookieTokenResponse;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IPassportClient passportClient = serviceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(user.IsOversea);
cookieTokenResponse = await passportClient
.GetCookieAccountInfoBySTokenAsync(user)
.ConfigureAwait(false);
}
if (!cookieTokenResponse.IsOk())
{

View File

@@ -35,11 +35,11 @@
<Configurations>Debug;Release</Configurations>
<!--
Required for .NET 8 MSIX packaging
10.2.4.1 Security - Software Dependencies
Products may depend on non-integrated software (such as another product or module)
to deliver primary functionality only as long as the additional required software
is disclosed within the first two lines of the description in the Store.
is disclosed within the first two lines of the description in the Store.
-->
<SelfContained>true</SelfContained>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
@@ -52,7 +52,7 @@
<AppxManifest Include="Package.appxmanifest" Condition="'$(ConfigurationName)'!='Debug'" />
<AppxManifest Include="Package.development.appxmanifest" Condition="'$(ConfigurationName)'=='Debug'" />
</ItemGroup>
<!-- Included Files -->
<ItemGroup>
<None Remove="Assets\LargeTile.scale-100.png" />
@@ -181,6 +181,8 @@
<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" />
@@ -205,7 +207,7 @@
<None Remove="View\TitleView.xaml" />
<None Remove="View\UserView.xaml" />
</ItemGroup>
<!-- Analyzer Files -->
<ItemGroup>
<AdditionalFiles Include="CodeMetricsConfig.txt" />
@@ -220,7 +222,7 @@
<AdditionalFiles Include="Resource\Localization\SH.ru.resx" />
<AdditionalFiles Include="Resource\Localization\SH.zh-Hant.resx" />
</ItemGroup>
<!-- Assets Files -->
<ItemGroup>
<Content Update="Assets\Logo.ico" />
@@ -327,6 +329,10 @@
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.16.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Snap.Hutao.Elevated.Launcher.Runtime" Version="1.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.0.7">
<PrivateAssets>all</PrivateAssets>
@@ -349,6 +355,16 @@
<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>
@@ -389,37 +405,37 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Control\Theme\ScrollViewer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Control\Theme\FlyoutStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\HutaoPassportUnregisterDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\HutaoPassportResetPasswordDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\HutaoPassportRegisterDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\HutaoPassportLoginDialog.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -569,13 +585,13 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="IdentifyMonitorWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Control\HutaoStatisticsCard.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -617,7 +633,7 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Pages -->
<ItemGroup>
<Page Update="View\Dialog\LaunchGamePackageConvertDialog.xaml">

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.View.Card;
@@ -15,7 +16,7 @@ internal sealed partial class AchievementCard : Button
/// </summary>
public AchievementCard()
{
DataContext = Ioc.Default.GetRequiredService<ViewModel.Achievement.AchievementViewModelSlim>();
this.InitializeDataContext<ViewModel.Achievement.AchievementViewModelSlim>();
InitializeComponent();
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.View.Card;
@@ -15,7 +16,7 @@ internal sealed partial class DailyNoteCard : Button
/// </summary>
public DailyNoteCard()
{
DataContext = Ioc.Default.GetRequiredService<ViewModel.DailyNote.DailyNoteViewModelSlim>();
this.InitializeDataContext<ViewModel.DailyNote.DailyNoteViewModelSlim>();
InitializeComponent();
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.View.Card;
@@ -15,7 +16,7 @@ internal sealed partial class GachaStatisticsCard : Button
/// </summary>
public GachaStatisticsCard()
{
DataContext = Ioc.Default.GetRequiredService<ViewModel.GachaLog.GachaLogViewModelSlim>();
this.InitializeDataContext<ViewModel.GachaLog.GachaLogViewModelSlim>();
InitializeComponent();
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.View.Card;
@@ -15,7 +16,7 @@ internal sealed partial class LaunchGameCard : Button
/// </summary>
public LaunchGameCard()
{
DataContext = Ioc.Default.GetRequiredService<ViewModel.Game.LaunchGameViewModelSlim>();
this.InitializeDataContext<ViewModel.Game.LaunchGameViewModelSlim>();
InitializeComponent();
}
}

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