mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
fix package convert issue
This commit is contained in:
@@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库集合上下文
|
||||
/// 数据库集合扩展
|
||||
/// </summary>
|
||||
public static class DbSetExtension
|
||||
{
|
||||
@@ -134,4 +134,4 @@ public static class DbSetExtension
|
||||
dbSet.Update(entity);
|
||||
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 可查询扩展
|
||||
/// </summary>
|
||||
public static class QueryableExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// source.Where(predicate).ExecuteDeleteAsync(token)
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">源类型</typeparam>
|
||||
/// <param name="source">源</param>
|
||||
/// <param name="predicate">条件</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>SQL返回个数</returns>
|
||||
public static Task<int> ExecuteDeleteWhereAsync<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate, CancellationToken token = default)
|
||||
{
|
||||
return source.Where(predicate).ExecuteDeleteAsync(token);
|
||||
}
|
||||
}
|
||||
42
src/Snap.Hutao/Snap.Hutao/Core/IO/Digest.cs
Normal file
42
src/Snap.Hutao/Snap.Hutao/Core/IO/Digest.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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 Digest
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步获取文件 Md5 摘要
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>文件 Md5 摘要</returns>
|
||||
public static async Task<string> GetFileMd5Async(string filePath, CancellationToken token = default)
|
||||
{
|
||||
using (FileStream stream = File.OpenRead(filePath))
|
||||
{
|
||||
return await GetStreamMd5Async(stream, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取流的 Md5 摘要
|
||||
/// </summary>
|
||||
/// <param name="stream">流</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>流 Md5 摘要</returns>
|
||||
public static async Task<string> GetStreamMd5Async(Stream stream, CancellationToken token = default)
|
||||
{
|
||||
using (MD5 md5 = MD5.Create())
|
||||
{
|
||||
byte[] bytes = await md5.ComputeHashAsync(stream, token).ConfigureAwait(false);
|
||||
return System.Convert.ToHexString(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/Snap.Hutao/Snap.Hutao/Core/IO/FileOperation.cs
Normal file
41
src/Snap.Hutao/Snap.Hutao/Core/IO/FileOperation.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 文件操作
|
||||
/// </summary>
|
||||
internal static class FileOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// 将指定文件移动到新位置,提供指定新文件名和覆盖目标文件(如果它已存在)的选项。
|
||||
/// </summary>
|
||||
/// <param name="sourceFileName">要移动的文件的名称。 可以包括相对或绝对路径。</param>
|
||||
/// <param name="destFileName">文件的新路径和名称。</param>
|
||||
/// <param name="overwrite">如果要覆盖目标文件</param>
|
||||
/// <returns>是否发生了移动操作</returns>
|
||||
public static bool Move(string sourceFileName, string destFileName, bool overwrite)
|
||||
{
|
||||
if (File.Exists(sourceFileName))
|
||||
{
|
||||
if (overwrite)
|
||||
{
|
||||
File.Move(sourceFileName, destFileName, overwrite);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!File.Exists(destFileName))
|
||||
{
|
||||
File.Move(sourceFileName, destFileName, overwrite);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,12 @@ internal sealed class TempFile : IDisposable
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
File.Delete(Path);
|
||||
try
|
||||
{
|
||||
File.Delete(Path);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@ using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.DailyNote;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
#if RELEASE
|
||||
using System.Security.Principal;
|
||||
#endif
|
||||
|
||||
namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
@@ -37,11 +39,15 @@ internal static class Activation
|
||||
/// <returns>是否提升了权限</returns>
|
||||
public static bool GetElevated()
|
||||
{
|
||||
#if RELEASE
|
||||
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
|
||||
{
|
||||
WindowsPrincipal principal = new(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio&comma DissmissedLight"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.4.0.0" />
|
||||
|
||||
<Properties>
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<Dependencies>
|
||||
<!--<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.22621.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
|
||||
@@ -72,8 +72,7 @@ internal class AchievementService : IAchievementService
|
||||
|
||||
// Cascade deleted the achievements.
|
||||
await appDbContext.AchievementArchives
|
||||
.Where(a => a.InnerId == archive.InnerId)
|
||||
.ExecuteDeleteAsync()
|
||||
.ExecuteDeleteWhereAsync(a => a.InnerId == archive.InnerId)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ internal class SummaryAvatarFactory
|
||||
FetterLevel = avatarInfo.FetterInfo?.ExpLevel ?? 0,
|
||||
Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap),
|
||||
CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}",
|
||||
LevelNumber = int.Parse(avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].Value ?? string.Empty),
|
||||
LevelNumber = avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].ValueInt32 ?? 0,
|
||||
|
||||
// processed webinfo part
|
||||
Weapon = reliquaryAndWeapon.Weapon,
|
||||
|
||||
@@ -105,7 +105,10 @@ internal class CultivationService : ICultivationService
|
||||
await ThreadHelper.SwitchToBackgroundAsync();
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
await scope.ServiceProvider.GetRequiredService<AppDbContext>().CultivateProjects.RemoveAndSaveAsync(project).ConfigureAwait(false);
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await appDbContext.CultivateProjects
|
||||
.ExecuteDeleteWhereAsync(p => p.InnerId == project.InnerId)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,14 +157,14 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RemoveDailyNote(DailyNoteEntry entry)
|
||||
public async Task RemoveDailyNoteAsync(DailyNoteEntry entry)
|
||||
{
|
||||
entries!.Remove(entry);
|
||||
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
// DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s)
|
||||
scope.ServiceProvider.GetRequiredService<AppDbContext>().DailyNotes.RemoveAndSave(entry);
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await appDbContext.DailyNotes.ExecuteDeleteWhereAsync(d => d.InnerId == entry.InnerId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,5 +36,6 @@ public interface IDailyNoteService
|
||||
/// 移除指定的实时便笺
|
||||
/// </summary>
|
||||
/// <param name="entry">指定的实时便笺</param>
|
||||
void RemoveDailyNote(DailyNoteEntry entry);
|
||||
/// <returns>任务</returns>
|
||||
Task RemoveDailyNoteAsync(DailyNoteEntry entry);
|
||||
}
|
||||
@@ -117,7 +117,14 @@ internal class GachaLogService : IGachaLogService
|
||||
public async Task<ObservableCollection<GachaArchive>> GetArchiveCollectionAsync()
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
return archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
|
||||
try
|
||||
{
|
||||
return archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
|
||||
}
|
||||
catch (SqliteException ex)
|
||||
{
|
||||
throw new Core.ExceptionService.UserdataCorruptedException($"无法获取祈愿记录: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -64,8 +64,8 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
|
||||
using (MemoryStream memoryStream = new())
|
||||
{
|
||||
await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
string? result = Match(memoryStream, !tempFile.Path.Contains("GenshinImpact_Data"));
|
||||
return new(!string.IsNullOrEmpty(result), result!);
|
||||
string? result = Match(memoryStream, cacheFile.Contains(GameConstants.GenshinImpactData));
|
||||
return new(!string.IsNullOrEmpty(result), result ?? "未找到可用的 Url");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
// 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>
|
||||
@@ -34,4 +17,4 @@ internal class GameFileOperationException : Exception
|
||||
: base($"游戏文件操作失败: {message}", innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,13 +179,17 @@ internal class GameService : IGameService
|
||||
elements = IniSerializer.Deserialize(readStream).ToList();
|
||||
}
|
||||
}
|
||||
catch (DirectoryNotFoundException dnfEx)
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", dnfEx);
|
||||
throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", ex);
|
||||
}
|
||||
catch (UnauthorizedAccessException uaEx)
|
||||
catch (DirectoryNotFoundException ex)
|
||||
{
|
||||
throw new GameFileOperationException($"无法读取或保存配置文件,请以管理员模式重试。", uaEx);
|
||||
throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", ex);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
throw new GameFileOperationException($"无法读取或保存配置文件,请以管理员模式重试。", ex);
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
@@ -244,10 +248,22 @@ internal class GameService : IGameService
|
||||
|
||||
if (!LaunchSchemeMatchesExecutable(launchScheme, gameFileName))
|
||||
{
|
||||
await packageConverter.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress).ConfigureAwait(false);
|
||||
bool replaced = await packageConverter
|
||||
.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// We need to change the gamePath if we switched.
|
||||
OverwriteGamePath(Path.Combine(gameFolder, launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName));
|
||||
if (replaced)
|
||||
{
|
||||
// We need to change the gamePath if we switched.
|
||||
string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName;
|
||||
OverwriteGamePath(Path.Combine(gameFolder, exeName));
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can't start the game
|
||||
// when we failed to convert game
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!launchScheme.IsOversea)
|
||||
@@ -364,7 +380,15 @@ internal class GameService : IGameService
|
||||
string? registrySdk = GameAccountRegistryInterop.Get();
|
||||
if (!string.IsNullOrEmpty(registrySdk))
|
||||
{
|
||||
GameAccount? account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
|
||||
GameAccount? account;
|
||||
try
|
||||
{
|
||||
account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
throw new Core.ExceptionService.UserdataCorruptedException("已存在多个匹配账号,请先删除重复的账号", ex);
|
||||
}
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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 System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http;
|
||||
using static Snap.Hutao.Service.Game.GameConstants;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Package;
|
||||
|
||||
/// <summary>
|
||||
/// 包转换异常
|
||||
/// </summary>
|
||||
public class PackageConvertException : Exception
|
||||
{
|
||||
/// <inheritdoc cref="Exception.Exception(string?, Exception?)"/>
|
||||
public PackageConvertException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,8 @@
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model.Binding.LaunchGame;
|
||||
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http;
|
||||
@@ -20,7 +18,6 @@ namespace Snap.Hutao.Service.Game.Package;
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class PackageConverter
|
||||
{
|
||||
private readonly ResourceClient resourceClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -30,9 +27,8 @@ internal class PackageConverter
|
||||
/// <param name="resourceClient">资源客户端</param>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
public PackageConverter(ResourceClient resourceClient, JsonSerializerOptions options, HttpClient httpClient)
|
||||
public PackageConverter(JsonSerializerOptions options, HttpClient httpClient)
|
||||
{
|
||||
this.resourceClient = resourceClient;
|
||||
this.options = options;
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
@@ -46,18 +42,25 @@ internal class PackageConverter
|
||||
/// <param name="gameFolder">游戏目录</param>
|
||||
/// <param name="progress">进度</param>
|
||||
/// <returns>替换结果与资源</returns>
|
||||
public async Task EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResouce, string gameFolder, IProgress<PackageReplaceStatus> progress)
|
||||
public async Task<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResouce, string gameFolder, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
await ThreadHelper.SwitchToBackgroundAsync();
|
||||
string scatteredFilesUrl = gameResouce.Game.Latest.DecompressedPath;
|
||||
Uri pkgVersionUri = new($"{scatteredFilesUrl}/pkg_version");
|
||||
ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese;
|
||||
|
||||
progress.Report(new("下载包版本信息"));
|
||||
progress.Report(new("获取 Package Version"));
|
||||
Dictionary<string, VersionItem> remoteItems;
|
||||
using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false))
|
||||
try
|
||||
{
|
||||
remoteItems = await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false);
|
||||
using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false))
|
||||
{
|
||||
remoteItems = await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
throw new PackageConvertException("下载 Package Version 失败", ex);
|
||||
}
|
||||
|
||||
Dictionary<string, VersionItem> localItems;
|
||||
@@ -67,7 +70,7 @@ internal class PackageConverter
|
||||
}
|
||||
|
||||
IEnumerable<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems);
|
||||
await ReplaceGameResourceAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false);
|
||||
return await ReplaceGameResourceAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -100,8 +103,8 @@ internal class PackageConverter
|
||||
{
|
||||
if (File.Exists(sdkDllBackup) && File.Exists(sdkVersionBackup))
|
||||
{
|
||||
File.Move(sdkDllBackup, sdkDll, false);
|
||||
File.Move(sdkVersionBackup, sdkVersion, false);
|
||||
FileOperation.Move(sdkDllBackup, sdkDll, false);
|
||||
FileOperation.Move(sdkVersionBackup, sdkVersion, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -124,15 +127,9 @@ internal class PackageConverter
|
||||
}
|
||||
else
|
||||
{
|
||||
if (File.Exists(sdkDll))
|
||||
{
|
||||
File.Move(sdkDll, sdkDllBackup, true);
|
||||
}
|
||||
|
||||
if (File.Exists(sdkVersion))
|
||||
{
|
||||
File.Move(sdkVersion, sdkVersionBackup, true);
|
||||
}
|
||||
// backup
|
||||
FileOperation.Move(sdkDll, sdkDllBackup, true);
|
||||
FileOperation.Move(sdkVersion, sdkVersionBackup, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,11 +187,17 @@ internal class PackageConverter
|
||||
// so we assume the data folder is present
|
||||
if (direction == ConvertDirection.ChineseToOversea)
|
||||
{
|
||||
Directory.Move(yuanShenData, genshinImpactData);
|
||||
if (Directory.Exists(yuanShenData))
|
||||
{
|
||||
Directory.Move(yuanShenData, genshinImpactData);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Directory.Move(genshinImpactData, yuanShenData);
|
||||
if (Directory.Exists(genshinImpactData))
|
||||
{
|
||||
Directory.Move(genshinImpactData, yuanShenData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,10 +207,42 @@ internal class PackageConverter
|
||||
File.Move(targetFullPath, cacheFilePath, true);
|
||||
}
|
||||
|
||||
private async Task ReplaceGameResourceAsync(IEnumerable<ItemOperationInfo> operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress<PackageReplaceStatus> progress)
|
||||
private static async Task CopyToWithProgressAsync(Stream source, Stream target, string name, long totalBytes, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
const int bufferSize = 81920;
|
||||
|
||||
long totalBytesRead = 0;
|
||||
int bytesRead;
|
||||
Memory<byte> buffer = new byte[bufferSize];
|
||||
do
|
||||
{
|
||||
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
|
||||
await target.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
progress.Report(new(name, totalBytesRead, totalBytes));
|
||||
|
||||
if (bytesRead <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (bytesRead > 0);
|
||||
}
|
||||
|
||||
private async Task<bool> ReplaceGameResourceAsync(IEnumerable<ItemOperationInfo> operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
// 重命名 _Data 目录
|
||||
RenameDataFolder(gameFolder, direction);
|
||||
try
|
||||
{
|
||||
RenameDataFolder(gameFolder, direction);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Access to the path is denied.
|
||||
// When user install the game in special folder like 'Program Files'
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure cache folder
|
||||
string cacheFolder = Path.Combine(gameFolder, "Screenshot", "HutaoCache");
|
||||
@@ -224,13 +259,12 @@ internal class PackageConverter
|
||||
switch (info.Type)
|
||||
{
|
||||
case ItemOperationType.Add:
|
||||
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info).ConfigureAwait(false);
|
||||
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info, progress).ConfigureAwait(false);
|
||||
break;
|
||||
case ItemOperationType.Replace:
|
||||
{
|
||||
MoveToCache(moveToFilePath, targetFilePath);
|
||||
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info).ConfigureAwait(false);
|
||||
|
||||
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info, progress).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -245,17 +279,18 @@ internal class PackageConverter
|
||||
|
||||
// 重新下载所有 *pkg_version 文件
|
||||
await ReplacePackageVersionsAsync(scatteredFilesUrl, gameFolder).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ReplaceFromCacheOrWebAsync(string cacheFilePath, string targetFilePath, string scatteredFilesUrl, ItemOperationInfo info)
|
||||
private async Task ReplaceFromCacheOrWebAsync(string cacheFilePath, string targetFilePath, string scatteredFilesUrl, ItemOperationInfo info, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
if (File.Exists(cacheFilePath))
|
||||
{
|
||||
string remoteMd5 = await FileDigest.GetMd5Async(cacheFilePath, CancellationToken.None).ConfigureAwait(false);
|
||||
string remoteMd5 = await Digest.GetFileMd5Async(cacheFilePath).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
|
||||
// There shouldn't be any file in the path/name
|
||||
File.Move(cacheFilePath, targetFilePath, false);
|
||||
return;
|
||||
}
|
||||
@@ -269,9 +304,32 @@ internal class PackageConverter
|
||||
// Cache no item, download it anyway.
|
||||
using (FileStream fileStream = File.Create(targetFilePath))
|
||||
{
|
||||
using (Stream webStream = await httpClient.GetStreamAsync($"{scatteredFilesUrl}/{info.Target}").ConfigureAwait(false))
|
||||
while (true)
|
||||
{
|
||||
await webStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
using (HttpResponseMessage response = await httpClient.GetAsync($"{scatteredFilesUrl}/{info.Target}", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||
{
|
||||
long totalBytes = response.Content.Headers.ContentLength ?? 0;
|
||||
using (Stream webStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
await CopyToWithProgressAsync(webStream, fileStream, info.Target, totalBytes, progress).ConfigureAwait(false);
|
||||
fileStream.Seek(0, SeekOrigin.Begin);
|
||||
string remoteMd5 = await Digest.GetStreamMd5Async(fileStream).ConfigureAwait(false);
|
||||
if (info.Md5 == remoteMd5.ToLowerInvariant())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// System.IO.IOException: The response ended prematurely.
|
||||
// System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream.
|
||||
|
||||
// We want to retry forever.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,6 +378,7 @@ internal class PackageConverter
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Common;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Package;
|
||||
|
||||
/// <summary>
|
||||
@@ -17,8 +19,40 @@ public class PackageReplaceStatus
|
||||
Description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的包更新状态
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <param name="bytesRead">读取的字节数</param>
|
||||
/// <param name="totalBytes">总字节数</param>
|
||||
public PackageReplaceStatus(string name, long bytesRead, long totalBytes)
|
||||
{
|
||||
Percent = (double)bytesRead / totalBytes;
|
||||
Description = $"{name}\n{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否有进度
|
||||
/// </summary>
|
||||
public bool IsIndeterminate { get => Percent < 0; }
|
||||
|
||||
/// <summary>
|
||||
/// 进度
|
||||
/// </summary>
|
||||
public double Percent { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 克隆
|
||||
/// </summary>
|
||||
/// <returns>克隆的实例</returns>
|
||||
public PackageReplaceStatus Clone()
|
||||
{
|
||||
// 进度需要在主线程上创建
|
||||
return new(Description) { Percent = Percent };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Win32;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Diagnostics.ToolHelp;
|
||||
using static Windows.Win32.PInvoke;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏帧率解锁器异常
|
||||
/// </summary>
|
||||
internal class GameFpsUnlockerException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的用户数据损坏异常
|
||||
/// </summary>
|
||||
/// <param name="message">消息</param>
|
||||
/// <param name="innerException">内部错误</param>
|
||||
public GameFpsUnlockerException(Exception innerException)
|
||||
: base($"解锁帧率失败: {innerException.Message}", innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ internal partial class MetadataService : IMetadataService, IMetadataServiceIniti
|
||||
string fileFullPath = Path.Combine(metadataFolderPath, fileFullName);
|
||||
if (File.Exists(fileFullPath))
|
||||
{
|
||||
skip = md5 == await FileDigest.GetMd5Async(fileFullPath, token).ConfigureAwait(false);
|
||||
skip = md5 == await Digest.GetFileMd5Async(fileFullPath, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!skip)
|
||||
|
||||
@@ -76,7 +76,14 @@ internal class UserService : IUserService
|
||||
if (currentUser != null)
|
||||
{
|
||||
currentUser.IsSelected = true;
|
||||
appDbContext.Users.UpdateAndSave(currentUser.Entity);
|
||||
try
|
||||
{
|
||||
appDbContext.Users.UpdateAndSave(currentUser.Entity);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
throw new Core.ExceptionService.UserdataCorruptedException($"用户 {currentUser.UserInfo?.Uid} 状态保存失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
messenger.Send(message);
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
</ContentDialog.Resources>
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock Text="祈愿记录Url已失效,请重新获取" Visibility="{x:Bind State.AuthKeyTimeout, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"/>
|
||||
<cwucont:HeaderedItemsControl
|
||||
x:Name="GachaItemsPresenter"
|
||||
Padding="0,8,0,0"
|
||||
|
||||
@@ -45,12 +45,14 @@ public sealed partial class GachaLogRefreshProgressDialog : ContentDialog
|
||||
{
|
||||
State = state;
|
||||
GachaItemsPresenter.Header = state.AuthKeyTimeout
|
||||
? null
|
||||
? "祈愿记录Url已失效,请重新获取"
|
||||
: (object)$"正在获取 {state.ConfigType.GetDescription()}";
|
||||
|
||||
// Binding not working here.
|
||||
GachaItemsPresenter.Items.Clear();
|
||||
foreach (ItemBase item in state.Items)
|
||||
|
||||
// System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
|
||||
foreach (ItemBase item in state.Items.ToList())
|
||||
{
|
||||
GachaItemsPresenter.Items.Add(new ItemIcon
|
||||
{
|
||||
|
||||
@@ -17,9 +17,12 @@
|
||||
<TextBlock Text="转换可能需要花费一段时间,请勿关闭胡桃"/>
|
||||
<TextBlock
|
||||
MinWidth="360"
|
||||
Margin="0,8,0,16"
|
||||
Margin="0,16,0,8"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding Description}"/>
|
||||
<ProgressBar IsIndeterminate="True"/>
|
||||
Text="{Binding State.Description}"/>
|
||||
<ProgressBar
|
||||
IsIndeterminate="{Binding State.IsIndeterminate}"
|
||||
Maximum="1"
|
||||
Value="{Binding State.Percent}"/>
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Service.Game.Package;
|
||||
|
||||
namespace Snap.Hutao.View.Dialog;
|
||||
|
||||
@@ -12,7 +13,7 @@ namespace Snap.Hutao.View.Dialog;
|
||||
/// </summary>
|
||||
public sealed partial class LaunchGamePackageConvertDialog : ContentDialog
|
||||
{
|
||||
private static readonly DependencyProperty DescriptionProperty = Property<LaunchGamePackageConvertDialog>.Depend(nameof(Description), "请稍候");
|
||||
private static readonly DependencyProperty StateProperty = Property<LaunchGamePackageConvertDialog>.Depend<PackageReplaceStatus>(nameof(State));
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的启动游戏客户端转换对话框
|
||||
@@ -27,9 +28,9 @@ public sealed partial class LaunchGamePackageConvertDialog : ContentDialog
|
||||
/// <summary>
|
||||
/// 描述
|
||||
/// </summary>
|
||||
public string Description
|
||||
public PackageReplaceStatus State
|
||||
{
|
||||
get { return (string)GetValue(DescriptionProperty); }
|
||||
set { SetValue(DescriptionProperty, value); }
|
||||
get { return (PackageReplaceStatus)GetValue(StateProperty); }
|
||||
set { SetValue(StateProperty, value); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,48 +81,6 @@
|
||||
<PivotItem Header="材料清单">
|
||||
<Grid>
|
||||
<Pivot Visibility="{Binding CultivateEntries.Count, Converter={StaticResource Int32ToVisibilityConverter}}">
|
||||
<PivotItem Header="材料统计">
|
||||
<cwucont:AdaptiveGridView
|
||||
Padding="16,16,4,4"
|
||||
cwua:ItemsReorderAnimation.Duration="0:0:0.1"
|
||||
DesiredWidth="320"
|
||||
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
|
||||
ItemsSource="{Binding StatisticsItems}"
|
||||
SelectionMode="None">
|
||||
<cwucont:AdaptiveGridView.Resources>
|
||||
<x:Double x:Key="GridViewItemMinHeight">0</x:Double>
|
||||
</cwucont:AdaptiveGridView.Resources>
|
||||
<cwucont:AdaptiveGridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shvc:ItemIcon
|
||||
Grid.Column="0"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Icon="{Binding Inner.Icon, Converter={StaticResource ItemIconConverter}}"
|
||||
Quality="{Binding Inner.RankLevel}"/>
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="16,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Inner.Name}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="16,0,4,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding CountFormatted}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</cwucont:AdaptiveGridView.ItemTemplate>
|
||||
</cwucont:AdaptiveGridView>
|
||||
</PivotItem>
|
||||
<PivotItem Header="养成物品">
|
||||
<cwucont:AdaptiveGridView
|
||||
Padding="16,16,4,4"
|
||||
@@ -273,6 +231,48 @@
|
||||
</cwucont:AdaptiveGridView.ItemTemplate>
|
||||
</cwucont:AdaptiveGridView>
|
||||
</PivotItem>
|
||||
<PivotItem Header="材料统计">
|
||||
<cwucont:AdaptiveGridView
|
||||
Padding="16,16,4,4"
|
||||
cwua:ItemsReorderAnimation.Duration="0:0:0.1"
|
||||
DesiredWidth="320"
|
||||
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
|
||||
ItemsSource="{Binding StatisticsItems}"
|
||||
SelectionMode="None">
|
||||
<cwucont:AdaptiveGridView.Resources>
|
||||
<x:Double x:Key="GridViewItemMinHeight">0</x:Double>
|
||||
</cwucont:AdaptiveGridView.Resources>
|
||||
<cwucont:AdaptiveGridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shvc:ItemIcon
|
||||
Grid.Column="0"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Icon="{Binding Inner.Icon, Converter={StaticResource ItemIconConverter}}"
|
||||
Quality="{Binding Inner.RankLevel}"/>
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="16,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Inner.Name}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="16,0,4,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding CountFormatted}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</cwucont:AdaptiveGridView.ItemTemplate>
|
||||
</cwucont:AdaptiveGridView>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
@@ -62,8 +62,8 @@
|
||||
<InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="{Binding IsElevated}"
|
||||
Message="切换国际服功能尚处于测试阶段,可能出现未预料的问题,请注意备份游戏资源!"
|
||||
Severity="Error"/>
|
||||
Message="切换国际服功能会在游戏截图文件创建缓存文件夹"
|
||||
Severity="Informational"/>
|
||||
<wsc:Setting
|
||||
Description="切换游戏服务器(国服/渠道服/国际服)"
|
||||
Header="服务器"
|
||||
|
||||
@@ -82,6 +82,13 @@ public sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.Pag
|
||||
switch (result)
|
||||
{
|
||||
case UserOptionResult.Added:
|
||||
ViewModel.UserViewModel vm = Ioc.Default.GetRequiredService<ViewModel.UserViewModel>();
|
||||
if (vm.Users!.Count == 1)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
vm.SelectedUser = vm.Users.Single();
|
||||
}
|
||||
|
||||
infoBarService.Success($"用户 [{nickname}] 添加成功");
|
||||
break;
|
||||
case UserOptionResult.Incomplete:
|
||||
|
||||
@@ -313,11 +313,22 @@ internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipien
|
||||
|
||||
if (result == ContentDialogResult.Primary)
|
||||
{
|
||||
await achievementService.RemoveArchiveAsync(SelectedArchive).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
ThrowIfViewDisposed();
|
||||
using (await DisposeLock.EnterAsync(CancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
ThrowIfViewDisposed();
|
||||
await achievementService.RemoveArchiveAsync(SelectedArchive).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Re-select first archive
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SelectedArchive = Archives.FirstOrDefault();
|
||||
// Re-select first archive
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
SelectedArchive = Archives.FirstOrDefault();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ internal class DailyNoteViewModel : Abstraction.ViewModel
|
||||
OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
|
||||
TrackRoleCommand = new AsyncRelayCommand<UserAndUid>(TrackRoleAsync);
|
||||
RefreshCommand = new AsyncRelayCommand(RefreshAsync);
|
||||
RemoveDailyNoteCommand = new RelayCommand<DailyNoteEntry>(RemoveDailyNote);
|
||||
RemoveDailyNoteCommand = new AsyncRelayCommand<DailyNoteEntry>(RemoveDailyNoteAsync);
|
||||
ModifyNotificationCommand = new AsyncRelayCommand<DailyNoteEntry>(ModifyDailyNoteNotificationAsync);
|
||||
DailyNoteVerificationCommand = new AsyncRelayCommand(VerifyDailyNoteVerificationAsync);
|
||||
}
|
||||
@@ -229,11 +229,11 @@ internal class DailyNoteViewModel : Abstraction.ViewModel
|
||||
await dailyNoteService.RefreshDailyNotesAsync(false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void RemoveDailyNote(DailyNoteEntry? entry)
|
||||
private async Task RemoveDailyNoteAsync(DailyNoteEntry? entry)
|
||||
{
|
||||
if (entry != null)
|
||||
{
|
||||
dailyNoteService.RemoveDailyNote(entry);
|
||||
await dailyNoteService.RemoveDailyNoteAsync(entry).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Service.Game.Unlocker;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
@@ -305,10 +306,13 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
|
||||
// access level is already high enough.
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
LaunchGamePackageConvertDialog dialog = new();
|
||||
Progress<Service.Game.Package.PackageReplaceStatus> progress = new(state => dialog.State = state.Clone());
|
||||
await using (await dialog.BlockAsync().ConfigureAwait(false))
|
||||
{
|
||||
Progress<Service.Game.Package.PackageReplaceStatus> progress = new(s => dialog.Description = s.Description);
|
||||
await gameService.EnsureGameResourceAsync(SelectedScheme, progress).ConfigureAwait(false);
|
||||
if (!await gameService.EnsureGameResourceAsync(SelectedScheme, progress).ConfigureAwait(false))
|
||||
{
|
||||
infoBarService.Warning("切换服务器失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +329,7 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
|
||||
LaunchConfiguration configuration = new(IsExclusive, IsFullScreen, IsBorderless, ScreenWidth, ScreenHeight, IsElevated && UnlockFps, TargetFps);
|
||||
await gameService.LaunchAsync(configuration).ConfigureAwait(false);
|
||||
}
|
||||
catch (GameFileOperationException ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
infoBarService.Error(ex);
|
||||
}
|
||||
@@ -334,7 +338,14 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
|
||||
|
||||
private async Task DetectGameAccountAsync()
|
||||
{
|
||||
await gameService.DetectGameAccountAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await gameService.DetectGameAccountAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Core.ExceptionService.UserdataCorruptedException ex)
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void AttachGameAccountToCurrentUserGameRole(GameAccount? gameAccount)
|
||||
|
||||
@@ -57,11 +57,11 @@ internal class WelcomeViewModel : ObservableObject
|
||||
|
||||
// Cancel all previous created jobs
|
||||
serviceProvider.GetRequiredService<BitsManager>().CancelAllJobs();
|
||||
await Task.WhenAll(downloadSummaries.Select(async d =>
|
||||
await Task.WhenAll(downloadSummaries.Select(async downloadTask =>
|
||||
{
|
||||
await d.DownloadAndExtractAsync().ConfigureAwait(false);
|
||||
await downloadTask.DownloadAndExtractAsync().ConfigureAwait(false);
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
DownloadSummaries.Remove(d);
|
||||
DownloadSummaries.Remove(downloadTask);
|
||||
})).ConfigureAwait(true);
|
||||
|
||||
serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
|
||||
|
||||
@@ -19,6 +19,7 @@ using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Collections.Immutable;
|
||||
using System.Runtime.InteropServices;
|
||||
using CalcAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
|
||||
using CalcClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
|
||||
using CalcConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
|
||||
@@ -178,7 +179,13 @@ internal class WikiAvatarViewModel : Abstraction.ViewModel
|
||||
|
||||
if (!Avatars.Contains(Selected))
|
||||
{
|
||||
Avatars.MoveCurrentToFirst();
|
||||
try
|
||||
{
|
||||
Avatars.MoveCurrentToFirst();
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -18,6 +18,7 @@ using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Collections.Immutable;
|
||||
using System.Runtime.InteropServices;
|
||||
using CalcAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
|
||||
using CalcClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
|
||||
using CalcConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
|
||||
@@ -176,7 +177,13 @@ internal class WikiWeaponViewModel : Abstraction.ViewModel
|
||||
|
||||
if (!Weapons.Contains(Selected))
|
||||
{
|
||||
Weapons.MoveCurrentToFirst();
|
||||
try
|
||||
{
|
||||
Weapons.MoveCurrentToFirst();
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -14,6 +14,7 @@ using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using Snap.Hutao.Web.Hoyolab.Passport;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Web.Bridge;
|
||||
@@ -338,7 +339,16 @@ public class MiHoYoJSInterface
|
||||
logger?.LogInformation("[ExecuteScript: {callback}]\n{payload}", callback, payload);
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
return await webView.ExecuteScriptAsync(js);
|
||||
try
|
||||
{
|
||||
return await webView.ExecuteScriptAsync(js);
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// COMException (0x8007139F): 组或资源的状态不是执行请求操作的正确状态。 (0x8007139F)
|
||||
// webview is disposing or disposed
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
|
||||
@@ -21,4 +21,17 @@ public class TypeValue
|
||||
/// </summary>
|
||||
[JsonPropertyName("val")]
|
||||
public string? Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 值 Int32
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public int ValueInt32
|
||||
{
|
||||
get
|
||||
{
|
||||
_ = int.TryParse(Value, out int result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,8 +59,15 @@ internal class GeetestClient
|
||||
/// </summary>
|
||||
/// <param name="registration">验证注册</param>
|
||||
/// <returns>验证方式</returns>
|
||||
public Task<GeetestResult<GeetestData>?> GetAjaxAsync(VerificationRegistration registration)
|
||||
public async Task<GeetestResult<GeetestData>?> GetAjaxAsync(VerificationRegistration registration)
|
||||
{
|
||||
return GetAjaxAsync(registration.Gt, registration.Challenge);
|
||||
try
|
||||
{
|
||||
return await GetAjaxAsync(registration.Gt, registration.Challenge).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user