Compare commits

..

1 Commits

Author SHA1 Message Date
qhy040404
544a2d7c6a fix hutao statistics unavailable 2024-04-11 13:50:12 +08:00
69 changed files with 777 additions and 1232 deletions

View File

@@ -110,6 +110,7 @@ dotnet_diagnostic.SA1642.severity = none
dotnet_diagnostic.IDE0005.severity = warning
dotnet_diagnostic.IDE0060.severity = none
dotnet_diagnostic.IDE0290.severity = none
# SA1208: System using directives should be placed before other using directives
dotnet_diagnostic.SA1208.severity = none
@@ -320,8 +321,7 @@ dotnet_diagnostic.CA2227.severity = suggestion
# CA2251: 使用 “string.Equals”
dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = false:none
csharp_style_prefer_primary_constructors = true:suggestion
[*.vb]
#### 命名样式 ####

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

@@ -5,9 +5,6 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.ViewModel.Guide;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.IO;
@@ -39,7 +36,6 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
private readonly IHttpClientFactory httpClientFactory;
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly IServiceProvider serviceProvider;
private readonly ILogger<ImageCache> logger;
@@ -173,49 +169,38 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
while (retryCount < 3)
{
HttpRequestMessageBuilder requestMessageBuilder = httpRequestMessageBuilderFactory
.Create()
.SetRequestUri(uri)
// These headers are only available for our own api
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase))
.Get();
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
if (message.RequestMessage is { RequestUri: { } target } && target != uri)
{
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
{
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
}
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
}
if (responseMessage.IsSuccessStatusCode)
if (message.IsSuccessStatusCode)
{
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (FileStream fileStream = File.Create(baseFile))
{
using (FileStream fileStream = File.Create(baseFile))
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
}
}
switch (responseMessage.StatusCode)
{
case HttpStatusCode.TooManyRequests:
{
retryCount++;
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;
}
switch (message.StatusCode)
{
case HttpStatusCode.TooManyRequests:
{
retryCount++;
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;
}
default:
return;
}
default:
return;
}
}
}

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

@@ -3,7 +3,6 @@
using Microsoft.Win32.SafeHandles;
using Snap.Hutao.Core.Diagnostics;
using System.Buffers;
using System.IO;
using System.Net.Http;
@@ -78,36 +77,34 @@ internal sealed class HttpShardCopyWorker<TStatus> : IDisposable
using (HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent())
Memory<byte> buffer = new byte[bufferSize];
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
{
Memory<byte> buffer = memoryOwner.Memory;
using (Stream stream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
int totalBytesRead = 0;
int bytesReadAfterPreviousReport = 0;
do
{
int totalBytesRead = 0;
int bytesReadAfterPreviousReport = 0;
do
int bytesRead = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
if (bytesRead <= 0)
{
int bytesRead = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
if (bytesRead <= 0)
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
break;
}
await RandomAccess.WriteAsync(destFileHandle, buffer[..bytesRead], shard.StartOffset + totalBytesRead, token).ConfigureAwait(false);
totalBytesRead += bytesRead;
bytesReadAfterPreviousReport += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
stopwatch = ValueStopwatch.StartNew();
}
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
break;
}
await RandomAccess.WriteAsync(destFileHandle, buffer[..bytesRead], shard.StartOffset + totalBytesRead, token).ConfigureAwait(false);
totalBytesRead += bytesRead;
bytesReadAfterPreviousReport += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 500)
{
progress.Report(new(bytesReadAfterPreviousReport));
bytesReadAfterPreviousReport = 0;
stopwatch = ValueStopwatch.StartNew();
}
while (true);
}
while (true);
}
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Diagnostics;
using System.Buffers;
using System.IO;
namespace Snap.Hutao.Core.IO;
@@ -52,30 +51,26 @@ internal class StreamCopyWorker<TStatus>
long totalBytesRead = 0;
int bytesRead;
Memory<byte> buffer = new byte[bufferSize];
using (IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(bufferSize))
do
{
Memory<byte> buffer = memoryOwner.Memory;
do
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
if (bytesRead == 0)
{
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
if (bytesRead is 0)
{
progress.Report(statusFactory(totalBytesRead));
break;
}
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
totalBytesRead += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000)
{
progress.Report(statusFactory(totalBytesRead));
stopwatch = ValueStopwatch.StartNew();
}
progress.Report(statusFactory(totalBytesRead));
break;
}
await destination.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
totalBytesRead += bytesRead;
if (stopwatch.GetElapsedTime().TotalMilliseconds > 1000)
{
progress.Report(statusFactory(totalBytesRead));
stopwatch = ValueStopwatch.StartNew();
}
while (bytesRead > 0);
}
while (bytesRead > 0);
}
}

View File

@@ -116,15 +116,15 @@ internal sealed partial class Activation : IActivation
// If it's the first time launch, we show the guide window anyway.
// Otherwise, we check if there's any unfulfilled resource category present.
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language) >= GuideState.StaticResourceBegin)
{
if (StaticResource.IsAnyUnfulfilledCategoryPresent())
{
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.StaticResourceBegin);
}
}
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language) < GuideState.Completed)
{
await taskContext.SwitchToMainThreadAsync();
serviceProvider.GetRequiredService<GuideWindow>();

View File

@@ -20,9 +20,7 @@ internal static class SettingKeys
#region Application
public const string LaunchTimes = "LaunchTimes";
public const string DataFolderPath = "DataFolderPath";
public const string Major1Minor10Revision0GuideState = "Major1Minor10Revision0GuideState1";
public const string StaticResourceImageQuality = "StaticResourceImageQuality";
public const string StaticResourceImageArchive = "StaticResourceImageArchive";
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled2";
#endregion
@@ -62,10 +60,6 @@ internal static class SettingKeys
#endregion
#region Obsolete
[Obsolete("重置新手引导状态")]
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
[Obsolete("重置调试控制台开关")]
public const string IsAllocConsoleDebugModeEnabledLegacy1 = "IsAllocConsoleDebugModeEnabled";
#endregion

View File

@@ -8,7 +8,7 @@
xmlns:shvg="using:Snap.Hutao.View.Guide"
mc:Ignorable="d">
<Grid x:Name="RootGrid" Background="{ThemeResource SolidBackgroundFillColorBaseBrush}">
<Grid x:Name="RootGrid">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>

View File

@@ -14,10 +14,10 @@ namespace Snap.Hutao;
internal sealed partial class GuideWindow : Window, IWindowOptionsSource, IMinMaxInfoHandler
{
private const int MinWidth = 1000;
private const int MinHeight = 650;
private const int MinHeight = 600;
private const int MaxWidth = 1200;
private const int MaxHeight = 800;
private const int MaxHeight = 750;
private readonly WindowOptions windowOptions;

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">
@@ -1382,9 +1382,6 @@
<data name="ViewGachaLogHeader" xml:space="preserve">
<value>祈愿记录</value>
</data>
<data name="ViewGuideStaticResourceDownloadSize" xml:space="preserve">
<value>预计下载大小:{0}</value>
</data>
<data name="ViewGuideStepAgreementIHaveReadText" xml:space="preserve">
<value>我已阅读并同意</value>
</data>
@@ -1400,12 +1397,6 @@
<data name="ViewGuideStepAgreementTermOfService" xml:space="preserve">
<value>用户使用协议与法律声明</value>
</data>
<data name="ViewGuideStepCommonSetting" xml:space="preserve">
<value>基础设置</value>
</data>
<data name="ViewGuideStepCommonSettingHint" xml:space="preserve">
<value>稍后可以在设置中修改</value>
</data>
<data name="ViewGuideStepDocument" xml:space="preserve">
<value>文档</value>
</data>
@@ -1416,7 +1407,7 @@
<value>安装完成后重启胡桃以查看是否正常生效</value>
</data>
<data name="ViewGuideStepEnvironmentFontDescription1" xml:space="preserve">
<value>如果上方的图标中存在乱码或方块字,请前往</value>
<value>如果上方的图标中存在乱码,请前往</value>
</data>
<data name="ViewGuideStepEnvironmentFontDescription2" xml:space="preserve">
<value>下载并自行安装图标字体</value>
@@ -1433,24 +1424,6 @@
<data name="ViewGuideStepStaticResource" xml:space="preserve">
<value>资源</value>
</data>
<data name="ViewGuideStepStaticResourceSetting" xml:space="preserve">
<value>图像资源设置</value>
</data>
<data name="ViewGuideStepStaticResourceSettingHint" xml:space="preserve">
<value>* 除非你卸载并重新安装胡桃,否则你将无法更改这些设置</value>
</data>
<data name="ViewGuideStepStaticResourceSettingMinimumHeader" xml:space="preserve">
<value>图片资源包体</value>
</data>
<data name="ViewGuideStepStaticResourceSettingMinimumOff" xml:space="preserve">
<value>完整包体</value>
</data>
<data name="ViewGuideStepStaticResourceSettingMinimumOn" xml:space="preserve">
<value>精简包体</value>
</data>
<data name="ViewGuideStepStaticResourceSettingQualityHeader" xml:space="preserve">
<value>图片资源质量</value>
</data>
<data name="ViewHutaoDatabaseHeader" xml:space="preserve">
<value>深渊统计</value>
</data>
@@ -1640,12 +1613,6 @@
<data name="ViewModelGuideActionStaticResourceBegin" xml:space="preserve">
<value>下载资源文件中,请稍候</value>
</data>
<data name="ViewModelGuideStaticResourceQualityHigh" xml:space="preserve">
<value>高质量</value>
</data>
<data name="ViewModelGuideStaticResourceQualityRaw" xml:space="preserve">
<value>原图</value>
</data>
<data name="ViewModelHutaoPassportEmailNotValidHint" xml:space="preserve">
<value>请输入正确的邮箱</value>
</data>
@@ -2636,12 +2603,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

@@ -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

@@ -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

@@ -44,8 +44,6 @@
<SelfContained>true</SelfContained>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<WindowsAppSdkUndockedRegFreeWinRTInitialize>false</WindowsAppSdkUndockedRegFreeWinRTInitialize>
<ServerGarbageCollection>true</ServerGarbageCollection>
<GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
</PropertyGroup>
<ItemGroup>
@@ -153,6 +151,7 @@
<None Remove="View\Control\DescParamComboBox.xaml" />
<None Remove="View\Control\Elevation.xaml" />
<None Remove="View\Control\HutaoStatisticsCard.xaml" />
<None Remove="View\Control\HutaoStatisticsPanel.xaml" />
<None Remove="View\Control\ItemIcon.xaml" />
<None Remove="View\Control\LaunchGameResourceExpander.xaml" />
<None Remove="View\Control\LoadingView.xaml" />
@@ -305,8 +304,8 @@
<PackageReference Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.0.240109" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.0.240109" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -321,7 +320,7 @@
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.3233" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240404000" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240311000" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.6.0" />
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.16.0">
@@ -349,6 +348,14 @@
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnablePreviewMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Control\HutaoStatisticsPanel.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\LaunchGameConfigurationFixDialog.xaml">
<Generator>MSBuild:Compile</Generator>

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

@@ -0,0 +1,13 @@
<shcp:HorizontalEqualPanel
x:Class="Snap.Hutao.View.Control.HutaoStatisticsPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcp="using:Snap.Hutao.Control.Panel"
xmlns:shvc="using:Snap.Hutao.View.Control"
mc:Ignorable="d">
<shvc:HutaoStatisticsCard DataContext="{x:Bind Statistics.AvatarEvent}"/>
<shvc:HutaoStatisticsCard DataContext="{x:Bind Statistics.AvatarEvent2}"/>
<shvc:HutaoStatisticsCard DataContext="{x:Bind Statistics.WeaponEvent}"/>
</shcp:HorizontalEqualPanel>

View File

@@ -0,0 +1,38 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Control.Panel;
using Snap.Hutao.ViewModel.GachaLog;
namespace Snap.Hutao.View.Control;
[DependencyProperty("Statistics", typeof(HutaoStatistics), default, nameof(OnStatisticsChanged))]
internal sealed partial class HutaoStatisticsPanel : HorizontalEqualPanel
{
public HutaoStatisticsPanel()
{
InitializeComponent();
if (DataContext is HutaoStatistics statistics && statistics.Chronicled is { } chronicled)
{
Children.Add(new HutaoStatisticsCard
{
DataContext = chronicled,
});
}
}
private static void OnStatisticsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
HutaoStatisticsPanel panel = (HutaoStatisticsPanel)obj;
if (args.NewValue is HutaoStatistics { Chronicled: { } chronicled })
{
panel.Children.Add(new HutaoStatisticsCard
{
DataContext = chronicled,
});
}
}
}

View File

@@ -49,7 +49,7 @@
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<cwc:SwitchPresenter ContentTransitions="{ThemeResource EntranceThemeTransitions}" Value="{Binding State, Mode=OneWay}">
<cwc:SwitchPresenter Value="{Binding State, Mode=OneWay}">
<cwc:Case Value="{shcm:UInt32 Value=0}">
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<GridView
@@ -125,11 +125,10 @@
<StackPanel
Margin="16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="{ThemeResource SettingsCardSpacing}">
VerticalAlignment="Center">
<TextBlock
Margin="1,0,0,5"
Style="{StaticResource TitleTextBlockStyle}"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
Text="Segoe Fluent Icons"/>
<StackPanel
Margin="0,8"
@@ -156,10 +155,7 @@
<Run Text="{shcm:ResourceString Name=ViewGuideStepEnvironmentFontDescription2}"/>
</TextBlock>
<TextBlock Text="{shcm:ResourceString Name=ViewGuideStepEnvironmentAfterInstallDescription}"/>
<TextBlock
Margin="1,32,0,5"
Style="{StaticResource TitleTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageSettingWebview2Header}"/>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingWebview2Header}"/>
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{Binding RuntimeOptions.WebView2Version}"/>
<TextBlock>
<Run Text="{shcm:ResourceString Name=ViewGuideStepEnvironmentWebView2Description1}"/>
@@ -173,78 +169,6 @@
</Grid>
</cwc:Case>
<cwc:Case Value="{shcm:UInt32 Value=3}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Margin="16"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingHomeAnnouncementRegionHeader}"/>
<ListView
MinWidth="320"
Margin="0,8,0,0"
DisplayMemberPath="Name"
ItemsSource="{Binding AppOptions.LazyRegions.Value}"
SelectedItem="{Binding SelectedRegion, Mode=TwoWay}"/>
</StackPanel>
<TextBlock
Grid.Row="1"
Margin="16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
HorizontalTextAlignment="Center"
Opacity="0.7"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewGuideStepCommonSettingHint}"/>
</Grid>
</cwc:Case>
<cwc:Case Value="{shcm:UInt32 Value=4}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Margin="16"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{shcm:ResourceString Name=ViewGuideStepStaticResourceSettingQualityHeader}"/>
<ListView
MinWidth="320"
Margin="0,8,0,32"
DisplayMemberPath="Name"
ItemsSource="{Binding StaticResourceOptions.ImageQualities}"
SelectedItem="{Binding StaticResourceOptions.ImageQuality, Mode=TwoWay}"/>
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{shcm:ResourceString Name=ViewGuideStepStaticResourceSettingMinimumHeader}"/>
<ListView
MinWidth="320"
Margin="0,8,0,32"
DisplayMemberPath="Name"
ItemsSource="{Binding StaticResourceOptions.ImageArchives}"
SelectedItem="{Binding StaticResourceOptions.ImageArchive, Mode=TwoWay}"/>
<TextBlock Margin="0,16,0,0" Text="{Binding StaticResourceOptions.SizeInformationText, Mode=OneWay}"/>
</StackPanel>
<TextBlock
Grid.Row="1"
Margin="16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
HorizontalTextAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewGuideStepStaticResourceSettingHint}"/>
</Grid>
</cwc:Case>
<cwc:Case Value="{shcm:UInt32 Value=5}">
<StackPanel Margin="32,0" HorizontalAlignment="Left">
<TextBlock
Margin="1,16,0,5"
@@ -266,23 +190,12 @@
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<cwc:Segmented
Margin="16"
HorizontalAlignment="Center"
IsHitTestVisible="False"
SelectedIndex="{Binding State, Mode=TwoWay}">
<!--
<cwc:SegmentedItem Content="{shcm:ResourceString Name=ViewGuideStepLanguage}" Icon="{shcm:FontIcon Glyph=&#xF2B7;}"/>
<cwc:SegmentedItem Content="{shcm:ResourceString Name=ViewGuideStepDocument}" Icon="{shcm:FontIcon Glyph=&#xF28B;}"/>
<cwc:SegmentedItem Content="{shcm:ResourceString Name=ViewGuideStepEnvironment}" Icon="{shcm:FontIcon Glyph=&#xE81E;}"/>
<cwc:SegmentedItem Content="{shcm:ResourceString Name=ViewGuideStepCommonSetting}" Icon="{shcm:FontIcon Glyph=&#xE713;}"/>
<cwc:SegmentedItem Content="{shcm:ResourceString Name=ViewGuideStepStaticResourceSetting}" Icon="{shcm:FontIcon Glyph=&#xE8BA;}"/>
<cwc:SegmentedItem Content="{shcm:ResourceString Name=ViewGuideStepStaticResource}" Icon="{shcm:FontIcon Glyph=&#xE8B9;}"/>
-->
<cwc:SegmentedItem Icon="{shcm:FontIcon Glyph=&#xF2B7;}"/>
<cwc:SegmentedItem Icon="{shcm:FontIcon Glyph=&#xF28B;}"/>
<cwc:SegmentedItem Icon="{shcm:FontIcon Glyph=&#xE81E;}"/>
<cwc:SegmentedItem Icon="{shcm:FontIcon Glyph=&#xE713;}"/>
<cwc:SegmentedItem Icon="{shcm:FontIcon Glyph=&#xE8BA;}"/>
<cwc:SegmentedItem Icon="{shcm:FontIcon Glyph=&#xE8B9;}"/>
</cwc:Segmented>
<Button
Command="{Binding NextOrCompleteCommand}"

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.ViewModel.Guide;
namespace Snap.Hutao.View.Guide;
@@ -15,6 +14,6 @@ internal sealed partial class GuideView : UserControl
public GuideView()
{
InitializeComponent();
DataContext = this.ServiceProvider().GetRequiredService<GuideViewModel>();
DataContext = Ioc.Default.GetRequiredService<GuideViewModel>();
}
}

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>
@@ -475,24 +468,15 @@
Margin="16"
CornerRadius="{ThemeResource ControlCornerRadius}"
IsLoading="{Binding HutaoCloudStatisticsViewModel.IsInitialized, Converter={StaticResource BoolNegationConverter}}"/>
<Grid
<shvc:HutaoStatisticsPanel
Margin="16"
ColumnSpacing="16"
Spacing="16"
Statistics="{Binding HutaoCloudStatisticsViewModel.Statistics}"
Visibility="{Binding HutaoCloudStatisticsViewModel.IsInitialized, Converter={StaticResource BoolToVisibilityConverter}}">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding HutaoCloudStatisticsViewModel.OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shvc:HutaoStatisticsCard Grid.Column="0" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.AvatarEvent}"/>
<shvc:HutaoStatisticsCard Grid.Column="1" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.AvatarEvent2}"/>
<shvc:HutaoStatisticsCard Grid.Column="2" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.WeaponEvent}"/>
<shvc:HutaoStatisticsCard Grid.Column="3" DataContext="{Binding HutaoCloudStatisticsViewModel.Statistics.Chronicled}"/>
</Grid>
</shvc:HutaoStatisticsPanel>
</Grid>
</PivotItem>
</Pivot>
@@ -514,35 +498,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 +537,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

@@ -26,5 +26,5 @@ internal sealed class HutaoStatistics
/// <summary>
/// 集录祈愿
/// </summary>
public HutaoWishSummary Chronicled { get; set; } = default!;
public HutaoWishSummary? Chronicled { get; set; }
}

View File

@@ -7,107 +7,105 @@ using Snap.Hutao.Core;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using System.Collections.Frozen;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
namespace Snap.Hutao.ViewModel.Guide;
/// <summary>
/// 下载信息
/// </summary>
internal sealed class DownloadSummary : ObservableObject
{
private static readonly FrozenSet<string?> AllowedMediaTypes = FrozenSet.ToFrozenSet(
[
"application/octet-stream",
"application/zip",
]);
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly IImageCache imageCache;
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly HttpClient httpClient;
private readonly string fileName;
private readonly string fileUrl;
private readonly IProgress<StreamCopyStatus> progress;
private readonly Progress<StreamCopyStatus> progress;
private string description = SH.ViewModelWelcomeDownloadSummaryDefault;
private double progressValue;
/// <summary>
/// 构造一个新的下载信息
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="fileName">压缩文件名称</param>
public DownloadSummary(IServiceProvider serviceProvider, string fileName)
{
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
httpRequestMessageBuilderFactory = serviceProvider.GetRequiredService<IHttpRequestMessageBuilderFactory>();
httpClient = serviceProvider.GetRequiredService<HttpClient>();
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(serviceProvider.GetRequiredService<RuntimeOptions>().UserAgent);
imageCache = serviceProvider.GetRequiredService<IImageCache>();
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(runtimeOptions.UserAgent);
this.serviceProvider = serviceProvider;
DisplayName = fileName;
this.fileName = fileName;
fileUrl = Web.HutaoEndpoints.StaticZip(fileName);
progress = serviceProvider.GetRequiredService<IProgressFactory>().CreateForMainThread<StreamCopyStatus>(UpdateProgressStatus);
progress = new(UpdateProgressStatus);
}
public string DisplayName { get => fileName; }
/// <summary>
/// 显示名称
/// </summary>
public string DisplayName { get; init; }
/// <summary>
/// 描述
/// </summary>
public string Description { get => description; private set => SetProperty(ref description, value); }
/// <summary>
/// 进度值最大1
/// </summary>
public double ProgressValue { get => progressValue; set => SetProperty(ref progressValue, value); }
/// <summary>
/// 异步下载并解压
/// </summary>
/// <returns>任务</returns>
public async ValueTask<bool> DownloadAndExtractAsync()
{
ILogger<DownloadSummary> logger = serviceProvider.GetRequiredService<ILogger<DownloadSummary>>();
try
{
HttpRequestMessage message = httpRequestMessageBuilderFactory
.Create()
.SetRequestUri(fileUrl)
.SetStaticResourceControlHeaders()
.Get()
.HttpRequestMessage;
HttpResponseMessage response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
using (message)
if (response.Content.Headers.ContentType?.MediaType is not ("application/octet-stream" or "application/zip"))
{
using (HttpResponseMessage response = await httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
if (!AllowedMediaTypes.Contains(response.Content.Headers.ContentType?.MediaType))
{
logger.LogWarning("Download Static Zip failed, Content-Type is {Type}", response.Content.Headers.ContentType);
await taskContext.SwitchToMainThreadAsync();
Description = SH.ViewModelWelcomeDownloadSummaryContentTypeNotMatch;
return false;
}
logger.LogWarning("Download Static Zip failed, Content-Type is {Type}", response.Content.Headers.ContentType);
await taskContext.SwitchToMainThreadAsync();
Description = SH.ViewModelWelcomeDownloadSummaryContentTypeNotMatch;
return false;
}
long contentLength = response.Content.Headers.ContentLength ?? 0;
logger.LogInformation("Begin download, size: {length}", Converters.ToFileSizeString(contentLength));
using (Stream content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
using (TempFileStream temp = new(FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
await new StreamCopyWorker(content, temp, contentLength).CopyAsync(progress).ConfigureAwait(false);
ExtractFiles(temp);
await taskContext.SwitchToMainThreadAsync();
ProgressValue = 1;
Description = SH.ViewModelWelcomeDownloadSummaryComplete;
return true;
}
}
long contentLength = response.Content.Headers.ContentLength ?? 0;
logger.LogInformation("Begin download, length: {length}", contentLength);
using (Stream content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
using (TempFileStream temp = new(FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
await new StreamCopyWorker(content, temp, contentLength).CopyAsync(progress).ConfigureAwait(false);
ExtractFiles(temp);
await taskContext.SwitchToMainThreadAsync();
ProgressValue = 1;
Description = SH.ViewModelWelcomeDownloadSummaryComplete;
return true;
}
}
}
catch (Exception ex)
{
logger.LogError(ex, "Download static zip failed");
logger.LogError(ex, "Download Static Zip failed");
await taskContext.SwitchToMainThreadAsync();
Description = ex is HttpRequestException httpRequestEx
? $"{SH.ViewModelWelcomeDownloadSummaryException} - [HTTP '{httpRequestEx.StatusCode:D}'] [Error '{httpRequestEx.HttpRequestError}']"
Description = ex is HttpRequestException httpRequestException
? $"{SH.ViewModelWelcomeDownloadSummaryException} - [HTTP '{httpRequestException.StatusCode:D}'] [Error '{httpRequestException.HttpRequestError}']"
: ex.Message;
return false;
}
@@ -116,7 +114,7 @@ internal sealed class DownloadSummary : ObservableObject
private void UpdateProgressStatus(StreamCopyStatus status)
{
Description = $"{Converters.ToFileSizeString(status.BytesCopied)}/{Converters.ToFileSizeString(status.TotalBytes)}";
ProgressValue = status.TotalBytes is 0 ? 0 : (double)status.BytesCopied / status.TotalBytes;
ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesCopied / status.TotalBytes;
}
private void ExtractFiles(Stream stream)

View File

@@ -24,17 +24,7 @@ internal enum GuideState : uint
Environment,
/// <summary>
/// 正在查看常用设置
/// </summary>
CommonSetting,
/// <summary>
/// 正在查看图像资源设置
/// </summary>
StaticResourceSetting,
/// <summary>
/// 开始下载图像资源
/// 开始下载资源
/// </summary>
StaticResourceBegin,

View File

@@ -6,9 +6,6 @@ using Snap.Hutao.Core;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Model;
using Snap.Hutao.Service;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Response;
using System.Collections.ObjectModel;
using System.Globalization;
@@ -23,15 +20,12 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel
{
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly StaticResourceOptions staticResourceOptions;
private readonly CultureOptions cultureOptions;
private readonly RuntimeOptions runtimeOptions;
private readonly AppOptions appOptions;
private string nextOrCompleteButtonText = SH.ViewModelGuideActionNext;
private bool isNextOrCompleteButtonEnabled = true;
private NameValue<CultureInfo>? selectedCulture;
private NameValue<Region>? selectedRegion;
private bool isTermOfServiceAgreed;
private bool isPrivacyPolicyAgreed;
private bool isIssueReportAgreed;
@@ -42,7 +36,8 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel
{
get
{
GuideState state = UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language);
uint value = LocalSetting.Get(SettingKeys.Major1Minor7Revision0GuideState, 0U);
GuideState state = (GuideState)value;
if (state is GuideState.Document)
{
@@ -66,12 +61,12 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel
(NextOrCompleteButtonText, IsNextOrCompleteButtonEnabled) = (SH.ViewModelGuideActionNext, true);
}
return (uint)state;
return value;
}
set
{
LocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, value);
LocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, value);
OnPropertyChanged();
}
}
@@ -84,10 +79,6 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel
public RuntimeOptions RuntimeOptions { get => runtimeOptions; }
public AppOptions AppOptions { get => appOptions; }
public StaticResourceOptions StaticResourceOptions { get => staticResourceOptions; }
public NameValue<CultureInfo>? SelectedCulture
{
get => selectedCulture ??= CultureOptions.GetCurrentCultureForSelectionOrDefault();
@@ -102,26 +93,13 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel
}
}
public NameValue<Region>? SelectedRegion
{
get => selectedRegion ??= AppOptions.GetCurrentRegionForSelectionOrDefault();
set
{
if (SetProperty(ref selectedRegion, value) && value is not null)
{
AppOptions.Region = value.Value;
}
}
}
#region Agreement
public bool IsTermOfServiceAgreed
{
get => isTermOfServiceAgreed; set
{
if (SetProperty(ref isTermOfServiceAgreed, value))
{
OnAgreementStateChanged();
OnAgreeSateChanged();
}
}
}
@@ -132,7 +110,7 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel
{
if (SetProperty(ref isPrivacyPolicyAgreed, value))
{
OnAgreementStateChanged();
OnAgreeSateChanged();
}
}
}
@@ -143,7 +121,7 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel
{
if (SetProperty(ref isIssueReportAgreed, value))
{
OnAgreementStateChanged();
OnAgreeSateChanged();
}
}
}
@@ -154,38 +132,27 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel
{
if (SetProperty(ref isOpenSourceLicenseAgreed, value))
{
OnAgreementStateChanged();
OnAgreeSateChanged();
}
}
}
#endregion
/// <summary>
/// 下载信息
/// </summary>
public ObservableCollection<DownloadSummary>? DownloadSummaries
{
get => downloadSummaries;
set => SetProperty(ref downloadSummaries, value);
}
protected override async ValueTask<bool> InitializeUIAsync()
{
HutaoInfrastructureClient hutaoInfrastructureClient = serviceProvider.GetRequiredService<HutaoInfrastructureClient>();
HutaoResponse<StaticResourceSizeInformation> response = await hutaoInfrastructureClient.GetStaticSizeAsync().ConfigureAwait(false);
if (response.IsOk())
{
await taskContext.SwitchToMainThreadAsync();
StaticResourceOptions.SizeInformation = response.Data;
}
return true;
}
[Command("NextOrCompleteCommand")]
private void NextOrComplete()
{
++State;
}
private void OnAgreementStateChanged()
private void OnAgreeSateChanged()
{
IsNextOrCompleteButtonEnabled = IsTermOfServiceAgreed && IsPrivacyPolicyAgreed && IsIssueReportAgreed && IsOpenSourceLicenseAgreed;
}
@@ -206,7 +173,7 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel
}).ConfigureAwait(false);
StaticResource.FulfillAll();
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Completed);
UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Completed);
AppInstance.Restart(string.Empty);
}
}

View File

@@ -16,7 +16,7 @@ internal static class StaticResource
private static readonly ApplicationDataCompositeValue DefaultResourceVersionMap = new()
{
// DO NOT MODIFY THIS MAP
// DO NOT MIDIFY THIS MAP
{ "AchievementIcon", 0 },
{ "AvatarCard", 0 },
{ "AvatarIcon", 0 },
@@ -46,31 +46,31 @@ internal static class StaticResource
private static readonly ApplicationDataCompositeValue LatestResourceVersionMap = new()
{
{ "AchievementIcon", 2 },
{ "AvatarCard", 2 },
{ "AvatarIcon", 5 },
{ "Bg", 3 },
{ "ChapterIcon", 3 },
{ "AchievementIcon", 1 },
{ "AvatarCard", 1 },
{ "AvatarIcon", 4 },
{ "Bg", 2 },
{ "ChapterIcon", 2 },
{ "CodexMonster", 0 },
{ "Costume", 2 },
{ "EmotionIcon", 3 },
{ "EquipIcon", 4 },
{ "GachaAvatarIcon", 4 },
{ "GachaAvatarImg", 4 },
{ "GachaEquipIcon", 4 },
{ "Costume", 1 },
{ "EmotionIcon", 2 },
{ "EquipIcon", 3 },
{ "GachaAvatarIcon", 3 },
{ "GachaAvatarImg", 3 },
{ "GachaEquipIcon", 3 },
{ "GcgCharAvatarIcon", 0 },
{ "IconElement", 3 },
{ "ItemIcon", 4 },
{ "LoadingPic", 2 },
{ "MonsterIcon", 3 },
{ "MonsterSmallIcon", 2 },
{ "NameCardIcon", 3 },
{ "NameCardPic", 4 },
{ "IconElement", 2 },
{ "ItemIcon", 3 },
{ "LoadingPic", 1 },
{ "MonsterIcon", 2 },
{ "MonsterSmallIcon", 1 },
{ "NameCardIcon", 2 },
{ "NameCardPic", 3 },
{ "NameCardPicAlpha", 0 },
{ "Property", 2 },
{ "RelicIcon", 4 },
{ "Skill", 4 },
{ "Talent", 4 },
{ "Property", 1 },
{ "RelicIcon", 3 },
{ "Skill", 3 },
{ "Talent", 3 },
};
public static void FulfillAll()

View File

@@ -1,14 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.ViewModel.Guide;
[Localization]
internal enum StaticResourceArchive
{
[LocalizationKey(nameof(SH.ViewGuideStepStaticResourceSettingMinimumOff))]
Full,
[LocalizationKey(nameof(SH.ViewGuideStepStaticResourceSettingMinimumOn))]
Minimum,
}

View File

@@ -1,26 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using System.Net.Http.Headers;
namespace Snap.Hutao.ViewModel.Guide;
internal static class StaticResourceHttpHeaderBuilderExtension
{
public static TBuilder SetStaticResourceControlHeaders<TBuilder>(this TBuilder builder)
where TBuilder : IHttpHeadersBuilder<HttpHeaders>
{
return builder
.SetHeader("x-quality", $"{UnsafeLocalSetting.Get(SettingKeys.StaticResourceImageQuality, StaticResourceQuality.Raw)}")
.SetHeader("x-archive", $"{UnsafeLocalSetting.Get(SettingKeys.StaticResourceImageArchive, StaticResourceArchive.Full)}");
}
public static TBuilder SetStaticResourceControlHeadersIf<TBuilder>(this TBuilder builder, bool condition)
where TBuilder : IHttpHeadersBuilder<HttpHeaders>
{
return condition ? builder.SetStaticResourceControlHeaders() : builder;
}
}

View File

@@ -1,82 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Common;
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Model;
using Snap.Hutao.Web.Hutao;
namespace Snap.Hutao.ViewModel.Guide;
[Injection(InjectAs.Singleton)]
internal sealed class StaticResourceOptions : ObservableObject
{
private readonly List<NameValue<StaticResourceQuality>> imageQualities = CollectionsNameValue.FromEnum<StaticResourceQuality>(q => q.GetLocalizedDescription());
private readonly List<NameValue<StaticResourceArchive>> imageArchives = CollectionsNameValue.FromEnum<StaticResourceArchive>(a => a.GetLocalizedDescription());
private NameValue<StaticResourceQuality>? imageQuality;
private NameValue<StaticResourceArchive>? imageArchive;
private string? sizeInformationText;
private StaticResourceSizeInformation? sizeInformation;
public List<NameValue<StaticResourceQuality>> ImageQualities { get => imageQualities; }
public NameValue<StaticResourceQuality>? ImageQuality
{
get => imageQuality ??= ImageQualities.First(q => q.Value == UnsafeLocalSetting.Get(SettingKeys.StaticResourceImageQuality, StaticResourceQuality.Raw));
set
{
if (SetProperty(ref imageQuality, value) && value is not null)
{
UnsafeLocalSetting.Set(SettingKeys.StaticResourceImageQuality, value.Value);
UpdateSizeInformationText();
}
}
}
public List<NameValue<StaticResourceArchive>> ImageArchives { get => imageArchives; }
public NameValue<StaticResourceArchive>? ImageArchive
{
get => imageArchive ??= ImageArchives.First(a => a.Value == UnsafeLocalSetting.Get(SettingKeys.StaticResourceImageArchive, StaticResourceArchive.Full));
set
{
if (SetProperty(ref imageArchive, value) && value is not null)
{
UnsafeLocalSetting.Set(SettingKeys.StaticResourceImageArchive, value.Value);
UpdateSizeInformationText();
}
}
}
public StaticResourceSizeInformation? SizeInformation
{
get => sizeInformation;
set
{
sizeInformation = value;
UpdateSizeInformationText();
}
}
public string? SizeInformationText { get => sizeInformationText; set => SetProperty(ref sizeInformationText, value); }
private void UpdateSizeInformationText()
{
if (SizeInformation is not null)
{
long result = (ImageQuality?.Value, ImageArchive?.Value) switch
{
(StaticResourceQuality.Raw, StaticResourceArchive.Full) => SizeInformation.RawFull,
(StaticResourceQuality.Raw, StaticResourceArchive.Minimum) => SizeInformation.RawMinimum,
(StaticResourceQuality.High, StaticResourceArchive.Full) => SizeInformation.HighFull,
(StaticResourceQuality.High, StaticResourceArchive.Minimum) => SizeInformation.HighMinimum,
_ => 0,
};
SizeInformationText = SH.FormatViewGuideStaticResourceDownloadSize(Converters.ToFileSizeString(result));
}
}
}

View File

@@ -1,14 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.ViewModel.Guide;
[Localization]
internal enum StaticResourceQuality
{
[LocalizationKey(nameof(SH.ViewModelGuideStaticResourceQualityRaw))]
Raw,
[LocalizationKey(nameof(SH.ViewModelGuideStaticResourceQualityHigh))]
High,
}

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

@@ -224,7 +224,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
private static void ResetStaticResource()
{
StaticResource.FailAll();
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.StaticResourceBegin);
UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.StaticResourceBegin);
AppInstance.Restart(string.Empty);
}

View File

@@ -84,7 +84,7 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel
[Command("ResetGuideStateCommand")]
private static void ResetGuideState()
{
UnsafeLocalSetting.Set(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language);
UnsafeLocalSetting.Set(SettingKeys.Major1Minor7Revision0GuideState, GuideState.Language);
}
[Command("ExceptionCommand")]

View File

@@ -17,16 +17,6 @@ internal sealed partial class HutaoInfrastructureClient
private readonly ILogger<HutaoInfrastructureClient> logger;
private readonly HttpClient httpClient;
public async ValueTask<HutaoResponse<StaticResourceSizeInformation>> GetStaticSizeAsync(CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(HutaoEndpoints.StaticSize)
.Get();
HutaoResponse<StaticResourceSizeInformation>? resp = await builder.TryCatchSendAsync<HutaoResponse<StaticResourceSizeInformation>>(httpClient, logger, token).ConfigureAwait(false);
return Web.Response.Response.DefaultIfNull(resp);
}
public async ValueTask<HutaoResponse<IPInformation>> GetIPInformationAsync(CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()

View File

@@ -1,19 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao;
internal sealed partial class StaticResourceSizeInformation
{
[JsonPropertyName("raw_full")]
public long RawFull { get; set; }
[JsonPropertyName("raw_minimum")]
public long RawMinimum { get; set; }
[JsonPropertyName("tiny_full")]
public long HighFull { get; set; }
[JsonPropertyName("tiny_minimum")]
public long HighMinimum { get; set; }
}

View File

@@ -270,8 +270,6 @@ internal static class HutaoEndpoints
{
return $"{ApiSnapGenshinStaticZip}/{fileName}.zip";
}
public const string StaticSize = $"{ApiSnapGenshin}/static/size";
#endregion
#region Wallpaper

View File

@@ -35,7 +35,8 @@ internal static class HttpContentBuilderExtension
}
[DebuggerStepThrough]
public static T SetFormUrlEncodedContent<T>(this T builder, IEnumerable<KeyValuePair<string, string>> content)
public static T SetFormUrlEncodedContent<T>(
this T builder, IEnumerable<KeyValuePair<string, string>> content)
where T : IHttpContentBuilder
{
ArgumentNullException.ThrowIfNull(builder);

View File

@@ -41,6 +41,12 @@ internal static partial class HttpHeadersBuilderExtension
return builder.ConfigureHeaders<TBuilder, THeaders>(headers => headers.Add(name, value));
}
public static T SetReferer<T>(this T builder, string referer)
where T : IHttpHeadersBuilder<HttpHeaders>
{
return builder.SetHeader("Referer", referer);
}
[DebuggerStepThrough]
public static T SetHeader<T>(this T builder, string name)
where T : IHttpHeadersBuilder<HttpHeaders>
@@ -74,12 +80,6 @@ internal static partial class HttpHeadersBuilderExtension
.AddHeader<TBuilder, THeaders>(name, value);
}
public static T SetReferer<T>(this T builder, string referer)
where T : IHttpHeadersBuilder<HttpHeaders>
{
return builder.SetHeader("Referer", referer);
}
[DebuggerStepThrough]
public static T RemoveHeader<T>(this T builder, params string?[]? names)
where T : IHttpHeadersBuilder<HttpHeaders>

View File

@@ -12,7 +12,7 @@ internal static class HttpHeadersExtension
ArgumentNullException.ThrowIfNull(name);
// We have to work around the .NET API a little bit. See the comment below for details.
values ??= [];
values ??= Enumerable.Empty<string?>();
values = values.Where(v => v is not null);
if (values.Any())