Compare commits

...

6 Commits

Author SHA1 Message Date
DismissedLight
bf5fcb70f8 Merge branch 'main' of https://github.com/DGP-Studio/Snap.Hutao 2022-10-21 16:40:45 +08:00
DismissedLight
fda642b72f hutao api v2 page 2022-10-21 16:40:10 +08:00
Masterain
fa650a95c5 Update PublishDistribution.yml 2022-10-18 12:23:53 -07:00
Masterain
02fae69d1e Create PublishDistribution.yml 2022-10-18 12:05:43 -07:00
DismissedLight
76800de6ee separate primary and secondary properties 2022-10-16 21:53:40 +08:00
DismissedLight
72b660119f support homa api 2022-10-16 13:10:06 +08:00
97 changed files with 2288 additions and 224 deletions

View File

@@ -0,0 +1,39 @@
name: PublishDistribution
on:
release:
types: [published]
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
Publish:
runs-on: ubuntu-latest
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Checkout Repo
uses: actions/checkout@v3
# Download Publish.zip
- name: Download Release
uses: robinraju/release-downloader@v1.5
with:
repository: "DGP-Studio/Snap.Hutao"
latest: true
fileName: "*.zip"
out-file-path: ./release-download
# Upload to OD21 (Testing)
- name: Upload OD21
env:
RCCONF: ${{ secrets.RCCONF }}
run: |
curl https://rclone.org/install.sh | sudo bash
mkdir -p ~/.config/rclone/
cat << EOF > ~/.config/rclone/rclone.conf
$RCCONF
EOF
rclone copy ./release-download/* dgpODCN:/snaphutao/Releases/

View File

@@ -9,6 +9,7 @@ using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.AppCenter;
using Snap.Hutao.Service.Metadata;
using System.Diagnostics;
using Windows.Storage;
@@ -27,13 +28,14 @@ public partial class App : Application
/// Initializes the singleton application object.
/// </summary>
/// <param name="logger">日志器</param>
public App(ILogger<App> logger)
/// <param name="appCenter">App Center</param>
public App(ILogger<App> logger, AppCenter appCenter)
{
// load app resource
InitializeComponent();
this.logger = logger;
_ = new ExceptionRecorder(this, logger);
_ = new ExceptionRecorder(this, logger, appCenter);
}
/// <inheritdoc/>
@@ -57,6 +59,8 @@ public partial class App : Application
.ImplictAs<IMetadataInitializer>()?
.InitializeInternalAsync()
.SafeForget(logger);
Ioc.Default.GetRequiredService<AppCenter>().Initialize();
}
else
{

View File

@@ -24,7 +24,7 @@ internal class I18NExtension : MarkupExtension
static I18NExtension()
{
string currentName = CultureInfo.CurrentUICulture.Name;
Type? languageType = ((IDictionary<string, Type>)TranslationMap).GetValueOrDefault2(currentName, typeof(LanguagezhCN));
Type? languageType = TranslationMap.GetValueOrDefault2(currentName, typeof(LanguagezhCN));
Translation = (ITranslation)Activator.CreateInstance(languageType!)!;
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.Control.Markup;

View File

@@ -4,7 +4,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Control.Panel;
@@ -28,8 +27,8 @@ public sealed partial class PanelSelector : UserControl
/// </summary>
public string Current
{
get { return (string)GetValue(CurrentProperty); }
set { SetValue(CurrentProperty, value); }
get => (string)GetValue(CurrentProperty);
set => SetValue(CurrentProperty, value);
}
private void SplitButtonLoaded(object sender, RoutedEventArgs e)

View File

@@ -11,6 +11,9 @@ namespace Snap.Hutao.Control;
/// <summary>
/// 表示支持取消加载的异步页面
/// 在被导航到其他页面前触发取消异步通知
/// <para/>
/// InitializeWith{T}();
/// InitializeComponent();
/// </summary>
public class ScopedPage : Page
{

View File

@@ -1,7 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Extension;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using Windows.ApplicationModel;
@@ -12,6 +15,9 @@ namespace Snap.Hutao.Core;
/// </summary>
internal static class CoreEnvironment
{
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
private const string MachineGuidValue = "MachineGuid";
// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// <summary>
@@ -49,6 +55,11 @@ internal static class CoreEnvironment
/// </summary>
public static readonly string HoyolabDeviceId;
/// <summary>
/// AppCenter 设备Id
/// </summary>
public static readonly string AppCenterDeviceId;
/// <summary>
/// 默认的Json序列化选项
/// </summary>
@@ -67,5 +78,15 @@ internal static class CoreEnvironment
// simply assign a random guid
HoyolabDeviceId = Guid.NewGuid().ToString();
AppCenterDeviceId = GetUniqueUserID();
}
private static string GetUniqueUserID()
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
byte[] bytes = Encoding.UTF8.GetBytes($"{userName}{machineGuid}");
byte[] hash = MD5.Create().ComputeHash(bytes);
return System.Convert.ToHexString(hash);
}
}

View File

@@ -36,6 +36,7 @@ internal static partial class IocHttpClientConfiguration
{
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);

View File

@@ -2,10 +2,8 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.Logging;
using System.Diagnostics;
using System.IO;
using Snap.Hutao.Service.AppCenter;
namespace Snap.Hutao.Core.Exception;
@@ -15,15 +13,18 @@ namespace Snap.Hutao.Core.Exception;
internal class ExceptionRecorder
{
private readonly ILogger logger;
private readonly AppCenter appCenter;
/// <summary>
/// 构造一个新的异常记录器
/// </summary>
/// <param name="application">应用程序</param>
/// <param name="logger">日志器</param>
public ExceptionRecorder(Application application, ILogger logger)
/// <param name="appCenter">App Center</param>
public ExceptionRecorder(Application application, ILogger logger, AppCenter appCenter)
{
this.logger = logger;
this.appCenter = appCenter;
application.UnhandledException += OnAppUnhandledException;
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
@@ -31,9 +32,7 @@ internal class ExceptionRecorder
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
// string fileName = $"ex-{DateTimeOffset.Now:yyyyMMddHHmmssffff}.txt";
// File.WriteAllText(Path.Combine(path, fileName), $"{e.Exception}\r\n{e.Exception.StackTrace}");
appCenter.TrackCrash(e.Exception);
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.IO.Ini;
/// <summary>
/// Ini 注释
/// </summary>
internal class IniComment : IniElement
{
/// <summary>
/// 构造一个新的 Ini 注释
/// </summary>
/// <param name="comment">注释</param>
public IniComment(string comment)
{
Comment = comment;
}
/// <summary>
/// 注释
/// </summary>
public string Comment { get; set; }
/// <inheritdoc/>
public override string ToString()
{
return $";{Comment}";
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.IO.Ini;
/// <summary>
/// Ini 元素
/// </summary>
internal abstract class IniElement
{
/// <summary>
/// 将当前元素转换到等价的字符串表示
/// </summary>
/// <returns>字符串</returns>
public new abstract string ToString();
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.IO.Ini;
/// <summary>
/// Ini 参数
/// </summary>
internal class IniParameter : IniElement
{
/// <summary>
/// Ini 参数
/// </summary>
/// <param name="key">键</param>
/// <param name="value">值</param>
public IniParameter(string key, string value)
{
Key = key;
Value = value;
}
/// <summary>
/// 键
/// </summary>
public string Key { get; set; }
/// <summary>
/// 值
/// </summary>
public string Value { get; set; }
/// <inheritdoc/>
public override string ToString()
{
return $"{Key}={Value}";
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.IO.Ini;
/// <summary>
/// Ini 节
/// </summary>
internal class IniSection : IniElement
{
/// <summary>
/// 构造一个新的Ini 节
/// </summary>
/// <param name="name">名称</param>
/// <param name="elements">元素</param>
public IniSection(string name)
{
Name = name;
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <inheritdoc/>
public override string ToString()
{
return $"[{Name}]";
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO.Ini;
/// <summary>
/// Ini 序列化器
/// </summary>
internal static class IniSerializer
{
/// <summary>
/// 异步反序列化
/// </summary>
/// <param name="fileStream">文件流</param>
/// <returns>Ini 元素集合</returns>
public static IEnumerable<IniElement> Deserialize(FileStream fileStream)
{
using (TextReader reader = new StreamReader(fileStream))
{
while (reader.ReadLine() is string line)
{
if (line.Length > 0)
{
if (line[0] == '[')
{
yield return new IniSection(line[1..^1]);
}
if (line[0] == ';')
{
yield return new IniComment(line[1..]);
}
if (line.IndexOf('=') > 0)
{
string[] parameters = line.Split('=', 2);
yield return new IniParameter(parameters[0], parameters[1]);
}
}
continue;
}
}
}
}

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Service.AppCenter;
namespace Snap.Hutao.Factory;
@@ -11,15 +12,18 @@ namespace Snap.Hutao.Factory;
[Injection(InjectAs.Transient, typeof(IAsyncRelayCommandFactory))]
internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
{
private readonly ILogger logger;
private readonly ILogger<AsyncRelayCommandFactory> logger;
private readonly AppCenter appCenter;
/// <summary>
/// 构造一个新的异步命令工厂
/// </summary>
/// <param name="logger">日志器</param>
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger)
/// <param name="appCenter">App Center</param>
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger, AppCenter appCenter)
{
this.logger = logger;
this.appCenter = appCenter;
}
/// <inheritdoc/>
@@ -94,6 +98,7 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
{
Exception baseException = exception.GetBaseException();
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(AsyncRelayCommand));
appCenter.TrackError(exception);
}
}
}

View File

@@ -11,8 +11,19 @@ public class Reliquary : EquipBase
/// <summary>
/// 副属性列表
/// </summary>
[Obsolete]
public List<ReliquarySubProperty> SubProperties { get; set; } = default!;
/// <summary>
/// 初始词条
/// </summary>
public List<ReliquarySubProperty> PrimarySubProperties { get; set; } = default!;
/// <summary>
/// 强化词条
/// </summary>
public List<ReliquarySubProperty> SecondarySubProperties { get; set; } = default!;
/// <summary>
/// 评分
/// </summary>

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Converter;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 角色
/// </summary>
internal class ComplexAvatar
{
/// <summary>
/// 构造一个胡桃数据库角色
/// </summary>
/// <param name="avatar">元数据角色</param>
/// <param name="rate">率</param>
public ComplexAvatar(Avatar avatar, double rate)
{
Name = avatar.Name;
Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
Quality = avatar.Quality;
Rate = $"{rate:P3}";
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
public Uri Icon { get; set; } = default!;
/// <summary>
/// 星级
/// </summary>
public ItemQuality Quality { get; set; }
/// <summary>
/// 比率
/// </summary>
public string Rate { get; set; } = default!;
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Converter;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 角色搭配
/// </summary>
internal class ComplexAvatarCollocation : ComplexAvatar
{
/// <summary>
/// 构造一个新的角色搭配
/// </summary>
/// <param name="avatar">角色</param>
/// <param name="rate">比率</param>
public ComplexAvatarCollocation(Avatar avatar)
: base(avatar, 0)
{
}
/// <summary>
/// 角色
/// </summary>
public List<ComplexAvatar> Avatars { get; set; } = default!;
/// <summary>
/// 武器
/// </summary>
public List<ComplexWeapon> Weapons { get; set; } = default!;
/// <summary>
/// 圣遗物套装
/// </summary>
public List<ComplexReliquarySet> ReliquarySets { get; set; } = default!;
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Avatar;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 角色命座信息
/// </summary>
internal class ComplexAvatarConstellationInfo : ComplexAvatar
{
/// <summary>
/// 构造一个新的角色命座信息
/// </summary>
/// <param name="avatar">角色</param>
/// <param name="rate">持有率</param>
/// <param name="rates">命座比率</param>
public ComplexAvatarConstellationInfo(Avatar avatar, double rate, IEnumerable<double> rates)
: base(avatar, rate)
{
Rates = rates.Select(r => $"{r:P3}").ToList();
}
/// <summary>
/// 命座比率
/// </summary>
public List<string> Rates { get; set; }
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 角色榜
/// </summary>
internal class ComplexAvatarRank
{
/// <summary>
/// 层数
/// </summary>
public string Floor { get; set; } = default!;
/// <summary>
/// 排行信息
/// </summary>
public List<ComplexAvatar> Avatars { get; set; } = default!;
}

View File

@@ -0,0 +1,66 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Web.Hutao.Model;
using System.Text;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 圣遗物套装
/// </summary>
internal class ComplexReliquarySet
{
/// <summary>
/// 构造一个新的胡桃数据库圣遗物套装
/// </summary>
/// <param name="reliquarySetRate">圣遗物套装率</param>
/// <param name="idReliquarySetMap">圣遗物套装映射</param>
public ComplexReliquarySet(ItemRate<ReliquarySets, double> reliquarySetRate, Dictionary<int, Metadata.Reliquary.ReliquarySet> idReliquarySetMap)
{
ReliquarySets sets = reliquarySetRate.Item;
if (sets.Count >= 1)
{
StringBuilder setStringBuilder = new();
List<Uri> icons = new();
foreach (ReliquarySet set in sets)
{
Metadata.Reliquary.ReliquarySet metaSet = idReliquarySetMap[set.EquipAffixId / 10];
if (setStringBuilder.Length != 0)
{
setStringBuilder.Append(Environment.NewLine);
}
setStringBuilder.Append(set.Count).Append('×').Append(metaSet.Name);
icons.Add(RelicIconConverter.IconNameToUri(metaSet.Icon));
}
Name = setStringBuilder.ToString();
Icons = icons;
}
else
{
Name = "无圣遗物";
}
Rate = $"{reliquarySetRate.Rate:P3}";
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
public List<Uri> Icons { get; set; } = default!;
/// <summary>
/// 比率
/// </summary>
public string Rate { get; set; } = default!;
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 队伍排行
/// </summary>
internal class ComplexTeamRank
{
/// <summary>
/// 构造一个新的队伍排行
/// </summary>
/// <param name="teamRank">队伍排行</param>
/// <param name="idAvatarMap">映射</param>
public ComplexTeamRank(TeamAppearance teamRank, Dictionary<int, Avatar> idAvatarMap)
{
Floor = $"第 {teamRank.Floor} 层";
Up = teamRank.Up.Select(teamRate => new Team(teamRate, idAvatarMap)).ToList();
Down = teamRank.Down.Select(teamRate => new Team(teamRate, idAvatarMap)).ToList();
}
/// <summary>
/// 层数
/// </summary>
public string Floor { get; set; } = default!;
/// <summary>
/// 上半阵容
/// </summary>
public List<Team> Up { get; set; } = default!;
/// <summary>
/// 下半阵容
/// </summary>
public List<Team> Down { get; set; } = default!;
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Weapon;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 胡桃数据库武器
/// </summary>
internal class ComplexWeapon
{
/// <summary>
/// 构造一个胡桃数据库武器
/// </summary>
/// <param name="weapon">元数据武器</param>
/// <param name="rate">率</param>
public ComplexWeapon(Weapon weapon, double rate)
{
Name = weapon.Name;
Icon = EquipIconConverter.IconNameToUri(weapon.Icon);
Quality = weapon.Quality;
Rate = $"{rate:P3}";
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
public Uri Icon { get; set; } = default!;
/// <summary>
/// 星级
/// </summary>
public ItemQuality Quality { get; set; }
/// <summary>
/// 比率
/// </summary>
public string Rate { get; set; } = default!;
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Model.Binding.Hutao;
/// <summary>
/// 队伍
/// </summary>
internal class Team : List<ComplexAvatar>
{
/// <summary>
/// 构造一个新的队伍
/// </summary>
/// <param name="team">队伍</param>
/// <param name="idAvatarMap">映射</param>
public Team(ItemRate<string, int> team, Dictionary<int, Avatar> idAvatarMap)
: base(4)
{
IEnumerable<int> ids = team.Item.Split(',').Select(i => int.Parse(i));
foreach (int id in ids)
{
Add(new(idAvatarMap[id], 0));
}
Rate = $"上场 {team.Rate} 次";
}
/// <summary>
/// 上场次数
/// </summary>
public string Rate { get; set; }
}

View File

@@ -27,11 +27,13 @@ public class UIGFInfo
/// 导出的时间戳
/// </summary>
[JsonPropertyName("export_timestamp")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long? ExportTimestamp { get; set; }
/// <summary>
/// 导出时间
/// </summary>
[JsonIgnore]
public DateTimeOffset ExportDateTime
{
get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);

View File

@@ -11,10 +11,13 @@ public static class AvatarIds
{
public const int Ayaka = 10000002;
public const int Qin = 10000003;
public const int Lisa = 10000006;
public const int Barbara = 10000014;
public const int Kaeya = 10000015;
public const int Diluc = 10000016;
public const int Razor = 10000020;
public const int Ambor = 10000021;
public const int Venti = 10000022;
@@ -23,6 +26,7 @@ public static class AvatarIds
public const int Xingqiu = 10000025;
public const int Xiao = 10000026;
public const int Ningguang = 10000027;
public const int Klee = 10000029;
public const int Zhongli = 10000030;
public const int Fischl = 10000031;
@@ -34,6 +38,7 @@ public static class AvatarIds
public const int Ganyu = 10000037;
public const int Albedo = 10000038;
public const int Diona = 10000039;
public const int Mona = 10000041;
public const int Keqing = 10000042;
public const int Sucrose = 10000043;
@@ -54,6 +59,7 @@ public static class AvatarIds
public const int Yae = 10000058;
public const int Heizou = 10000059;
public const int Yelan = 10000060;
public const int Aloy = 10000062;
public const int Shenhe = 10000063;
public const int Yunjin = 10000064;
@@ -62,6 +68,9 @@ public static class AvatarIds
public const int Collei = 10000067;
public const int Dori = 10000068;
public const int Tighnari = 10000069;
public const int Nilou = 10000070;
public const int Cyno = 10000071;
public const int Candace = 10000072;
public const int Nahida = 10000073;
public const int Layla = 10000074;
}

View File

@@ -11,15 +11,30 @@ public class ReliquarySet
/// <summary>
/// 套装Id
/// </summary>
public int SetId { get; set; } = default!;
public int SetId { get; set; }
/// <summary>
/// 装备被动Id
/// </summary>
public int EquipAffixId { get; set; }
/// <summary>
/// 套装名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 套装图标
/// </summary>
public string Icon { get; set; } = default!;
/// <summary>
/// 需要的数量
/// </summary>
public IEnumerable<int> NeedNumber { get; set; } = default!;
public List<int> NeedNumber { get; set; } = default!;
/// <summary>
/// 描述
/// </summary>
public IEnumerable<string> Descriptions { get; set; } = default!;
public List<string> Descriptions { get; set; } = default!;
}

View File

@@ -61,7 +61,10 @@ public class Weapon : IStatisticsItemSource, ISummaryItemSource, INameQuality
/// <inheritdoc/>
[JsonIgnore]
public ItemQuality Quality => RankLevel;
public ItemQuality Quality
{
get => RankLevel;
}
/// <summary>
/// 转换为基础物品

View File

@@ -9,7 +9,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.1.9.0" />
Version="1.1.13.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -20,6 +20,7 @@ public static partial class Program
/// <summary>
/// 主线程队列
/// </summary>
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[SuppressMessage("", "SA1401")]
internal static volatile DispatcherQueue? DispatcherQueue;

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,48 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Service.Abstraction;
/// <summary>
/// 胡桃 API 服务
/// </summary>
internal interface IHutaoService
{
/// <summary>
/// 异步获取角色上场率
/// </summary>
/// <returns>角色上场率</returns>
ValueTask<List<AvatarAppearanceRank>> GetAvatarAppearanceRanksAsync();
/// <summary>
/// 异步获取角色搭配
/// </summary>
/// <returns>角色搭配</returns>
ValueTask<List<AvatarCollocation>> GetAvatarCollocationsAsync();
/// <summary>
/// 异步获取角色持有率信息
/// </summary>
/// <returns>角色持有率信息</returns>
ValueTask<List<AvatarConstellationInfo>> GetAvatarConstellationInfosAsync();
/// <summary>
/// 异步获取角色使用率
/// </summary>
/// <returns>角色使用率</returns>
ValueTask<List<AvatarUsageRank>> GetAvatarUsageRanksAsync();
/// <summary>
/// 异步获取统计数据
/// </summary>
/// <returns>统计数据</returns>
ValueTask<Overview?> GetOverviewAsync();
/// <summary>
/// 异步获取队伍上场
/// </summary>
/// <returns>队伍上场</returns>
ValueTask<List<TeamAppearance>> GetTeamAppearancesAsync();
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Service.AppCenter.Model;
using Snap.Hutao.Service.AppCenter.Model.Log;
using Snap.Hutao.Web.Hoyolab;
using System.Net.Http;
namespace Snap.Hutao.Service.AppCenter;
[SuppressMessage("", "SA1600")]
[Injection(InjectAs.Singleton)]
public sealed class AppCenter : IDisposable
{
private const string AppSecret = "de5bfc48-17fc-47ee-8e7e-dee7dc59d554";
private const string API = "https://in.appcenter.ms/logs?api-version=1.0.0";
private readonly TaskCompletionSource uploadTaskCompletionSource = new();
private readonly CancellationTokenSource uploadTaskCancllationTokenSource = new();
private readonly HttpClient httpClient;
private readonly List<Log> queue;
private readonly Device deviceInfo;
private readonly JsonSerializerOptions options;
private Guid sessionID;
public AppCenter()
{
options = new(CoreEnvironment.JsonOptions);
options.Converters.Add(new LogConverter());
httpClient = new() { DefaultRequestHeaders = { { "Install-ID", CoreEnvironment.AppCenterDeviceId }, { "App-Secret", AppSecret } } };
queue = new List<Log>();
deviceInfo = new Device();
Task.Run(async () =>
{
while (!uploadTaskCancllationTokenSource.Token.IsCancellationRequested)
{
await UploadAsync().ConfigureAwait(false);
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
}
uploadTaskCompletionSource.TrySetResult();
}).SafeForget();
}
public async Task UploadAsync()
{
if (queue.Count == 0)
{
return;
}
string? uploadStatus = null;
do
{
queue.ForEach(log => log.Status = LogStatus.Uploading);
LogContainer container = new(queue);
LogUploadResult? response = await httpClient
.TryCatchPostAsJsonAsync<LogContainer, LogUploadResult>(API, container, options)
.ConfigureAwait(false);
uploadStatus = response?.Status;
}
while (uploadStatus != "Success");
queue.RemoveAll(log => log.Status == LogStatus.Uploading);
}
public void Initialize()
{
sessionID = Guid.NewGuid();
queue.Add(new StartServiceLog("Analytics", "Crashes").Initialize(sessionID, deviceInfo));
queue.Add(new StartSessionLog().Initialize(sessionID, deviceInfo).Initialize(sessionID, deviceInfo));
}
public void TrackCrash(Exception exception, bool isFatal = true)
{
queue.Add(new ManagedErrorLog(exception, isFatal).Initialize(sessionID, deviceInfo));
}
public void TrackError(Exception exception)
{
queue.Add(new HandledErrorLog(exception).Initialize(sessionID, deviceInfo));
}
[SuppressMessage("", "VSTHRD002")]
public void Dispose()
{
uploadTaskCancllationTokenSource.Cancel();
uploadTaskCompletionSource.Task.GetAwaiter().GetResult();
}
}

View File

@@ -0,0 +1,64 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Windowing;
using Microsoft.Win32;
using Windows.Graphics;
namespace Snap.Hutao.Service.AppCenter;
/// <summary>
/// 设备帮助类
/// </summary>
[SuppressMessage("", "SA1600")]
public static class DeviceHelper
{
private static readonly RegistryKey? BiosKey = Registry.LocalMachine.OpenSubKey("HARDWARE\\DESCRIPTION\\System\\BIOS");
private static readonly RegistryKey? GeoKey = Registry.CurrentUser.OpenSubKey("Control Panel\\International\\Geo");
private static readonly RegistryKey? CurrentVersionKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion");
public static string? GetOem()
{
string? oem = BiosKey?.GetValue("SystemManufacturer") as string;
return oem == "System manufacturer" ? null : oem;
}
public static string? GetModel()
{
string? model = BiosKey?.GetValue("SystemProductName") as string;
return model == "System Product Name" ? null : model;
}
public static string GetScreenSize()
{
RectInt32 screen = DisplayArea.Primary.OuterBounds;
return $"{screen.Width}x{screen.Height}";
}
public static string? GetCountry()
{
return GeoKey?.GetValue("Name") as string;
}
public static string GetSystemVersion()
{
object? majorVersion = CurrentVersionKey?.GetValue("CurrentMajorVersionNumber");
if (majorVersion != null)
{
object? minorVersion = CurrentVersionKey?.GetValue("CurrentMinorVersionNumber", "0");
object? buildNumber = CurrentVersionKey?.GetValue("CurrentBuildNumber", "0");
return $"{majorVersion}.{minorVersion}.{buildNumber}";
}
else
{
object? version = CurrentVersionKey?.GetValue("CurrentVersion", "0.0");
object? buildNumber = CurrentVersionKey?.GetValue("CurrentBuild", "0");
return $"{version}.{buildNumber}";
}
}
public static int GetSystemBuild()
{
return (int)(CurrentVersionKey?.GetValue("UBR") ?? 0);
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model;
[SuppressMessage("", "SA1600")]
public class AppCenterException
{
[JsonPropertyName("type")]
public string Type { get; set; } = "UnknownType";
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("stackTrace")]
public string? StackTrace { get; set; }
[JsonPropertyName("innerExceptions")]
public List<AppCenterException>? InnerExceptions { get; set; }
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using System.Globalization;
namespace Snap.Hutao.Service.AppCenter.Model;
[SuppressMessage("", "SA1600")]
public class Device
{
[JsonPropertyName("sdkName")]
public string SdkName { get; set; } = "appcenter.winui";
[JsonPropertyName("sdkVersion")]
public string SdkVersion { get; set; } = "4.5.0";
[JsonPropertyName("osName")]
public string OsName { get; set; } = "WINDOWS";
[JsonPropertyName("osVersion")]
public string OsVersion { get; set; } = DeviceHelper.GetSystemVersion();
[JsonPropertyName("osBuild")]
public string OsBuild { get; set; } = $"{DeviceHelper.GetSystemVersion()}.{DeviceHelper.GetSystemBuild()}";
[JsonPropertyName("model")]
public string? Model { get; set; } = DeviceHelper.GetModel();
[JsonPropertyName("oemName")]
public string? OemName { get; set; } = DeviceHelper.GetOem();
[JsonPropertyName("screenSize")]
public string ScreenSize { get; set; } = DeviceHelper.GetScreenSize();
[JsonPropertyName("carrierCountry")]
public string Country { get; set; } = DeviceHelper.GetCountry() ?? "CN";
[JsonPropertyName("locale")]
public string Locale { get; set; } = CultureInfo.CurrentCulture.Name;
[JsonPropertyName("timeZoneOffset")]
public int TimeZoneOffset { get; set; } = (int)TimeZoneInfo.Local.BaseUtcOffset.TotalMinutes;
[JsonPropertyName("appVersion")]
public string AppVersion { get; set; } = CoreEnvironment.Version.ToString();
[JsonPropertyName("appBuild")]
public string AppBuild { get; set; } = CoreEnvironment.Version.ToString();
[JsonPropertyName("appNamespace")]
public string AppNamespace { get; set; } = typeof(App).Namespace ?? string.Empty;
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class EventLog : PropertiesLog
{
public EventLog(string name)
{
Name = name;
}
[JsonPropertyName("type")]
public override string Type { get => "event"; }
[JsonPropertyName("id")]
public Guid Id { get; set; } = Guid.NewGuid();
[JsonPropertyName("name")]
public string Name { get; set; }
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class HandledErrorLog : PropertiesLog
{
public HandledErrorLog(Exception exception)
{
Id = Guid.NewGuid();
Exception = LogHelper.Create(exception);
}
[JsonPropertyName("id")]
public Guid? Id { get; set; }
[JsonPropertyName("exception")]
public AppCenterException Exception { get; set; }
[JsonPropertyName("type")]
public override string Type { get => "handledError"; }
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public abstract class Log
{
[JsonIgnore]
public LogStatus Status { get; set; } = LogStatus.Pending;
[JsonPropertyName("type")]
public abstract string Type { get; }
[JsonPropertyName("sid")]
public Guid Session { get; set; }
[JsonPropertyName("timestamp")]
public string Timestamp { get; set; } = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ");
[JsonPropertyName("device")]
public Device Device { get; set; } = default!;
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class LogContainer
{
public LogContainer(IEnumerable<Log> logs)
{
Logs = logs;
}
[JsonPropertyName("logs")]
public IEnumerable<Log> Logs { get; set; }
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
/// <summary>
/// 日志转换器
/// </summary>
public class LogConverter : JsonConverter<Log>
{
/// <inheritdoc/>
public override Log? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw Must.NeverHappen();
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, Log value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public static class LogHelper
{
public static T Initialize<T>(this T log, Guid sid, Device device)
where T : Log
{
log.Session = sid;
log.Device = device;
return log;
}
public static AppCenterException Create(Exception exception)
{
AppCenterException current = new()
{
Type = exception.GetType().ToString(),
Message = exception.Message,
StackTrace = exception.ToString(),
};
if (exception is AggregateException aggregateException)
{
if (aggregateException.InnerExceptions.Count != 0)
{
current.InnerExceptions = new();
foreach (var innerException in aggregateException.InnerExceptions)
{
current.InnerExceptions.Add(Create(innerException));
}
}
}
else if (exception.InnerException != null)
{
current.InnerExceptions ??= new();
current.InnerExceptions.Add(Create(exception.InnerException));
}
return current;
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
[SuppressMessage("", "SA1602")]
public enum LogStatus
{
Pending,
Uploading,
Uploaded,
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using System.Diagnostics;
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class ManagedErrorLog : Log
{
public ManagedErrorLog(Exception exception, bool fatal = true)
{
var p = Process.GetCurrentProcess();
Id = Guid.NewGuid();
Fatal = fatal;
UserId = CoreEnvironment.AppCenterDeviceId;
ProcessId = p.Id;
Exception = LogHelper.Create(exception);
ProcessName = p.ProcessName;
Architecture = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE");
AppLaunchTimestamp = p.StartTime.ToUniversalTime();
}
[JsonPropertyName("id")]
public Guid Id { get; set; }
[JsonPropertyName("userId")]
public string? UserId { get; set; }
[JsonPropertyName("processId")]
public int ProcessId { get; set; }
[JsonPropertyName("processName")]
public string ProcessName { get; set; }
[JsonPropertyName("fatal")]
public bool Fatal { get; set; }
[JsonPropertyName("appLaunchTimestamp")]
public DateTime? AppLaunchTimestamp { get; set; }
[JsonPropertyName("architecture")]
public string? Architecture { get; set; }
[JsonPropertyName("exception")]
public AppCenterException Exception { get; set; }
[JsonPropertyName("type")]
public override string Type { get => "managedError"; }
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class PageLog : PropertiesLog
{
public PageLog(string name)
{
Name = name;
}
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("type")]
public override string Type { get => "page"; }
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public abstract class PropertiesLog : Log
{
[JsonPropertyName("properties")]
public IDictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class StartServiceLog : Log
{
public StartServiceLog(params string[] services)
{
Services = services;
}
[JsonPropertyName("services")]
public string[] Services { get; set; }
[JsonPropertyName("type")]
public override string Type { get => "startService"; }
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class StartSessionLog : Log
{
[JsonPropertyName("type")]
public override string Type { get => "startSession"; }
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model;
[SuppressMessage("", "SA1600")]
public class LogUploadResult
{
[JsonPropertyName("status")]
public string Status { get; set; } = null!;
[JsonPropertyName("validDiagnosticsIds")]
public List<Guid> ValidDiagnosticsIds { get; set; } = null!;
[JsonPropertyName("throttledDiagnosticsIds")]
public List<Guid> ThrottledDiagnosticsIds { get; set; } = null!;
[JsonPropertyName("correlationId")]
public Guid CorrelationId { get; set; }
}

View File

@@ -11,8 +11,14 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// </summary>
internal static partial class ReliquaryWeightConfiguration
{
/// <summary>
/// 默认
/// </summary>
public static readonly AffixWeight Default = new(0, 100, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } };
/// <summary>
/// 词条权重
/// https://docs.qq.com/sheet/DUG52SFJlTUN3cmNL?tab=BB08J2
/// </summary>
public static readonly List<AffixWeight> AffixWeights = new()
{
@@ -73,6 +79,8 @@ internal static partial class ReliquaryWeightConfiguration
new(AvatarIds.Collei, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_GRASS_ADD_HURT, 100 } },
new(AvatarIds.Dori, 100, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } },
new(AvatarIds.Tighnari, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_GRASS_ADD_HURT, 100 } },
new(AvatarIds.Nilou, 100, 75, 0, 100, 100, 0, 55, 0, "直伤流") { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
new(AvatarIds.Nilou, 100, 75, 0, 100, 100, 0, 55, 0, "反应流") { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
new(AvatarIds.Cyno, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } },
new(AvatarIds.Candace, 100, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
};

View File

@@ -3,7 +3,6 @@
using Snap.Hutao.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Reliquary;

View File

@@ -7,6 +7,7 @@ using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Web.Enka.Model;
using System.Runtime.InteropServices;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
using MetadataReliquaryAffix = Snap.Hutao.Model.Metadata.Reliquary.ReliquaryAffix;
using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
@@ -61,6 +62,12 @@ internal class SummaryReliquaryFactory
{
MetadataReliquary reliquary = reliquaries.Single(r => r.Ids.Contains(equip.ItemId));
List<ReliquarySubProperty> subProperty = equip.Reliquary!.AppendPropIdList.Select(id => CreateSubProperty(id)).ToList();
int affixCount = GetAffixCount(reliquary);
Span<ReliquarySubProperty> span = CollectionsMarshal.AsSpan(subProperty);
List<ReliquarySubProperty> primary = new(span[..^affixCount].ToArray());
List<ReliquarySubProperty> secondary = new(span[^affixCount..].ToArray());
ReliquaryLevel relicLevel = reliqueryLevels.Single(r => r.Level == equip.Reliquary!.Level && r.Quality == reliquary.RankLevel);
FightProperty property = idRelicMainPropMap[equip.Reliquary.MainPropId];
@@ -77,21 +84,55 @@ internal class SummaryReliquaryFactory
MainProperty = new(property.GetDescription(), PropertyInfoDescriptor.FormatValue(property, relicLevel.Properties[property])),
// Reliquary
SubProperties = subProperty,
// SubProperties = subProperty,
PrimarySubProperties = primary,
SecondarySubProperties = secondary,
Score = ScoreReliquary(property, reliquary, relicLevel, subProperty),
};
}
private int GetAffixCount(MetadataReliquary reliquary)
{
return (reliquary.RankLevel, equip.Reliquary!.Level) switch
{
(ItemQuality.QUALITY_ORANGE, > 20) => 5,
(ItemQuality.QUALITY_ORANGE, > 16) => 4,
(ItemQuality.QUALITY_ORANGE, > 12) => 3,
(ItemQuality.QUALITY_ORANGE, > 8) => 2,
(ItemQuality.QUALITY_ORANGE, > 4) => 1,
(ItemQuality.QUALITY_ORANGE, _) => 0,
(ItemQuality.QUALITY_PURPLE, > 16) => 4,
(ItemQuality.QUALITY_PURPLE, > 12) => 3,
(ItemQuality.QUALITY_PURPLE, > 8) => 2,
(ItemQuality.QUALITY_PURPLE, > 4) => 1,
(ItemQuality.QUALITY_PURPLE, _) => 0,
(ItemQuality.QUALITY_BLUE, > 12) => 3,
(ItemQuality.QUALITY_BLUE, > 8) => 2,
(ItemQuality.QUALITY_BLUE, > 4) => 1,
(ItemQuality.QUALITY_BLUE, _) => 0,
(ItemQuality.QUALITY_GREEN, > 4) => 1,
(ItemQuality.QUALITY_GREEN, _) => 0,
(ItemQuality.QUALITY_WHITE, > 4) => 1,
(ItemQuality.QUALITY_WHITE, _) => 0,
_ => 0,
};
}
private double ScoreReliquary(FightProperty property, MetadataReliquary reliquary, ReliquaryLevel relicLevel, List<ReliquarySubProperty> subProperties)
{
// 沙 杯 头
if (equip.Flat.EquipType is EquipType.EQUIP_SHOES or EquipType.EQUIP_RING or EquipType.EQUIP_DRESS)
{
AffixWeight weightConfig = GetAffixWeightForAvatarId(avatarInfo.AvatarId);
AffixWeight weightConfig = GetAffixWeightForAvatarId();
ReliquaryLevel maxRelicLevel = reliqueryLevels.Where(r => r.Quality == reliquary.RankLevel).MaxBy(r => r.Level)!;
double percent = relicLevel.Properties[property] / maxRelicLevel.Properties[property];
double baseScore = 8 * percent * weightConfig[property];
double baseScore = 8 * percent * weightConfig.GetValueOrDefault(property, 0);
double score = subProperties.Sum(p => p.Score);
return ((score + baseScore) / 1700) * 66;
@@ -103,9 +144,9 @@ internal class SummaryReliquaryFactory
}
}
private AffixWeight GetAffixWeightForAvatarId(int avatarId)
private AffixWeight GetAffixWeightForAvatarId()
{
return ReliquaryWeightConfiguration.AffixWeights.First(w => w.AvatarId == avatarId);
return ReliquaryWeightConfiguration.AffixWeights.FirstOrDefault(w => w.AvatarId == avatarInfo.AvatarId, ReliquaryWeightConfiguration.Default);
}
private ReliquarySubProperty CreateSubProperty(int appendPropId)
@@ -121,7 +162,7 @@ internal class SummaryReliquaryFactory
{
MetadataReliquaryAffix affix = idReliquaryAffixMap[appendId];
AffixWeight weightConfig = GetAffixWeightForAvatarId(avatarInfo.AvatarId);
AffixWeight weightConfig = GetAffixWeightForAvatarId();
double weight = weightConfig.GetValueOrDefault(affix.Type, 0) / 100D;
// 小字词条,转换到等效百分比计算

View File

@@ -106,7 +106,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
{
Verify.Operation(IsInitialized, "祈愿记录服务未能正常初始化");
var list = appDbContext.GachaItems
List<UIGFItem> list = appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId)
.AsEnumerable()
.Select(i => i.ToUIGFItem(GetNameQualityByItemId(i.ItemId)))

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading;
using Snap.Hutao.View.Dialog;
namespace Snap.Hutao.Service.GachaLog;
@@ -15,8 +16,26 @@ internal class GachaLogUrlManualInputProvider : IGachaLogUrlProvider
public string Name { get => nameof(GachaLogUrlManualInputProvider); }
/// <inheritdoc/>
public Task<ValueResult<bool, string>> GetQueryAsync()
public async Task<ValueResult<bool, string>> GetQueryAsync()
{
throw new NotImplementedException();
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
await ThreadHelper.SwitchToMainThreadAsync();
ValueResult<bool, string> result = await new GachaLogUrlDialog(mainWindow).GetInputUrlAsync().ConfigureAwait(false);
if (result.IsOk)
{
if (result.Value.Contains("&auth_appid=webview_gacha"))
{
return result;
}
else
{
return new(false, "提供的Url无效");
}
}
else
{
return new(false, null!);
}
}
}

View File

@@ -36,9 +36,9 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
public async Task<ValueResult<bool, string>> GetQueryAsync()
{
Model.Binding.User? user = userService.Current;
if (user != null)
if (user != null && user.SelectedUserGameRole != null)
{
if (user.Cookie!.ContainsSToken() && user.SelectedUserGameRole != null)
if (user.Cookie!.ContainsSToken())
{
PlayerUid uid = (PlayerUid)user.SelectedUserGameRole;
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(uid);
@@ -48,9 +48,19 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
{
return new(true, GachaLogConfigration.AsQuery(data, authkey));
}
else
{
return new(false, "请求验证密钥失败");
}
}
else
{
return new(false, "当前用户的Cookie不包含 Stoken");
}
}
else
{
return new(false, "尚未选择要刷新的用户以及角色");
}
}
}

View File

@@ -40,7 +40,17 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
string folder = Path.GetDirectoryName(path) ?? string.Empty;
string cacheFile = Path.Combine(folder, @"YuanShen_Data\webCaches\Cache\Cache_Data\data_2");
using (TemporaryFile tempFile = TemporaryFile.CreateFromFileCopy(cacheFile))
TemporaryFile tempFile;
try
{
tempFile = TemporaryFile.CreateFromFileCopy(cacheFile);
}
catch (DirectoryNotFoundException)
{
return new(false, $"找不到原神内置浏览器缓存路径:\n{cacheFile}");
}
using (tempFile)
{
using (FileStream fileStream = new(tempFile.Path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
@@ -74,7 +84,7 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
}
else
{
return new(false, null!);
return new(false, $"未正确提供原神路径,或当前设置的路径不正确");
}
}

View File

@@ -2,7 +2,10 @@
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Core.IO.Ini;
using Snap.Hutao.Core.Threading;
using System.IO;
using System.Text.RegularExpressions;
namespace Snap.Hutao.Service.Game.Locator;
@@ -18,8 +21,30 @@ internal class RegistryLauncherLocator : IGameLocator
/// <inheritdoc/>
public Task<ValueResult<bool, string>> LocateGamePathAsync()
{
// TODO: fix folder moved issue
return Task.FromResult(LocateInternal("InstallPath", "\\Genshin Impact Game\\YuanShen.exe"));
ValueResult<bool, string> result = LocateInternal("DisplayIcon");
if (result.IsOk == false)
{
return Task.FromResult(result);
}
else
{
string path = result.Value;
string configPath = Path.Combine(path, "config.ini");
string? escapedPath = null;
using (FileStream stream = File.OpenRead(configPath))
{
IEnumerable<IniElement> elements = IniSerializer.Deserialize(stream);
escapedPath = elements.OfType<IniParameter>().FirstOrDefault(p => p.Key == "game_install_path")?.Value;
}
if (escapedPath != null)
{
return Task.FromResult<ValueResult<bool, string>>(new(true, Unescape(escapedPath)));
}
}
return Task.FromResult<ValueResult<bool, string>>(new(false, null!));
}
/// <inheritdoc/>
@@ -28,18 +53,13 @@ internal class RegistryLauncherLocator : IGameLocator
return Task.FromResult(LocateInternal("DisplayIcon"));
}
private static ValueResult<bool, string> LocateInternal(string key, string? append = null)
private static ValueResult<bool, string> LocateInternal(string key)
{
RegistryKey? uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神");
if (uninstallKey != null)
{
if (uninstallKey.GetValue(key) is string path)
{
if (!string.IsNullOrEmpty(append))
{
path += append;
}
return new(true, path);
}
else
@@ -52,4 +72,18 @@ internal class RegistryLauncherLocator : IGameLocator
return new(false, null!);
}
}
private static string Unescape(string str)
{
string? hex4Result = Regex.Replace(str, @"\\x([0-9a-f]{4})", @"\u$1");
// 不包含中文
if (!hex4Result.Contains(@"\u"))
{
// fix path with \
hex4Result = hex4Result.Replace(@"\", @"\\");
}
return Regex.Unescape(hex4Result);
}
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Service;
/// <summary>
/// 胡桃 API 服务
/// </summary>
[Injection(InjectAs.Transient, typeof(IHutaoService))]
internal class HutaoService : IHutaoService
{
private readonly HomaClient homaClient;
private readonly IMemoryCache memoryCache;
/// <summary>
/// 构造一个新的胡桃 API 服务
/// </summary>
/// <param name="homaClient">胡桃 API 客户端</param>
/// <param name="memoryCache">内存缓存</param>
public HutaoService(HomaClient homaClient, IMemoryCache memoryCache)
{
this.homaClient = homaClient;
this.memoryCache = memoryCache;
}
/// <inheritdoc/>
public ValueTask<Overview?> GetOverviewAsync()
{
return FromCacheOrWebAsync(nameof(Overview), homaClient.GetOverviewAsync);
}
/// <inheritdoc/>
public ValueTask<List<AvatarAppearanceRank>> GetAvatarAppearanceRanksAsync()
{
return FromCacheOrWebAsync(nameof(AvatarAppearanceRank), homaClient.GetAvatarAttendanceRatesAsync);
}
/// <inheritdoc/>
public ValueTask<List<AvatarUsageRank>> GetAvatarUsageRanksAsync()
{
return FromCacheOrWebAsync(nameof(AvatarUsageRank), homaClient.GetAvatarUtilizationRatesAsync);
}
/// <inheritdoc/>
public ValueTask<List<AvatarConstellationInfo>> GetAvatarConstellationInfosAsync()
{
return FromCacheOrWebAsync(nameof(AvatarConstellationInfo), homaClient.GetAvatarHoldingRatesAsync);
}
/// <inheritdoc/>
public ValueTask<List<AvatarCollocation>> GetAvatarCollocationsAsync()
{
return FromCacheOrWebAsync(nameof(AvatarCollocation), homaClient.GetAvatarCollocationsAsync);
}
/// <inheritdoc/>
public ValueTask<List<TeamAppearance>> GetTeamAppearancesAsync()
{
return FromCacheOrWebAsync(nameof(TeamAppearance), homaClient.GetTeamCombinationsAsync);
}
private async ValueTask<T> FromCacheOrWebAsync<T>(string typeName, Func<CancellationToken, Task<T>> taskFunc)
{
string key = $"{nameof(HutaoService)}.Cache.{typeName}";
if (memoryCache.TryGetValue(key, out object? cache))
{
return (T)cache;
}
T web = await taskFunc(default).ConfigureAwait(false);
return memoryCache.Set(key, web, TimeSpan.FromMinutes(30));
}
}

View File

@@ -43,6 +43,13 @@ internal interface IMetadataService
/// <returns>角色列表</returns>
ValueTask<List<Avatar>> GetAvatarsAsync(CancellationToken token = default);
/// <summary>
/// 异步获取装备被动Id到圣遗物套装的映射
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>装备被动Id到圣遗物套装的映射</returns>
ValueTask<Dictionary<int, ReliquarySet>> GetEquipAffixIdToReliquarySetMapAsync(CancellationToken token = default);
/// <summary>
/// 异步获取卡池配置列表
/// </summary>
@@ -126,4 +133,11 @@ internal interface IMetadataService
/// <param name="token">取消令牌</param>
/// <returns>武器列表</returns>
ValueTask<List<Weapon>> GetWeaponsAsync(CancellationToken token = default);
/// <summary>
/// 异步获取圣遗物套装
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>圣遗物套装列表</returns>
ValueTask<List<ReliquarySet>> GetReliquarySetsAsync(CancellationToken token = default);
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Achievement;
using Snap.Hutao.Model.Metadata.Avatar;
@@ -39,42 +38,6 @@ internal partial class MetadataService
return FromCacheOrFileAsync<List<GachaEvent>>("GachaEvent", token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, Avatar>("Avatar", a => a.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, ReliquaryAffix>> GetIdReliquaryAffixMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, ReliquaryAffix>("ReliquaryAffix", a => a.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, FightProperty>> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, FightProperty, ReliquaryAffixBase>("ReliquaryMainAffix", r => r.Id, r => r.Type, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, Weapon>("Weapon", w => w.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<string, Avatar>> GetNameToAvatarMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<string, Avatar>("Avatar", a => a.Name, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<string, Weapon>> GetNameToWeaponMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<string, Weapon>("Weapon", w => w.Name, token);
}
/// <inheritdoc/>
public ValueTask<List<Reliquary>> GetReliquariesAsync(CancellationToken token = default)
{
@@ -99,6 +62,12 @@ internal partial class MetadataService
return FromCacheOrFileAsync<List<ReliquaryAffixBase>>("ReliquaryMainAffix", token);
}
/// <inheritdoc/>
public ValueTask<List<ReliquarySet>> GetReliquarySetsAsync(CancellationToken token = default)
{
return FromCacheOrFileAsync<List<ReliquarySet>>("ReliquarySet", token);
}
/// <inheritdoc/>
public ValueTask<List<Weapon>> GetWeaponsAsync(CancellationToken token = default)
{

View File

@@ -0,0 +1,57 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Metadata.Weapon;
namespace Snap.Hutao.Service.Metadata;
/// <summary>
/// 索引部分
/// </summary>
internal partial class MetadataService
{
/// <inheritdoc/>
public ValueTask<Dictionary<int, ReliquarySet>> GetEquipAffixIdToReliquarySetMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, ReliquarySet>("ReliquarySet", r => r.EquipAffixId, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, Avatar>("Avatar", a => a.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, ReliquaryAffix>> GetIdReliquaryAffixMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, ReliquaryAffix>("ReliquaryAffix", a => a.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, FightProperty>> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, FightProperty, ReliquaryAffixBase>("ReliquaryMainAffix", r => r.Id, r => r.Type, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<int, Weapon>("Weapon", w => w.Id, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<string, Avatar>> GetNameToAvatarMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<string, Avatar>("Avatar", a => a.Name, token);
}
/// <inheritdoc/>
public ValueTask<Dictionary<string, Weapon>> GetNameToWeaponMapAsync(CancellationToken token = default)
{
return FromCacheAsDictionaryAsync<string, Weapon>("Weapon", w => w.Name, token);
}
}

View File

@@ -1,13 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.Model.Binding.User;

View File

@@ -137,6 +137,7 @@ internal class UserService : IUserService
/// <inheritdoc/>
public async Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie)
{
cookie.Trim();
Must.NotNull(userCollection!);
// 检查 uid 是否存在
@@ -154,12 +155,18 @@ internal class UserService : IUserService
{
// insert stoken directly
userWithSameUid.Cookie.InsertSToken(uid, cookie);
appDbContext.Users.Update(userWithSameUid.Entity);
appDbContext.SaveChanges();
return new(UserOptionResult.Upgraded, uid);
}
if (cookie.ContainsLTokenAndCookieToken())
{
UpdateUserCookie(cookie, userWithSameUid);
userWithSameUid.Cookie = cookie;
appDbContext.Users.Update(userWithSameUid.Entity);
appDbContext.SaveChanges();
return new(UserOptionResult.Updated, uid);
}
}
@@ -189,17 +196,8 @@ internal class UserService : IUserService
}
}
private void UpdateUserCookie(Cookie cookie, BindingUser user)
{
user.Cookie = cookie;
appDbContext.Users.Update(user.Entity);
appDbContext.SaveChanges();
}
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(ObservableCollection<BindingUser> users, Cookie cookie)
{
cookie.Trim();
BindingUser? newUser = await BindingUser.CreateAsync(cookie, userClient, bindingClient).ConfigureAwait(false);
if (newUser != null)
{

View File

@@ -29,6 +29,12 @@
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION;DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT</DefineConstants>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<None Remove="Control\Panel\PanelSelector.xaml" />
@@ -38,6 +44,7 @@
<None Remove="Resource\Icon\UI_BagTabIcon_Weapon.png" />
<None Remove="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
<None Remove="Resource\Icon\UI_BtnIcon_Gacha.png" />
<None Remove="Resource\Icon\UI_ChapterIcon_Hutao.png" />
<None Remove="Resource\Icon\UI_Icon_Achievement.png" />
<None Remove="Resource\Icon\UI_Icon_BoostUp.png" />
<None Remove="Resource\Icon\UI_Icon_Locked.png" />
@@ -54,6 +61,7 @@
<None Remove="View\Dialog\AvatarInfoQueryDialog.xaml" />
<None Remove="View\Dialog\GachaLogImportDialog.xaml" />
<None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" />
<None Remove="View\Dialog\GachaLogUrlDialog.xaml" />
<None Remove="View\Dialog\UserAutoCookieDialog.xaml" />
<None Remove="View\Dialog\UserDialog.xaml" />
<None Remove="View\MainView.xaml" />
@@ -62,6 +70,7 @@
<None Remove="View\Page\AnnouncementPage.xaml" />
<None Remove="View\Page\AvatarPropertyPage.xaml" />
<None Remove="View\Page\GachaLogPage.xaml" />
<None Remove="View\Page\HutaoDatabasePage.xaml" />
<None Remove="View\Page\SettingPage.xaml" />
<None Remove="View\Page\WikiAvatarPage.xaml" />
<None Remove="View\TitleView.xaml" />
@@ -87,6 +96,7 @@
<Content Include="Resource\Icon\UI_BagTabIcon_Weapon.png" />
<Content Include="Resource\Icon\UI_BtnIcon_ActivityEntry.png" />
<Content Include="Resource\Icon\UI_BtnIcon_Gacha.png" />
<Content Include="Resource\Icon\UI_ChapterIcon_Hutao.png" />
<Content Include="Resource\Icon\UI_Icon_Achievement.png" />
<Content Include="Resource\Icon\UI_Icon_BoostUp.png" />
<Content Include="Resource\Icon\UI_Icon_Locked.png" />
@@ -99,10 +109,9 @@
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Behaviors" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
<!-- Prevent NewtownSoft.Json -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.10" />
<!-- The PrivateAssets & IncludeAssets of Microsoft.EntityFrameworkCore.Tools should be remove to prevent multiple deps files-->
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.10" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
@@ -139,6 +148,16 @@
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\HutaoDatabasePage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\GachaLogUrlDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\AvatarInfoQueryDialog.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -5,7 +5,6 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
using Snap.Hutao.Model.Metadata;
using System.Diagnostics;
namespace Snap.Hutao.View.Control;

View File

@@ -0,0 +1,19 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.GachaLogUrlDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="手动输入祈愿记录Url"
DefaultButton="Primary"
PrimaryButtonText="确认"
CloseButtonText="取消"
Style="{StaticResource DefaultContentDialogStyle}">
<Grid>
<TextBox
x:Name="InputText"
PlaceholderText="请输入Url"/>
</Grid>
</ContentDialog>

View File

@@ -0,0 +1,36 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.Threading;
namespace Snap.Hutao.View.Dialog;
/// <summary>
/// 祈愿记录Url对话框
/// </summary>
public sealed partial class GachaLogUrlDialog : ContentDialog
{
/// <summary>
/// 初始化一个新的祈愿记录Url对话框
/// </summary>
/// <param name="window">窗体</param>
public GachaLogUrlDialog(Window window)
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
}
/// <summary>
/// 获取输入的Url
/// </summary>
/// <returns>输入的结果</returns>
public async Task<ValueResult<bool, string>> GetInputUrlAsync()
{
ContentDialogResult result = await ShowAsync();
string url = InputText.Text;
return new(result == ContentDialogResult.Primary, url);
}
}

View File

@@ -44,7 +44,7 @@ public sealed partial class UserAutoCookieDialog : ContentDialog
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
foreach (var item in cookies)
foreach (CoreWebView2Cookie item in cookies)
{
manager.DeleteCookie(item);
}

View File

@@ -43,6 +43,11 @@
shvh:NavHelper.NavigateTo="shvp:AvatarPropertyPage"
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_Icon_BoostUp.png}"/>
<NavigationViewItem
Content="深渊统计"
shvh:NavHelper.NavigateTo="shvp:HutaoDatabasePage"
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_ChapterIcon_Hutao.png}"/>
<NavigationViewItemHeader Content="WIKI"/>
<NavigationViewItem

View File

@@ -8,7 +8,6 @@ using Snap.Hutao.Core.Threading;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.View.Page;
using Windows.UI.ViewManagement;
namespace Snap.Hutao.View;

View File

@@ -520,25 +520,28 @@
Width="80"
Height="80"
Source="{Binding Icon}"/>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="16"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ItemsControl
MinWidth="156"
Grid.Column="0"
VerticalAlignment="Stretch"
Grid.Row="2"
ItemsSource="{Binding SubProperties}">
ItemsSource="{Binding PrimarySubProperties}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwucont:UniformGrid
Columns="2"
Columns="1"
Rows="5"
ColumnSpacing="16"
Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Padding="2" MinWidth="152" Opacity="{Binding Opacity}">
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Padding="2" Opacity="{Binding Opacity}">
<TextBlock
Text="{Binding Name}"/>
<TextBlock
@@ -549,6 +552,34 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl
MinWidth="156"
Grid.Column="2"
VerticalAlignment="Stretch"
ItemsSource="{Binding SecondarySubProperties}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwucont:UniformGrid
Columns="1"
Rows="5"
Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Padding="2" Opacity="{Binding Opacity}">
<TextBlock
Text="{Binding Name}"/>
<TextBlock
Text="{Binding Value}"
HorizontalAlignment="Right"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<Rectangle Height="1" Grid.Row="3" Margin="0,6" Opacity="0.8">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding Quality,Converter={StaticResource QualityColorConverter}}"/>

View File

@@ -72,6 +72,8 @@
Command="{Binding ImportFromUIGFJsonCommand}"/>
<MenuFlyoutItem
Text="从 UIGF Excel 文件导入"
IsEnabled="False"
Visibility="Collapsed"
Command="{Binding ImportFromUIGFExcelCommand}"/>
</MenuFlyout>
</AppBarButton.Flyout>
@@ -84,6 +86,8 @@
Command="{Binding ExportToUIGFJsonCommand}"/>
<MenuFlyoutItem
Text="导出到 UIGF Excel 文件"
IsEnabled="False"
Visibility="Collapsed"
Command="{Binding ExportToUIGFExcelCommand}"/>
</MenuFlyout>
</AppBarButton.Flyout>

View File

@@ -0,0 +1,309 @@
<shc:ScopedPage
x:Class="Snap.Hutao.View.Page.HutaoDatabasePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shvc="using:Snap.Hutao.View.Control"
xmlns:shv="using:Snap.Hutao.ViewModel"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance shv:HutaoDatabaseViewModel}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<shc:ScopedPage.Resources>
<Thickness x:Key="PivotHeaderItemMargin">8,0,8,0</Thickness>
<Thickness x:Key="PivotItemMargin">0</Thickness>
</shc:ScopedPage.Resources>
<Grid>
<Pivot>
<PivotItem Header="角色使用">
<Pivot ItemsSource="{Binding AvatarUsageRanks}">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Floor}"/>
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate>
<ScrollViewer>
<GridView
SelectionMode="None"
ItemsSource="{Binding Avatars}"
Margin="12,12,0,0">
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
</GridView.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{StaticResource CardBackgroundFillColorDefault}">
<StackPanel>
<shvc:ItemIcon
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
<TextBlock
Margin="0,0,0,2"
HorizontalAlignment="Center"
Text="{Binding Rate}"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</GridView>
</ScrollViewer>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
</PivotItem>
<PivotItem Header="角色出场">
<Pivot ItemsSource="{Binding AvatarAppearanceRanks}">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Floor}"/>
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate>
<ScrollViewer>
<GridView
SelectionMode="None"
ItemsSource="{Binding Avatars}"
Margin="12,12,0,0">
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
</GridView.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{StaticResource CardBackgroundFillColorDefault}">
<StackPanel>
<shvc:ItemIcon
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
<TextBlock
Margin="0,0,0,2"
HorizontalAlignment="Center"
Text="{Binding Rate}"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</GridView>
</ScrollViewer>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
</PivotItem>
<PivotItem Header="角色持有">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Margin="12,0,12,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48"/>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="角色" Margin="6" HorizontalAlignment="Center" Style="{StaticResource CaptionTextBlockStyle}"/>
<TextBlock Grid.Column="1" Text="持有" Margin="6" HorizontalAlignment="Center" Style="{StaticResource CaptionTextBlockStyle}"/>
<TextBlock Grid.Column="2" Text="0 命" Margin="6" HorizontalAlignment="Center" Style="{StaticResource CaptionTextBlockStyle}"/>
<TextBlock Grid.Column="3" Text="1 命" Margin="6" HorizontalAlignment="Center" Style="{StaticResource CaptionTextBlockStyle}"/>
<TextBlock Grid.Column="4" Text="2 命" Margin="6" HorizontalAlignment="Center" Style="{StaticResource CaptionTextBlockStyle}"/>
<TextBlock Grid.Column="5" Text="3 命" Margin="6" HorizontalAlignment="Center" Style="{StaticResource CaptionTextBlockStyle}"/>
<TextBlock Grid.Column="6" Text="4 命" Margin="6" HorizontalAlignment="Center" Style="{StaticResource CaptionTextBlockStyle}"/>
<TextBlock Grid.Column="7" Text="5 命" Margin="6" HorizontalAlignment="Center" Style="{StaticResource CaptionTextBlockStyle}"/>
<TextBlock Grid.Column="8" Text="6 命" Margin="6" HorizontalAlignment="Center" Style="{StaticResource CaptionTextBlockStyle}"/>
</Grid>
<ScrollViewer Grid.Row="1">
<ItemsControl
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding AvatarConstellationInfos}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
Background="{StaticResource CardBackgroundFillColorDefault}"
CornerRadius="{StaticResource CompatCornerRadius}"
Margin="12,0,12,12">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shvc:ItemIcon
Height="48"
Width="48"
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Rate}"/>
<ItemsControl Grid.Column="1" ItemsSource="{Binding Rates}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid Columns="7"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</PivotItem>
<PivotItem Header="队伍出场">
<Pivot ItemsSource="{Binding TeamAppearances}">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Floor}"/>
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0">
<ItemsControl ItemsSource="{Binding Up}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
CornerRadius="{StaticResource CompatCornerRadius}"
Margin="12,12,12,0"
Background="{StaticResource CardBackgroundFillColorDefault}">
<Grid Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<ItemsControl ItemsSource="{Binding}" HorizontalAlignment="Left">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid Columns="4" ColumnSpacing="6"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<shvc:ItemIcon
Width="48"
Height="48"
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Rate}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<ScrollViewer Grid.Column="1">
<ItemsControl ItemsSource="{Binding Down}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
CornerRadius="{StaticResource CompatCornerRadius}"
Margin="12,12,12,0"
Background="{StaticResource CardBackgroundFillColorDefault}">
<Grid Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<ItemsControl ItemsSource="{Binding}" HorizontalAlignment="Left">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid Columns="4" ColumnSpacing="6"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<shvc:ItemIcon
Width="48"
Height="48"
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Rate}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
</PivotItem>
</Pivot>
</Grid>
</shc:ScopedPage>

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Control;
using Snap.Hutao.ViewModel;
namespace Snap.Hutao.View.Page;
/// <summary>
/// 胡桃数据库页面
/// </summary>
public sealed partial class HutaoDatabasePage : ScopedPage
{
/// <summary>
/// 构造一个新的胡桃数据库页面
/// </summary>
public HutaoDatabasePage()
{
InitializeWith<HutaoDatabaseViewModel>();
InitializeComponent();
}
}

View File

@@ -95,6 +95,15 @@
<Button Content="打开" Command="{Binding Experimental.OpenCacheFolderCommand}"/>
</sc:Setting.ActionContent>
</sc:Setting>
<sc:Setting
Icon="&#xE898;"
Header="上传深渊数据"
Description="将当前账号的深渊数据上传到胡桃数据库">
<sc:Setting.ActionContent>
<Button Content="上传" Command="{Binding Experimental.UploadSpiralAbyssRecordCommand}"/>
</sc:Setting.ActionContent>
</sc:Setting>
</sc:SettingsGroup>
</StackPanel>

View File

@@ -4,6 +4,10 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Context.FileSystem.Location;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Model.Post;
using Windows.Storage;
using Windows.System;
@@ -22,8 +26,6 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
/// </summary>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
/// <param name="hutaoLocation">数据文件夹</param>
/// <param name="signService">签到客户端</param>
/// <param name="infoBarService">信息条服务</param>
public ExperimentalFeaturesViewModel(
IAsyncRelayCommandFactory asyncRelayCommandFactory,
HutaoLocation hutaoLocation)
@@ -32,6 +34,7 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
OpenCacheFolderCommand = asyncRelayCommandFactory.Create(OpenCacheFolderAsync);
OpenDataFolderCommand = asyncRelayCommandFactory.Create(OpenDataFolderAsync);
UploadSpiralAbyssRecordCommand = asyncRelayCommandFactory.Create(UploadSpiralAbyssRecordAsync);
}
/// <summary>
@@ -44,6 +47,11 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
/// </summary>
public ICommand OpenDataFolderCommand { get; }
/// <summary>
/// 上传深渊记录命令
/// </summary>
public ICommand UploadSpiralAbyssRecordCommand { get; }
private Task OpenCacheFolderAsync()
{
return Launcher.LaunchFolderAsync(ApplicationData.Current.TemporaryFolder).AsTask();
@@ -53,4 +61,22 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
{
return Launcher.LaunchFolderPathAsync(hutaoLocation.GetPath()).AsTask();
}
private async Task UploadSpiralAbyssRecordAsync()
{
HomaClient homaClient = Ioc.Default.GetRequiredService<HomaClient>();
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
if (userService.Current is Model.Binding.User user)
{
SimpleRecord record = await homaClient.GetPlayerRecordAsync(user).ConfigureAwait(false);
Web.Response.Response<string>? response = await homaClient.UploadRecordAsync(record).ConfigureAwait(false);
if (response != null && response.IsOk())
{
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
infoBarService.Success(response.Message);
}
}
}
}

View File

@@ -233,6 +233,13 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
dialog.DefaultButton = ContentDialogButton.Primary;
}
}
else
{
if (query is string message)
{
infoBarService.Warning(message);
}
}
}
}

View File

@@ -0,0 +1,151 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Control;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.ViewModel;
/// <summary>
/// 胡桃数据库视图模型
/// </summary>
[Injection(InjectAs.Transient)]
internal class HutaoDatabaseViewModel : ObservableObject, ISupportCancellation
{
private readonly IHutaoService hutaoService;
private readonly IMetadataService metadataService;
private List<ComplexAvatarRank>? avatarUsageRanks;
private List<ComplexAvatarRank>? avatarAppearanceRanks;
private List<ComplexAvatarConstellationInfo>? avatarConstellationInfos;
private List<ComplexTeamRank>? teamAppearances;
/// <summary>
/// 构造一个新的胡桃数据库视图模型
/// </summary>
/// <param name="hutaoService">胡桃服务</param>
/// <param name="metadataService">元数据服务</param>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public HutaoDatabaseViewModel(IHutaoService hutaoService, IMetadataService metadataService, IAsyncRelayCommandFactory asyncRelayCommandFactory)
{
this.hutaoService = hutaoService;
this.metadataService = metadataService;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 角色使用率
/// </summary>
public List<ComplexAvatarRank>? AvatarUsageRanks { get => avatarUsageRanks; set => SetProperty(ref avatarUsageRanks, value); }
/// <summary>
/// 角色上场率
/// </summary>
public List<ComplexAvatarRank>? AvatarAppearanceRanks { get => avatarAppearanceRanks; set => SetProperty(ref avatarAppearanceRanks, value); }
/// <summary>
/// 角色命座信息
/// </summary>
public List<ComplexAvatarConstellationInfo>? AvatarConstellationInfos { get => avatarConstellationInfos; set => avatarConstellationInfos = value; }
/// <summary>
/// 队伍出场
/// </summary>
public List<ComplexTeamRank>? TeamAppearances { get => teamAppearances; set => SetProperty(ref teamAppearances, value); }
/// <summary>
/// 打开界面命令
/// </summary>
public ICommand OpenUICommand { get; }
private async Task OpenUIAsync()
{
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
Dictionary<int, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
idAvatarMap = new(idAvatarMap)
{
[10000005] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
[10000007] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
};
Dictionary<int, Weapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
Dictionary<int, Model.Metadata.Reliquary.ReliquarySet> idReliquarySetMap = await metadataService.GetEquipAffixIdToReliquarySetMapAsync().ConfigureAwait(false);
List<ComplexAvatarRank> avatarAppearanceRanksLocal = default!;
List<ComplexAvatarRank> avatarUsageRanksLocal = default!;
List<ComplexAvatarConstellationInfo> avatarConstellationInfosLocal = default!;
List<ComplexTeamRank> teamAppearancesLocal = default!;
Task avatarAppearanceRankTask = Task.Run(async () =>
{
// AvatarAppearanceRank
List<AvatarAppearanceRank> avatarAppearanceRanksRaw = await hutaoService.GetAvatarAppearanceRanksAsync().ConfigureAwait(false);
avatarAppearanceRanksLocal = avatarAppearanceRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
{
Floor = $"第 {rank.Floor} 层",
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
}).ToList();
});
Task avatarUsageRank = Task.Run(async () =>
{
// AvatarUsageRank
List<AvatarUsageRank> avatarUsageRanksRaw = await hutaoService.GetAvatarUsageRanksAsync().ConfigureAwait(false);
avatarUsageRanksLocal = avatarUsageRanksRaw.OrderByDescending(r => r.Floor).Select(rank => new ComplexAvatarRank
{
Floor = $"第 {rank.Floor} 层",
Avatars = rank.Ranks.OrderByDescending(r => r.Rate).Select(rank => new ComplexAvatar(idAvatarMap[rank.Item], rank.Rate)).ToList(),
}).ToList();
});
Task avatarConstellationInfoTask = Task.Run(async () =>
{
// AvatarConstellationInfo
List<AvatarConstellationInfo> avatarConstellationInfosRaw = await hutaoService.GetAvatarConstellationInfosAsync().ConfigureAwait(false);
avatarConstellationInfosLocal = avatarConstellationInfosRaw.OrderBy(i => i.HoldingRate).Select(info =>
{
return new ComplexAvatarConstellationInfo(idAvatarMap[info.AvatarId], info.HoldingRate, info.Constellations.Select(x => x.Rate));
}).ToList();
});
Task teamAppearanceTask = Task.Run(async () =>
{
List<TeamAppearance> teamAppearancesRaw = await hutaoService.GetTeamAppearancesAsync().ConfigureAwait(false);
teamAppearancesLocal = teamAppearancesRaw.OrderByDescending(t => t.Floor).Select(team => new ComplexTeamRank(team, idAvatarMap)).ToList();
});
await Task.WhenAll(avatarAppearanceRankTask, avatarUsageRank, avatarConstellationInfoTask, teamAppearanceTask).ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
AvatarAppearanceRanks = avatarAppearanceRanksLocal;
AvatarUsageRanks = avatarUsageRanksLocal;
AvatarConstellationInfos = avatarConstellationInfosLocal;
TeamAppearances = teamAppearancesLocal;
//// AvatarCollocation
//List<AvatarCollocation> avatarCollocationsRaw = await hutaoService.GetAvatarCollocationsAsync().ConfigureAwait(false);
//List<ComplexAvatarCollocation> avatarCollocationsLocal = avatarCollocationsRaw.Select(co =>
//{
// return new ComplexAvatarCollocation(idAvatarMap[co.AvatarId])
// {
// Avatars = co.Avatars.Select(a => new ComplexAvatar(idAvatarMap[a.Item], a.Rate)).ToList(),
// Weapons = co.Weapons.Select(w => new ComplexWeapon(idWeaponMap[w.Item], w.Rate)).ToList(),
// ReliquarySets = co.Reliquaries.Select(r => new ComplexReliquarySet(r, idReliquarySetMap)).ToList(),
// };
//}).ToList();
}
}
}

View File

@@ -51,7 +51,7 @@ internal static class ApiEndpoints
/// <returns>游戏记录主页字符串</returns>
public static string GameRecordIndex(string uid, string server)
{
return $"{ApiTakumiRecordApi}/index?role_id={uid}&server={server}";
return $"{ApiTakumiRecordApi}/index?server={server}&role_id={uid}";
}
/// <summary>

View File

@@ -38,7 +38,7 @@ public partial class Cookie
public static Cookie Parse(string cookieString)
{
SortedDictionary<string, string> cookieMap = new();
cookieString = cookieString.Replace(" ", string.Empty);
string[] values = cookieString.TrimEnd(';').Split(';');
foreach (string[] parts in values.Select(c => c.Split('=', 2)))
{

View File

@@ -29,7 +29,8 @@ internal abstract class DynamicSecretProvider2 : Md5Convert
string b = postBody is null ? string.Empty : JsonSerializer.Serialize(postBody, options);
// query
string q = string.Join('&', new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x));
string[] queries = queryUrl.Split('?', 2);
string q = queries.Length == 2 ? string.Join('&', queries[1].Split('&').OrderBy(x => x)) : string.Empty;
// check
string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecret2Salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant();

View File

@@ -24,7 +24,7 @@ public class GachaLogItem
/// </summary>
[ExcelColumn(Name = "gacha_type")]
[JsonPropertyName("gacha_type")]
[JsonConverter(typeof(JsonStringEnumConverter))]
[JsonConverter(typeof(EnumStringValueConverter<GachaConfigType>))]
public GachaConfigType GachaType { get; set; } = default!;
/// <summary>

View File

@@ -45,6 +45,21 @@ internal static class HttpClientExtensions
}
}
/// <inheritdoc cref="HttpClientJsonExtensions.PostAsJsonAsync{TValue}(HttpClient, string?, TValue, JsonSerializerOptions?, CancellationToken)"/>
internal static async Task<TResult?> TryCatchPostAsJsonAsync<TValue, TResult>(this HttpClient httpClient, string requestUri, TValue value, JsonSerializerOptions options, CancellationToken token = default)
where TResult : class
{
try
{
HttpResponseMessage message = await httpClient.PostAsJsonAsync(requestUri, value, options, token).ConfigureAwait(false);
return await message.Content.ReadFromJsonAsync<TResult>(options, token).ConfigureAwait(false);
}
catch (HttpRequestException)
{
return null;
}
}
/// <summary>
/// 设置用户的Cookie
/// </summary>

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Response;
using System.Net.Http;

View File

@@ -18,11 +18,11 @@ public class Offering
/// 等级
/// </summary>
[JsonPropertyName("level")]
public string Level { get; set; } = default!;
public int Level { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
public Uri Icon { get; set; } = default!;
}

View File

@@ -24,7 +24,7 @@ public class Floor
/// 是否解锁
/// </summary>
[JsonPropertyName("is_unlock")]
public string IsUnlock { get; set; } = default!;
public bool IsUnlock { get; set; } = default!;
/// <summary>
/// 结束时间

View File

@@ -25,7 +25,7 @@ public class WorldExploration
/// 图标
/// </summary>
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
public Uri Icon { get; set; } = default!;
/// <summary>
/// 名称
@@ -39,7 +39,8 @@ public class WorldExploration
/// Reputation
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = default!;
[JsonConverter(typeof(JsonStringEnumConverter))]
public WorldExplorationType Type { get; set; } = default!;
/// <summary>
/// 供奉进度
@@ -63,7 +64,7 @@ public class WorldExploration
/// 地图链接
/// </summary>
[JsonPropertyName("map_url")]
public string MapUrl { get; set; } = default!;
public Uri MapUrl { get; set; } = default!;
/// <summary>
/// 攻略链接 无攻略时为 <see cref="string.Empty"/>
@@ -75,41 +76,25 @@ public class WorldExploration
/// 背景图片
/// </summary>
[JsonPropertyName("background_image")]
public string BackgroundImage { get; set; } = default!;
public Uri BackgroundImage { get; set; } = default!;
/// <summary>
/// 反色图标
/// </summary>
[JsonPropertyName("inner_icon")]
public string InnerIcon { get; set; } = default!;
public Uri InnerIcon { get; set; } = default!;
/// <summary>
/// 背景图片
/// </summary>
[JsonPropertyName("cover")]
public string Cover { get; set; } = default!;
public Uri Cover { get; set; } = default!;
/// <summary>
/// 百分比*100进度
/// </summary>
public double ExplorationPercentageBy10
{
get => ExplorationPercentage / 10.0;
}
/// <summary>
/// 类型名称转换器
/// </summary>
public string TypeFormatted
{
get => IsReputation ? "声望等级" : "供奉等级";
}
/// <summary>
/// 指示当前是否为声望
/// </summary>
public bool IsReputation
{
get => Type == "Reputation";
get => ExplorationPercentage / 1000.0;
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 世界探索类型
/// </summary>
public enum WorldExplorationType
{
/// <summary>
/// 声望
/// </summary>
Reputation,
/// <summary>
/// 供奉
/// </summary>
Offering,
}

View File

@@ -20,13 +20,14 @@ namespace Snap.Hutao.Web.Hutao;
/// </summary>
// [Injection(InjectAs.Transient)]
[HttpClient(HttpClientConfigration.Default)]
internal class HutaoClient
internal class HomaClient
{
private const string HutaoAPI = "https://hutao-api.snapgenshin.com";
private const string HutaoAPI = "https://homa.snapgenshin.com";
private readonly HttpClient httpClient;
private readonly GameRecordClient gameRecordClient;
private readonly JsonSerializerOptions options;
private readonly ILogger<HomaClient> logger;
/// <summary>
/// 构造一个新的胡桃API客户端
@@ -34,11 +35,13 @@ internal class HutaoClient
/// <param name="httpClient">http客户端</param>
/// <param name="gameRecordClient">游戏记录客户端</param>
/// <param name="options">json序列化选项</param>
public HutaoClient(HttpClient httpClient, GameRecordClient gameRecordClient, JsonSerializerOptions options)
/// <param name="logger">日志器</param>
public HomaClient(HttpClient httpClient, GameRecordClient gameRecordClient, JsonSerializerOptions options, ILogger<HomaClient> logger)
{
this.httpClient = httpClient;
this.gameRecordClient = gameRecordClient;
this.options = options;
this.logger = logger;
}
/// <summary>
@@ -94,10 +97,10 @@ internal class HutaoClient
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色出场率</returns>
public async Task<IEnumerable<AvatarAppearanceRank>> GetAvatarAttendanceRatesAsync(CancellationToken token = default)
public async Task<List<AvatarAppearanceRank>> GetAvatarAttendanceRatesAsync(CancellationToken token = default)
{
Response<IEnumerable<AvatarAppearanceRank>>? resp = await httpClient
.GetFromJsonAsync<Response<IEnumerable<AvatarAppearanceRank>>>($"{HutaoAPI}/Statistics/Avatar/AttendanceRate", token)
Response<List<AvatarAppearanceRank>>? resp = await httpClient
.GetFromJsonAsync<Response<List<AvatarAppearanceRank>>>($"{HutaoAPI}/Statistics/Avatar/AttendanceRate", token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data);
@@ -105,14 +108,14 @@ internal class HutaoClient
/// <summary>
/// 异步获取角色使用率
/// GET /Statistics2/AvatarParticipation
/// GET /Statistics/Avatar/UtilizationRate
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色出场率</returns>
public async Task<IEnumerable<AvatarUsageRank>> GetAvatarParticipations2Async(CancellationToken token = default)
public async Task<List<AvatarUsageRank>> GetAvatarUtilizationRatesAsync(CancellationToken token = default)
{
Response<IEnumerable<AvatarUsageRank>>? resp = await httpClient
.GetFromJsonAsync<Response<IEnumerable<AvatarUsageRank>>>($"{HutaoAPI}/Statistics/Avatar/HoldingRate", token)
Response<List<AvatarUsageRank>>? resp = await httpClient
.GetFromJsonAsync<Response<List<AvatarUsageRank>>>($"{HutaoAPI}/Statistics/Avatar/UtilizationRate", token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data);
@@ -120,14 +123,14 @@ internal class HutaoClient
/// <summary>
/// 异步获取角色/武器/圣遗物搭配
/// GET /Statistics/AvatarReliquaryUsage
/// GET /Statistics/Avatar/AvatarCollocation
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色/武器/圣遗物搭配</returns>
public async Task<IEnumerable<AvatarCollocation>> GetAvatarCollocationsAsync(CancellationToken token = default)
public async Task<List<AvatarCollocation>> GetAvatarCollocationsAsync(CancellationToken token = default)
{
Response<IEnumerable<AvatarCollocation>>? resp = await httpClient
.GetFromJsonAsync<Response<IEnumerable<AvatarCollocation>>>($"{HutaoAPI}/Statistics/Avatar/AvatarCollocation", token)
Response<List<AvatarCollocation>>? resp = await httpClient
.GetFromJsonAsync<Response<List<AvatarCollocation>>>($"{HutaoAPI}/Statistics/Avatar/AvatarCollocation", token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data);
@@ -135,14 +138,14 @@ internal class HutaoClient
/// <summary>
/// 异步获取角色命座信息
/// GET /Statistics/Constellation
/// GET /Statistics/Avatar/HoldingRate
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色图片列表</returns>
public async Task<IEnumerable<AvatarConstellationInfo>> GetAvatarConstellationInfosAsync(CancellationToken token = default)
public async Task<List<AvatarConstellationInfo>> GetAvatarHoldingRatesAsync(CancellationToken token = default)
{
Response<IEnumerable<AvatarConstellationInfo>>? resp = await httpClient
.GetFromJsonAsync<Response<IEnumerable<AvatarConstellationInfo>>>($"{HutaoAPI}/Statistics/Constellation", token)
Response<List<AvatarConstellationInfo>>? resp = await httpClient
.GetFromJsonAsync<Response<List<AvatarConstellationInfo>>>($"{HutaoAPI}/Statistics/Avatar/HoldingRate", token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data);
@@ -150,14 +153,14 @@ internal class HutaoClient
/// <summary>
/// 异步获取队伍出场次数
/// GET /Statistics/TeamCombination
/// GET /Team/Combination
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>队伍出场列表</returns>
public async Task<IEnumerable<TeamAppearance>> GetTeamCombinationsAsync(CancellationToken token = default)
public async Task<List<TeamAppearance>> GetTeamCombinationsAsync(CancellationToken token = default)
{
Response<IEnumerable<TeamAppearance>>? resp = await httpClient
.GetFromJsonAsync<Response<IEnumerable<TeamAppearance>>>($"{HutaoAPI}/Team/Combination", token)
Response<List<TeamAppearance>>? resp = await httpClient
.GetFromJsonAsync<Response<List<TeamAppearance>>>($"{HutaoAPI}/Statistics/Team/Combination", token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data);
@@ -195,14 +198,8 @@ internal class HutaoClient
/// <param name="playerRecord">玩家记录</param>
/// <param name="token">取消令牌</param>
/// <returns>响应</returns>
public async Task<Response<string>?> UploadRecordAsync(SimpleRecord playerRecord, CancellationToken token = default)
public Task<Response<string>?> UploadRecordAsync(SimpleRecord playerRecord, CancellationToken token = default)
{
HttpResponseMessage response = await httpClient
.PostAsJsonAsync($"{HutaoAPI}/Record/Upload", playerRecord, options, token)
.ConfigureAwait(false);
return await response.Content
.ReadFromJsonAsync<Response<string>>(options, token)
.ConfigureAwait(false);
return httpClient.TryCatchPostAsJsonAsync<SimpleRecord, Response<string>>($"{HutaoAPI}/Record/Upload", playerRecord, options, logger, token);
}
}

View File

@@ -8,12 +8,14 @@ namespace Snap.Hutao.Web.Hutao.Model.Converter;
/// </summary>
internal class ReliquarySetsConverter : JsonConverter<ReliquarySets>
{
private const char Separator = ',';
/// <inheritdoc/>
public override ReliquarySets? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.GetString() is string source)
{
string[] sets = source.Split(';');
string[] sets = source.Split(Separator, StringSplitOptions.RemoveEmptyEntries);
return new(sets.Select(set => new ReliquarySet(set)));
}
else
@@ -25,6 +27,6 @@ internal class ReliquarySetsConverter : JsonConverter<ReliquarySets>
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, ReliquarySets value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(';', value));
writer.WriteStringValue(string.Join(Separator, value));
}
}

View File

@@ -19,6 +19,7 @@ public class SimpleAvatar
AvatarId = character.Id;
WeaponId = character.Weapon.Id;
ReliquarySetIds = character.Reliquaries.Select(r => r.ReliquarySet.Id);
ActivedConstellationNumber = character.ActivedConstellationNum;
}
/// <summary>

View File

@@ -16,14 +16,14 @@ public class ReliquarySet
{
string[]? deconstructed = set.Split('-');
Id = int.Parse(deconstructed[0]);
EquipAffixId = int.Parse(deconstructed[0]);
Count = int.Parse(deconstructed[1]);
}
/// <summary>
/// Id
/// </summary>
public int Id { get; }
public int EquipAffixId { get; }
/// <summary>
/// 个数
@@ -33,6 +33,6 @@ public class ReliquarySet
/// <inheritdoc/>
public override string ToString()
{
return $"{Id}-{Count}";
return $"{EquipAffixId}-{Count}";
}
}

View File

@@ -2,11 +2,17 @@
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 队伍出场次数
/// </summary>
public class TeamAppearance
{
/// <summary>
/// 层
/// </summary>
public int Floor { get; set; }
/// <summary>
/// 上半
/// </summary>

View File

@@ -40,16 +40,6 @@ public class Response : ISupportValidation
[JsonPropertyName("message")]
public string Message { get; set; } = default!;
/// <summary>
/// 响应是否正常
/// </summary>
/// <param name="response">响应</param>
/// <returns>是否Ok</returns>
public static bool IsOk(Response? response)
{
return response is not null && response.ReturnCode == 0;
}
/// <inheritdoc/>
public bool Validate()
{
@@ -89,6 +79,17 @@ public class Response<TData> : Response
[JsonPropertyName("data")]
public TData? Data { get; set; }
/// <summary>
/// 响应是否正常
/// </summary>
/// <param name="response">响应</param>
/// <returns>是否Ok</returns>
[MemberNotNullWhen(true, nameof(Data))]
public bool IsOk()
{
return ReturnCode == 0;
}
/// <inheritdoc/>
public override int GetHashCode()
{

View File

@@ -7,7 +7,7 @@ using Windows.Win32.System.Diagnostics.ToolHelp;
namespace Snap.Hutao.Win32;
/// <summary>
/// 内存拓展 for <see cref="Memory{T}"/> <see cref="Span{T}"/>
/// 内存拓展 for <see cref="Memory{T}"/> and <see cref="Span{T}"/>
/// </summary>
internal static class MemoryExtensions
{

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Windows.Graphics;
using Windows.Win32.System.Diagnostics.ToolHelp;
namespace Snap.Hutao.Win32;