temp state

This commit is contained in:
Lightczx
2023-04-11 18:42:44 +08:00
parent f37f4fe37d
commit 4407166005
33 changed files with 839 additions and 401 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -4,50 +4,63 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading;
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
/// <summary>
/// 注入HttpClient代码生成器
/// 旨在使用源生成器提高注入效率
/// 防止在运行时动态查找注入类型
/// </summary>
[Generator]
internal sealed class HttpClientGenerator : ISourceGenerator
[Generator(LanguageNames.CSharp)]
internal sealed class HttpClientGenerator : IIncrementalGenerator
{
private const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
private const string DefaultName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.Default";
private const string XRpcName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc";
private const string XRpc2Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc2";
private const string XRpc3Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc3";
private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute";
private const string DynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
private const string UseDynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
private const string CRLF = "\r\n";
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
private static readonly DiagnosticDescriptor invalidConfigurationDescriptor = new("SH100", "无效的 HttpClientConfiguration", "尚未支持生成 {0} 配置", "Quality", DiagnosticSeverity.Error, true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Register a syntax receiver that will be created for each generation pass
context.RegisterForSyntaxNotifications(() => new HttpClientSyntaxContextReceiver());
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses =
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
.Where(GeneratorSyntaxContext2.NotNull)!
.Collect();
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddHttpClientsImplementation);
}
/// <inheritdoc/>
public void Execute(GeneratorExecutionContext context)
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
{
// retrieve the populated receiver
if (context.SyntaxContextReceiver is not HttpClientSyntaxContextReceiver receiver)
return node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0;
}
private static GeneratorSyntaxContext2 HttpClientClass(GeneratorSyntaxContext context, CancellationToken token)
{
if (context.SemanticModel.GetDeclaredSymbol(context.Node, token) is INamedTypeSymbol classSymbol)
{
return;
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
{
return new(context, classSymbol, attributes);
}
}
StringBuilder sourceCodeBuilder = new();
return default;
}
sourceCodeBuilder.Append($$"""
private static void GenerateAddHttpClientsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
{
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
@@ -64,45 +77,40 @@ internal sealed class HttpClientGenerator : ISourceGenerator
{
""");
FillWithHttpClients(receiver, sourceCodeBuilder);
FillUpWithAddHttpClient(sourceBuilder, context, context2s);
sourceCodeBuilder.Append("""
sourceBuilder.Append("""
return services;
}
}
""");
context.AddSource("IocHttpClientConfiguration.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
context.AddSource("IocHttpClientConfiguration.g.cs", sourceBuilder.ToString());
}
private static void FillWithHttpClients(HttpClientSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
private static void FillUpWithAddHttpClient(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
{
List<string> lines = new();
StringBuilder lineBuilder = new();
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
foreach (GeneratorSyntaxContext2 context in contexts)
{
lineBuilder.Clear().Append("\r\n");
lineBuilder.Clear().Append(CRLF);
lineBuilder.Append(@" services.AddHttpClient<");
AttributeData httpClientInfo = classSymbol
.GetAttributes()
.Single(attr => attr.AttributeClass!.ToDisplayString() == HttpClientSyntaxContextReceiver.AttributeName);
ImmutableArray<TypedConstant> arguments = httpClientInfo.ConstructorArguments;
AttributeData httpClientData = context.SingleAttributeWithName(AttributeName);
ImmutableArray<TypedConstant> arguments = httpClientData.ConstructorArguments;
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.Value}, ");
lineBuilder.Append($"{arguments[1].Value}, ");
}
TypedConstant configuration = arguments[0];
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>(");
lineBuilder.Append($"{classSymbol.ToDisplayString()}>(");
string injectAsName = configuration.ToCSharpString();
switch (injectAsName)
string configurationName = arguments[0].ToCSharpString();
switch (configurationName)
{
case DefaultName:
lineBuilder.Append("DefaultConfiguration)");
@@ -117,16 +125,13 @@ internal sealed class HttpClientGenerator : ISourceGenerator
lineBuilder.Append("XRpc3Configuration)");
break;
default:
throw new InvalidOperationException($"非法的 HttpClientConfiguration 值: [{injectAsName}]");
production.ReportDiagnostic(Diagnostic.Create(invalidConfigurationDescriptor, null, configurationName));
break;
}
AttributeData? handlerInfo = classSymbol
.GetAttributes()
.SingleOrDefault(attr => attr.AttributeClass!.ToDisplayString() == PrimaryHttpMessageHandlerAttributeName);
if (handlerInfo != null)
if (context.SingleOrDefaultAttributeWithName(PrimaryHttpMessageHandlerAttributeName) is AttributeData handlerData)
{
ImmutableArray<KeyValuePair<string, TypedConstant>> properties = handlerInfo.NamedArguments;
ImmutableArray<KeyValuePair<string, TypedConstant>> properties = handlerData.NamedArguments;
lineBuilder.Append(@".ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() {");
foreach (KeyValuePair<string, TypedConstant> property in properties)
@@ -141,7 +146,7 @@ internal sealed class HttpClientGenerator : ISourceGenerator
lineBuilder.Append(" })");
}
if (classSymbol.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == DynamicSecretAttributeName))
if (context.HasAttributeWithName(UseDynamicSecretAttributeName))
{
lineBuilder.Append(".AddHttpMessageHandler<DynamicSecretHandler>()");
}
@@ -153,37 +158,7 @@ internal sealed class HttpClientGenerator : ISourceGenerator
foreach (string line in lines.OrderBy(x => x))
{
sourceCodeBuilder.Append(line);
}
}
private class HttpClientSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
sourceBuilder.Append(line);
}
}
}

View File

@@ -4,46 +4,58 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using Snap.Hutao.SourceGeneration.Primitive;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading;
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
/// <summary>
/// 注入代码生成器
/// 旨在使用源生成器提高注入效率
/// 防止在运行时动态查找注入类型
/// </summary>
[Generator]
internal sealed class InjectionGenerator : ISourceGenerator
[Generator(LanguageNames.CSharp)]
internal sealed class InjectionGenerator : IIncrementalGenerator
{
private const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
private const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
private const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
private const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
private const string CRLF = "\r\n";
/// <inheritdoc/>
public void Initialize(GeneratorInitializationContext context)
private static readonly DiagnosticDescriptor invalidInjectionDescriptor = new("SH101", "无效的 InjectAs 枚举值", "尚未支持生成 {0} 配置", "Quality", DiagnosticSeverity.Error, true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Register a syntax receiver that will be created for each generation pass
context.RegisterForSyntaxNotifications(() => new InjectionSyntaxContextReceiver());
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses =
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
.Where(GeneratorSyntaxContext2.NotNull)!
.Collect();
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddInjectionsImplementation);
}
/// <inheritdoc/>
public void Execute(GeneratorExecutionContext context)
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
{
// retrieve the populated receiver
if (context.SyntaxContextReceiver is not InjectionSyntaxContextReceiver receiver)
return node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0;
}
private static GeneratorSyntaxContext2 HttpClientClass(GeneratorSyntaxContext context, CancellationToken token)
{
if (context.SemanticModel.GetDeclaredSymbol(context.Node, token) is INamedTypeSymbol classSymbol)
{
return;
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
{
return new(context, classSymbol, attributes);
}
}
StringBuilder sourceCodeBuilder = new();
sourceCodeBuilder.Append($$"""
return default;
}
private static void GenerateAddInjectionsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
{
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
@@ -57,36 +69,30 @@ internal sealed class InjectionGenerator : ISourceGenerator
{
""");
FillWithInjectionServices(receiver, sourceCodeBuilder);
sourceCodeBuilder.Append("""
FillUpWithAddServices(sourceBuilder, context, context2s);
sourceBuilder.Append("""
return services;
}
}
""");
context.AddSource("ServiceCollectionExtension.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
context.AddSource("ServiceCollectionExtension.g.cs", sourceBuilder.ToString());
}
private static void FillWithInjectionServices(InjectionSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
private static void FillUpWithAddServices(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
{
List<string> lines = new();
StringBuilder lineBuilder = new();
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
foreach (GeneratorSyntaxContext2 context in contexts)
{
AttributeData injectionInfo = classSymbol
.GetAttributes()
.Single(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
lineBuilder
.Clear()
.Append(CRLF);
lineBuilder.Clear().Append(CRLF);
AttributeData injectionInfo = context.SingleAttributeWithName(AttributeName);
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.ToCSharpString();
string injectAsName = arguments[0].ToCSharpString();
switch (injectAsName)
{
case InjectAsSingletonName:
@@ -99,53 +105,23 @@ internal sealed class InjectionGenerator : ISourceGenerator
lineBuilder.Append(@" services.AddScoped<");
break;
default:
throw new InvalidOperationException($"非法的 InjectAs 值: [{injectAsName}]");
production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, null, injectAsName));
break;
}
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.Value}, ");
lineBuilder.Append($"{arguments[1].Value}, ");
}
lineBuilder.Append($"{classSymbol.ToDisplayString()}>();");
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>();");
lines.Add(lineBuilder.ToString());
}
foreach (string line in lines.OrderBy(x => x))
{
sourceCodeBuilder.Append(line);
}
}
private class InjectionSyntaxContextReceiver : ISyntaxContextReceiver
{
/// <summary>
/// 注入特性的名称
/// </summary>
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
/// <summary>
/// 所有需要注入的类型符号
/// </summary>
public List<INamedTypeSymbol> Classes { get; } = new();
/// <inheritdoc/>
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// any class with at least one attribute is a candidate for injection generation
if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0)
{
// get as named type symbol
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is INamedTypeSymbol classSymbol)
{
if (classSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == AttributeName))
{
Classes.Add(classSymbol);
}
}
}
sourceBuilder.Append(line);
}
}
}

View File

@@ -5,4 +5,4 @@
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:启用分析器发布跟踪", Justification = "<挂起>", Scope = "member", Target = "~F:Snap.Hutao.SourceGeneration.TypeInternalAnalyzer.typeInternalDescriptor")]
[assembly: SuppressMessage("", "RS2008")]

View File

@@ -0,0 +1,44 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.CodeAnalysis;
using System.Collections.Immutable;
using System.Linq;
namespace Snap.Hutao.SourceGeneration.Primitive;
internal readonly struct GeneratorSyntaxContext2
{
public readonly GeneratorSyntaxContext Context;
public readonly INamedTypeSymbol Symbol;
public readonly ImmutableArray<AttributeData> Attributes;
public readonly bool HasValue = false;
public GeneratorSyntaxContext2(GeneratorSyntaxContext context, INamedTypeSymbol symbol, ImmutableArray<AttributeData> attributes)
{
Context = context;
Symbol = symbol;
Attributes = attributes;
HasValue = true;
}
public static bool NotNull(GeneratorSyntaxContext2 context)
{
return context.HasValue;
}
public bool HasAttributeWithName(string name)
{
return Attributes.Any(attr => attr.AttributeClass!.ToDisplayString() == name);
}
public AttributeData SingleAttributeWithName(string name)
{
return Attributes.Single(attribute => attribute.AttributeClass!.ToDisplayString() == name);
}
public AttributeData? SingleOrDefaultAttributeWithName(string name)
{
return Attributes.SingleOrDefault(attribute => attribute.AttributeClass!.ToDisplayString() == name);
}
}

View File

@@ -1,7 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
@@ -9,29 +8,32 @@ using System.Text;
namespace Snap.Hutao.SourceGeneration;
[Generator]
internal sealed class ReliquaryWeightConfigurationGenerator : ISourceGenerator
[Generator(LanguageNames.CSharp)]
internal sealed class ReliquaryWeightConfigurationGenerator : IIncrementalGenerator
{
private const string ReliquaryWeightConfigurationFileName = "ReliquaryWeightConfiguration.json";
private const string FileName = "ReliquaryWeightConfiguration.json";
public void Initialize(GeneratorInitializationContext context)
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ImmutableArray<AdditionalText>> provider = context.AdditionalTextsProvider.Where(MatchFileName).Collect();
context.RegisterSourceOutput(provider, GenerateReliquaryWeightConfiguration);
}
public void Execute(GeneratorExecutionContext context)
private static bool MatchFileName(AdditionalText text)
{
try
{
return Path.GetFileName(text.Path) == FileName;
}
AdditionalText configurationJsonFile = context.AdditionalFiles
.First(af => Path.GetFileName(af.Path) == ReliquaryWeightConfigurationFileName);
private static void GenerateReliquaryWeightConfiguration(SourceProductionContext context,ImmutableArray<AdditionalText> additionalTexts)
{
AdditionalText jsonFile = additionalTexts.Single();
string configurationJson = configurationJsonFile.GetText(context.CancellationToken)!.ToString();
string configurationJson = jsonFile.GetText(context.CancellationToken)!.ToString();
Dictionary<string, ReliquaryWeightConfigurationMetadata> metadataMap =
JsonParser.FromJson<Dictionary<string, ReliquaryWeightConfigurationMetadata>>(configurationJson)!;
StringBuilder sourceCodeBuilder = new();
sourceCodeBuilder.Append($$"""
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
@@ -59,24 +61,18 @@ internal sealed class ReliquaryWeightConfigurationGenerator : ISourceGenerator
foreach (KeyValuePair<string, ReliquaryWeightConfigurationMetadata> kvp in metadataMap.OrderBy(kvp => kvp.Key))
{
AppendAffixWeight(sourceCodeBuilder, kvp.Key, kvp.Value);
AppendAffixWeight(sourceBuilder, kvp.Key, kvp.Value);
}
sourceCodeBuilder.Append($$"""
sourceBuilder.Append($$"""
};
}
""");
context.AddSource("ReliquaryWeightConfiguration.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
}
catch (Exception ex)
{
context.AddSource("ReliquaryWeightConfiguration.g.cs", ex.ToString());
}
context.AddSource("ReliquaryWeightConfiguration.g.cs", sourceBuilder.ToString());
}
private void AppendAffixWeight(StringBuilder builder, string id, ReliquaryWeightConfigurationMetadata metadata)
private static void AppendAffixWeight(StringBuilder builder, string id, ReliquaryWeightConfigurationMetadata metadata)
{
StringBuilder lineBuilder = new StringBuilder()
.Append(" new AffixWeight(").Append(id).Append(',')
@@ -97,7 +93,6 @@ internal sealed class ReliquaryWeightConfigurationGenerator : ISourceGenerator
lineBuilder.Append(',');
builder.AppendLine(lineBuilder.ToString());
}

View File

@@ -61,4 +61,9 @@ internal static class SettingKeys
/// 静态资源合约V4 刷新 AvatarIcon
/// </summary>
public const string StaticResourceV4Contract = "StaticResourceV4Contract";
/// <summary>
/// 静态资源合约V5 刷新 AvatarIcon
/// </summary>
public const string StaticResourceV5Contract = "StaticResourceV5Contract";
}

View File

@@ -53,5 +53,6 @@ internal static class StaticResource
LocalSetting.Set(SettingKeys.StaticResourceV2Contract, state);
LocalSetting.Set(SettingKeys.StaticResourceV3Contract, state);
LocalSetting.Set(SettingKeys.StaticResourceV4Contract, state);
LocalSetting.Set(SettingKeys.StaticResourceV5Contract, state);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1575,9 +1575,9 @@ namespace Snap.Hutao.Resource.Localization {
/// <summary>
/// 查找类似 保底 的本地化字符串。
/// </summary>
internal static string ViewControlStatisticsCardGuarenteeText {
internal static string ViewControlStatisticsCardGuaranteeText {
get {
return ResourceManager.GetString("ViewControlStatisticsCardGuarenteeText", resourceCulture);
return ResourceManager.GetString("ViewControlStatisticsCardGuaranteeText", resourceCulture);
}
}
@@ -3516,6 +3516,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 设置 的本地化字符串。
/// </summary>
internal static string ViewPageHomeLaunchGameSettingAction {
get {
return ResourceManager.GetString("ViewPageHomeLaunchGameSettingAction", resourceCulture);
}
}
/// <summary>
/// 查找类似 详情 的本地化字符串。
/// </summary>

View File

@@ -609,7 +609,7 @@
<data name="ViewControlStatisticsCardBlueText" xml:space="preserve">
<value>三星</value>
</data>
<data name="ViewControlStatisticsCardGuarenteeText" xml:space="preserve">
<data name="ViewControlStatisticsCardGuaranteeText" xml:space="preserve">
<value>保底</value>
</data>
<data name="ViewControlStatisticsCardOrangeAveragePullText" xml:space="preserve">
@@ -1959,4 +1959,7 @@
<data name="ViewPageWiKiWeaponAfterAscensionTitle" xml:space="preserve">
<value>突破后</value>
</data>
<data name="ViewPageHomeLaunchGameSettingAction" xml:space="preserve">
<value>设置</value>
</data>
</root>

View File

@@ -142,9 +142,9 @@ internal sealed class TypedWishSummaryBuilder
MaxOrangePull = maxOrangePullTracker,
MinOrangePull = minOrangePullTracker,
LastOrangePull = lastOrangePullTracker,
GuarenteeOrangeThreshold = guarenteeOrangeThreshold,
GuaranteeOrangeThreshold = guarenteeOrangeThreshold,
LastPurplePull = lastPurplePullTracker,
GuarenteePurpleThreshold = guarenteePurpleThreshold,
GuaranteePurpleThreshold = guarenteePurpleThreshold,
TotalOrangePull = totalOrangePullTracker,
TotalPurplePull = totalPurplePullTracker,
TotalBluePull = totalBluePullTracker,

View File

@@ -407,4 +407,41 @@ internal sealed class GachaLogService : IGachaLogService
appDbContext.GachaItems.AddRangeAndSave(itemsToAdd);
}
}
}
}
/// <summary>
/// 祈愿记录导出服务
/// </summary>
internal sealed class GachaLogExportService
{
AppDbContext appDbContext;
/// <inheritdoc/>
public Task<UIGF> ExportToUIGFAsync(GachaArchive archive)
{
List<UIGFItem> list = appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId)
.AsEnumerable()
.Select(i => i.ToUIGFItem(GetNameQualityByItemId(i.ItemId)))
.ToList();
UIGF uigf = new()
{
Info = UIGFInfo.Create(archive.Uid),
List = list,
};
return Task.FromResult(uigf);
}
private INameQuality GetNameQualityByItemId(int id)
{
int place = id.Place();
return place switch
{
8 => idAvatarMap![id],
5 => idWeaponMap![id],
_ => throw Must.NeverHappen($"Id places: {place}"),
};
}
}

View File

@@ -289,6 +289,7 @@ internal sealed class GameService : IGameService
string gamePath = appOptions.GamePath;
if (string.IsNullOrWhiteSpace(gamePath))
{
// TODO: throw exception
return;
}

View File

@@ -44,6 +44,7 @@ internal sealed class LaunchOptions : DbStoreOptions
primaryScreenHeight = primaryRect.Height;
// This list can't use foreach
// https://github.com/microsoft/microsoft-ui-xaml/issues/6454
IReadOnlyList<DisplayArea> displayAreas = DisplayArea.FindAll();
for (int i = 0; i < displayAreas.Count; i++)
{
@@ -148,7 +149,13 @@ internal sealed class LaunchOptions : DbStoreOptions
public NameValue<int> Monitor
{
get => GetOption(ref monitor, SettingEntry.LaunchMonitor, index => Monitors[int.Parse(index) - 1], Monitors[0]);
set => SetOption(ref monitor, SettingEntry.LaunchMonitor, value, selected => selected?.Value.ToString() ?? "1");
set
{
if (value != null)
{
SetOption(ref monitor, SettingEntry.LaunchMonitor, value, selected => selected.Value.ToString() ?? "1");
}
}
}
/// <summary>

View File

@@ -78,6 +78,7 @@
<None Remove="ReliquaryWeightConfiguration.json" />
<None Remove="Resource\Font\CascadiaMono.ttf" />
<None Remove="Resource\Font\MiSans-Regular.ttf" />
<None Remove="Resource\HutaoIconSourceTransparentBackgroundGradient1.png" />
<None Remove="Resource\Icon\UI_AchievementIcon_3_3.png" />
<None Remove="Resource\Icon\UI_BagTabIcon_Avatar.png" />
<None Remove="Resource\Icon\UI_BagTabIcon_Weapon.png" />
@@ -199,6 +200,7 @@
<ItemGroup>
<Content Include="Resource\Font\CascadiaMono.ttf" />
<Content Include="Resource\Font\MiSans-Regular.ttf" />
<Content Include="Resource\HutaoIconSourceTransparentBackgroundGradient1.png" />
<Content Include="Resource\Icon\UI_AchievementIcon_3_3.png" />
<Content Include="Resource\Icon\UI_BagTabIcon_Avatar.png" />
<Content Include="Resource\Icon\UI_BagTabIcon_Weapon.png" />
@@ -263,7 +265,7 @@
<!-- Projects -->
<ItemGroup>
<ProjectReference Include="..\Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" SetTargetFramework="TargetFramework=netstandard2.0"/>
<ProjectReference Include="..\Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" SetTargetFramework="TargetFramework=netstandard2.0" />
</ItemGroup>
<!-- Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging

View File

@@ -44,7 +44,7 @@
Margin="0,0,8,0"
VerticalAlignment="Center"
Foreground="#FF0063FF"
Text="{shcm:ResourceString Name=ViewControlStatisticsCardGuarenteeText}"
Text="{shcm:ResourceString Name=ViewControlStatisticsCardGuaranteeText}"
Visibility="{Binding IsGuarantee, Converter={StaticResource BoolToVisibilityConverter}}"/>
<TextBlock
Margin="0,0,8,0"
@@ -180,7 +180,7 @@
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
Foreground="{StaticResource OrangeBrush}"
IsIndeterminate="False"
Maximum="{Binding GuarenteeOrangeThreshold}"
Maximum="{Binding GuaranteeOrangeThreshold}"
Value="{Binding LastOrangePull}"/>
<TextBlock
Grid.Column="0"
@@ -212,7 +212,7 @@
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
Foreground="{StaticResource PurpleBrush}"
IsIndeterminate="False"
Maximum="{Binding GuarenteePurpleThreshold}"
Maximum="{Binding GuaranteePurpleThreshold}"
Value="{Binding LastPurplePull}"/>
<TextBlock
Grid.Column="0"

View File

@@ -2,6 +2,7 @@
x:Class="Snap.Hutao.View.Page.AnnouncementPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:clw="using:CommunityToolkit.Labs.WinUI"
xmlns:cwu="using:CommunityToolkit.WinUI.UI"
xmlns:cwua="using:CommunityToolkit.WinUI.UI.Animations"
xmlns:cwub="using:CommunityToolkit.WinUI.UI.Behaviors"
@@ -154,13 +155,178 @@
<Grid>
<ScrollViewer Padding="0,0,4,0">
<StackPanel>
<ItemsControl>
<ItemsControl.ItemTemplate>
<DataTemplate>
<InfoBar/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel>
<TextBlock
Margin="16,16,16,0"
Style="{StaticResource TitleTextBlockStyle}"
Text="Greeting Text"/>
<TextBlock Margin="16,0,16,0" Text="账号邮箱@xx.com"/>
<cwucont:AdaptiveGridView
Margin="16,16,0,0"
HorizontalAlignment="Stretch"
cwua:ItemsReorderAnimation.Duration="0:0:0.1"
DesiredWidth="300"
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
SelectionMode="None">
<!-- 启动游戏 -->
<Button
Height="120"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
Command="{Binding LaunchGameViewModelSlim.LaunchCommand}"
Style="{StaticResource AccentButtonStyle}">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding LaunchGameViewModelSlim.OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Grid CornerRadius="{ThemeResource ControlCornerRadius}">
<Image
Margin="0,40,0,0"
HorizontalAlignment="Right"
Source="ms-appx:///Resource/HutaoIconSourceTransparentBackgroundGradient1.png"
Stretch="Uniform"/>
<Grid Margin="12" ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<FontIcon
Grid.Row="0"
VerticalAlignment="Center"
Glyph="&#xE7FC;"/>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{shcm:ResourceString Name=ViewLaunchGameHeader}"/>
<Button
Grid.Column="2"
MinHeight="37.2"
Background="Transparent"
BorderBrush="{x:Null}"
BorderThickness="0"
Command="{Binding LaunchGameViewModelSlim.NavigateCommand}"
Content="&#xE713;"
FontFamily="{StaticResource SymbolThemeFontFamily}"
Foreground="{ThemeResource AccentButtonForeground}"
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageHomeLaunchGameSettingAction}"/>
<ComboBox
Grid.Row="1"
Grid.ColumnSpan="3"
VerticalAlignment="Bottom"
DisplayMemberPath="Name"
ItemsSource="{Binding LaunchGameViewModelSlim.GameAccounts}"
PlaceholderText="选择账号或直接启动"
SelectedItem="{Binding LaunchGameViewModelSlim.SelectedGameAccount, Mode=TwoWay}"/>
</Grid>
</Grid>
</Button>
<Border Style="{StaticResource BorderCardStyle}">
<FlipView Background="{x:Null}">
<Grid Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="卡池名称 1"/>
<shvc:ItemIcon
Width="40"
Height="40"
Margin="0,12,0,0"
HorizontalAlignment="Left"
Icon="{Binding Icon}"
Quality="QUALITY_ORANGE"/>
<TextBlock Margin="0,6,0,0" Text="XX 抽"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="卡池名称 1"/>
<shvc:ItemIcon
Width="40"
Height="40"
Margin="0,12,0,0"
HorizontalAlignment="Left"
Icon="{Binding Icon}"
Quality="QUALITY_ORANGE"/>
<TextBlock Margin="0,6,0,0" Text="XX 抽"/>
</StackPanel>
</Grid>
<Grid Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="卡池名称 1"/>
<shvc:ItemIcon
Width="40"
Height="40"
Margin="0,12,0,0"
HorizontalAlignment="Left"
Icon="{Binding Icon}"
Quality="QUALITY_ORANGE"/>
<TextBlock Margin="0,6,0,0" Text="XX 抽"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Style="{StaticResource BaseTextBlockStyle}" Text="卡池名称 1"/>
<shvc:ItemIcon
Width="40"
Height="40"
Margin="0,12,0,0"
HorizontalAlignment="Left"
Icon="{Binding Icon}"
Quality="QUALITY_ORANGE"/>
<TextBlock Margin="0,6,0,0" Text="XX 抽"/>
</StackPanel>
</Grid>
</FlipView>
</Border>
<Border Style="{StaticResource BorderCardStyle}">
<FlipView Background="{x:Null}">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<TextBlock Style="{StaticResource TitleTextBlockStyle}" Text="100/800"/>
<TextBlock Text="12.5%"/>
</StackPanel>
<TextBlock Grid.Row="1" Text="Archive Name"/>
</Grid>
</FlipView>
</Border>
<Border Style="{StaticResource BorderCardStyle}">
<TextBlock Text="实时便笺"/>
</Border>
<Border Style="{StaticResource BorderCardStyle}">
<TextBlock Text="养成计划"/>
</Border>
<Border Style="{StaticResource BorderCardStyle}">
<TextBlock Text="深渊"/>
</Border>
</cwucont:AdaptiveGridView>
</StackPanel>
<Pivot Style="{StaticResource DefaultPivotStyle}">
<PivotItem
Content="{Binding Announcement.List[0]}"

View File

@@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:clw="using:CommunityToolkit.Labs.WinUI"
xmlns:cwua="using:CommunityToolkit.WinUI.UI.Animations"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -122,32 +123,39 @@
</CommandBar>
<ScrollViewer Grid.Row="1">
<ItemsControl Margin="0,0,0,16" ItemsSource="{Binding DailyNoteEntries}">
<ItemsControl.ItemContainerTransitions>
<AddDeleteThemeTransition/>
<ContentThemeTransition/>
<EntranceThemeTransition IsStaggeringEnabled="False"/>
</ItemsControl.ItemContainerTransitions>
<cwuc:AdaptiveGridView
Margin="16,16,4,-4"
cwua:ItemsReorderAnimation.Duration="0:0:0.1"
DesiredWidth="280"
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
ItemsSource="{Binding DailyNoteEntries}"
SelectionMode="None">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
Margin="16,16,16,0"
Padding="8"
Style="{StaticResource BorderCardStyle}">
<Grid>
<Border Padding="8" Style="{StaticResource BorderCardStyle}">
<Grid Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid MinHeight="40" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="4,0,0,4"
VerticalAlignment="Center"
Style="{StaticResource BaseTextBlockStyle}"
Text="{Binding UserGameRole}"/>
Text="{Binding UserGameRole}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"/>
<StackPanel
x:Name="ButtonPanel"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal"
Visibility="Collapsed">
@@ -179,214 +187,159 @@
</StackPanel>
</Grid>
<cwuc:UniformGrid
<StackPanel
Grid.Row="1"
Margin="0,8,0,0"
ColumnSpacing="8"
Columns="5">
Spacing="12">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
Width="40"
Height="40"
VerticalAlignment="Center">
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_ItemIcon_210_256.png"/>
<ProgressRing
Width="40"
Height="40">
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_ItemIcon_210_256.png"/>
<ProgressRing
Width="40"
Height="40"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
IsIndeterminate="False"
Maximum="{Binding DailyNote.MaxResin, Mode=OneWay}"
Value="{Binding DailyNote.CurrentResin, Mode=OneWay}"/>
</Grid>
<TextBlock
Grid.Column="1"
Margin="12,0,0,4"
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding DailyNote.ResinFormatted, Mode=OneWay}"/>
Height="40"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
IsIndeterminate="False"
Maximum="{Binding DailyNote.MaxResin, Mode=OneWay}"
Value="{Binding DailyNote.CurrentResin, Mode=OneWay}"/>
</Grid>
<TextBlock
Grid.Row="1"
Margin="4,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding DailyNote.ResinRecoveryTargetTime, Mode=OneWay}"/>
<StackPanel Grid.Column="1" Margin="12,0,0,0">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{Binding DailyNote.ResinFormatted, Mode=OneWay}"/>
<TextBlock
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding DailyNote.ResinRecoveryTargetTime, Mode=OneWay}"/>
</StackPanel>
</Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
Width="40"
Height="40"
VerticalAlignment="Center">
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_ItemIcon_204.png"/>
<ProgressRing
Width="40"
Height="40">
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_ItemIcon_204.png"/>
<ProgressRing
Width="40"
Height="40"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
IsIndeterminate="False"
Maximum="{Binding DailyNote.MaxHomeCoin, Mode=OneWay}"
Value="{Binding DailyNote.CurrentHomeCoin, Mode=OneWay}"/>
</Grid>
<TextBlock
Grid.Column="1"
Margin="12,0,0,4"
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding DailyNote.HomeCoinFormatted, Mode=OneWay}"/>
Height="40"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
IsIndeterminate="False"
Maximum="{Binding DailyNote.MaxHomeCoin, Mode=OneWay}"
Value="{Binding DailyNote.CurrentHomeCoin, Mode=OneWay}"/>
</Grid>
<TextBlock
Grid.Row="1"
Margin="4,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding DailyNote.HomeCoinRecoveryTargetTimeFormatted, Mode=OneWay}"/>
<StackPanel Grid.Column="1" Margin="12,0,0,0">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{Binding DailyNote.HomeCoinFormatted, Mode=OneWay}"/>
<TextBlock
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding DailyNote.HomeCoinRecoveryTargetTimeFormatted, Mode=OneWay}"/>
</StackPanel>
</Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
Width="40"
Height="40"
VerticalAlignment="Center">
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_MarkQuest_Events_Proce.png"/>
<ProgressRing
Width="40"
Height="40">
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_MarkQuest_Events_Proce.png"/>
<ProgressRing
Width="40"
Height="40"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
IsIndeterminate="False"
Maximum="{Binding DailyNote.TotalTaskNum, Mode=OneWay}"
Value="{Binding DailyNote.FinishedTaskNum, Mode=OneWay}"/>
</Grid>
<TextBlock
Grid.Column="1"
Margin="12,0,0,4"
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding DailyNote.TaskFormatted, Mode=OneWay}"/>
Height="40"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
IsIndeterminate="False"
Maximum="{Binding DailyNote.TotalTaskNum, Mode=OneWay}"
Value="{Binding DailyNote.FinishedTaskNum, Mode=OneWay}"/>
</Grid>
<TextBlock
Grid.Row="1"
Margin="4,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding DailyNote.ExtraTaskRewardDescription, Mode=OneWay}"/>
<StackPanel Grid.Column="1" Margin="12,0,0,0">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{Binding DailyNote.TaskFormatted, Mode=OneWay}"/>
<TextBlock
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding DailyNote.ExtraTaskRewardDescription, Mode=OneWay}"/>
</StackPanel>
</Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
Width="40"
Height="40"
VerticalAlignment="Center">
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_MarkTower.png"/>
<ProgressRing
Width="40"
Height="40">
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_MarkTower.png"/>
<ProgressRing
Width="40"
Height="40"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
IsIndeterminate="False"
Maximum="{Binding DailyNote.ResinDiscountNumLimit, Mode=OneWay}"
Value="{Binding DailyNote.ResinDiscountUsedNum, Mode=OneWay}"/>
</Grid>
<TextBlock
Grid.Column="1"
Margin="12,0,0,4"
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding DailyNote.ResinDiscountFormatted, Mode=OneWay}"/>
Height="40"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
IsIndeterminate="False"
Maximum="{Binding DailyNote.ResinDiscountNumLimit, Mode=OneWay}"
Value="{Binding DailyNote.ResinDiscountUsedNum, Mode=OneWay}"/>
</Grid>
<TextBlock
Grid.Row="1"
Margin="4,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageDailyNoteResinDiscountUsed}"/>
<StackPanel Grid.Column="1" Margin="12,0,0,0">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{Binding DailyNote.ResinDiscountFormatted, Mode=OneWay}"/>
<TextBlock
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageDailyNoteResinDiscountUsed}"/>
</StackPanel>
</Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
Width="40"
Height="40">
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_ItemIcon_220021.png"/>
<ProgressRing
Width="40"
Height="40">
<Image Width="32" Source="ms-appx:///Resource/Icon/UI_ItemIcon_220021.png"/>
<ProgressRing
Width="40"
Height="40"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
IsIndeterminate="False"
Maximum="604800"
Value="{Binding DailyNote.Transformer.RecoveryTime.TotalSeconds, Mode=OneWay}"/>
</Grid>
<TextBlock
Grid.Column="1"
Margin="12,0,0,4"
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding DailyNote.Transformer.RecoveryTime.ReachedFormatted, Mode=OneWay}"/>
Height="40"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
IsIndeterminate="False"
Maximum="604800"
Value="{Binding DailyNote.Transformer.RecoveryTime.TotalSeconds, Mode=OneWay}"/>
</Grid>
<StackPanel Grid.Column="1" Margin="12,0,0,0">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{Binding DailyNote.Transformer.RecoveryTime.ReachedFormatted, Mode=OneWay}"/>
<TextBlock
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding DailyNote.Transformer.RecoveryTime.TimeFormatted, Mode=OneWay}"/>
</StackPanel>
<TextBlock
Grid.Row="1"
Margin="4,4,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding DailyNote.Transformer.RecoveryTime.TimeFormatted, Mode=OneWay}"/>
</Grid>
</cwuc:UniformGrid>
</StackPanel>
<MenuFlyoutSeparator Grid.Row="2" Margin="8,16,8,0"/>
<ItemsControl
Grid.Row="2"
Grid.Row="3"
Margin="0,16,0,0"
ItemsSource="{Binding DailyNote.Expeditions, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid ColumnSpacing="8" Columns="5"/>
<cwuc:UniformGrid
ColumnSpacing="8"
Columns="2"
RowSpacing="8"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
@@ -463,8 +416,7 @@
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</cwuc:AdaptiveGridView>
</ScrollViewer>
</Grid>
</shc:ScopedPage>

View File

@@ -253,6 +253,16 @@
DataContext="{Binding GameResource.PreDownloadGame.Latest, Mode=OneWay}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourcePreDownloadHeader}"
Visibility="{Binding FallbackValue={StaticResource VisibilityCollapsed}, Converter={StaticResource EmptyObjectToVisibilityConverter}}"/>
<ItemsControl Margin="0,0,0,0" ItemsSource="{Binding GameResource.PreDownloadGame.Diffs, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<shvc:LaunchGameResourceExpander
Margin="16,16,16,0"
DataContext="{Binding Mode=OneWay}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceDiffHeader}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<shvc:LaunchGameResourceExpander
Margin="16,16,16,0"
DataContext="{Binding GameResource.Game.Latest, Mode=OneWay}"

View File

@@ -32,7 +32,11 @@
Margin="0,0,0,8"
Style="{StaticResource BodyTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewWelcomeBody}"/>
<ItemsControl Margin="0,0,0,32" ItemsSource="{Binding DownloadSummaries}">
<ItemsControl
Width="256"
Margin="0,0,0,32"
HorizontalAlignment="Left"
ItemsSource="{Binding DownloadSummaries}">
<ItemsControl.ItemContainerTransitions>
<TransitionCollection>
<AddDeleteThemeTransition/>
@@ -47,7 +51,6 @@
<StackPanel Margin="8">
<TextBlock Text="{Binding DisplayName}"/>
<ProgressBar
Width="240"
Margin="0,4,0,0"
Maximum="1"
Value="{Binding ProgressValue}"/>

View File

@@ -3,6 +3,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Service.Navigation;
namespace Snap.Hutao.ViewModel.Abstraction;

View File

@@ -0,0 +1,60 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Service.Navigation;
namespace Snap.Hutao.ViewModel.Abstraction;
/// <summary>
/// 简化的视图模型抽象类
/// </summary>
/// <typeparam name="TPage">页面类型</typeparam>
internal abstract class ViewModelSlim<TPage> : ObservableObject
where TPage : Microsoft.UI.Xaml.Controls.Page
{
/// <summary>
/// 构造一个新的简化的视图模型抽象类
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public ViewModelSlim(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
NavigateCommand = new RelayCommand(Navigate);
}
/// <summary>
/// 打开页面命令
/// </summary>
public ICommand OpenUICommand { get; }
/// <summary>
/// 导航命令
/// </summary>
public ICommand NavigateCommand { get; }
/// <summary>
/// 服务提供器
/// </summary>
protected IServiceProvider ServiceProvider { get; }
/// <summary>
/// 打开界面执行
/// </summary>
/// <returns>任务</returns>
protected virtual Task OpenUIAsync()
{
return Task.CompletedTask;
}
/// <summary>
/// 导航到指定的页面类型
/// </summary>
protected virtual void Navigate()
{
ServiceProvider.GetRequiredService<INavigationService>().Navigate<TPage>(INavigationAwaiter.Default, true);
}
}

View File

@@ -20,20 +20,23 @@ internal sealed class AnnouncementViewModel : Abstraction.ViewModel
/// <summary>
/// 构造一个公告视图模型
/// </summary>
/// <param name="announcementService">公告服务</param>
public AnnouncementViewModel(IAnnouncementService announcementService)
/// <param name="serviceProvider">服务提供器</param>
public AnnouncementViewModel(IServiceProvider serviceProvider)
{
this.announcementService = announcementService;
announcementService = serviceProvider.GetRequiredService<IAnnouncementService>();
LaunchGameViewModelSlim = serviceProvider.GetRequiredService<Game.LaunchGameViewModelSlim>();
}
/// <summary>
/// 公告
/// </summary>
public AnnouncementWrapper? Announcement
{
get => announcement;
set => SetProperty(ref announcement, value);
}
public AnnouncementWrapper? Announcement { get => announcement; set => SetProperty(ref announcement, value); }
/// <summary>
/// 启动游戏视图模型
/// </summary>
public Game.LaunchGameViewModelSlim LaunchGameViewModelSlim { get; }
/// <inheritdoc/>
protected override async Task OpenUIAsync()

View File

@@ -332,7 +332,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel
/// 需要从主线程调用
/// </summary>
/// <param name="archive">存档</param>
/// <param name="forceUpdate">强制刷新即使Uid相同也刷新该Uid的记录</param>
/// <param name="forceUpdate">强制刷新即使Uid相同也刷新该 Uid 的记录</param>
private void SetSelectedArchiveAndUpdateStatistics(GachaArchive? archive, bool forceUpdate = false)
{
bool changed = false;

View File

@@ -0,0 +1,32 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.ViewModel.GachaLog;
/// <summary>
/// 简化的祈愿记录视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal sealed class GachaLogViewModelSlim : Abstraction.ViewModelSlim<View.Page.GachaLogPage>
{
private List<GachaStatisticsSlim>? statisticsList;
/// <summary>
/// 构造一个新的简化的祈愿记录视图模型
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public GachaLogViewModelSlim(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
/// <summary>
/// 统计列表
/// </summary>
public List<GachaStatisticsSlim>? StatisticsList { get => statisticsList; set => SetProperty(ref statisticsList, value); }
/// <inheritdoc/>
protected override Task OpenUIAsync()
{
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.ViewModel.GachaLog;
/// <summary>
/// 简化的祈愿统计
/// </summary>
internal sealed class GachaStatisticsSlim
{
/// <summary>
/// Uid
/// </summary>
public string Uid { get; set; } = default!;
/// <summary>
/// 角色活动
/// </summary>
public TypedWishSummary AvatarWish { get; set; } = default!;
/// <summary>
/// 神铸赋形
/// </summary>
public TypedWishSummary WeaponWish { get; set; } = default!;
/// <summary>
/// 奔行世间
/// </summary>
public TypedWishSummary StandardWish { get; set; } = default!;
}

View File

@@ -26,24 +26,24 @@ internal sealed class TypedWishSummary : Wish
}
/// <summary>
/// 上个五星抽数
/// 上个五星抽数
/// </summary>
public int LastOrangePull { get; set; }
/// <summary>
/// 上个四星抽数
/// 上个四星抽数
/// </summary>
public int LastPurplePull { get; set; }
/// <summary>
/// 五星保底阈值
/// </summary>
public int GuarenteeOrangeThreshold { get; set; }
public int GuaranteeOrangeThreshold { get; set; }
/// <summary>
/// 四星保底阈值
/// </summary>
public int GuarenteePurpleThreshold { get; set; }
public int GuaranteePurpleThreshold { get; set; }
/// <summary>
/// 五星格式化字符串

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.ViewModel.GachaLog;
/// <summary>
/// 简化的类型化的祈愿概览
/// </summary>
internal sealed class TypedWishSummarySlim
{
/// <summary>
/// 卡池名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 距上个五星抽数
/// </summary>
public int LastOrangePull { get; set; }
/// <summary>
/// 距上个四星抽数
/// </summary>
public int LastPurplePull { get; set; }
/// <summary>
/// 五星保底阈值
/// </summary>
public int GuaranteeOrangeThreshold { get; set; }
/// <summary>
/// 四星保底阈值
/// </summary>
public int GuaranteePurpleThreshold { get; set; }
}

View File

@@ -149,7 +149,9 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel
/// <inheritdoc/>
protected override async Task OpenUIAsync()
{
if (File.Exists(serviceProvider.GetRequiredService<AppOptions>().GamePath))
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
if (File.Exists(AppOptions.GamePath))
{
try
{
@@ -162,7 +164,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel
}
else
{
serviceProvider.GetRequiredService<IInfoBarService>().Warning(SH.ViewModelLaunchGameMultiChannelReadFail);
infoBarService.Warning(SH.ViewModelLaunchGameMultiChannelReadFail);
}
ObservableCollection<GameAccount> accounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(false);
@@ -186,7 +188,7 @@ internal sealed class LaunchGameViewModel : Abstraction.ViewModel
}
else
{
serviceProvider.GetRequiredService<IInfoBarService>().Warning(SH.ViewModelLaunchGamePathInvalid);
infoBarService.Warning(SH.ViewModelLaunchGamePathInvalid);
await ThreadHelper.SwitchToMainThreadAsync();
await serviceProvider.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.SettingPage>(INavigationAwaiter.Default, true)

View File

@@ -0,0 +1,83 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Game;
using System.Collections.ObjectModel;
namespace Snap.Hutao.ViewModel.Game;
/// <summary>
/// 简化的启动游戏视图模型
/// </summary>
[Injection(InjectAs.Scoped)]
internal sealed class LaunchGameViewModelSlim : Abstraction.ViewModelSlim<View.Page.LaunchGamePage>
{
private readonly IGameService gameService;
private ObservableCollection<GameAccount>? gameAccounts;
private GameAccount? selectedGameAccount;
/// <summary>
/// 构造一个新的简化的启动游戏视图模型
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public LaunchGameViewModelSlim(IServiceProvider serviceProvider)
: base(serviceProvider)
{
gameService = serviceProvider.GetRequiredService<IGameService>();
LaunchCommand = new AsyncRelayCommand(LaunchAsync);
}
/// <summary>
/// 游戏账号集合
/// </summary>
public ObservableCollection<GameAccount>? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); }
/// <summary>
/// 选中的账号
/// </summary>
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
/// <summary>
/// 启动游戏命令
/// </summary>
public ICommand LaunchCommand { get; }
/// <inheritdoc/>
protected override async Task OpenUIAsync()
{
ObservableCollection<GameAccount> accounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
GameAccounts = accounts;
// Try set to the current account.
SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
}
private async Task LaunchAsync()
{
IInfoBarService infoBarService = ServiceProvider.GetRequiredService<IInfoBarService>();
try
{
if (SelectedGameAccount != null)
{
if (!gameService.SetGameAccount(SelectedGameAccount))
{
infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail);
return;
}
}
await gameService.LaunchAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
infoBarService.Error(ex);
}
}
}

View File

@@ -115,6 +115,11 @@ internal sealed class WelcomeViewModel : ObservableObject
downloadSummaries.TryAdd("AvatarIcon", new(serviceProvider, "AvatarIcon"));
}
if (StaticResource.IsContractUnfulfilled(SettingKeys.StaticResourceV5Contract))
{
downloadSummaries.TryAdd("MonsterIcon", new(serviceProvider, "MonsterIcon"));
}
return downloadSummaries.Select(x => x.Value);
}

View File

@@ -115,6 +115,7 @@ internal class WikiWeaponViewModel : Abstraction.ViewModel
.Where(weapon => !skippedWeapons.Contains(weapon.Id))
.OrderByDescending(weapon => weapon.RankLevel)
.ThenBy(weapon => weapon.WeaponType)
.ThenByDescending(weapon => weapon.Id.Value)
.ToList();
await CombineWithWeaponCollocationsAsync(sorted).ConfigureAwait(false);