package convert impl

This commit is contained in:
DismissedLight
2023-01-27 11:22:25 +08:00
parent 7d4a8cdcd9
commit 2518ae0b90
48 changed files with 1267 additions and 126 deletions

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Installer;
internal class Program internal class Program
{ {
private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Appx"; private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock";
private const string ValueName = "AllowDevelopmentWithoutDevLicense"; private const string ValueName = "AllowDevelopmentWithoutDevLicense";
public static async Task Main(string[] args) public static async Task Main(string[] args)

View File

@@ -25,7 +25,6 @@ public partial class App : Application
/// Initializes the singleton application object. /// Initializes the singleton application object.
/// </summary> /// </summary>
/// <param name="logger">日志器</param> /// <param name="logger">日志器</param>
/// <param name="appCenter">App Center</param>
public App(ILogger<App> logger) public App(ILogger<App> logger)
{ {
// load app resource // load app resource

View File

@@ -38,7 +38,9 @@ internal static class IocConfiguration
{ {
if (context.Database.GetPendingMigrations().Any()) if (context.Database.GetPendingMigrations().Any())
{ {
#if DEBUG
Debug.WriteLine("[Debug] Performing AppDbContext Migrations"); Debug.WriteLine("[Debug] Performing AppDbContext Migrations");
#endif
context.Database.Migrate(); context.Database.Migrate();
} }
} }

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using System.Security.Cryptography;
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 文件摘要
/// </summary>
internal static class FileDigest
{
/// <summary>
/// 异步获取文件 Md5 摘要
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="token">取消令牌</param>
/// <returns>文件 Md5 摘要</returns>
public static async Task<string> GetMd5Async(string filePath, CancellationToken token)
{
using (FileStream stream = File.OpenRead(filePath))
{
using (MD5 md5 = MD5.Create())
{
byte[] bytes = await md5.ComputeHashAsync(stream, token).ConfigureAwait(false);
return System.Convert.ToHexString(bytes);
}
}
}
}

View File

@@ -109,4 +109,4 @@ internal readonly struct FilePath : IEquatable<FilePath>
{ {
return Value.GetHashCode(); return Value.GetHashCode();
} }
} }

View File

@@ -24,7 +24,10 @@ internal static class SettingKeys
public const string LaunchTimes = "LaunchTimes"; public const string LaunchTimes = "LaunchTimes";
/// <summary> /// <summary>
/// 静态资源合约V1 /// 静态资源合约
/// 新增合约时 请注意
/// <see cref="StaticResource.FulfillAllContracts"/>
/// 与 <see cref="StaticResource.IsAnyUnfulfilledContractPresent"/>
/// </summary> /// </summary>
public const string StaticResourceV1Contract = "StaticResourceV1Contract"; public const string StaticResourceV1Contract = "StaticResourceV1Contract";
@@ -37,4 +40,9 @@ internal static class SettingKeys
/// 静态资源合约V3 刷新 Skill Talent /// 静态资源合约V3 刷新 Skill Talent
/// </summary> /// </summary>
public const string StaticResourceV3Contract = "StaticResourceV3Contract"; public const string StaticResourceV3Contract = "StaticResourceV3Contract";
/// <summary>
/// 静态资源合约V4 刷新 AvatarIcon
/// </summary>
public const string StaticResourceV4Contract = "StaticResourceV4Contract";
} }

View File

@@ -8,6 +8,17 @@ namespace Snap.Hutao.Core.Setting;
/// </summary> /// </summary>
internal static class StaticResource internal static class StaticResource
{ {
/// <summary>
/// 完成所有合约
/// </summary>
public static void FulfillAllContracts()
{
LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV2Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV3Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV4Contract, true);
}
/// <summary> /// <summary>
/// 提供的合约是否未完成 /// 提供的合约是否未完成
/// </summary> /// </summary>
@@ -26,6 +37,7 @@ internal static class StaticResource
{ {
return !LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false) return !LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false)
|| (!LocalSetting.Get(SettingKeys.StaticResourceV2Contract, false)) || (!LocalSetting.Get(SettingKeys.StaticResourceV2Contract, false))
|| (!LocalSetting.Get(SettingKeys.StaticResourceV3Contract, false)); || (!LocalSetting.Get(SettingKeys.StaticResourceV3Contract, false))
|| (!LocalSetting.Get(SettingKeys.StaticResourceV4Contract, false));
} }
} }

View File

@@ -1,36 +1,45 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using System.Collections.Immutable;
namespace Snap.Hutao.Model.Binding.LaunchGame; namespace Snap.Hutao.Model.Binding.LaunchGame;
/// <summary>
/// 服务器方案
/// </summary>
/// <summary> /// <summary>
/// 启动方案 /// 启动方案
/// </summary> /// </summary>
public class LaunchScheme public class LaunchScheme
{ {
/// <summary>
/// 已知的启动方案
/// </summary>
public static readonly ImmutableList<LaunchScheme> KnownSchemes = new List<LaunchScheme>()
{
new LaunchScheme("官方服 | 天空岛", "eYd89JmJ", "18", "1", "1"),
new LaunchScheme("渠道服 | 世界树", "KAtdSsoQ", "17", "14", "0"),
new LaunchScheme("国际服 | 部分支持", "gcStgarh", "10", "1", "0"),
}.ToImmutableList();
/// <summary> /// <summary>
/// 构造一个新的启动方案 /// 构造一个新的启动方案
/// </summary> /// </summary>
/// <param name="name">名称</param> /// <param name="displayName">名称</param>
/// <param name="channel">通道</param> /// <param name="channel">通道</param>
/// <param name="cps">通道描述字符串</param>
/// <param name="subChannel">子通道</param> /// <param name="subChannel">子通道</param>
/// <param name="launcherId">启动器Id</param> /// <param name="launcherId">启动器Id</param>
public LaunchScheme(string name, string channel, string subChannel, string launcherId) private LaunchScheme(string displayName, string key, string launcherId, string channel, string subChannel)
{ {
Name = name; DisplayName = displayName;
Channel = channel; Channel = channel;
SubChannel = subChannel; SubChannel = subChannel;
LauncherId = launcherId; LauncherId = launcherId;
Key = key;
} }
/// <summary> /// <summary>
/// 名称 /// 名称
/// </summary> /// </summary>
public string Name { get; set; } public string DisplayName { get; set; }
/// <summary> /// <summary>
/// 通道 /// 通道
@@ -46,4 +55,14 @@ public class LaunchScheme
/// 启动器Id /// 启动器Id
/// </summary> /// </summary>
public string LauncherId { get; set; } public string LauncherId { get; set; }
/// <summary>
/// API Key
/// </summary>
public string Key { get; set; }
/// <summary>
/// 是否为海外
/// </summary>
public bool IsOversea { get => LauncherId == "10"; }
} }

View File

@@ -101,12 +101,14 @@ public static class AvatarIds
{ {
Name = "旅行者", Name = "旅行者",
Icon = "UI_AvatarIcon_PlayerBoy", Icon = "UI_AvatarIcon_PlayerBoy",
SideIcon = "UI_AvatarIcon_Side_PlayerBoy",
Quality = Intrinsic.ItemQuality.QUALITY_ORANGE, Quality = Intrinsic.ItemQuality.QUALITY_ORANGE,
}, },
[PlayerGirl] = new() [PlayerGirl] = new()
{ {
Name = "旅行者", Name = "旅行者",
Icon = "UI_AvatarIcon_PlayerGirl", Icon = "UI_AvatarIcon_PlayerGirl",
SideIcon = "UI_AvatarIcon_Side_PlayerGirl",
Quality = Intrinsic.ItemQuality.QUALITY_ORANGE, Quality = Intrinsic.ItemQuality.QUALITY_ORANGE,
}, },
}; };

View File

@@ -11,7 +11,7 @@
<Identity <Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d" Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio" Publisher="CN=DGP Studio&amp;comma DissmissedLight"
Version="1.3.13.0" /> Version="1.3.13.0" />
<Properties> <Properties>
@@ -24,7 +24,7 @@
<Dependencies> <Dependencies>
<!--<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />--> <!--<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />-->
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.18362.0" MaxVersionTested="10.0.22000.0" /> <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.18362.0" MaxVersionTested="10.0.22621.0" />
</Dependencies> </Dependencies>
<Resources> <Resources>

View File

@@ -74,11 +74,8 @@ public static class GachaStatisticsExtensions
private static Color GetColorByName(string name) private static Color GetColorByName(string name)
{ {
byte[] codes = MD5.HashData(Encoding.UTF8.GetBytes(name)); Span<byte> codes = MD5.HashData(Encoding.UTF8.GetBytes(name));
Span<byte> first = new(codes, 0, 5); Color color = Color.FromArgb(255, codes.Slice(0, 5).Average(), codes.Slice(5, 5).Average(), codes.Slice(10, 5).Average());
Span<byte> second = new(codes, 5, 5);
Span<byte> third = new(codes, 10, 5);
Color color = Color.FromArgb(255, first.Average(), second.Average(), third.Average());
return color; return color;
} }
} }

View File

@@ -54,7 +54,8 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
bool isEmptyHistoryWishVisible = entry.GetBoolean(); bool isEmptyHistoryWishVisible = entry.GetBoolean();
IOrderedEnumerable<GachaItem> orderedItems = items.OrderBy(i => i.Id); IOrderedEnumerable<GachaItem> orderedItems = items.OrderBy(i => i.Id);
return await Task.Run(() => CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap, isEmptyHistoryWishVisible)).ConfigureAwait(false); await ThreadHelper.SwitchToBackgroundAsync();
return CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap, isEmptyHistoryWishVisible);
} }
private static GachaStatistics CreateCore( private static GachaStatistics CreateCore(

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.IO.Ini;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Game.Unlocker;
using Snap.Hutao.View.Dialog;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 游戏文件操作异常
/// </summary>
internal class GameFileOperationException : Exception
{
/// <summary>
/// 构造一个新的用户数据损坏异常
/// </summary>
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public GameFileOperationException(string message, Exception innerException)
: base($"游戏文件操作失败: {message}", innerException)
{
}
}

View File

@@ -12,8 +12,11 @@ using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Game.Locator; using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.Unlocker; using Snap.Hutao.Service.Game.Unlocker;
using Snap.Hutao.View.Dialog; using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using Snap.Hutao.Web.Response;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@@ -24,13 +27,15 @@ namespace Snap.Hutao.Service.Game;
/// 游戏服务 /// 游戏服务
/// </summary> /// </summary>
[Injection(InjectAs.Singleton, typeof(IGameService))] [Injection(InjectAs.Singleton, typeof(IGameService))]
internal class GameService : IGameService, IDisposable [SuppressMessage("", "CA1001")]
internal class GameService : IGameService
{ {
private const string GamePathKey = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}"; private const string GamePathKey = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}";
private const string ConfigFile = "config.ini"; private const string ConfigFile = "config.ini";
private readonly IServiceScopeFactory scopeFactory; private readonly IServiceScopeFactory scopeFactory;
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;
private readonly PackageConverter packageConverter;
private readonly SemaphoreSlim gameSemaphore = new(1); private readonly SemaphoreSlim gameSemaphore = new(1);
private ObservableCollection<GameAccount>? gameAccounts; private ObservableCollection<GameAccount>? gameAccounts;
@@ -40,10 +45,12 @@ internal class GameService : IGameService, IDisposable
/// </summary> /// </summary>
/// <param name="scopeFactory">范围工厂</param> /// <param name="scopeFactory">范围工厂</param>
/// <param name="memoryCache">内存缓存</param> /// <param name="memoryCache">内存缓存</param>
public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache) /// <param name="packageConverter">游戏文件包转换器</param>
public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache, PackageConverter packageConverter)
{ {
this.scopeFactory = scopeFactory; this.scopeFactory = scopeFactory;
this.memoryCache = memoryCache; this.memoryCache = memoryCache;
this.packageConverter = packageConverter;
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -159,15 +166,26 @@ internal class GameService : IGameService, IDisposable
} }
/// <inheritdoc/> /// <inheritdoc/>
public void SetMultiChannel(LaunchScheme scheme) public bool SetMultiChannel(LaunchScheme scheme)
{ {
string gamePath = GetGamePathSkipLocator(); string gamePath = GetGamePathSkipLocator();
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile); string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile);
List<IniElement> elements; List<IniElement> elements;
using (FileStream readStream = File.OpenRead(configPath)) try
{ {
elements = IniSerializer.Deserialize(readStream).ToList(); using (FileStream readStream = File.OpenRead(configPath))
{
elements = IniSerializer.Deserialize(readStream).ToList();
}
}
catch (DirectoryNotFoundException dnfEx)
{
throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", dnfEx);
}
catch (UnauthorizedAccessException uaEx)
{
throw new GameFileOperationException($"无法读取或保存配置文件,请以管理员模式重试。", uaEx);
} }
bool changed = false; bool changed = false;
@@ -203,10 +221,36 @@ internal class GameService : IGameService, IDisposable
IniSerializer.Serialize(writeStream, elements); IniSerializer.Serialize(writeStream, elements);
} }
} }
return changed;
}
/// <inheritdoc/>
public async Task<bool> ReplaceGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{
string gamePath = GetGamePathSkipLocator();
string gameFolder = Path.GetDirectoryName(gamePath)!;
string gameFileName = Path.GetFileName(gamePath);
if (launchScheme.IsOversea && gameFileName == "GenshinImpact.exe")
{
// Already that scheme, no need to replace files
return true;
}
else if (!launchScheme.IsOversea && gameFileName == "YuanShen.exe")
{
// Already that scheme, no need to replace files
return true;
}
// TODO: we still need to handle the Bilibili scheme.
await packageConverter.ReplaceGameResourceAsync(launchScheme, gameFolder, progress).ConfigureAwait(false);
// We need to change the gamePath if we switch.
return true;
} }
/// <inheritdoc/> /// <inheritdoc/>
[SuppressMessage("", "IDE0046")]
public bool IsGameRunning() public bool IsGameRunning()
{ {
if (gameSemaphore.CurrentCount == 0) if (gameSemaphore.CurrentCount == 0)
@@ -387,10 +431,4 @@ internal class GameService : IGameService, IDisposable
await scope.ServiceProvider.GetRequiredService<AppDbContext>().GameAccounts.RemoveAndSaveAsync(gameAccount).ConfigureAwait(false); await scope.ServiceProvider.GetRequiredService<AppDbContext>().GameAccounts.RemoveAndSaveAsync(gameAccount).ConfigureAwait(false);
} }
} }
/// <inheritdoc/>
public void Dispose()
{
gameSemaphore?.Dispose();
}
} }

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Model.Binding.LaunchGame; using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Package;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game; namespace Snap.Hutao.Service.Game;
@@ -83,6 +84,14 @@ internal interface IGameService
/// <returns>任务</returns> /// <returns>任务</returns>
ValueTask RemoveGameAccountAsync(GameAccount gameAccount); ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
/// <summary>
/// 替换游戏资源
/// </summary>
/// <param name="launchScheme">目标启动方案</param>
/// <param name="progress">进度</param>
/// <returns>是否替换成功</returns>
Task<bool> ReplaceGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
/// <summary> /// <summary>
/// 修改注册表中的账号信息 /// 修改注册表中的账号信息
/// </summary> /// </summary>
@@ -94,5 +103,6 @@ internal interface IGameService
/// 设置多通道值 /// 设置多通道值
/// </summary> /// </summary>
/// <param name="scheme">方案</param> /// <param name="scheme">方案</param>
void SetMultiChannel(LaunchScheme scheme); /// <returns>是否更改了ini文件</returns>
bool SetMultiChannel(LaunchScheme scheme);
} }

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 转换方向
/// </summary>
internal enum ConvertDirection
{
/// <summary>
/// 国际服转国服
/// </summary>
OverseaToChinese,
/// <summary>
/// 国服转国际服
/// </summary>
ChineseToOversea,
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包操作
/// </summary>
[DebuggerDisplay("Action:{Type} Target:{Target} Cache:{Cache}")]
internal class ItemOperationInfo
{
/// <summary>
/// 构造一个新的包操作
/// </summary>
/// <param name="type">操作类型</param>
/// <param name="target">目标</param>
/// <param name="cache">缓存</param>
public ItemOperationInfo(ItemOperationType type, VersionItem target, VersionItem cache)
{
Type = type;
Target = target.RemoteName;
Cache = cache.RemoteName;
Md5 = target.Md5;
TotalBytes = target.FileSize;
}
/// <summary>
/// 操作的类型
/// </summary>
public ItemOperationType Type { get; set; }
/// <summary>
/// 目标文件
/// </summary>
public string Target { get; set; }
/// <summary>
/// 移动至中时的名称
/// </summary>
public string Cache { get; set; }
/// <summary>
/// 文件的目标Md5
/// </summary>
public string Md5 { get; set; }
/// <summary>
/// 文件的目标大小 Byte
/// </summary>
public long TotalBytes { get; set; }
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包文件操作的类型
/// </summary>
internal enum ItemOperationType
{
/// <summary>
/// 添加
/// </summary>
Add,
/// <summary>
/// 删除
/// </summary>
Remove,
/// <summary>
/// 替换
/// </summary>
Replace,
}

View File

@@ -0,0 +1,274 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using Snap.Hutao.Web.Response;
using System.IO;
using System.Net.Http;
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 游戏文件包转换器
/// </summary>
[HttpClient(HttpClientConfigration.Default)]
internal class PackageConverter
{
private const string GenshinImpactData = "GenshinImpact_Data";
private const string YuanShenData = "YuanShen_Data";
private readonly ResourceClient resourceClient;
private readonly JsonSerializerOptions options;
private readonly HttpClient httpClient;
/// <summary>
/// 构造一个新的游戏文件转换器
/// </summary>
/// <param name="resourceClient">资源客户端</param>
/// <param name="options">Json序列化选项</param>
/// <param name="httpClient">http客户端</param>
public PackageConverter(ResourceClient resourceClient, JsonSerializerOptions options, HttpClient httpClient)
{
this.resourceClient = resourceClient;
this.options = options;
this.httpClient = httpClient;
}
/// <summary>
/// 异步替换游戏资源
/// 调用前需要确认本地文件与服务器上的不同
/// </summary>
/// <param name="targetScheme">目标启动方案</param>
/// <param name="gameFolder">游戏目录</param>
/// <param name="progress">进度</param>
/// <returns>任务</returns>
public async Task<bool> ReplaceGameResourceAsync(LaunchScheme targetScheme, string gameFolder, IProgress<PackageReplaceStatus> progress)
{
await ThreadHelper.SwitchToBackgroundAsync();
progress.Report(new("查询游戏资源信息"));
Response<GameResource> response = await resourceClient.GetResourceAsync(targetScheme).ConfigureAwait(false);
if (response.IsOk())
{
GameResource remoteGameResouce = response.Data;
string scatteredFilesUrl = remoteGameResouce.Game.Latest.DecompressedPath;
Uri pkgVersionUri = new($"{scatteredFilesUrl}/pkg_version");
ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese;
progress.Report(new("下载包版本信息"));
Dictionary<string, VersionItem> remoteItems;
using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false))
{
remoteItems = await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false);
}
Dictionary<string, VersionItem> localItems;
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, "pkg_version")))
{
localItems = await GetVersionItemsAsync(localSteam, direction, ConvertRemoteName).ConfigureAwait(false);
}
IEnumerable<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems);
var a = diffOperations.ToList();
await ReplaceGameResourceCoreAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false);
return true;
}
return false;
}
private static string ConvertRemoteName(string remoteName, ConvertDirection direction)
{
// 我们已经提前重命名了整个 Data 文件夹 所以需要将 RemoteName 中的 Data 同样替换
if (remoteName.StartsWith(YuanShenData) || remoteName.StartsWith(GenshinImpactData))
{
return direction switch
{
ConvertDirection.OverseaToChinese => $"{YuanShenData}{remoteName[GenshinImpactData.Length..]}",
ConvertDirection.ChineseToOversea => $"{GenshinImpactData}{remoteName[YuanShenData.Length..]}",
_ => remoteName,
};
}
return remoteName;
}
private static IEnumerable<ItemOperationInfo> GetItemOperationInfos(Dictionary<string, VersionItem> remote, Dictionary<string, VersionItem> local)
{
foreach ((string remoteName, VersionItem remoteItem) in remote)
{
if (local.TryGetValue(remoteName, out VersionItem? localItem))
{
if (remoteItem.Md5 != localItem.Md5)
{
// 本地发现了同名且不同MD5的项需要替换为服务器上的项
yield return new(ItemOperationType.Replace, remoteItem, localItem);
}
local.Remove(remoteName);
}
else
{
// 本地没有发现同名项
yield return new(ItemOperationType.Add, remoteItem, remoteItem);
}
}
IEnumerable<ItemOperationInfo> removes = local.Select(kvp => new ItemOperationInfo(ItemOperationType.Remove, kvp.Value, kvp.Value));
foreach (ItemOperationInfo item in removes)
{
yield return item;
}
}
private static void RenameDataFolder(string gameFolder, ConvertDirection direction)
{
string yuanShenData = Path.Combine(gameFolder, YuanShenData);
string genshinImpactData = Path.Combine(gameFolder, GenshinImpactData);
// We have check the exe path previously
// so we assume the data folder is present
if (direction == ConvertDirection.ChineseToOversea)
{
Directory.Move(yuanShenData, genshinImpactData);
}
else
{
Directory.Move(genshinImpactData, yuanShenData);
}
}
private static void MoveToCache(string cacheFolder, string cacheName, string targetFullPath)
{
string cacheFilePath = Path.Combine(cacheFolder, cacheName);
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)!);
File.Move(targetFullPath, cacheFilePath, true);
}
private async Task ReplaceGameResourceCoreAsync(IEnumerable<ItemOperationInfo> operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress<PackageReplaceStatus> progress)
{
// 重命名 _Data 目录
RenameDataFolder(gameFolder, direction);
// Ensure cache folder
string cacheFolder = Path.Combine(gameFolder, "Screenshot", "HutaoCache");
// 执行下载与移动操作
foreach (ItemOperationInfo info in operations)
{
progress.Report(new($"{info.Target}"));
string targetFilePath = Path.Combine(gameFolder, info.Target);
string cacheFilePath = Path.Combine(cacheFolder, info.Cache);
switch (info.Type)
{
case ItemOperationType.Add:
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info).ConfigureAwait(false);
break;
case ItemOperationType.Replace:
{
MoveToCache(cacheFolder, info.Cache, targetFilePath);
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info).ConfigureAwait(false);
break;
}
case ItemOperationType.Remove:
MoveToCache(cacheFolder, info.Cache, targetFilePath);
break;
default:
break;
}
}
// 重新下载所有 *pkg_version 文件
await ReplacePackageVersionsAsync(scatteredFilesUrl, gameFolder).ConfigureAwait(false);
}
private async Task ReplaceFromCacheOrWebAsync(string cacheFilePath, string targetFilePath, string scatteredFilesUrl, ItemOperationInfo info)
{
if (File.Exists(cacheFilePath))
{
string remoteMd5 = await FileDigest.GetMd5Async(cacheFilePath, CancellationToken.None).ConfigureAwait(false);
if (info.Md5 == remoteMd5.ToLowerInvariant() && new FileInfo(cacheFilePath).Length == info.TotalBytes)
{
// Valid, move it to target path
// There shouldn't be any file in the same name
File.Move(cacheFilePath, targetFilePath, false);
return;
}
else
{
// Invalid file, delete it
File.Delete(cacheFilePath);
}
}
// Cache no item, download it anyway.
using (FileStream fileStream = File.Create(targetFilePath))
{
using (Stream webStream = await httpClient.GetStreamAsync($"{scatteredFilesUrl}/{info.Target}").ConfigureAwait(false))
{
await webStream.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
}
private async Task ReplacePackageVersionsAsync(string scatteredFilesUrl, string gameFolder)
{
foreach (string audioPkgVersionFilePath in Directory.EnumerateFiles(gameFolder, "*pkg_version"))
{
string audioPkgVersionFileName = Path.GetFileName(audioPkgVersionFilePath);
using (FileStream audioPkgVersionFileStream = File.Create(audioPkgVersionFilePath))
{
using (Stream webStream = await httpClient.GetStreamAsync($"{scatteredFilesUrl}/{audioPkgVersionFileName}").ConfigureAwait(false))
{
await webStream.CopyToAsync(audioPkgVersionFileStream).ConfigureAwait(false);
}
}
}
}
private async Task<Dictionary<string, VersionItem>> GetVersionItemsAsync(Stream stream)
{
Dictionary<string, VersionItem> results = new();
using (StreamReader reader = new(stream))
{
while (await reader.ReadLineAsync().ConfigureAwait(false) is string raw)
{
if (!string.IsNullOrEmpty(raw))
{
VersionItem item = JsonSerializer.Deserialize<VersionItem>(raw, options)!;
results.Add(item.RemoteName, item);
}
}
}
return results;
}
private async Task<Dictionary<string, VersionItem>> GetVersionItemsAsync(Stream stream, ConvertDirection direction, Func<string, ConvertDirection, string> nameConverter)
{
Dictionary<string, VersionItem> results = new();
using (StreamReader reader = new(stream))
{
while (await reader.ReadLineAsync().ConfigureAwait(false) is string raw)
{
if (!string.IsNullOrEmpty(raw))
{
VersionItem item = JsonSerializer.Deserialize<VersionItem>(raw, options)!;
results.Add(nameConverter(item.RemoteName, direction), item);
}
}
}
return results;
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包更新状态
/// </summary>
public class PackageReplaceStatus
{
/// <summary>
/// 构造一个新的包更新状态
/// </summary>
/// <param name="description">描述</param>
public PackageReplaceStatus(string description)
{
Description = description;
}
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; }
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包版本项
/// </summary>
internal class VersionItem
{
/// <summary>
/// 服务器上的名称
/// </summary>
[JsonPropertyName("remoteName")]
public string RemoteName { get; set; } = default!;
/// <summary>
/// MD5校验值
/// </summary>
[JsonPropertyName("md5")]
public string Md5 { get; set; } = default!;
/// <summary>
/// 文件尺寸
/// </summary>
[JsonPropertyName("fileSize")]
public long FileSize { get; set; }
}

View File

@@ -4,6 +4,7 @@
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension; using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
@@ -131,9 +132,10 @@ internal partial class MetadataService : IMetadataService, IMetadataServiceIniti
string fileFullName = $"{fileName}.json"; string fileFullName = $"{fileName}.json";
bool skip = false; bool skip = false;
if (File.Exists(Path.Combine(metadataFolderPath, fileFullName))) string fileFullPath = Path.Combine(metadataFolderPath, fileFullName);
if (File.Exists(fileFullPath))
{ {
skip = md5 == await GetFileMd5Async(fileFullName, token).ConfigureAwait(false); skip = md5 == await FileDigest.GetMd5Async(fileFullPath, token).ConfigureAwait(false);
} }
if (!skip) if (!skip)
@@ -145,18 +147,6 @@ internal partial class MetadataService : IMetadataService, IMetadataServiceIniti
}); });
} }
private async Task<string> GetFileMd5Async(string fileFullName, CancellationToken token)
{
using (FileStream stream = File.OpenRead(Path.Combine(metadataFolderPath, fileFullName)))
{
byte[] bytes = await MD5.Create()
.ComputeHashAsync(stream, token)
.ConfigureAwait(false);
return Convert.ToHexString(bytes);
}
}
private async Task DownloadMetadataAsync(string fileFullName, CancellationToken token) private async Task DownloadMetadataAsync(string fileFullName, CancellationToken token)
{ {
Stream sourceStream = await httpClient Stream sourceStream = await httpClient

View File

@@ -87,6 +87,7 @@
<None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" /> <None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" />
<None Remove="View\Dialog\GachaLogUrlDialog.xaml" /> <None Remove="View\Dialog\GachaLogUrlDialog.xaml" />
<None Remove="View\Dialog\GameAccountNameDialog.xaml" /> <None Remove="View\Dialog\GameAccountNameDialog.xaml" />
<None Remove="View\Dialog\LaunchGamePackageConvertDialog.xaml" />
<None Remove="View\Dialog\LoginMihoyoBBSDialog.xaml" /> <None Remove="View\Dialog\LoginMihoyoBBSDialog.xaml" />
<None Remove="View\Dialog\SignInWebViewDialog.xaml" /> <None Remove="View\Dialog\SignInWebViewDialog.xaml" />
<None Remove="View\Dialog\UserDialog.xaml" /> <None Remove="View\Dialog\UserDialog.xaml" />
@@ -99,6 +100,7 @@
<None Remove="View\Page\DailyNotePage.xaml" /> <None Remove="View\Page\DailyNotePage.xaml" />
<None Remove="View\Page\GachaLogPage.xaml" /> <None Remove="View\Page\GachaLogPage.xaml" />
<None Remove="View\Page\HutaoDatabasePage.xaml" /> <None Remove="View\Page\HutaoDatabasePage.xaml" />
<None Remove="View\Page\HutaoDatabasePresentPage.xaml" />
<None Remove="View\Page\LaunchGamePage.xaml" /> <None Remove="View\Page\LaunchGamePage.xaml" />
<None Remove="View\Page\LoginMihoyoUserPage.xaml" /> <None Remove="View\Page\LoginMihoyoUserPage.xaml" />
<None Remove="View\Page\SettingPage.xaml" /> <None Remove="View\Page\SettingPage.xaml" />
@@ -171,11 +173,11 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.0.65" /> <PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.0.65" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.138-beta"> <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.188-beta">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25276-preview" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25281-preview" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221209.1" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221209.1" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.435"> <PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.435">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@@ -199,6 +201,16 @@
<ItemGroup> <ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" /> <None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\LaunchGamePackageConvertDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\HutaoDatabasePresentPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="View\WelcomeView.xaml"> <Page Update="View\WelcomeView.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>

View File

@@ -2,6 +2,7 @@
x:Class="Snap.Hutao.View.Control.StatisticsCard" x:Class="Snap.Hutao.View.Control.StatisticsCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:CommunityToolkit.WinUI.UI.Converters"
xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls" xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -62,9 +63,17 @@
</Grid> </Grid>
</DataTemplate> </DataTemplate>
<converters:BoolToObjectConverter
x:Key="BoolToBrushConverter"
FalseValue="{ThemeResource SystemFillColorCriticalBackgroundBrush}"
TrueValue="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
<DataTemplate x:Key="OrangeGridTemplate" x:DataType="shmbg:SummaryItem"> <DataTemplate x:Key="OrangeGridTemplate" x:DataType="shmbg:SummaryItem">
<Grid Width="40" Margin="0,4,4,0"> <Grid Width="40" Margin="0,4,4,0">
<Border Style="{StaticResource BorderCardStyle}" ToolTipService.ToolTip="{Binding TimeFormatted}"> <Border
Background="{Binding IsUp, Converter={StaticResource BoolToBrushConverter}}"
Style="{StaticResource BorderCardStyle}"
ToolTipService.ToolTip="{Binding TimeFormatted}">
<StackPanel> <StackPanel>
<shvc:ItemIcon <shvc:ItemIcon
Width="40" Width="40"

View File

@@ -0,0 +1,25 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.LaunchGamePackageConvertDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shvd="using:Snap.Hutao.View.Dialog"
Title="ת<><D7AA><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>"
d:DataContext="{d:DesignInstance shvd:LaunchGamePackageConvertDialog}"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<StackPanel>
<TextBlock Text="ת<><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD>һ<EFBFBD><D2BB>ʱ<EFBFBD><CAB1>"/>
<TextBlock
MinWidth="480"
Margin="0,8,0,2"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Description}"/>
<ProgressBar IsIndeterminate="True"/>
</StackPanel>
</ContentDialog>

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control;
namespace Snap.Hutao.View.Dialog;
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϸ<EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ת<EFBFBD><D7AA><EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>
/// </summary>
public sealed partial class LaunchGamePackageConvertDialog : ContentDialog
{
private static readonly DependencyProperty DescriptionProperty = Property<LaunchGamePackageConvertDialog>.Depend(nameof(Description), "<22><><EFBFBD>Ժ<EFBFBD>");
/// <summary>
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µ<EFBFBD><C2B5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϸ<EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>ת<EFBFBD><D7AA><EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>
/// </summary>
public LaunchGamePackageConvertDialog()
{
InitializeComponent();
XamlRoot = Ioc.Default.GetRequiredService<MainWindow>().Content.XamlRoot;
DataContext = this;
}
/// <summary>
/// <20><><EFBFBD><EFBFBD>
/// </summary>
public string Description
{
get { return (string)GetValue(DescriptionProperty); }
set { SetValue(DescriptionProperty, value); }
}
}

View File

@@ -138,8 +138,9 @@
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="auto"/> <RowDefinition Height="auto"/>
<RowDefinition Height="auto"/> <RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Margin="8"> <Grid Grid.Row="0" Margin="8">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/> <ColumnDefinition Width="auto"/>
<ColumnDefinition/> <ColumnDefinition/>
@@ -164,14 +165,20 @@
Width="48" Width="48"
Height="48" Height="48"
Margin="8,0,0,0" Margin="8,0,0,0"
Background="Transparent"
Command="{Binding Path=DataContext.RemoveEntryCommand, Source={StaticResource BindingProxy}}" Command="{Binding Path=DataContext.RemoveEntryCommand, Source={StaticResource BindingProxy}}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Content="&#xE74D;" Content="&#xE74D;"
FontFamily="{StaticResource SymbolThemeFontFamily}" FontFamily="{StaticResource SymbolThemeFontFamily}"
Style="{StaticResource ButtonRevealStyle}"
ToolTipService.ToolTip="删除清单"/> ToolTipService.ToolTip="删除清单"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
<ScrollViewer Grid.Row="1" Height="240"> <MenuFlyoutSeparator Grid.Row="1"/>
<ScrollViewer
Grid.Row="2"
Height="296"
Margin="0,2,0,0">
<ItemsControl Margin="8,0,8,8" ItemsSource="{Binding Items}"> <ItemsControl Margin="8,0,8,8" ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
@@ -201,10 +208,9 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
Background="Transparent" Background="Transparent"
BorderBrush="{x:Null}"
BorderThickness="0"
Command="{Binding Path=DataContext.FinishStateCommand, Source={StaticResource BindingProxy}}" Command="{Binding Path=DataContext.FinishStateCommand, Source={StaticResource BindingProxy}}"
CommandParameter="{Binding}"> CommandParameter="{Binding}"
Style="{StaticResource ButtonRevealStyle}">
<Grid Opacity="{Binding IsFinished, Converter={StaticResource BoolToOpacityConverter}}"> <Grid Opacity="{Binding IsFinished, Converter={StaticResource BoolToOpacityConverter}}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/> <ColumnDefinition Width="auto"/>
@@ -225,7 +231,6 @@
Text="{Binding Entity.Count}"/> Text="{Binding Entity.Count}"/>
</Grid> </Grid>
</Button> </Button>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>

View File

@@ -26,7 +26,7 @@
<Rectangle <Rectangle
Height="48" Height="48"
VerticalAlignment="Top" VerticalAlignment="Top"
Fill="{StaticResource CardBackgroundFillColorDefaultBrush}" Fill="{ThemeResource CardBackgroundFillColorDefaultBrush}"
IsHitTestVisible="False"/> IsHitTestVisible="False"/>
<Pivot> <Pivot>
<Pivot.LeftHeader> <Pivot.LeftHeader>
@@ -86,9 +86,9 @@
<PivotItem Header="总览"> <PivotItem Header="总览">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="388"/> <ColumnDefinition MaxWidth="390"/>
<ColumnDefinition MaxWidth="388"/> <ColumnDefinition MaxWidth="390"/>
<ColumnDefinition MaxWidth="388"/> <ColumnDefinition MaxWidth="390"/>
<ColumnDefinition Width="auto" MinWidth="16"/> <ColumnDefinition Width="auto" MinWidth="16"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<shvc:StatisticsCard <shvc:StatisticsCard

View File

@@ -8,7 +8,6 @@
xmlns:mxi="using:Microsoft.Xaml.Interactivity" xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shc="using:Snap.Hutao.Control" xmlns:shc="using:Snap.Hutao.Control"
xmlns:shcb="using:Snap.Hutao.Control.Behavior" xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shcm="using:Snap.Hutao.Control.Markup" xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shmbh="using:Snap.Hutao.Model.Binding.Hutao" xmlns:shmbh="using:Snap.Hutao.Model.Binding.Hutao"
xmlns:shv="using:Snap.Hutao.ViewModel" xmlns:shv="using:Snap.Hutao.ViewModel"

View File

@@ -0,0 +1,311 @@
<shc:ScopedPage
x:Class="Snap.Hutao.View.Page.HutaoDatabasePresentPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:image="using:Snap.Hutao.Control.Image"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shmbh="using:Snap.Hutao.Model.Binding.Hutao"
xmlns:shv="using:Snap.Hutao.ViewModel"
xmlns:shvc="using:Snap.Hutao.View.Control"
xmlns:wsc="using:WinUICommunity.SettingsUI.Controls"
d:DataContext="{d:DesignInstance shv:HutaoDatabaseViewModel}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources>
<DataTemplate x:Key="TeamItemTemplate" x:DataType="shmbh:Team">
<Border Margin="0,0,8,8" Style="{StaticResource BorderCardStyle}">
<Grid Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="108"/>
</Grid.ColumnDefinitions>
<ItemsControl HorizontalAlignment="Left" ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwuc:UniformGrid ColumnSpacing="6" Columns="4"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<image:CachedImage
Width="48"
Height="48"
Margin="-4,-20,-4,-4"
Source="{Binding SideIcon}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="{Binding Rate}"/>
<TextBlock
Margin="0,2,0,0"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Name}"/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<CommandBar DefaultLabelPosition="Right">
<AppBarButton
Command="{Binding ExportAsImageCommand}"
CommandParameter="{Binding ElementName=RenderTargetContainer}"
Label="导出图片"/>
</CommandBar>
<ScrollViewer Grid.Row="1">
<StackPanel
Name="RenderTargetContainer"
MaxWidth="956"
Padding="16"
Background="Transparent">
<TextBlock
Margin="0,0,0,16"
Style="{StaticResource SubheaderTextBlockStyle}"
Text="胡桃数据库 统计数据"/>
<wsc:Setting Content="{Binding Overview.RefreshTime}" Header="数据刷新时间"/>
<cwuc:UniformGrid
Margin="0,16"
ColumnSpacing="16"
Columns="3"
Orientation="Vertical"
RowSpacing="16">
<wsc:Setting Content="{Binding Overview.RecordTotal}" Header="上传记录总数"/>
<wsc:Setting Content="{Binding Overview.SpiralAbyssTotal}" Header="总计深渊记录"/>
<wsc:Setting Padding="16,8" Header="通关深渊记录">
<StackPanel>
<TextBlock Text="{Binding Overview.SpiralAbyssPassedPercent}"/>
<TextBlock
HorizontalAlignment="Right"
Opacity="0.7"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Overview.SpiralAbyssPassed}"/>
</StackPanel>
</wsc:Setting>
<wsc:Setting Padding="16,8" Header="满星深渊记录">
<StackPanel>
<TextBlock Text="{Binding Overview.SpiralAbyssFullStarPercent}"/>
<TextBlock
HorizontalAlignment="Right"
Opacity="0.7"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Overview.SpiralAbyssFullStar}"/>
</StackPanel>
</wsc:Setting>
<wsc:Setting Content="{Binding Overview.SpiralAbyssStarAverage}" Header="平均获取渊星"/>
<wsc:Setting Content="{Binding Overview.SpiralAbyssBattleAverage}" Header="平均战斗次数"/>
</cwuc:UniformGrid>
<TextBlock
Margin="0,0,0,16"
Style="{StaticResource TitleTextBlockStyle}"
Text="第 12 层 角色使用率"
Visibility="Collapsed"/>
<GridView
Margin="0,0,-16,-8"
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
ItemsSource="{Binding AvatarUsageRanks[0].Avatars}"
SelectionMode="None"
Visibility="Collapsed">
<ItemsControl.ItemTemplate>
<DataTemplate>
<shvc:BottomTextControl Text="{Binding Rate}">
<shvc:ItemIcon Icon="{Binding Icon}" Quality="{Binding Quality}"/>
</shvc:BottomTextControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</GridView>
<TextBlock
Margin="0,0,0,16"
Style="{StaticResource TitleTextBlockStyle}"
Text="第 12 层 角色出场率"
Visibility="Collapsed"/>
<GridView
Margin="0,0,-16,-8"
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
ItemsSource="{Binding AvatarAppearanceRanks[0].Avatars}"
SelectionMode="None"
Visibility="Collapsed">
<ItemsControl.ItemTemplate>
<DataTemplate>
<shvc:BottomTextControl Text="{Binding Rate}">
<shvc:ItemIcon Icon="{Binding Icon}" Quality="{Binding Quality}"/>
</shvc:BottomTextControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</GridView>
<StackPanel Visibility="Visible">
<TextBlock
Margin="0,0,0,16"
Style="{StaticResource TitleTextBlockStyle}"
Text="第 12 层 队伍出场次数"/>
<TextBlock
Margin="0,0,0,8"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="上半"/>
<GridView
Margin="0,0,-16,-8"
ItemTemplate="{StaticResource TeamItemTemplate}"
ItemsSource="{Binding TeamAppearances[0].Up}"
SelectionMode="None"/>
<TextBlock
Margin="0,0,0,8"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="下半"/>
<GridView
Margin="0,0,-16,-8"
ItemTemplate="{StaticResource TeamItemTemplate}"
ItemsSource="{Binding TeamAppearances[0].Down}"
SelectionMode="None"/>
</StackPanel>
<StackPanel Visibility="Collapsed">
<TextBlock
Margin="0,0,0,16"
Style="{StaticResource SubheaderTextBlockStyle}"
Text="角色/命座持有率"/>
<Grid Margin="0,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48"/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="6"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="角色"/>
<TextBlock
Grid.Column="1"
Margin="6"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="持有"/>
<TextBlock
Grid.Column="2"
Margin="6"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="0 命"/>
<TextBlock
Grid.Column="3"
Margin="6"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="1 命"/>
<TextBlock
Grid.Column="4"
Margin="6"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="2 命"/>
<TextBlock
Grid.Column="5"
Margin="6"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="3 命"/>
<TextBlock
Grid.Column="6"
Margin="6"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="4 命"/>
<TextBlock
Grid.Column="7"
Margin="6"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="5 命"/>
<TextBlock
Grid.Column="8"
Margin="6"
HorizontalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="6 命"/>
</Grid>
<ItemsControl
Margin="0,0,0,8"
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding AvatarConstellationInfos}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="0,0,0,4" Style="{StaticResource BorderCardStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shvc:ItemIcon
Width="48"
Height="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>
</StackPanel>
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="Snap Hutao API @ DGP Studio"/>
</StackPanel>
</ScrollViewer>
</Grid>
</shc:ScopedPage>

View File

@@ -0,0 +1,23 @@
// 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 HutaoDatabasePresentPage : ScopedPage
{
/// <summary>
/// 构造一个新的胡桃数据库页面
/// </summary>
public HutaoDatabasePresentPage()
{
InitializeComponent();
InitializeWith<HutaoDatabaseViewModel>();
}
}

View File

@@ -32,7 +32,7 @@
<Setter Property="MinWidth" Value="156"/> <Setter Property="MinWidth" Value="156"/>
</Style> </Style>
<Style TargetType="NumberBox"> <Style TargetType="NumberBox">
<Setter Property="MinWidth" Value="158"/> <Setter Property="MinWidth" Value="156"/>
</Style> </Style>
</Page.Resources> </Page.Resources>
<Grid> <Grid>
@@ -46,7 +46,12 @@
<ColumnDefinition MaxWidth="800"/> <ColumnDefinition MaxWidth="800"/>
<ColumnDefinition Width="auto"/> <ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel Margin="16,-16,16,16"> <StackPanel Margin="16">
<InfoBar
IsClosable="False"
IsOpen="True"
Message="所有选项仅会在启动游戏成功后保存"
Severity="Informational"/>
<wsc:SettingsGroup Margin="0,0,0,0" Header="常规"> <wsc:SettingsGroup Margin="0,0,0,0" Header="常规">
<wsc:Setting <wsc:Setting
Description="切换游戏服务器B服用户需要自备额外的 PCGameSDK.dll 文件" Description="切换游戏服务器B服用户需要自备额外的 PCGameSDK.dll 文件"
@@ -54,7 +59,7 @@
Icon="&#xE8AB;"> Icon="&#xE8AB;">
<wsc:Setting.ActionContent> <wsc:Setting.ActionContent>
<ComboBox <ComboBox
DisplayMemberPath="Name" DisplayMemberPath="DisplayName"
ItemsSource="{Binding KnownSchemes}" ItemsSource="{Binding KnownSchemes}"
SelectedItem="{Binding SelectedScheme, Mode=TwoWay}"/> SelectedItem="{Binding SelectedScheme, Mode=TwoWay}"/>
</wsc:Setting.ActionContent> </wsc:Setting.ActionContent>
@@ -198,7 +203,7 @@
Header="宽度" Header="宽度"
Icon="&#xE76F;"> Icon="&#xE76F;">
<wsc:Setting.ActionContent> <wsc:Setting.ActionContent>
<NumberBox Width="160" Value="{Binding ScreenWidth, Mode=TwoWay}"/> <NumberBox Width="156" Value="{Binding ScreenWidth, Mode=TwoWay}"/>
</wsc:Setting.ActionContent> </wsc:Setting.ActionContent>
</wsc:Setting> </wsc:Setting>
<wsc:Setting <wsc:Setting
@@ -206,26 +211,54 @@
Header="高度" Header="高度"
Icon="&#xE784;"> Icon="&#xE784;">
<wsc:Setting.ActionContent> <wsc:Setting.ActionContent>
<NumberBox Width="160" Value="{Binding ScreenHeight, Mode=TwoWay}"/> <NumberBox Width="156" Value="{Binding ScreenHeight, Mode=TwoWay}"/>
</wsc:Setting.ActionContent> </wsc:Setting.ActionContent>
</wsc:Setting> </wsc:Setting>
</wsc:SettingsGroup> </wsc:SettingsGroup>
<wsc:SettingsGroup Header="Dangerous Feature" IsEnabled="{Binding IsElevated}"> <wsc:SettingsGroup Header="高级功能" IsEnabled="{Binding IsElevated}">
<InfoBar
IsClosable="False"
IsOpen="True"
Message="需要读写游戏进程或与游戏窗体交互,因此只在管理员模式下生效!"
Severity="Warning"/>
<wsc:Setting <wsc:Setting
Description="Requires administrator privilege. Otherwise the option will be disabled." Description="在启动游戏前尝试终止运行中的游戏进程"
Header="Unlock frame rate limit" Header="快速切换账号"
Icon="&#xE8BB;"
Visibility="Collapsed">
<wsc:Setting.ActionContent>
<ToggleSwitch
Width="120"
IsOn="False"
Style="{StaticResource ToggleSwitchSettingStyle}"/>
</wsc:Setting.ActionContent>
</wsc:Setting>
<InfoBar
IsClosable="False"
IsOpen="True"
Message="下面的功能十分危险,如果您不愿承担因此可能带来的后果,请勿启用!"
Severity="Error"/>
<wsc:Setting
Description="请在游戏内关闭垂直同步选项,需要高性能的显卡以支持更高的帧率"
Header="解锁帧率限制"
Icon="&#xE785;"> Icon="&#xE785;">
<wsc:Setting.ActionContent> <wsc:Setting.ActionContent>
<ToggleSwitch <ToggleSwitch
Width="120" Width="120"
IsOn="{Binding UnlockFps, Mode=TwoWay}" IsOn="{Binding UnlockFps, Mode=TwoWay}"
OffContent="Disable" OffContent="禁用"
OnContent="Enable" OnContent="启用"
Style="{StaticResource ToggleSwitchSettingStyle}"/> Style="{StaticResource ToggleSwitchSettingStyle}"/>
</wsc:Setting.ActionContent> </wsc:Setting.ActionContent>
</wsc:Setting> </wsc:Setting>
<wsc:Setting Description="{Binding TargetFps}" Header="Set frame rate"> <wsc:Setting Header="设置当前帧率">
<wsc:Setting.Description>
<StackPanel>
<TextBlock Text="在游戏时可以随时调整"/>
<TextBlock Text="{Binding TargetFps}"/>
</StackPanel>
</wsc:Setting.Description>
<wsc:Setting.ActionContent> <wsc:Setting.ActionContent>
<Slider <Slider
Width="400" Width="400"

View File

@@ -109,7 +109,6 @@
ItemsSource="{Binding BackdropTypes}" ItemsSource="{Binding BackdropTypes}"
SelectedItem="{Binding SelectedBackdropType, Mode=TwoWay}"/> SelectedItem="{Binding SelectedBackdropType, Mode=TwoWay}"/>
</wsc:Setting> </wsc:Setting>
</wsc:SettingsGroup> </wsc:SettingsGroup>
<wsc:SettingsGroup Header="祈愿记录"> <wsc:SettingsGroup Header="祈愿记录">
@@ -130,7 +129,7 @@
<InfoBar <InfoBar
IsClosable="False" IsClosable="False"
IsOpen="True" IsOpen="True"
Message="设置游戏路径时请选择游戏本体YuanShen.exe 或 Genshin Impact.exe而不是启动器launcher.exe" Message="设置游戏路径时请选择游戏本体YuanShen.exe 或 GenshinImpact.exe而不是启动器launcher.exe"
Severity="Informational"/> Severity="Informational"/>
<wsc:Setting <wsc:Setting
Description="{Binding GamePath}" Description="{Binding GamePath}"
@@ -185,7 +184,7 @@
</wsc:Setting> </wsc:Setting>
</wsc:SettingsGroup> </wsc:SettingsGroup>
<wsc:SettingsGroup Foreground="{StaticResource SystemFillColorCriticalBrush}" Header="危险功能"> <wsc:SettingsGroup Foreground="{ThemeResource SystemFillColorCriticalBrush}" Header="危险功能">
<InfoBar <InfoBar
IsClosable="False" IsClosable="False"
IsOpen="True" IsOpen="True"
@@ -193,7 +192,7 @@
Severity="Error"/> Severity="Error"/>
<wsc:Setting <wsc:Setting
Background="{StaticResource SystemFillColorCriticalBackgroundBrush}" Background="{ThemeResource SystemFillColorCriticalBackgroundBrush}"
Description="删除注册的计划任务,卸载前务必点击此项" Description="删除注册的计划任务,卸载前务必点击此项"
Header="删除所有计划任务" Header="删除所有计划任务"
Icon="&#xE7C4;"> Icon="&#xE7C4;">

View File

@@ -32,6 +32,10 @@
<wsc:Setting Header="DownloadStaticFileTest"> <wsc:Setting Header="DownloadStaticFileTest">
<Button Command="{Binding DownloadStaticFileCommand}" Content="Download"/> <Button Command="{Binding DownloadStaticFileCommand}" Content="Download"/>
</wsc:Setting> </wsc:Setting>
<wsc:Setting Header="HutaoDatabasePresentTest">
<Button Command="{Binding HutaoDatabasePresentCommand}" Content="Navigate"/>
</wsc:Setting>
</wsc:SettingsGroup> </wsc:SettingsGroup>
</ScrollViewer> </ScrollViewer>
</shc:ScopedPage> </shc:ScopedPage>

View File

@@ -129,24 +129,22 @@
Margin="12,0,0,0" Margin="12,0,0,0"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Background="Transparent" Background="Transparent"
BorderBrush="{x:Null}"
BorderThickness="0"
Command="{Binding DataContext.CopyCookieCommand, Source={StaticResource ViewModelBindingProxy}}" Command="{Binding DataContext.CopyCookieCommand, Source={StaticResource ViewModelBindingProxy}}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Content="&#xE8C8;" Content="&#xE8C8;"
FontFamily="{StaticResource SymbolThemeFontFamily}" FontFamily="{StaticResource SymbolThemeFontFamily}"
Style="{StaticResource ButtonRevealStyle}"
ToolTipService.ToolTip="复制 Cookie"/> ToolTipService.ToolTip="复制 Cookie"/>
<Button <Button
Margin="6,0,0,0" Margin="6,0,0,0"
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Background="Transparent" Background="Transparent"
BorderBrush="{x:Null}"
BorderThickness="0"
Command="{Binding DataContext.RemoveUserCommand, Source={StaticResource ViewModelBindingProxy}}" Command="{Binding DataContext.RemoveUserCommand, Source={StaticResource ViewModelBindingProxy}}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Content="&#xE74D;" Content="&#xE74D;"
FontFamily="{StaticResource SymbolThemeFontFamily}" FontFamily="{StaticResource SymbolThemeFontFamily}"
Style="{StaticResource ButtonRevealStyle}"
ToolTipService.ToolTip="移除用户"/> ToolTipService.ToolTip="移除用户"/>
</StackPanel> </StackPanel>

View File

@@ -2,7 +2,6 @@
x:Class="Snap.Hutao.View.WelcomeView" x:Class="Snap.Hutao.View.WelcomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity" xmlns:mxi="using:Microsoft.Xaml.Interactivity"
@@ -32,6 +31,10 @@
Style="{StaticResource BodyTextBlockStyle}" Style="{StaticResource BodyTextBlockStyle}"
Text="你可以继续使用电脑,丝毫不受影响"/> Text="你可以继续使用电脑,丝毫不受影响"/>
<ItemsControl Margin="0,0,0,32" ItemsSource="{Binding DownloadSummaries}"> <ItemsControl Margin="0,0,0,32" ItemsSource="{Binding DownloadSummaries}">
<ItemsControl.ItemContainerTransitions>
<EntranceThemeTransition IsStaggeringEnabled="False"/>
<AddDeleteThemeTransition/>
</ItemsControl.ItemContainerTransitions>
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Margin="0,8,0,0"> <StackPanel Margin="0,8,0,0">

View File

@@ -280,7 +280,7 @@ internal class AvatarPropertyViewModel : Abstraction.ViewModel
IBuffer buffer = await bitmap.GetPixelsAsync(); IBuffer buffer = await bitmap.GetPixelsAsync();
bool clipboardOpened = false; bool clipboardOpened = false;
using (SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(buffer, BitmapPixelFormat.Bgra8, bitmap.PixelWidth, bitmap.PixelHeight, BitmapAlphaMode.Ignore)) using (SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(buffer, BitmapPixelFormat.Bgra8, bitmap.PixelWidth, bitmap.PixelHeight))
{ {
Color tintColor = (Color)Ioc.Default.GetRequiredService<App>().Resources["CompatBackgroundColor"]; Color tintColor = (Color)Ioc.Default.GetRequiredService<App>().Resources["CompatBackgroundColor"];
Bgra8 tint = Bgra8.FromColor(tintColor); Bgra8 tint = Bgra8.FromColor(tintColor);

View File

@@ -2,9 +2,15 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Model.Binding.Hutao; using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Service.Hutao; using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Web.Hutao.Model; using Snap.Hutao.Web.Hutao.Model;
using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
namespace Snap.Hutao.ViewModel; namespace Snap.Hutao.ViewModel;
@@ -31,6 +37,7 @@ internal class HutaoDatabaseViewModel : Abstraction.ViewModel
this.hutaoCache = hutaoCache; this.hutaoCache = hutaoCache;
OpenUICommand = new AsyncRelayCommand(OpenUIAsync); OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
ExportAsImageCommand = new AsyncRelayCommand<UIElement>(ExportAsImageAsync);
} }
/// <summary> /// <summary>
@@ -46,7 +53,7 @@ internal class HutaoDatabaseViewModel : Abstraction.ViewModel
/// <summary> /// <summary>
/// 角色命座信息 /// 角色命座信息
/// </summary> /// </summary>
public List<ComplexAvatarConstellationInfo>? AvatarConstellationInfos { get => avatarConstellationInfos; set => avatarConstellationInfos = value; } public List<ComplexAvatarConstellationInfo>? AvatarConstellationInfos { get => avatarConstellationInfos; set => SetProperty(ref avatarConstellationInfos, value); }
/// <summary> /// <summary>
/// 队伍出场 /// 队伍出场
@@ -63,6 +70,11 @@ internal class HutaoDatabaseViewModel : Abstraction.ViewModel
/// </summary> /// </summary>
public ICommand OpenUICommand { get; } public ICommand OpenUICommand { get; }
/// <summary>
/// 导出为图片命令
/// </summary>
public ICommand ExportAsImageCommand { get; }
private async Task OpenUIAsync() private async Task OpenUIAsync()
{ {
if (await hutaoCache.InitializeForDatabaseViewModelAsync().ConfigureAwait(true)) if (await hutaoCache.InitializeForDatabaseViewModelAsync().ConfigureAwait(true))
@@ -74,4 +86,27 @@ internal class HutaoDatabaseViewModel : Abstraction.ViewModel
Overview = hutaoCache.Overview; Overview = hutaoCache.Overview;
} }
} }
private async Task ExportAsImageAsync(UIElement? element)
{
if (element == null)
{
return;
}
RenderTargetBitmap bitmap = new();
await bitmap.RenderAsync(element);
IBuffer buffer = await bitmap.GetPixelsAsync();
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
using (FileStream file = File.Create(Path.Combine(desktop, "hutao-database.png")))
{
using (IRandomAccessStream randomFileStream = file.AsRandomAccessStream())
{
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, randomFileStream);
encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Straight, (uint)bitmap.PixelWidth, (uint)bitmap.PixelHeight, 96, 96, buffer.ToArray());
await encoder.FlushAsync();
}
}
}
} }

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.LifeCycle; using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Model.Binding.LaunchGame; using Snap.Hutao.Model.Binding.LaunchGame;
@@ -13,6 +14,7 @@ using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Game; using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.User; using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding; using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
@@ -37,12 +39,7 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
private readonly AppDbContext appDbContext; private readonly AppDbContext appDbContext;
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;
private readonly List<LaunchScheme> knownSchemes = new() private readonly List<LaunchScheme> knownSchemes = LaunchScheme.KnownSchemes.ToList();
{
new LaunchScheme(name: "官方服 | 天空岛", channel: "1", subChannel: "1", launcherId: "18"),
new LaunchScheme(name: "渠道服 | 世界树", channel: "14", subChannel: "0", launcherId: "17"),
new LaunchScheme(name: "国际服 | 部分支持", channel: "1", subChannel: "0", launcherId: "unknown"),
};
private LaunchScheme? selectedScheme; private LaunchScheme? selectedScheme;
private ObservableCollection<GameAccount>? gameAccounts; private ObservableCollection<GameAccount>? gameAccounts;
@@ -301,30 +298,38 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
{ {
try try
{ {
gameService.SetMultiChannel(SelectedScheme); if (gameService.SetMultiChannel(SelectedScheme))
{
// Channel changed, we need to change local file.
// Note that if channel changed successfully, the
// access level is already high enough.
await ThreadHelper.SwitchToMainThreadAsync();
LaunchGamePackageConvertDialog dialog = new();
await using (await dialog.BlockAsync().ConfigureAwait(false))
{
Progress<Service.Game.Package.PackageReplaceStatus> progress = new(s => dialog.Description = s.Description);
await gameService.ReplaceGameResourceAsync(SelectedScheme, progress).ConfigureAwait(false);
}
}
if (SelectedGameAccount != null)
{
if (!gameService.SetGameAccount(SelectedGameAccount))
{
infoBarService.Warning("切换账号失败");
}
}
SaveSetting();
LaunchConfiguration configuration = new(IsExclusive, IsFullScreen, IsBorderless, ScreenWidth, ScreenHeight, IsElevated && UnlockFps, TargetFps);
await gameService.LaunchAsync(configuration).ConfigureAwait(false);
} }
catch (DirectoryNotFoundException) catch (GameFileOperationException ex)
{ {
infoBarService.Warning("找不到游戏配置文件 config.ini"); infoBarService.Error(ex);
}
catch (UnauthorizedAccessException)
{
infoBarService.Warning("无法读取或保存配置文件,请以管理员模式启动胡桃。");
} }
} }
if (SelectedGameAccount != null)
{
if (!gameService.SetGameAccount(SelectedGameAccount))
{
infoBarService.Warning("切换账号失败");
}
}
SaveSetting();
LaunchConfiguration configuration = new(IsExclusive, IsFullScreen, IsBorderless, ScreenWidth, ScreenHeight, IsElevated && UnlockFps, TargetFps);
await gameService.LaunchAsync(configuration).ConfigureAwait(false);
} }
private async Task DetectGameAccountAsync() private async Task DetectGameAccountAsync()

View File

@@ -27,6 +27,7 @@ internal class TestViewModel : Abstraction.ViewModel
ShowAdoptCalculatorDialogCommand = new AsyncRelayCommand(ShowAdoptCalculatorDialogAsync); ShowAdoptCalculatorDialogCommand = new AsyncRelayCommand(ShowAdoptCalculatorDialogAsync);
DangerousLoginMihoyoBbsCommand = new AsyncRelayCommand(DangerousLoginMihoyoBbsAsync); DangerousLoginMihoyoBbsCommand = new AsyncRelayCommand(DangerousLoginMihoyoBbsAsync);
DownloadStaticFileCommand = new AsyncRelayCommand(DownloadStaticFileAsync); DownloadStaticFileCommand = new AsyncRelayCommand(DownloadStaticFileAsync);
HutaoDatabasePresentCommand = new RelayCommand(HutaoDatabasePresent);
} }
/// <summary> /// <summary>
@@ -49,6 +50,11 @@ internal class TestViewModel : Abstraction.ViewModel
/// </summary> /// </summary>
public ICommand DownloadStaticFileCommand { get; } public ICommand DownloadStaticFileCommand { get; }
/// <summary>
/// 胡桃数据库呈现命令
/// </summary>
public ICommand HutaoDatabasePresentCommand { get; }
private async Task ShowCommunityGameRecordDialogAsync() private async Task ShowCommunityGameRecordDialogAsync()
{ {
// ContentDialog must be created by main thread. // ContentDialog must be created by main thread.
@@ -115,4 +121,11 @@ internal class TestViewModel : Abstraction.ViewModel
} }
} }
} }
private void HutaoDatabasePresent()
{
Ioc.Default
.GetRequiredService<Service.Navigation.INavigationService>()
.Navigate<View.Page.HutaoDatabasePresentPage>(Service.Navigation.INavigationAwaiter.Default);
}
} }

View File

@@ -65,11 +65,7 @@ internal class WelcomeViewModel : ObservableObject
})).ConfigureAwait(true); })).ConfigureAwait(true);
serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage()); serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
StaticResource.FulfillAllContracts();
// Complete StaticResourceContracts
LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV2Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV3Contract, true);
try try
{ {
@@ -115,6 +111,11 @@ internal class WelcomeViewModel : ObservableObject
downloadSummaries.TryAdd("Talent", new(serviceProvider, "命之座图标更新", "Talent")); downloadSummaries.TryAdd("Talent", new(serviceProvider, "命之座图标更新", "Talent"));
} }
if (StaticResource.IsContractUnfulfilled(SettingKeys.StaticResourceV4Contract))
{
downloadSummaries.TryAdd("AvatarIcon", new(serviceProvider, "角色图标更新", "AvatarIcon"));
}
return downloadSummaries.Select(x => x.Value); return downloadSummaries.Select(x => x.Value);
} }

View File

@@ -316,13 +316,11 @@ internal static class ApiEndpoints
/// <summary> /// <summary>
/// 启动器资源 /// 启动器资源
/// </summary> /// </summary>
/// <param name="launcherId">启动器Id</param> /// <param name="scheme">启动方案</param>
/// <param name="channel">通道</param>
/// <param name="subChannel">子通道</param>
/// <returns>启动器资源字符串</returns> /// <returns>启动器资源字符串</returns>
public static string SdkStaticLauncherResource(string launcherId, string channel, string subChannel) public static string SdkStaticLauncherResource(Model.Binding.LaunchGame.LaunchScheme scheme)
{ {
return $"{SdkStaticLauncherApi}/resource?key=eYd89JmJ&launcher_id={launcherId}&channel_id={channel}&sub_channel_id={subChannel}"; return $"{SdkStaticLauncherApi}/resource?key={scheme.Key}&launcher_id={scheme.LauncherId}&channel_id={scheme.Channel}&sub_channel_id={scheme.SubChannel}";
} }
// https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/content?filter_adv=true&key=eYd89JmJ&language=zh-cn&launcher_id=18 // https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/content?filter_adv=true&key=eYd89JmJ&language=zh-cn&launcher_id=18

View File

@@ -23,8 +23,24 @@ internal static class ApiOsEndpoints
} }
#endregion #endregion
#region SdkStaticLauncherApi
/// <summary>
/// 启动器资源
/// </summary>
/// <param name="scheme">启动方案</param>
/// <returns>启动器资源字符串</returns>
public static string SdkOsStaticLauncherResource(Model.Binding.LaunchGame.LaunchScheme scheme)
{
return $"{SdkOsStaticLauncherApi}/resource?key={scheme.Key}&launcher_id={scheme.LauncherId}&channel_id={scheme.Channel}&sub_channel_id={scheme.SubChannel}";
}
#endregion
#region Hosts | Queries #region Hosts | Queries
private const string Hk4eApiOs = "https://hk4e-api-os.hoyoverse.com"; private const string Hk4eApiOs = "https://hk4e-api-os.hoyoverse.com";
private const string Hk4eApiOsGachaInfoApi = $"{Hk4eApiOs}/event/gacha_info/api"; private const string Hk4eApiOsGachaInfoApi = $"{Hk4eApiOs}/event/gacha_info/api";
private const string SdkOsStatic = "https://sdk-os-static.mihoyo.com";
private const string SdkOsStaticLauncherApi = $"{SdkOsStatic}/hk4e_global/mdk/launcher/api";
#endregion #endregion
} }

View File

@@ -45,10 +45,10 @@ public class GameResource
public List<NameMd5> DeprecatedPackages { get; set; } = default!; public List<NameMd5> DeprecatedPackages { get; set; } = default!;
/// <summary> /// <summary>
/// 渠道服sdk /// 渠道服 sdk
/// </summary> /// </summary>
[JsonPropertyName("sdk")] [JsonPropertyName("sdk")]
public object? Sdk { get; set; } public Sdk? Sdk { get; set; }
/// <summary> /// <summary>
/// 过期的单个文件 /// 过期的单个文件

View File

@@ -38,8 +38,14 @@ public class Package : PathMd5
[JsonPropertyName("voice_packs")] [JsonPropertyName("voice_packs")]
public List<VoicePackage> VoicePacks { get; set; } = default!; public List<VoicePackage> VoicePacks { get; set; } = default!;
// We don't want to support /// <summary>
// decompressed_path & segments /// 松散文件
/// 用于校验完整性
/// </summary>
[JsonPropertyName("decompressed_path")]
public string DecompressedPath { get; set; } = default!;
// We don't want to support `segments` downloading
/// <summary> /// <summary>
/// 包大小 bytes /// 包大小 bytes
@@ -47,4 +53,4 @@ public class Package : PathMd5
[JsonPropertyName("package_size")] [JsonPropertyName("package_size")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long PackageSize { get; set; } = default!; public long PackageSize { get; set; } = default!;
} }

View File

@@ -39,7 +39,10 @@ internal class ResourceClient
/// <returns>游戏资源</returns> /// <returns>游戏资源</returns>
public async Task<Response<GameResource>> GetResourceAsync(LaunchScheme scheme, CancellationToken token = default) public async Task<Response<GameResource>> GetResourceAsync(LaunchScheme scheme, CancellationToken token = default)
{ {
string url = ApiEndpoints.SdkStaticLauncherResource(scheme.LauncherId, scheme.Channel, scheme.SubChannel); string url = scheme.LauncherId == "10"
? ApiOsEndpoints.SdkOsStaticLauncherResource(scheme)
: ApiEndpoints.SdkStaticLauncherResource(scheme);
Response<GameResource>? response = await httpClient Response<GameResource>? response = await httpClient
.TryCatchGetFromJsonAsync<Response<GameResource>>(url, options, logger, token) .TryCatchGetFromJsonAsync<Response<GameResource>>(url, options, logger, token)
.ConfigureAwait(false); .ConfigureAwait(false);

View File

@@ -0,0 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
/// <summary>
/// Sdk
/// </summary>
public class Sdk : PathMd5
{
/// <summary>
/// 版本
/// </summary>
[JsonPropertyName("version")]
public string Version { get; set; } = default!;
/// <summary>
/// 常量 sdk_pkg_version
/// </summary>
[JsonPropertyName("pkg_version")]
public string PackageVersion { get; set; } = default!;
/// <summary>
/// 描述
/// </summary>
[JsonPropertyName("desc")]
public string Description { get; set; } = default!;
}

View File

@@ -63,6 +63,11 @@ public enum KnownReturnCode
/// </summary> /// </summary>
LoginDataOutdated = -262, LoginDataOutdated = -262,
/// <summary>
/// 无效的 Key
/// </summary>
InvalidKey = -205,
/// <summary> /// <summary>
/// 访问过于频繁 /// 访问过于频繁
/// </summary> /// </summary>
@@ -118,6 +123,11 @@ public enum KnownReturnCode
/// </summary> /// </summary>
DataIsNotPublicForTheUser = 10102, DataIsNotPublicForTheUser = 10102,
/// <summary>
/// 实时便笺
/// </summary>
CODE10103 = 10103,
/// <summary> /// <summary>
/// 实时便笺 /// 实时便笺
/// </summary> /// </summary>