Compare commits

..

1 Commits

Author SHA1 Message Date
qhy040404
a845dff6ee Revert "temporary fix qr login"
This reverts commit d4bd610fe2.
2024-04-14 13:55:16 +08:00
40 changed files with 532 additions and 679 deletions

View File

@@ -10,6 +10,8 @@ using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Shell;
using System.Diagnostics;
using System.Text;
using static Snap.Hutao.Core.Logging.ConsoleVirtualTerminalSequences;
namespace Snap.Hutao;

View File

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.Specialized;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Windows.Foundation;

View File

@@ -2,6 +2,8 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;

View File

@@ -1,32 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Collection;
internal sealed class TwoEnumerbleEnumerator<TFirst, TSecond> : IDisposable
{
private readonly IEnumerator<TFirst> firstEnumerator;
private readonly IEnumerator<TSecond> secondEnumerator;
public TwoEnumerbleEnumerator(IEnumerable<TFirst> firstEnumerable, IEnumerable<TSecond> secondEnumerable)
{
firstEnumerator = firstEnumerable.GetEnumerator();
secondEnumerator = secondEnumerable.GetEnumerator();
}
public (TFirst First, TSecond Second) Current { get => (firstEnumerator.Current, secondEnumerator.Current); }
public bool MoveNext(ref bool moveFirst, ref bool moveSecond)
{
moveFirst = moveFirst && firstEnumerator.MoveNext();
moveSecond = moveSecond && secondEnumerator.MoveNext();
return moveFirst || moveSecond;
}
public void Dispose()
{
firstEnumerator.Dispose();
secondEnumerator.Dispose();
}
}

View File

@@ -13,6 +13,13 @@ namespace Snap.Hutao.Core.Database;
[HighQuality]
internal static class DbSetExtension
{
/// <summary>
/// 添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int AddAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -20,13 +27,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
/// <summary>
/// 异步添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Add(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
/// <summary>
/// 添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
@@ -34,13 +55,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities, CancellationToken token = default)
/// <summary>
/// 异步添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
dbSet.AddRange(entities);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
/// <summary>
/// 移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -48,13 +83,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
/// <summary>
/// 异步移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Remove(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
/// <summary>
/// 更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -62,11 +111,18 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
/// <summary>
/// 异步更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Update(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -80,11 +136,11 @@ internal static class DbSetExtension
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet, CancellationToken token = default)
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class
{
DbContext dbContext = dbSet.Context();
int count = await dbContext.SaveChangesAsync(token).ConfigureAwait(false);
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
dbContext.ChangeTracker.Clear();
return count;
}

View File

@@ -47,12 +47,6 @@ internal sealed class HutaoException : Exception
throw new HutaoException(HutaoExceptionKind.GachaStatisticsInvalidItemId, message, innerException);
}
[DoesNotReturn]
public static HutaoException UserdataCorrupted(string message, Exception? innerException = default)
{
throw new HutaoException(HutaoExceptionKind.UserdataCorrupted, message, innerException);
}
[DoesNotReturn]
public static InvalidCastException InvalidCast<TFrom, TTo>(string name, Exception? innerException = default)
{

View File

@@ -9,8 +9,6 @@ internal enum HutaoExceptionKind
// Foundation
ImageCacheInvalidUri,
DatabaseCorrupted,
UserdataCorrupted,
// IO
FileSystemCreateFileInsufficientPermissions,

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Entity.Abstraction;
internal interface IAppDbEntity
{
Guid InnerId { get; set; }
}

View File

@@ -16,8 +16,8 @@ namespace Snap.Hutao.Model.Entity;
[HighQuality]
[Table("achievements")]
[SuppressMessage("", "SA1124")]
internal sealed class Achievement : IAppDbEntity,
IEquatable<Achievement>,
internal sealed class Achievement
: IEquatable<Achievement>,
IDbMappingForeignKeyFrom<Achievement, AchievementId>,
IDbMappingForeignKeyFrom<Achievement, UIAFItem>
{

View File

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

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>Open UIAF Json File</value>
</data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>Multiple identical achievement IDs found in a single achievement archive</value>
</data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>Buka berkas UIAF Json</value>
</data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>Terdapat beberapa ID pencapaian yang identik dalam satu arsip pencapaian</value>
</data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>UIAF Json ファイルを開く</value>
</data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>複数の同一アチーブメント Idがアーカイブに混在しています</value>
</data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>打开 UIAF Json 文件</value>
</data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>한 업적 아카이브에서 Id 동일한 업적 발견됨</value>
</data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>Abrir arquivo Json UIAF</value>
</data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>Várias IDs de conquistas idênticas encontradas em um único arquivo de conquistas</value>
</data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">

View File

@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>打开 UIAF Json 文件</value>
</data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>单个成就存档内发现多个相同的成就 Id</value>
</data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
@@ -2636,12 +2636,6 @@
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>快捷键</value>
</data>
<data name="ViewPageSettingNeverHeldItemVisibleDescription" xml:space="preserve">
<value>在祈愿记录页面显示或隐藏未持有过的角色或武器</value>
</data>
<data name="ViewPageSettingNeverHeldItemVisibleHeader" xml:space="preserve">
<value>未持有过的角色或武器</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>前往官网</value>
</data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>Открыть UIAF Json файл</value>
</data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>В одном архиве достижений обнаружено несколько одинаковых идентификаторов достижений</value>
</data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -653,7 +653,7 @@
<data name="ServiceAchievementUIAFImportPickerTitile" xml:space="preserve">
<value>打開 UIAF Json 文件</value>
</data>
<data name="ServiceAchievementUserdataCorruptedAchievementIdNotUnique" xml:space="preserve">
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>單個成就存檔內發現多個相同的成就 Id</value>
</data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Abstraction;
namespace Snap.Hutao.Service.Abstraction;
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));
}
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);
}
}

View File

@@ -1,152 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Database;
using System.Linq.Expressions;
namespace Snap.Hutao.Service.Abstraction;
internal static class AppDbServiceExtension
{
public static TResult Execute<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, TResult> func)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return func(appDbContext.Set<TEntity>());
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, ValueTask<TResult>> asyncFunc)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>()).ConfigureAwait(false);
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, CancellationToken, ValueTask<TResult>> asyncFunc, CancellationToken token)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>(), token).ConfigureAwait(false);
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, Task<TResult>> asyncFunc)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>()).ConfigureAwait(false);
}
}
public static async ValueTask<TResult> ExecuteAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<DbSet<TEntity>, CancellationToken, Task<TResult>> asyncFunc, CancellationToken token)
where TEntity : class
{
using (IServiceScope scope = service.ServiceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
return await asyncFunc(appDbContext.Set<TEntity>(), token).ConfigureAwait(false);
}
}
public static int Add<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class
{
return service.Execute(dbset => dbset.AddAndSave(entity));
}
public static ValueTask<int> AddAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.AddAndSaveAsync(entity, token), token);
}
public static int AddRange<TEntity>(this IAppDbService<TEntity> service, IEnumerable<TEntity> entities)
where TEntity : class
{
return service.Execute(dbset => dbset.AddRangeAndSave(entities));
}
public static ValueTask<int> AddRangeAsync<TEntity>(this IAppDbService<TEntity> service, IEnumerable<TEntity> entities, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.AddRangeAndSaveAsync(entities, token), token);
}
public static TEntity Single<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate)
where TEntity : class
{
return service.Execute(dbset => dbset.AsNoTracking().Single(predicate));
}
public static ValueTask<TEntity> SingleAsync<TEntity>(this IAppDbService<TEntity> service, Expression<Func<TEntity, bool>> predicate, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.AsNoTracking().SingleAsync(predicate, token), token);
}
public static TResult Query<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, TResult> func)
where TEntity : class
{
return service.Execute(dbset => func(dbset.AsNoTracking()));
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, ValueTask<TResult>> func)
where TEntity : class
{
return service.ExecuteAsync(dbset => func(dbset.AsNoTracking()));
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, CancellationToken, ValueTask<TResult>> func, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => func(dbset.AsNoTracking(), token), token);
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, Task<TResult>> func)
where TEntity : class
{
return service.ExecuteAsync(dbset => func(dbset.AsNoTracking()));
}
public static ValueTask<TResult> QueryAsync<TEntity, TResult>(this IAppDbService<TEntity> service, Func<IQueryable<TEntity>, CancellationToken, Task<TResult>> func, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => func(dbset.AsNoTracking(), token), token);
}
public static int Update<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class
{
return service.Execute(dbset => dbset.UpdateAndSave(entity));
}
public static ValueTask<int> UpdateAsync<TEntity>(this IAppDbService<TEntity> service, TEntity entity, CancellationToken token = default)
where TEntity : class
{
return service.ExecuteAsync((dbset, token) => dbset.UpdateAndSaveAsync(entity, token), token);
}
public static int Delete<TEntity>(this IAppDbService<TEntity> service, TEntity entity)
where TEntity : class
{
return service.Execute(dbset => dbset.RemoveAndSave(entity));
}
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);
}
}

View File

@@ -14,10 +14,20 @@ namespace Snap.Hutao.Service.Abstraction;
/// 数据库存储选项的设置
/// </summary>
[ConstructorGenerated]
internal abstract partial class DbStoreOptions : ObservableObject
internal abstract partial class DbStoreOptions : ObservableObject, IOptions<DbStoreOptions>
{
private readonly IServiceProvider serviceProvider;
/// <inheritdoc/>
public DbStoreOptions Value { get => this; }
/// <summary>
/// 从数据库中获取字符串数据
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>值</returns>
protected string GetOption(ref string? storage, string key, string defaultValue = "")
{
return GetOption(ref storage, key, () => defaultValue);
@@ -39,6 +49,13 @@ internal abstract partial class DbStoreOptions : ObservableObject
return storage;
}
/// <summary>
/// 从数据库中获取bool数据
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>值</returns>
protected bool GetOption(ref bool? storage, string key, bool defaultValue = false)
{
return GetOption(ref storage, key, () => defaultValue);
@@ -61,6 +78,13 @@ internal abstract partial class DbStoreOptions : ObservableObject
return storage.Value;
}
/// <summary>
/// 从数据库中获取int数据
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>值</returns>
protected int GetOption(ref int? storage, string key, int defaultValue = 0)
{
return GetOption(ref storage, key, () => defaultValue);
@@ -83,6 +107,15 @@ internal abstract partial class DbStoreOptions : ObservableObject
return storage.Value;
}
/// <summary>
/// 从数据库中获取任何类型的数据
/// </summary>
/// <typeparam name="T">数据的类型</typeparam>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="deserializer">反序列化器</param>
/// <param name="defaultValue">默认值</param>
/// <returns>值</returns>
[return: NotNull]
protected T GetOption<T>(ref T? storage, string key, Func<string, T> deserializer, [DisallowNull] T defaultValue)
{
@@ -127,6 +160,13 @@ internal abstract partial class DbStoreOptions : ObservableObject
return storage;
}
/// <summary>
/// 将值存入数据库
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="propertyName">属性名称</param>
protected void SetOption(ref string? storage, string key, string? value, [CallerMemberName] string? propertyName = null)
{
if (!SetProperty(ref storage, value, propertyName))
@@ -142,6 +182,14 @@ internal abstract partial class DbStoreOptions : ObservableObject
}
}
/// <summary>
/// 将值存入数据库
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="propertyName">属性名称</param>
/// <returns>是否设置了值</returns>
protected bool SetOption(ref bool? storage, string key, bool value, [CallerMemberName] string? propertyName = null)
{
bool set = SetProperty(ref storage, value, propertyName);
@@ -160,6 +208,13 @@ internal abstract partial class DbStoreOptions : ObservableObject
return set;
}
/// <summary>
/// 将值存入数据库
/// </summary>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="propertyName">属性名称</param>
protected void SetOption(ref int? storage, string key, int value, [CallerMemberName] string? propertyName = null)
{
if (!SetProperty(ref storage, value, propertyName))
@@ -175,6 +230,15 @@ internal abstract partial class DbStoreOptions : ObservableObject
}
}
/// <summary>
/// 将值存入数据库
/// </summary>
/// <typeparam name="T">数据的类型</typeparam>
/// <param name="storage">存储字段</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="serializer">序列化器</param>
/// <param name="propertyName">属性名称</param>
protected void SetOption<T>(ref T? storage, string key, T value, Func<T, string> serializer, [CallerMemberName] string? propertyName = null)
{
if (!SetProperty(ref storage, value, propertyName))

View File

@@ -4,7 +4,7 @@
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
namespace Snap.Hutao.Service.Announcement;
namespace Snap.Hutao.Service.Abstraction;
/// <summary>
/// 公告服务

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Abstraction;
internal interface IAppDbService<TEntity> : IAppInfrastructureService
where TEntity : class
{
}

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Abstraction;
internal interface IAppInfrastructureService
{
IServiceProvider ServiceProvider { get; }
}

View File

@@ -1,6 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Abstraction;
internal interface IAppService;

View File

@@ -1,27 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity.Database;
namespace Snap.Hutao.Service.Abstraction;
internal static class ServiceScopeExtension
{
public static TService GetRequiredService<TService>(this IServiceScope scope)
where TService : class
{
return scope.ServiceProvider.GetRequiredService<TService>();
}
public static TDbContext GetDbContext<TDbContext>(this IServiceScope scope)
where TDbContext : DbContext
{
return scope.GetRequiredService<TDbContext>();
}
public static AppDbContext GetAppDbContext(this IServiceScope scope)
{
return scope.GetDbContext<AppDbContext>();
}
}

View File

@@ -2,11 +2,9 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Collection;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.InterChange.Achievement;
using Snap.Hutao.Service.Abstraction;
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
namespace Snap.Hutao.Service.Achievement;
@@ -23,155 +21,197 @@ internal sealed partial class AchievementDbBulkOperation
private readonly IServiceProvider serviceProvider;
private readonly ILogger<AchievementDbBulkOperation> logger;
/// <summary>
/// 合并
/// </summary>
/// <param name="archiveId">成就id</param>
/// <param name="items">待合并的项</param>
/// <param name="aggressive">是否贪婪</param>
/// <returns>导入结果</returns>
public ImportResult Merge(Guid archiveId, IEnumerable<UIAFItem> items, bool aggressive)
{
logger.LogInformation("Perform merge operation for [Archive: {Id}], [Aggressive: {Aggressive}]", archiveId, aggressive);
logger.LogInformation("Perform {Method} Operation for archive: {Id}, Aggressive: {Aggressive}", nameof(Merge), archiveId, aggressive);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IOrderedQueryable<EntityAchievement> oldData = appDbContext.Achievements
.AsNoTracking()
.Where(a => a.ArchiveId == archiveId)
.OrderBy(a => a.Id);
(int add, int update) = (0, 0);
int add = 0;
int update = 0;
using (TwoEnumerbleEnumerator<EntityAchievement, UIAFItem> enumerator = new(oldData, items))
using (IEnumerator<EntityAchievement> entityEnumerator = oldData.GetEnumerator())
{
(bool moveEntity, bool moveUIAF) = (true, true);
while (true)
using (IEnumerator<UIAFItem> uiafEnumerator = items.GetEnumerator())
{
if (!enumerator.MoveNext(ref moveEntity, ref moveUIAF))
{
break;
}
bool moveEntity = true;
bool moveUIAF = true;
(EntityAchievement? entity, UIAFItem? uiaf) = enumerator.Current;
switch (entity, uiaf)
while (true)
{
case (null, not null):
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
add++;
continue;
case (not null, null):
continue; // Skipped
default:
bool moveEntityResult = moveEntity && entityEnumerator.MoveNext();
bool moveUIAFResult = moveUIAF && uiafEnumerator.MoveNext();
if (!(moveEntityResult || moveUIAFResult))
{
break;
}
else
{
EntityAchievement? entity = entityEnumerator.Current;
UIAFItem? uiaf = uiafEnumerator.Current;
if (entity is null && uiaf is not null)
{
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
add++;
continue;
}
else if (entity is not null && uiaf is null)
{
// skip
continue;
}
ArgumentNullException.ThrowIfNull(entity);
ArgumentNullException.ThrowIfNull(uiaf);
switch (entity.Id.CompareTo(uiaf.Id))
if (entity.Id < uiaf.Id)
{
case < 0:
(moveEntity, moveUIAF) = (true, false);
break;
case 0:
(moveEntity, moveUIAF) = (true, true);
if (aggressive)
{
appDbContext.Achievements.RemoveAndSave(entity);
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
update++;
}
break;
case > 0:
(moveEntity, moveUIAF) = (false, true);
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
add++;
break;
moveEntity = true;
moveUIAF = false;
}
else if (entity.Id == uiaf.Id)
{
moveEntity = true;
moveUIAF = true;
break;
if (aggressive)
{
appDbContext.Achievements.RemoveAndSave(entity);
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
update++;
}
}
else
{
// entity.Id > uiaf.Id
moveEntity = false;
moveUIAF = true;
appDbContext.Achievements.AddAndSave(EntityAchievement.From(archiveId, uiaf));
add++;
}
}
}
}
}
logger.LogInformation("Merge operation complete, [Add: {Add}], [Update: {Update}]", add, update);
logger.LogInformation("{Method} Operation Complete, Add: {Add}, Update: {Update}", nameof(Merge), add, update);
return new(add, update, 0);
}
}
/// <summary>
/// 覆盖
/// </summary>
/// <param name="archiveId">成就id</param>
/// <param name="items">待覆盖的项</param>
/// <returns>导入结果</returns>
public ImportResult Overwrite(Guid archiveId, IEnumerable<EntityAchievement> items)
{
logger.LogInformation("Perform Overwrite Operation for [Archive: {Id}]", archiveId);
logger.LogInformation("Perform {Method} Operation for archive: {Id}", nameof(Overwrite), archiveId);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.GetAppDbContext();
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IOrderedQueryable<EntityAchievement> oldData = appDbContext.Achievements
.AsNoTracking()
.Where(a => a.ArchiveId == archiveId)
.OrderBy(a => a.Id);
(int add, int update, int remove) = (0, 0, 0);
int add = 0;
int update = 0;
int remove = 0;
using (TwoEnumerbleEnumerator<EntityAchievement, EntityAchievement> enumerator = new(oldData, items))
using (IEnumerator<EntityAchievement> oldDataEnumerator = oldData.GetEnumerator())
{
(bool moveOld, bool moveNew) = (true, true);
while (true)
using (IEnumerator<EntityAchievement> newDataEnumerator = items.GetEnumerator())
{
if (!enumerator.MoveNext(ref moveOld, ref moveNew))
{
break;
}
bool moveOld = true;
bool moveNew = true;
(EntityAchievement? oldEntity, EntityAchievement? newEntity) = enumerator.Current;
switch (oldEntity, newEntity)
while (true)
{
case (null, not null):
appDbContext.Achievements.AddAndSave(newEntity);
add++;
continue;
case (not null, null):
appDbContext.Achievements.RemoveAndSave(oldEntity);
remove++;
continue;
default:
bool moveOldResult = moveOld && oldDataEnumerator.MoveNext();
bool moveNewResult = moveNew && newDataEnumerator.MoveNext();
if (moveOldResult || moveNewResult)
{
EntityAchievement? oldEntity = oldDataEnumerator.Current;
EntityAchievement? newEntity = newDataEnumerator.Current;
if (oldEntity is null && newEntity is not null)
{
appDbContext.Achievements.AddAndSave(newEntity);
add++;
continue;
}
else if (oldEntity is not null && newEntity is null)
{
appDbContext.Achievements.RemoveAndSave(oldEntity);
remove++;
continue;
}
ArgumentNullException.ThrowIfNull(oldEntity);
ArgumentNullException.ThrowIfNull(newEntity);
switch (oldEntity.Id.CompareTo(newEntity.Id))
if (oldEntity.Id < newEntity.Id)
{
case < 0:
(moveOld, moveNew) = (true, false);
break;
case 0:
(moveOld, moveNew) = (true, true);
if (oldEntity.Equals(newEntity))
{
// Skip same entry, reduce write operation.
continue;
}
else
{
appDbContext.Achievements.RemoveAndSave(oldEntity);
appDbContext.Achievements.AddAndSave(newEntity);
update++;
}
break;
case > 0:
(moveOld, moveNew) = (false, true);
appDbContext.Achievements.AddAndSave(newEntity);
add++;
break;
moveOld = true;
moveNew = false;
appDbContext.Achievements.RemoveAndSave(oldEntity);
remove++;
}
else if (oldEntity.Id == newEntity.Id)
{
moveOld = true;
moveNew = true;
if (oldEntity.Equals(newEntity))
{
// skip same entry.
continue;
}
else
{
appDbContext.Achievements.RemoveAndSave(oldEntity);
appDbContext.Achievements.AddAndSave(newEntity);
update++;
}
}
else
{
// entity.Id > uiaf.Id
moveOld = false;
moveNew = true;
appDbContext.Achievements.AddAndSave(newEntity);
add++;
}
}
else
{
break;
}
}
}
}
logger.LogInformation("Overwrite Operation Complete, Add: {Add}, Update: {Update}, Remove: {Remove}", add, update, remove);
logger.LogInformation("{Method} Operation Complete, Add: {Add}, Update: {Update}, Remove: {Remove}", nameof(Overwrite), add, update, remove);
return new(add, update, remove);
}
}

View File

@@ -2,10 +2,11 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Abstraction;
using System.Collections.ObjectModel;
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
@@ -20,95 +21,148 @@ internal sealed partial class AchievementDbService : IAchievementDbService
{
private readonly IServiceProvider serviceProvider;
public IServiceProvider ServiceProvider { get => serviceProvider; }
public Dictionary<AchievementId, EntityAchievement> GetAchievementMapByArchiveId(Guid archiveId)
{
Dictionary<AchievementId, EntityAchievement> entities;
try
{
return this.Query<EntityAchievement, Dictionary<AchievementId, EntityAchievement>>(query => query
.Where(a => a.ArchiveId == archiveId)
.ToDictionary(a => (AchievementId)a.Id));
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
entities = appDbContext.Achievements
.AsNoTracking()
.Where(a => a.ArchiveId == archiveId)
.ToDictionary(a => (AchievementId)a.Id);
}
}
catch (ArgumentException ex)
{
throw HutaoException.UserdataCorrupted(SH.ServiceAchievementUserdataCorruptedAchievementIdNotUnique, ex);
throw ThrowHelper.DatabaseCorrupted(SH.ServiceAchievementUserdataCorruptedInnerIdNotUnique, ex);
}
return entities;
}
public async ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.Achievements
.AsNoTracking()
.Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.CountAsync()
.ConfigureAwait(false);
}
}
public ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId, CancellationToken token = default)
{
return this.QueryAsync<EntityAchievement, int>(
(query, token) => query
.Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.CountAsync(token),
token);
}
[SuppressMessage("", "CA1305")]
public ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take, CancellationToken token = default)
public async ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take)
{
return this.QueryAsync<EntityAchievement, List<EntityAchievement>>(
(query, token) => query
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.Achievements
.AsNoTracking()
.Where(a => a.ArchiveId == archiveId)
.Where(a => a.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
.OrderByDescending(a => a.Time.ToString())
.Take(take)
.ToListAsync(token),
token);
.ToListAsync()
.ConfigureAwait(false);
}
}
public void OverwriteAchievement(EntityAchievement achievement)
{
this.DeleteByInnerId(achievement);
if (achievement.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
using (IServiceScope scope = serviceProvider.CreateScope())
{
this.Add(achievement);
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Delete exists one.
appDbContext.Achievements.ExecuteDeleteWhere(e => e.InnerId == achievement.InnerId);
if (achievement.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
{
appDbContext.Achievements.AddAndSave(achievement);
}
}
}
public async ValueTask OverwriteAchievementAsync(EntityAchievement achievement, CancellationToken token = default)
public async ValueTask OverwriteAchievementAsync(EntityAchievement achievement)
{
await this.DeleteByInnerIdAsync(achievement, token).ConfigureAwait(false);
if (achievement.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
using (IServiceScope scope = serviceProvider.CreateScope())
{
await this.AddAsync(achievement, token).ConfigureAwait(false);
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Delete exists one.
await appDbContext.Achievements.ExecuteDeleteWhereAsync(e => e.InnerId == achievement.InnerId).ConfigureAwait(false);
if (achievement.Status >= Model.Intrinsic.AchievementStatus.STATUS_FINISHED)
{
await appDbContext.Achievements.AddAndSaveAsync(achievement).ConfigureAwait(false);
}
}
}
public ObservableCollection<AchievementArchive> GetAchievementArchiveCollection()
{
return this.Query<AchievementArchive, ObservableCollection<AchievementArchive>>(query => query.ToObservableCollection());
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return appDbContext.AchievementArchives.AsNoTracking().ToObservableCollection();
}
}
public async ValueTask RemoveAchievementArchiveAsync(AchievementArchive archive, CancellationToken token = default)
public async ValueTask RemoveAchievementArchiveAsync(AchievementArchive archive)
{
// It will cascade deleted the achievements.
await this.DeleteAsync(archive, token).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// It will cascade deleted the achievements.
await appDbContext.AchievementArchives.RemoveAndSaveAsync(archive).ConfigureAwait(false);
}
}
public List<EntityAchievement> GetAchievementListByArchiveId(Guid archiveId)
{
return this.Query<EntityAchievement, List<EntityAchievement>>(query => [.. query.Where(a => a.ArchiveId == archiveId)]);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IQueryable<EntityAchievement> result = appDbContext.Achievements.AsNoTracking().Where(i => i.ArchiveId == archiveId);
return [.. result];
}
}
public ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId, CancellationToken token = default)
public async ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId)
{
return this.QueryAsync<EntityAchievement, List<EntityAchievement>>(
(query, token) => query
.Where(a => a.ArchiveId == archiveId)
.ToListAsync(token),
token);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.Achievements
.AsNoTracking()
.Where(i => i.ArchiveId == archiveId)
.ToListAsync()
.ConfigureAwait(false);
}
}
public List<AchievementArchive> GetAchievementArchiveList()
{
return this.Query<AchievementArchive, List<AchievementArchive>>(query => [.. query]);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IQueryable<AchievementArchive> result = appDbContext.AchievementArchives.AsNoTracking();
return [.. result];
}
}
public async ValueTask<List<AchievementArchive>> GetAchievementArchiveListAsync(CancellationToken token = default)
public async ValueTask<List<AchievementArchive>> GetAchievementArchiveListAsync()
{
return await this.QueryAsync<AchievementArchive, List<AchievementArchive>>(query => query.ToListAsync()).ConfigureAwait(false);
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.AchievementArchives.AsNoTracking().ToListAsync().ConfigureAwait(false);
}
}
}

View File

@@ -2,33 +2,32 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Abstraction;
using System.Collections.ObjectModel;
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
namespace Snap.Hutao.Service.Achievement;
internal interface IAchievementDbService : IAppDbService<Model.Entity.AchievementArchive>, IAppDbService<EntityAchievement>
internal interface IAchievementDbService
{
ValueTask RemoveAchievementArchiveAsync(Model.Entity.AchievementArchive archive, CancellationToken token = default);
ValueTask RemoveAchievementArchiveAsync(Model.Entity.AchievementArchive archive);
ObservableCollection<Model.Entity.AchievementArchive> GetAchievementArchiveCollection();
List<Model.Entity.AchievementArchive> GetAchievementArchiveList();
ValueTask<List<Model.Entity.AchievementArchive>> GetAchievementArchiveListAsync(CancellationToken token = default);
ValueTask<List<Model.Entity.AchievementArchive>> GetAchievementArchiveListAsync();
List<EntityAchievement> GetAchievementListByArchiveId(Guid archiveId);
ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId, CancellationToken token = default);
ValueTask<List<EntityAchievement>> GetAchievementListByArchiveIdAsync(Guid archiveId);
Dictionary<AchievementId, EntityAchievement> GetAchievementMapByArchiveId(Guid archiveId);
ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId, CancellationToken token = default);
ValueTask<int> GetFinishedAchievementCountByArchiveIdAsync(Guid archiveId);
ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take, CancellationToken token = default);
ValueTask<List<EntityAchievement>> GetLatestFinishedAchievementListByArchiveIdAsync(Guid archiveId, int take);
void OverwriteAchievement(EntityAchievement achievement);
ValueTask OverwriteAchievementAsync(EntityAchievement achievement, CancellationToken token = default);
ValueTask OverwriteAchievementAsync(EntityAchievement achievement);
}

View File

@@ -3,14 +3,13 @@
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core;
using Snap.Hutao.Service.Announcement;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using Snap.Hutao.Web.Response;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using WebAnnouncement = Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement.Announcement;
namespace Snap.Hutao.Service;
@@ -73,7 +72,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
// 将公告内容联入公告列表
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
{
foreach (ref readonly WebAnnouncement item in CollectionsMarshal.AsSpan(listWrapper.List))
foreach (ref readonly Announcement item in CollectionsMarshal.AsSpan(listWrapper.List))
{
contentMap.TryGetValue(item.AnnId, out string? rawContent);
item.Content = rawContent ?? string.Empty;
@@ -84,7 +83,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
foreach (ref readonly AnnouncementListWrapper listWrapper in CollectionsMarshal.AsSpan(announcementListWrappers))
{
foreach (ref readonly WebAnnouncement item in CollectionsMarshal.AsSpan(listWrapper.List))
foreach (ref readonly Announcement item in CollectionsMarshal.AsSpan(listWrapper.List))
{
item.Subtitle = new StringBuilder(item.Subtitle)
.Replace("\r<br>", string.Empty)
@@ -100,12 +99,12 @@ internal sealed partial class AnnouncementService : IAnnouncementService
private static void AdjustAnnouncementTime(List<AnnouncementListWrapper> announcementListWrappers, in TimeSpan offset)
{
// 活动公告
List<WebAnnouncement> activities = announcementListWrappers
List<Announcement> activities = announcementListWrappers
.Single(wrapper => wrapper.TypeId == 1)
.List;
// 更新公告
WebAnnouncement versionUpdate = announcementListWrappers
Announcement versionUpdate = announcementListWrappers
.Single(wrapper => wrapper.TypeId == 2)
.List
.Single(ann => AnnouncementRegex.VersionUpdateTitleRegex.IsMatch(ann.Title));
@@ -117,7 +116,7 @@ internal sealed partial class AnnouncementService : IAnnouncementService
DateTimeOffset versionUpdateTime = UnsafeDateTimeOffset.ParseDateTime(versionMatch.Groups[1].ValueSpan, offset);
foreach (ref readonly WebAnnouncement announcement in CollectionsMarshal.AsSpan(activities))
foreach (ref readonly Announcement announcement in CollectionsMarshal.AsSpan(activities))
{
if (AnnouncementRegex.PermanentActivityAfterUpdateTimeRegex.Match(announcement.Content) is { Success: true } permanent)
{

View File

@@ -15,7 +15,6 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Singleton)]
internal sealed partial class AppOptions : DbStoreOptions
{
private bool? isNeverHeldStatisticsItemVisible;
private bool? isEmptyHistoryWishVisible;
private BackdropType? backdropType;
private ElementTheme? elementTheme;
@@ -29,12 +28,6 @@ internal sealed partial class AppOptions : DbStoreOptions
set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value);
}
public bool IsNeverHeldStatisticsItemVisible
{
get => GetOption(ref isNeverHeldStatisticsItemVisible, SettingEntry.IsNeverHeldStatisticsItemVisible);
set => SetOption(ref isNeverHeldStatisticsItemVisible, SettingEntry.IsNeverHeldStatisticsItemVisible, value);
}
public List<NameValue<BackdropType>> BackdropTypes { get; } = CollectionsNameValue.FromEnum<BackdropType>(type => type >= 0);
public BackdropType BackdropType

View File

@@ -4,7 +4,6 @@
using Snap.Discord.GameSDK.ABI;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Unicode;
namespace Snap.Hutao.Service.Discord;
@@ -159,7 +158,7 @@ internal static class DiscordController
static unsafe void DebugWriteDiscordMessage(void* state, DiscordLogLevel logLevel, sbyte* ptr)
{
ReadOnlySpan<byte> utf8 = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr);
string message = Encoding.UTF8.GetString(utf8);
string message = System.Text.Encoding.UTF8.GetString(utf8);
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK]:[{logLevel}]:{message}");
}
}
@@ -225,18 +224,18 @@ internal static class DiscordController
}
}
private static unsafe void SetString(sbyte* reference, int length, in ReadOnlySpan<char> source)
private static unsafe void SetString(sbyte* reference, int length, string source)
{
Span<byte> bytes = new(reference, length);
bytes.Clear();
Utf8.FromUtf16(source, bytes, out _, out _);
Span<sbyte> sbytes = new(reference, length);
sbytes.Clear();
Utf8.FromUtf16(source.AsSpan(), MemoryMarshal.Cast<sbyte, byte>(sbytes), out _, out _);
}
private static unsafe void SetString(sbyte* reference, int length, in ReadOnlySpan<byte> source)
{
Span<byte> bytes = new(reference, length);
bytes.Clear();
source.CopyTo(bytes);
Span<sbyte> sbytes = new(reference, length);
sbytes.Clear();
source.CopyTo(MemoryMarshal.Cast<sbyte, byte>(sbytes));
}
private struct DiscordAsyncAction

View File

@@ -3,7 +3,6 @@
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;
@@ -11,7 +10,6 @@ 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;
@@ -24,16 +22,6 @@ namespace Snap.Hutao.Service.GachaLog.Factory;
[Injection(InjectAs.Scoped, typeof(IGachaStatisticsFactory))]
internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
{
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 IMetadataService metadataService;
private readonly HomaGachaLogClient homaGachaLogClient;
private readonly ITaskContext taskContext;
@@ -45,7 +33,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, options.IsNeverHeldStatisticsItemVisible);
return CreateCore(taskContext, homaGachaLogClient, items, historyWishBuilders, context, options.IsEmptyHistoryWishVisible);
}
private static GachaStatistics CreateCore(
@@ -54,8 +42,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
List<Model.Entity.GachaItem> items,
List<HistoryWishBuilder> historyWishBuilders,
in GachaLogServiceMetadataContext context,
bool isEmptyHistoryWishVisible,
bool isNeverHeldStatisticsItemVisible)
bool isEmptyHistoryWishVisible)
{
TypedWishSummaryBuilderContext standardContext = TypedWishSummaryBuilderContext.StandardWish(taskContext, gachaLogClient);
TypedWishSummaryBuilder standardWishBuilder = new(standardContext);
@@ -75,45 +62,6 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
Dictionary<Weapon, int> purpleWeaponCounter = [];
Dictionary<Weapon, int> blueWeaponCounter = [];
if (isNeverHeldStatisticsItemVisible)
{
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)

View File

@@ -1,11 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hutao.HutaoAsAService;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Hutao;
internal interface IHutaoAsAService
{
ValueTask<ObservableCollection<Web.Hutao.HutaoAsAService.Announcement>> GetHutaoAnnouncementCollectionAsync(CancellationToken token = default);
ValueTask<ObservableCollection<Announcement>> GetHutaoAnnouncementCollectionAsync(CancellationToken token = default);
}

View File

@@ -52,13 +52,9 @@
Grid.Row="0"
HorizontalAlignment="Right"
Text="{Binding DisplayName}"/>
<Viewbox
Grid.Row="1"
HorizontalAlignment="Left"
StretchDirection="DownOnly">
<Viewbox Grid.Row="1" StretchDirection="DownOnly">
<TextBlock
Margin="0,4,0,0"
HorizontalAlignment="Left"
Style="{StaticResource TitleTextBlockStyle}"
Text="{Binding FinishDescription}"/>
</Viewbox>

View File

@@ -3,8 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cw="using:CommunityToolkit.WinUI"
xmlns:cwcont="using:CommunityToolkit.WinUI.Controls"
xmlns:cwconv="using:CommunityToolkit.WinUI.Converters"
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
@@ -25,11 +24,6 @@
</mxi:Interaction.Behaviors>
<Page.Resources>
<cwconv:DoubleToObjectConverter x:Key="DoubleToOpacityConverter" GreaterThan="0">
<cwconv:DoubleToObjectConverter.TrueValue>1.0</cwconv:DoubleToObjectConverter.TrueValue>
<cwconv:DoubleToObjectConverter.FalseValue>0.4</cwconv:DoubleToObjectConverter.FalseValue>
</cwconv:DoubleToObjectConverter>
<Flyout x:Key="HutaoCloudFlyout">
<Grid>
<mxi:Interaction.Behaviors>
@@ -126,12 +120,12 @@
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageGachaLogHutaoCloudNotAllowed}"
TextAlignment="Center"/>
<cwcont:SettingsCard
<cwc:SettingsCard
Command="{Binding HutaoCloudViewModel.NavigateToSpiralAbyssRecordCommand}"
Description="{shcm:ResourceString Name=ViewPageGachaLogHutaoCloudSpiralAbyssActivityDescription}"
Header="{shcm:ResourceString Name=ViewPageGachaLogHutaoCloudSpiralAbyssActivityHeader}"
IsClickEnabled="True"/>
<cwcont:SettingsCard
<cwc:SettingsCard
Command="{Binding HutaoCloudViewModel.NavigateToAfdianSkuCommand}"
Description="{shcm:ResourceString Name=ViewPageGachaLogHutaoCloudAfdianPurchaseDescription}"
Header="{shcm:ResourceString Name=ViewPageGachaLogHutaoCloudAfdianPurchaseHeader}"
@@ -216,7 +210,6 @@
<shvc:ItemIcon
Badge="{Binding Badge}"
Icon="{Binding Icon}"
Opacity="{Binding Count, Converter={StaticResource DoubleToOpacityConverter}}"
Quality="{Binding Quality}"/>
<Border
HorizontalAlignment="Right"
@@ -335,14 +328,14 @@
HorizontalAlignment="Left"
cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Grid HorizontalAlignment="Center" Style="{StaticResource GridCardStyle}">
<cwcont:ConstrainedBox AspectRatio="1080:533">
<cwc:ConstrainedBox AspectRatio="1080:533">
<shci:CachedImage
HorizontalAlignment="Center"
VerticalAlignment="Center"
CornerRadius="{ThemeResource ControlCornerRadius}"
Source="{Binding SelectedHistoryWish.BannerImage}"
Stretch="UniformToFill"/>
</cwcont:ConstrainedBox>
</cwc:ConstrainedBox>
<Border Grid.ColumnSpan="2" Background="{ThemeResource DarkOnlyOverlayMaskColorBrush}"/>
</Grid>
</Border>
@@ -514,35 +507,35 @@
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageGachaLogHint}"/>
<StackPanel Margin="0,24,0,0" Spacing="{StaticResource SettingsCardSpacing}">
<cwcont:SettingsCard
<cwc:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageGachaLogRefreshAction}"
Command="{Binding RefreshBySTokenCommand}"
Description="{shcm:ResourceString Name=ViewPageGachaLogRefreshBySTokenDescription}"
Header="{shcm:ResourceString Name=ViewPageGachaLogRefreshBySToken}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE192;}"
IsClickEnabled="True"/>
<cwcont:SettingsCard
<cwc:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageGachaLogRefreshAction}"
Command="{Binding RefreshByWebCacheCommand}"
Description="{shcm:ResourceString Name=ViewPageGachaLogRefreshByWebCacheDescription}"
Header="{shcm:ResourceString Name=ViewPageGachaLogRefreshByWebCache}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE81E;}"
IsClickEnabled="True"/>
<cwcont:SettingsCard
<cwc:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageGachaLogInputAction}"
Command="{Binding RefreshByManualInputCommand}"
Description="{shcm:ResourceString Name=ViewPageGachaLogRefreshByManualInputDescription}"
Header="{shcm:ResourceString Name=ViewPageGachaLogRefreshByManualInput}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE765;}"
IsClickEnabled="True"/>
<cwcont:SettingsCard
<cwc:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageGachaLogImportAction}"
Command="{Binding ImportFromUIGFJsonCommand}"
Description="{shcm:ResourceString Name=ViewPageGachaLogImportDescription}"
Header="{shcm:ResourceString Name=ViewPageGachaLogImportHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE8B5;}"
IsClickEnabled="True"/>
<cwcont:SettingsCard
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageGachaLogRecoverFromHutaoCloudDescription}"
FlyoutBase.AttachedFlyout="{StaticResource HutaoCloudFlyout}"
Header="{shcm:ResourceString Name=ViewPageGachaLogHutaoCloud}"
@@ -553,7 +546,7 @@
<shcb:ShowAttachedFlyoutAction/>
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
</cwcont:SettingsCard>
</cwc:SettingsCard>
</StackPanel>
</StackPanel>
</Border>

View File

@@ -559,15 +559,6 @@
OffContent="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleOff}"
OnContent="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleOn}"/>
</cwc:SettingsCard>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageSettingNeverHeldItemVisibleDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingNeverHeldItemVisibleHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE890;}">
<ToggleSwitch
IsOn="{Binding AppOptions.IsNeverHeldStatisticsItemVisible, Mode=TwoWay}"
OffContent="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleOff}"
OnContent="{shcm:ResourceString Name=ViewPageSettingEmptyHistoryVisibleOn}"/>
</cwc:SettingsCard>
</StackPanel>
</Border>
</Border>

View File

@@ -3,7 +3,7 @@
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service;
using Snap.Hutao.Service.Announcement;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.View.Card;
using Snap.Hutao.View.Card.Primitive;

View File

@@ -19,12 +19,10 @@ internal sealed partial class PandaClient
public async ValueTask<Response<UrlWrapper>> QRCodeFetchAsync(CancellationToken token = default)
{
// Use 12 (zzz) instead of 4 (gi) temporarily to get legacy game token
GameLoginRequest options = GameLoginRequest.Create(12, HoyolabOptions.DeviceId40);
GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.QrCodeFetch)
.SetHeader("x-rpc-device_id", HoyolabOptions.DeviceId40)
.PostJson(options);
Response<UrlWrapper>? resp = await builder
@@ -36,11 +34,10 @@ internal sealed partial class PandaClient
public async ValueTask<Response<GameLoginResult>> QRCodeQueryAsync(string ticket, CancellationToken token = default)
{
GameLoginRequest options = GameLoginRequest.Create(12, HoyolabOptions.DeviceId40, ticket);
GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40, ticket);
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.QrCodeQuery)
.SetHeader("x-rpc-device_id", HoyolabOptions.DeviceId40)
.PostJson(options);
Response<GameLoginResult>? resp = await builder

View File

@@ -79,7 +79,6 @@ internal sealed partial class PassportClient2
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken)
.SetHeader("x-rpc-device_id", HoyolabOptions.DeviceId40)
.PostJson(data);
Response<LoginResult>? resp = await builder