diff --git a/README.md b/README.md
index bf7b4077..193d0bd7 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,13 @@
# [Snap.Hutao](https://hut.ao)
+
+
+
> 唷,找本堂主有何贵干呀?

# 特别感谢
-### 原神组织与个人
-
* [HolographicHat](https://github.com/HolographicHat)
* [UIGF organization](https://uigf.org)
diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml b/src/Snap.Hutao/Snap.Hutao/App.xaml
index b8ea48cd..53814262 100644
--- a/src/Snap.Hutao/Snap.Hutao/App.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/App.xaml
@@ -11,6 +11,7 @@
+
@@ -27,8 +28,6 @@
-
- ms-appx:///Resource/Font/Segoe Fluent Icons.ttf#Segoe Fluent Icons
6,16,16,16
16,0,0,0
@@ -90,6 +89,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs
index 074aec4f..47f85863 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs
@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Logging;
+using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO;
using System.Net;
@@ -24,7 +25,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string CacheFolderName = nameof(ImageCache);
- private static readonly ImmutableDictionary RetryCountToDelay = new Dictionary()
+ private static readonly Dictionary RetryCountToDelay = new Dictionary()
{
[0] = TimeSpan.FromSeconds(4),
[1] = TimeSpan.FromSeconds(16),
@@ -32,13 +33,13 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
[3] = TimeSpan.FromSeconds(4),
[4] = TimeSpan.FromSeconds(16),
[5] = TimeSpan.FromSeconds(64),
- }.ToImmutableDictionary();
+ };
private readonly ILogger logger;
-
- // violate di rule
private readonly HttpClient httpClient;
+ private readonly ConcurrentDictionary concurrentTasks = new();
+
private string? baseFolder;
private string? cacheFolder;
@@ -100,11 +101,30 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
///
public async Task GetFileFromCacheAsync(Uri uri)
{
- string filePath = Path.Combine(GetCacheFolder(), GetCacheFileName(uri));
+ string fileName = GetCacheFileName(uri);
+ string filePath = Path.Combine(GetCacheFolder(), fileName);
if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0)
{
- await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
+ TaskCompletionSource taskCompletionSource = new();
+ try
+ {
+ if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
+ {
+ await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
+ }
+ else
+ {
+ if (concurrentTasks.TryGetValue(fileName, out Task? task))
+ {
+ await task.ConfigureAwait(false);
+ }
+ }
+ }
+ finally
+ {
+ taskCompletionSource.TrySetResult();
+ }
}
return filePath;
@@ -191,7 +211,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
if (retryCount == 3)
{
- uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao, }.Uri;
+ uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao }.Uri;
}
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceScopeExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceScopeExtension.cs
index 97e63433..f52fd970 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceScopeExtension.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/ServiceScopeExtension.cs
@@ -10,7 +10,8 @@ namespace Snap.Hutao.Core.DependencyInjection;
///
public static class ServiceScopeExtension
{
- private static IServiceScope? scopeReference;
+ // Allow GC to Collect the IServiceScope
+ private static readonly WeakReference ScopeReference = new(null!);
///
/// 追踪服务范围
@@ -19,7 +20,7 @@ public static class ServiceScopeExtension
public static void Track(this IServiceScope scope)
{
DisposeLast();
- scopeReference = scope;
+ ScopeReference.SetTarget(scope);
}
///
@@ -27,6 +28,9 @@ public static class ServiceScopeExtension
///
public static void DisposeLast()
{
- scopeReference?.Dispose();
+ if (ScopeReference.TryGetTarget(out IServiceScope? scope))
+ {
+ scope.Dispose();
+ }
}
-}
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs
index 7c3d6d76..1b0a21fa 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ExceptionRecorder.cs
@@ -45,4 +45,4 @@ internal class ExceptionRecorder
{
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);
}
-}
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/UserdataCorruptedException.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/UserdataCorruptedException.cs
new file mode 100644
index 00000000..bb4c8c31
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/UserdataCorruptedException.cs
@@ -0,0 +1,20 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Core.ExceptionService;
+
+///
+/// 用户数据损坏异常
+///
+internal class UserdataCorruptedException : Exception
+{
+ ///
+ /// 构造一个新的用户数据损坏异常
+ ///
+ /// 消息
+ /// 内部错误
+ public UserdataCorruptedException(string message, Exception innerException)
+ : base($"用户数据已损坏: {message}", innerException)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ScheduleTaskHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/ScheduleTaskHelper.cs
index 7c460e34..8cf91b96 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/ScheduleTaskHelper.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/ScheduleTaskHelper.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.Win32.TaskScheduler;
+using System.IO;
using System.Runtime.InteropServices;
using SchedulerTask = Microsoft.Win32.TaskScheduler.Task;
@@ -23,6 +24,7 @@ internal static class ScheduleTaskHelper
{
try
{
+ // TODO: 似乎可以不删除任务,直接注册已经包含了更新功能
SchedulerTask? targetTask = TaskService.Instance.GetTask(DailyNoteRefreshTaskName);
if (targetTask != null)
{
@@ -36,12 +38,9 @@ internal static class ScheduleTaskHelper
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);
return true;
}
- catch (UnauthorizedAccessException)
- {
- return false;
- }
- catch (COMException)
+ catch (Exception ex)
{
+ _ = ex;
return false;
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs
index 0c952a1f..bbc79453 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs
@@ -32,4 +32,9 @@ internal static class SettingKeys
/// 静态资源合约V2 成就图标与物品图标
///
public const string StaticResourceV2Contract = "StaticResourceV2Contract";
+
+ ///
+ /// 静态资源合约V3 刷新 Skill Talent
+ ///
+ public const string StaticResourceV3Contract = "StaticResourceV3Contract";
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs
index f549a814..01cf3deb 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Threading/ThreadHelper.cs
@@ -39,6 +39,13 @@ internal static class ThreadHelper
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void InvokeOnMainThread(Action action)
{
- Program.DispatcherQueue!.Invoke(action);
+ if (Program.DispatcherQueue!.HasThreadAccess)
+ {
+ action();
+ }
+ else
+ {
+ Program.DispatcherQueue.Invoke(action);
+ }
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ImmutableIntrinsics.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/IntrinsicImmutables.cs
similarity index 97%
rename from src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ImmutableIntrinsics.cs
rename to src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/IntrinsicImmutables.cs
index 9c5b93b2..c5f872d9 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/ImmutableIntrinsics.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/IntrinsicImmutables.cs
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Intrinsic;
///
/// 不可变的原生枚举
///
-public static class ImmutableIntrinsics
+public static class IntrinsicImmutables
{
///
/// 所属地区
diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
index 4c8c4dc7..287f36e0 100644
--- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
+++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest
@@ -12,7 +12,7 @@
+ Version="1.3.10.0" />
胡桃
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Font/CascadiaMono.ttf b/src/Snap.Hutao/Snap.Hutao/Resource/Font/CascadiaMono.ttf
new file mode 100644
index 00000000..d15637ef
Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Resource/Font/CascadiaMono.ttf differ
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Font/MiSans-Regular.ttf b/src/Snap.Hutao/Snap.Hutao/Resource/Font/MiSans-Regular.ttf
new file mode 100644
index 00000000..9cbfff50
Binary files /dev/null and b/src/Snap.Hutao/Snap.Hutao/Resource/Font/MiSans-Regular.ttf differ
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Font/Segoe Fluent Icons.ttf b/src/Snap.Hutao/Snap.Hutao/Resource/Font/Segoe Fluent Icons.ttf
deleted file mode 100644
index 8f05a4bb..00000000
Binary files a/src/Snap.Hutao/Snap.Hutao/Resource/Font/Segoe Fluent Icons.ttf and /dev/null differ
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs
index f492e7b1..08653279 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Achievement/AchievementService.cs
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
+using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity.Database;
@@ -114,7 +115,17 @@ internal class AchievementService : IAchievementService
List results = new();
foreach (MetadataAchievement meta in metadata)
{
- EntityAchievement entity = entities.SingleOrDefault(e => e.Id == meta.Id) ?? EntityAchievement.Create(archiveId, meta.Id);
+ EntityAchievement? entity;
+ try
+ {
+ entity = entities.SingleOrDefault(e => e.Id == meta.Id);
+ }
+ catch (InvalidOperationException ex)
+ {
+ throw new UserdataCorruptedException("单个成就存档内发现多个相同的成就 Id", ex);
+ }
+
+ entity ??= EntityAchievement.Create(archiveId, meta.Id);
results.Add(new(meta, entity));
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs
index 05a8e176..a74cbd80 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs
@@ -22,7 +22,6 @@ namespace Snap.Hutao.Service.AvatarInfo;
[Injection(InjectAs.Scoped, typeof(IAvatarInfoService))]
internal class AvatarInfoService : IAvatarInfoService
{
- private readonly AppDbContext appDbContext;
private readonly ISummaryFactory summaryFactory;
private readonly IMetadataService metadataService;
private readonly ILogger logger;
@@ -42,7 +41,6 @@ internal class AvatarInfoService : IAvatarInfoService
ISummaryFactory summaryFactory,
ILogger logger)
{
- this.appDbContext = appDbContext;
this.metadataService = metadataService;
this.summaryFactory = summaryFactory;
this.logger = logger;
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs
index 540ae603..56de2214 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs
@@ -43,7 +43,7 @@ internal class SummaryAvatarFactory
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
MetadataAvatar avatar = metadataContext.IdAvatarMap[avatarInfo.AvatarId];
- return new()
+ PropertyAvatar propertyAvatar = new()
{
Id = avatar.Id,
Name = avatar.Name,
@@ -52,8 +52,6 @@ internal class SummaryAvatarFactory
NameCard = AvatarNameCardPicConverter.AvatarToUri(avatar),
Quality = avatar.Quality,
Element = ElementNameIconConverter.ElementNameToElementType(avatar.FetterInfo.VisionBefore),
- Level = $"Lv.{avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value}",
- LevelNumber = int.Parse(avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value ?? string.Empty),
FetterLevel = avatarInfo.FetterInfo.ExpLevel,
Weapon = reliquaryAndWeapon.Weapon,
Reliquaries = reliquaryAndWeapon.Reliquaries,
@@ -62,7 +60,11 @@ internal class SummaryAvatarFactory
Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap),
Score = reliquaryAndWeapon.Reliquaries.Sum(r => r.Score).ToString("F2"),
CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}",
+ LevelNumber = int.Parse(avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].Value ?? string.Empty),
};
+
+ propertyAvatar.Level = $"Lv.{propertyAvatar.LevelNumber}";
+ return propertyAvatar;
}
private ReliquaryAndWeapon ProcessEquip(List equipments)
@@ -85,7 +87,7 @@ internal class SummaryAvatarFactory
}
}
- return new(reliquaryList, weapon!);
+ return new(reliquaryList, weapon);
}
private PropertyWeapon CreateWeapon(Equip equip)
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs
index 639707cb..d2efc73e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs
@@ -47,6 +47,7 @@ internal class DailyNoteService : IDailyNoteService, IRecipient
{
+ // Database items have been deleted by cascade deleting.
entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
});
}
@@ -151,6 +152,7 @@ internal class DailyNoteService : IDailyNoteService, IRecipient().DailyNotes.RemoveAndSave(entry);
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
index 64fdc997..f83897b3 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
+using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
@@ -296,6 +297,7 @@ internal class GachaLogService : IGachaLogService
break;
}
+ token.ThrowIfCancellationRequested();
SaveGachaItems(itemsToAdd, isLazy, archive, configration.EndId);
await RandomDelayAsync(token).ConfigureAwait(false);
}
@@ -327,14 +329,21 @@ internal class GachaLogService : IGachaLogService
if (archive != null)
{
- // TODO: replace with MaxBy
- // https://github.com/dotnet/efcore/issues/25566
- // .MaxBy(i => i.Id);
- item = appDbContext.GachaItems
- .Where(i => i.ArchiveId == archive.InnerId)
- .Where(i => i.QueryType == configType)
- .OrderByDescending(i => i.Id)
- .FirstOrDefault();
+ try
+ {
+ // TODO: replace with MaxBy
+ // https://github.com/dotnet/efcore/issues/25566
+ // .MaxBy(i => i.Id);
+ item = appDbContext.GachaItems
+ .Where(i => i.ArchiveId == archive.InnerId)
+ .Where(i => i.QueryType == configType)
+ .OrderByDescending(i => i.Id)
+ .FirstOrDefault();
+ }
+ catch (SqliteException ex)
+ {
+ throw new Core.ExceptionService.UserdataCorruptedException("无法获取祈愿记录 End Id", ex);
+ }
}
return item?.Id ?? 0L;
@@ -369,11 +378,12 @@ internal class GachaLogService : IGachaLogService
private INameQuality GetNameQualityByItemId(int id)
{
- return id.Place() switch
+ int place = id.Place();
+ return place switch
{
8 => idAvatarMap![id],
5 => idWeaponMap![id],
- _ => throw Must.NeverHappen(),
+ _ => throw Must.NeverHappen($"Id places: {place}"),
};
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs
index 1b402513..c86e24d0 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs
@@ -143,6 +143,11 @@ internal class GameService : IGameService, IDisposable
string gamePath = GetGamePathSkipLocator();
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFile);
+ if (!File.Exists(configPath))
+ {
+ return new(null, null, configPath);
+ }
+
using (FileStream stream = File.OpenRead(configPath))
{
List elements = IniSerializer.Deserialize(stream).ToList();
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/MultiChannel.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/MultiChannel.cs
index b50a2c3f..85247165 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/MultiChannel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/MultiChannel.cs
@@ -18,14 +18,21 @@ public struct MultiChannel
///
public string SubChannel;
+ ///
+ /// 配置文件路径 当不为 null 时则存在文件读写问题
+ ///
+ public string? ConfigFilePath;
+
///
/// 构造一个新的多通道
///
/// 通道
/// 子通道
- public MultiChannel(string? channel, string? subChannel)
+ /// 配置文件路径
+ public MultiChannel(string? channel, string? subChannel, string? configFilePath = null)
{
Channel = channel ?? string.Empty;
SubChannel = subChannel ?? string.Empty;
+ ConfigFilePath = configFilePath;
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs
index 83b38deb..507d3d4f 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs
@@ -6,6 +6,7 @@ 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;
@@ -75,21 +76,24 @@ internal class GameFpsUnlocker : IGameFpsUnlocker
private static unsafe MODULEENTRY32 FindModule(int processId, string moduleName)
{
- using (SafeFileHandle snapshot = CreateToolhelp32Snapshot_SafeHandle(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, (uint)processId))
+ HANDLE snapshot = CreateToolhelp32Snapshot(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, (uint)processId);
+ using (snapshot.AutoClose())
{
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
MODULEENTRY32 entry = StructMarshal.MODULEENTRY32();
bool found = false;
- // First module must be exe. Ignoring it.
- for (Module32First(snapshot, ref entry); Module32Next(snapshot, ref entry);)
+ bool loop = Module32First(snapshot, &entry);
+ while (loop)
{
if (entry.th32ProcessID == processId && entry.szModule.AsString() == moduleName)
{
found = true;
break;
}
+
+ loop = Module32Next(snapshot, &entry);
}
return found ? entry : default;
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs
index 864cfecb..52a2cf63 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Navigation/NavigationService.cs
@@ -181,7 +181,7 @@ internal class NavigationService : INavigationService
///
public void GoBack()
{
- Program.DispatcherQueue!.TryEnqueue(() =>
+ ThreadHelper.InvokeOnMainThread(() =>
{
bool canGoBack = Frame?.CanGoBack ?? false;
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
index 731b23f4..759094c6 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
@@ -124,7 +124,14 @@ internal class UserService : IUserService
}
userCollection = users.ToObservableCollection();
- Current = users.SingleOrDefault(user => user.IsSelected);
+ try
+ {
+ Current = users.SingleOrDefault(user => user.IsSelected);
+ }
+ catch (InvalidOperationException ex)
+ {
+ throw new Core.ExceptionService.UserdataCorruptedException("无法设置当前用户", ex);
+ }
}
return userCollection;
@@ -157,8 +164,16 @@ internal class UserService : IUserService
{
if (userCollection != null)
{
- // TODO: optimize match speed.
- return userCollection.SelectMany(u => u.UserGameRoles).SingleOrDefault(r => r.GameUid == uid);
+ try
+ {
+ // TODO: optimize match speed.
+ return userCollection.SelectMany(u => u.UserGameRoles).SingleOrDefault(r => r.GameUid == uid);
+ }
+ catch (InvalidOperationException)
+ {
+ // Sequence contains more than one matching element
+ // TODO: return a specialize UserGameRole to indicate error
+ }
}
return null;
diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
index 7183b8c8..abe0a59b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
+++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
@@ -37,9 +37,12 @@
+
+
+
@@ -122,7 +125,8 @@
-
+
+
@@ -154,8 +158,8 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -171,14 +175,14 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
@@ -415,4 +419,9 @@
MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml
index 3bad90a3..67a1e04b 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml
@@ -64,25 +64,18 @@
-
+
-
@@ -94,7 +87,7 @@
-
+
@@ -108,19 +101,23 @@
HorizontalContentAlignment="Stretch"
Background="Transparent"
BorderBrush="{x:Null}"
+ BorderThickness="0"
+ CornerRadius="4,4,0,0"
IsExpanded="True">
+
+ 0,0,0,1
+
-
+
-
+
@@ -165,14 +157,13 @@
Width="40"
Height="40"
Margin="4"
- Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
+ Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
Foreground="{StaticResource OrangeBrush}"
IsIndeterminate="False"
Maximum="{Binding GuarenteeOrangeThreshold}"
Value="{Binding LastOrangePull}"/>
-
+
@@ -204,14 +189,13 @@
Width="40"
Height="40"
Margin="4"
- Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
+ Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
Foreground="{StaticResource PurpleBrush}"
IsIndeterminate="False"
Maximum="{Binding GuarenteePurpleThreshold}"
Value="{Binding LastPurplePull}"/>
@@ -260,7 +240,7 @@
Text="五星"/>
@@ -272,7 +252,7 @@
Text="四星"/>
@@ -284,7 +264,7 @@
Text="三星"/>
@@ -294,7 +274,6 @@
@@ -302,19 +281,16 @@
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AdoptCalculatorDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AdoptCalculatorDialog.xaml
index 4b8a488d..63c5b3de 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AdoptCalculatorDialog.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AdoptCalculatorDialog.xaml
@@ -4,17 +4,18 @@
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"
- Title="米游社养成计算"
Closed="OnContentDialogClosed"
- DefaultButton="Primary"
- PrimaryButtonText="完成"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
-
+
+ 0
+ 0
+
+ Height="500"
+ AllowFocusOnInteraction="False"/>
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CommunityGameRecordDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CommunityGameRecordDialog.xaml
index 89b35a8d..58bab248 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CommunityGameRecordDialog.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CommunityGameRecordDialog.xaml
@@ -8,15 +8,17 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Closed="OnContentDialogClosed"
- DefaultButton="Primary"
- PrimaryButtonText="完成"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
-
+
+ 0
+ 0
+
+ Height="580"
+ AllowFocusOnInteraction="True"/>
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml
index 457c3760..1c079c31 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml
@@ -4,17 +4,19 @@
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"
- Title="米游社实时便笺"
Closed="OnContentDialogClosed"
- DefaultButton="Primary"
- PrimaryButtonText="完成"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
-
+
+ 0
+ 0
+
+ Height="448"
+ AllowFocusOnInteraction="False"
+ DefaultBackgroundColor="Transparent"/>
-
+
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml.cs
index f87d66a1..f58f4f1a 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml.cs
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/DailyNoteVerificationDialog.xaml.cs
@@ -1,4 +1,4 @@
-// Copyright (c) DGP Studio. All rights reserved.
+// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
@@ -11,20 +11,19 @@ using Snap.Hutao.Web.Bridge;
namespace Snap.Hutao.View.Dialog;
///
-/// ʵʱ�����֤�Ի���
+/// 实时便笺验证对话框
///
public sealed partial class DailyNoteVerificationDialog : ContentDialog
{
private readonly IServiceScope scope;
private readonly UserAndUid userAndUid;
- [SuppressMessage("", "IDE0052")]
private DailyNoteJsInterface? dailyNoteJsInterface;
///
- /// ����һ���µ�ʵʱ�����֤�Ի���
+ /// 构造一个新的实时便笺验证对话框
///
- /// �û����ɫ
+ /// 用户与角色
public DailyNoteVerificationDialog(UserAndUid userAndUid)
{
InitializeComponent();
@@ -46,14 +45,21 @@ public sealed partial class DailyNoteVerificationDialog : ContentDialog
Model.Entity.User user = userAndUid.User;
coreWebView2.SetCookie(user.CookieToken, user.Ltoken, null).SetMobileUserAgent();
dailyNoteJsInterface = new(coreWebView2, scope.ServiceProvider);
+ dailyNoteJsInterface.ClosePageRequested += OnClosePageRequested;
string query = $"?role_id={userAndUid.Uid.Value}&server={userAndUid.Uid.Region}";
coreWebView2.Navigate($"https://webstatic.mihoyo.com/app/community-game-records/index.html?bbs_presentation_style=fullscreen#/ys/daily/{query}");
}
+ private void OnClosePageRequested()
+ {
+ ThreadHelper.InvokeOnMainThread(Hide);
+ }
+
private void OnContentDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args)
{
+ dailyNoteJsInterface.ClosePageRequested -= OnClosePageRequested;
dailyNoteJsInterface = null;
scope.Dispose();
}
-}
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Extension/CoreWebView2EnvironmentExtension.cs b/src/Snap.Hutao/Snap.Hutao/View/Extension/CoreWebView2EnvironmentExtension.cs
new file mode 100644
index 00000000..da3d522e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Extension/CoreWebView2EnvironmentExtension.cs
@@ -0,0 +1,31 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.Web.WebView2.Core;
+using System.Diagnostics;
+
+namespace Snap.Hutao.View.Extension;
+
+///
+/// 扩展
+///
+internal static class CoreWebView2EnvironmentExtension
+{
+ ///
+ /// 退出
+ ///
+ /// 环境
+ public static void Exit(this CoreWebView2Environment environment)
+ {
+ IReadOnlyList processInfos = environment.GetProcessInfos();
+
+ foreach (CoreWebView2ProcessInfo processInfo in processInfos)
+ {
+ Process p = Process.GetProcessById(processInfo.ProcessId);
+ if (p.ProcessName == "msedgewebview2.exe")
+ {
+ p.Kill();
+ }
+ }
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml
index 3671b0c7..d58e02e2 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AchievementPage.xaml
@@ -160,6 +160,9 @@
Margin="16,0,0,16"
ItemsPanel="{StaticResource ItemsStackPanelTemplate}"
ItemsSource="{Binding Achievements}">
+
+
+
+ protected override void OnNavigatedFrom(NavigationEventArgs e)
+ {
+ base.OnNavigatedFrom(e);
+ //WebView.CoreWebView2.Environment.Exit();
+ }
+
private static string? ReplaceForeground(string? rawContent, ElementTheme theme)
{
if (string.IsNullOrWhiteSpace(rawContent))
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml
index 3ac6b3ea..d85347b9 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AnnouncementPage.xaml
@@ -149,7 +149,7 @@
-
+
+ Text="{Binding SelectedAvatar.FetterLevel}"
+ TextAlignment="Center"
+ TextTrimming="None"
+ TextWrapping="NoWrap"/>
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml
index 5eedd865..e14257c0 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml
@@ -481,7 +481,8 @@
+ Icon="{shcm:FontIcon Glyph=}"
+ Style="{StaticResource DefaultSettingStyle}">
rawAchievements = await metadataService.GetAchievementsAsync(CancellationToken).ConfigureAwait(false);
- List combined = achievementService.GetAchievements(archive, rawAchievements);
+ List combined;
+ try
+ {
+ combined = achievementService.GetAchievements(archive, rawAchievements);
+ }
+ catch (Core.ExceptionService.UserdataCorruptedException ex)
+ {
+ infoBarService.Error(ex);
+ return;
+ }
// Assemble achievements on the UI thread.
await ThreadHelper.SwitchToMainThreadAsync();
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs
index 76703968..2eff3677 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs
@@ -88,9 +88,15 @@ internal class DailyNoteViewModel : Abstraction.ViewModel
{
if (value != null)
{
- refreshSecondsEntry!.SetInt32(value.Value);
- appDbContext.Settings.UpdateAndSave(refreshSecondsEntry!);
- ScheduleTaskHelper.RegisterForDailyNoteRefresh(value.Value);
+ if (!ScheduleTaskHelper.RegisterForDailyNoteRefresh(value.Value))
+ {
+ Ioc.Default.GetRequiredService().Warning("注册计划任务失败,请使用管理员模式重试");
+ }
+ else
+ {
+ refreshSecondsEntry!.SetInt32(value.Value);
+ appDbContext.Settings.UpdateAndSave(refreshSecondsEntry!);
+ }
}
}
}
@@ -170,24 +176,38 @@ internal class DailyNoteViewModel : Abstraction.ViewModel
private async Task OpenUIAsync()
{
- UserAndUids = await userService.GetRoleCollectionAsync().ConfigureAwait(true);
+ UserAndUids = await userService.GetRoleCollectionAsync().ConfigureAwait(false);
+ try
+ {
+ ThrowIfViewDisposed();
+ using (await DisposeLock.EnterAsync().ConfigureAwait(false))
+ {
+ ThrowIfViewDisposed();
+ await ThreadHelper.SwitchToMainThreadAsync();
- refreshSecondsEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteRefreshSeconds, "480");
- selectedRefreshTime = refreshTimes.Single(t => t.Value == refreshSecondsEntry.GetInt32());
- ScheduleTaskHelper.RegisterForDailyNoteRefresh(480);
- OnPropertyChanged(nameof(SelectedRefreshTime));
+ refreshSecondsEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteRefreshSeconds, "480");
+ selectedRefreshTime = refreshTimes.Single(t => t.Value == refreshSecondsEntry.GetInt32());
+ OnPropertyChanged(nameof(SelectedRefreshTime));
+ ScheduleTaskHelper.RegisterForDailyNoteRefresh(480);
- reminderNotifyEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteReminderNotify, SettingEntryHelper.FalseString);
- isReminderNotification = reminderNotifyEntry.GetBoolean();
- OnPropertyChanged(nameof(IsReminderNotification));
+ reminderNotifyEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteReminderNotify, SettingEntryHelper.FalseString);
+ isReminderNotification = reminderNotifyEntry.GetBoolean();
+ OnPropertyChanged(nameof(IsReminderNotification));
- silentModeEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, SettingEntryHelper.FalseString);
- isSilentWhenPlayingGame = silentModeEntry.GetBoolean();
- OnPropertyChanged(nameof(IsSilentWhenPlayingGame));
+ silentModeEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, SettingEntryHelper.FalseString);
+ isSilentWhenPlayingGame = silentModeEntry.GetBoolean();
+ OnPropertyChanged(nameof(IsSilentWhenPlayingGame));
- ObservableCollection temp = await dailyNoteService.GetDailyNoteEntriesAsync().ConfigureAwait(false);
- await ThreadHelper.SwitchToMainThreadAsync();
- DailyNoteEntries = temp;
+ await ThreadHelper.SwitchToBackgroundAsync();
+ }
+
+ ObservableCollection temp = await dailyNoteService.GetDailyNoteEntriesAsync().ConfigureAwait(false);
+ await ThreadHelper.SwitchToMainThreadAsync();
+ DailyNoteEntries = temp;
+ }
+ catch (OperationCanceledException)
+ {
+ }
}
private async Task TrackRoleAsync(UserAndUid? role)
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs
index a1db507c..9e1280e1 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLogViewModel.cs
@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
+using Microsoft.Data.Sqlite;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.IO;
@@ -199,7 +200,26 @@ internal class GachaLogViewModel : Abstraction.ViewModel
GachaLogRefreshProgressDialog dialog = new();
IAsyncDisposable dialogHider = await dialog.BlockAsync().ConfigureAwait(false);
Progress progress = new(dialog.OnReport);
- bool authkeyValid = await gachaLogService.RefreshGachaLogAsync(query, strategy, progress, default).ConfigureAwait(false);
+ bool authkeyValid;
+
+ using (await DisposeLock.EnterAsync().ConfigureAwait(false))
+ {
+ try
+ {
+ authkeyValid = await gachaLogService.RefreshGachaLogAsync(query, strategy, progress, CancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // We set true here in order to hide the dialog.
+ authkeyValid = true;
+ infoBarService.Warning("祈愿记录刷新操作被异常取消");
+ }
+ catch (Core.ExceptionService.UserdataCorruptedException ex)
+ {
+ authkeyValid = false;
+ infoBarService.Error(ex);
+ }
+ }
await ThreadHelper.SwitchToMainThreadAsync();
if (authkeyValid)
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs
index e0d7860e..56d066bf 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs
@@ -221,13 +221,21 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
ThrowIfViewDisposed();
MultiChannel multi = gameService.GetMultiChannel();
- SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
+ if (string.IsNullOrEmpty(multi.ConfigFilePath))
+ {
+ SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
+ }
+ else
+ {
+ Ioc.Default.GetRequiredService().Warning("无法读取游戏配置文件");
+ }
+
GameAccounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(true);
// Sync uid
if (memoryCache.TryGetValue(DesiredUid, out object? value) && value is string uid)
{
- SelectedGameAccount = GameAccounts.SingleOrDefault(g => g.AttachUid == uid);
+ SelectedGameAccount = GameAccounts.FirstOrDefault(g => g.AttachUid == uid);
}
// Sync from Settings
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs
index 4a244547..a455fa40 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SettingViewModel.cs
@@ -207,7 +207,16 @@ internal class SettingViewModel : Abstraction.ViewModel
IInfoBarService infoBarService = Ioc.Default.GetRequiredService();
if (Directory.Exists(cacheFolder))
{
- Directory.Delete(cacheFolder, true);
+ try
+ {
+ Directory.Delete(cacheFolder, true);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ infoBarService.Warning($"清除失败,文件目录权限不足,请使用管理员模式重试");
+ return;
+ }
+
infoBarService.Success("清除完成");
}
else
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs
index df00071c..6cbf89f6 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs
@@ -52,7 +52,42 @@ internal class WelcomeViewModel : ObservableObject
private async Task OpenUIAsync()
{
- List downloadSummaries = new();
+ HashSet downloadSummaries = GenerateStaticResourceDownloadTasks();
+
+ DownloadSummaries = downloadSummaries.ToObservableCollection();
+
+ // Cancel all previous created jobs
+ serviceProvider.GetRequiredService().CancelAllJobs();
+ await Task.WhenAll(downloadSummaries.Select(async d =>
+ {
+ await d.DownloadAndExtractAsync().ConfigureAwait(false);
+ await ThreadHelper.SwitchToMainThreadAsync();
+ DownloadSummaries.Remove(d);
+ })).ConfigureAwait(true);
+
+ serviceProvider.GetRequiredService().Send(new Message.WelcomeStateCompleteMessage());
+
+ // Complete StaticResourceContracts
+ LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true);
+ LocalSetting.Set(SettingKeys.StaticResourceV2Contract, true);
+ LocalSetting.Set(SettingKeys.StaticResourceV3Contract, true);
+
+ try
+ {
+ new ToastContentBuilder()
+ .AddText("下载完成")
+ .AddText("现在可以开始使用胡桃了")
+ .Show();
+ }
+ catch (COMException)
+ {
+ // 0x803E0105
+ }
+ }
+
+ private HashSet GenerateStaticResourceDownloadTasks()
+ {
+ HashSet downloadSummaries = new(EqualityComparer.Default);
if (!LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false))
{
@@ -75,35 +110,20 @@ internal class WelcomeViewModel : ObservableObject
downloadSummaries.Add(new(serviceProvider, "圣遗物图标", "RelicIcon"));
}
- DownloadSummaries = new(downloadSummaries);
-
- // Cancel all previous created jobs
- serviceProvider.GetRequiredService().CancelAllJobs();
- await Task.WhenAll(downloadSummaries.Select(d => d.DownloadAndExtractAsync())).ConfigureAwait(true);
-
- serviceProvider.GetRequiredService().Send(new Message.WelcomeStateCompleteMessage());
-
- // Complete StaticResourceContracts
- LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true);
- LocalSetting.Set(SettingKeys.StaticResourceV2Contract, true);
-
- try
+ if (!LocalSetting.Get(SettingKeys.StaticResourceV3Contract, false))
{
- new ToastContentBuilder()
- .AddText("下载完成")
- .AddText("现在可以开始使用胡桃了")
- .Show();
- }
- catch (COMException)
- {
- // 0x803E0105
+ downloadSummaries.Add(new(serviceProvider, "天赋图标更新", "Skill"));
+ downloadSummaries.Add(new(serviceProvider, "命之座图标更新", "Talent"));
}
+
+ return downloadSummaries;
}
///
/// 下载信息
///
- public class DownloadSummary : ObservableObject
+ [SuppressMessage("", "CA1067")]
+ public class DownloadSummary : ObservableObject, IEquatable
{
private readonly IServiceProvider serviceProvider;
private readonly BitsManager bitsManager;
@@ -145,6 +165,12 @@ internal class WelcomeViewModel : ObservableObject
///
public double ProgressValue { get => progressValue; set => SetProperty(ref progressValue, value); }
+ ///
+ public bool Equals(DownloadSummary? other)
+ {
+ return fileName == other?.fileName;
+ }
+
///
/// 异步下载并解压
///
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiAvatarViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiAvatarViewModel.cs
index 6fa84a05..36c56cbe 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiAvatarViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiAvatarViewModel.cs
@@ -211,25 +211,25 @@ internal class WikiAvatarViewModel : Abstraction.ViewModel
continue;
}
- if (ImmutableIntrinsics.AssociationTypes.Contains(value))
+ if (IntrinsicImmutables.AssociationTypes.Contains(value))
{
keep = keep || avatar.FetterInfo.Association.GetDescriptionOrNull() == value;
continue;
}
- if (ImmutableIntrinsics.WeaponTypes.Contains(value))
+ if (IntrinsicImmutables.WeaponTypes.Contains(value))
{
keep = keep || avatar.Weapon.GetDescriptionOrNull() == value;
continue;
}
- if (ImmutableIntrinsics.ItemQualities.Contains(value))
+ if (IntrinsicImmutables.ItemQualities.Contains(value))
{
keep = keep || avatar.Quality.GetDescriptionOrNull() == value;
continue;
}
- if (ImmutableIntrinsics.BodyTypes.Contains(value))
+ if (IntrinsicImmutables.BodyTypes.Contains(value))
{
keep = keep || avatar.Body.GetDescriptionOrNull() == value;
continue;
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiWeaponViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiWeaponViewModel.cs
index d87ff679..38ab890b 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiWeaponViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/WikiWeaponViewModel.cs
@@ -203,19 +203,19 @@ internal class WikiWeaponViewModel : Abstraction.ViewModel
{
string value = segment.ToString();
- if (ImmutableIntrinsics.WeaponTypes.Contains(value))
+ if (IntrinsicImmutables.WeaponTypes.Contains(value))
{
keep = keep || weapon.WeaponType.GetDescriptionOrNull() == value;
continue;
}
- if (ImmutableIntrinsics.ItemQualities.Contains(value))
+ if (IntrinsicImmutables.ItemQualities.Contains(value))
{
keep = keep || weapon.Quality.GetDescriptionOrNull() == value;
continue;
}
- if (ImmutableIntrinsics.FightProperties.Contains(value))
+ if (IntrinsicImmutables.FightProperties.Contains(value))
{
keep = keep || weapon.Property.Properties.ElementAtOrDefault(1).GetDescriptionOrNull() == value;
continue;
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs
index c230620f..a7bc6e00 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs
@@ -41,6 +41,7 @@ public class MiHoYoJSInterface
private readonly CoreWebView2 webView;
private readonly IServiceProvider serviceProvider;
private readonly ILogger logger;
+ private readonly SemaphoreSlim webMessageSemaphore = new(1);
///
/// 构造一个新的调用桥
@@ -59,6 +60,8 @@ public class MiHoYoJSInterface
webView.NavigationStarting += OnNavigationStarting;
}
+ public event Action? ClosePageRequested;
+
///
/// 获取ActionTicket
///
@@ -160,7 +163,7 @@ public class MiHoYoJSInterface
string q = param.Payload.GetQueryParam();
string check = Md5Convert.ToHexString($"salt={salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant();
- return new() { Data = new() { ["DS"] = $"{t},{r},{check}", }, };
+ return new() { Data = new() { ["DS"] = $"{t},{r},{check}" } };
static int GetRandom()
{
@@ -214,9 +217,12 @@ public class MiHoYoJSInterface
cookieToken = cookieTokenResponse.Data.CookieToken;
}
+ // Check null and create a new one to avoid System.NullReferenceException
+ user.CookieToken ??= new();
+
// sync ui and database
- user.CookieToken![Cookie.COOKIE_TOKEN] = cookieToken!;
- Ioc.Default.GetRequiredService().Users.UpdateAndSave(user.Entity);
+ user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken!;
+ serviceProvider.GetRequiredService().Users.UpdateAndSave(user.Entity);
}
else
{
@@ -234,6 +240,7 @@ public class MiHoYoJSInterface
[JsMethod("closePage")]
public virtual IJsResult? ClosePage(JsParam param)
{
+ ClosePageRequested?.Invoke();
return null;
}
@@ -256,7 +263,7 @@ public class MiHoYoJSInterface
[JsMethod("getStatusBarHeight")]
public virtual JsResult> GetStatusBarHeight(JsParam param)
{
- return new() { Data = new() { { "statusBarHeight", 0 } } };
+ return new() { Data = new() { ["statusBarHeight"] = 0 } };
}
[JsMethod("pushPage")]
@@ -352,11 +359,14 @@ public class MiHoYoJSInterface
JsParam param = JsonSerializer.Deserialize(message)!;
logger.LogInformation("[OnMessage]\nMethod : {method}\nPayload : {payload}\nCallback: {callback}", param.Method, param.Payload, param.Callback);
- IJsResult? result = await TryGetJsResultFromJsParamAsync(param).ConfigureAwait(false);
-
- if (result != null && param.Callback != null)
+ using (await webMessageSemaphore.EnterAsync().ConfigureAwait(false))
{
- await ExecuteCallbackScriptAsync(param.Callback, result.ToString()).ConfigureAwait(false);
+ IJsResult? result = await TryGetJsResultFromJsParamAsync(param).ConfigureAwait(false);
+
+ if (result != null && param.Callback != null)
+ {
+ await ExecuteCallbackScriptAsync(param.Callback, result.ToString()).ConfigureAwait(false);
+ }
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
index d20a8ac3..21a239a1 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
@@ -45,7 +45,7 @@ public partial class Cookie
{
SortedDictionary cookieMap = new();
cookieString = cookieString.Replace(" ", string.Empty);
- string[] values = cookieString.TrimEnd(';').Split(';');
+ string[] values = cookieString.Split(';', StringSplitOptions.RemoveEmptyEntries);
foreach (string[] parts in values.Select(c => c.Split('=', 2)))
{
string name = parts[0].Trim();
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Game.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Game.cs
index a8ff3562..a51372a4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Game.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Game.cs
@@ -15,8 +15,8 @@ public class Game
public Package Latest { get; set; } = default!;
///
- /// 差异文件
+ /// 相对于当前版本的之前版本的差异文件(非预下载)
///
[JsonPropertyName("diffs")]
- public IList Diffs { get; set; } = default!;
+ public List Diffs { get; set; } = default!;
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/GameResource.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/GameResource.cs
index 6ef548ea..b2e5a682 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/GameResource.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/GameResource.cs
@@ -21,13 +21,13 @@ public class GameResource
public Plugin Plugin { get; set; } = default!;
///
- /// 官网地址
+ /// 官网地址 https://ys.mihoyo.com/launcher
///
[JsonPropertyName("web_url")]
public Uri WebUrl { get; set; } = default!;
///
- /// 强制更新文件
+ /// 强制更新文件 null
///
[JsonPropertyName("force_update")]
public object? ForceUpdate { get; set; }
@@ -36,17 +36,23 @@ public class GameResource
/// 预下载
///
[JsonPropertyName("pre_download_game")]
- public object? PreDownloadGame { get; set; }
+ public Game? PreDownloadGame { get; set; }
///
/// 过期更新包
///
[JsonPropertyName("deprecated_packages")]
- public List DeprecatedPackages { get; set; } = default!;
+ public List DeprecatedPackages { get; set; } = default!;
+
+ ///
+ /// 渠道服sdk
+ ///
+ [JsonPropertyName("sdk")]
+ public object? Sdk { get; set; }
///
/// 过期的单个文件
///
[JsonPropertyName("deprecated_files")]
- public List? DeprecatedFiles { get; set; }
+ public List? DeprecatedFiles { get; set; }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/LocalFile.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/NameMd5.cs
similarity index 95%
rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/LocalFile.cs
rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/NameMd5.cs
index 80800a1e..5df04138 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/LocalFile.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/NameMd5.cs
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
///
/// 资源文件
///
-public class LocalFile
+public class NameMd5
{
///
/// 文件名称
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Package.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Package.cs
index 875d0313..0cb9f311 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Package.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Package.cs
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
///
/// 最新客户端
///
-public class Package : DownloadFile
+public class Package : PathMd5
{
///
/// 名称 空
@@ -27,14 +27,22 @@ public class Package : DownloadFile
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long Size { get; set; } = default!;
+ ///
+ /// 主程序相对路径 YuanShen.exe
+ ///
+ public string Entry { get; set; } = default!;
+
///
/// 语音包
///
[JsonPropertyName("voice_packs")]
public List VoicePacks { get; set; } = default!;
+ // We don't want to support
+ // decompressed_path & segments
+
///
- /// 包大小
+ /// 包大小 bytes
///
[JsonPropertyName("package_size")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/DownloadFile.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/PathMd5.cs
similarity index 94%
rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/DownloadFile.cs
rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/PathMd5.cs
index 2c7395fd..c54de31e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/DownloadFile.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/PathMd5.cs
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
///
/// 下载的文件
///
-public class DownloadFile
+public class PathMd5
{
///
/// 下载地址
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/PluginItem.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/PluginItem.cs
index f5f8e8fa..12a42b8d 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/PluginItem.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/PluginItem.cs
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
///
/// 插件项
///
-public class PluginItem : LocalFile
+public class PluginItem : NameMd5
{
///
/// 版本 一般为空
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/VoicePackage.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/VoicePackage.cs
index c2ae4b30..4a967a8f 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/VoicePackage.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/VoicePackage.cs
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
///
/// 语音包
///
-public class VoicePackage : DownloadFile
+public class VoicePackage : PathMd5
{
///
/// 语音
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient.cs
index 42fa36bc..02ea534b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient.cs
@@ -51,7 +51,7 @@ internal class HomaClient
public async Task> CheckRecordUploadedAsync(PlayerUid uid, CancellationToken token = default)
{
Response? resp = await httpClient
- .GetFromJsonAsync>(HutaoEndpoints.RecordCheck(uid.Value), token)
+ .TryCatchGetFromJsonAsync>(HutaoEndpoints.RecordCheck(uid.Value), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
@@ -67,7 +67,7 @@ internal class HomaClient
public async Task> GetRankAsync(PlayerUid uid, CancellationToken token = default)
{
Response? resp = await httpClient
- .GetFromJsonAsync>(HutaoEndpoints.RecordRank(uid.Value), token)
+ .TryCatchGetFromJsonAsync>(HutaoEndpoints.RecordRank(uid.Value), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
@@ -82,7 +82,7 @@ internal class HomaClient
public async Task> GetOverviewAsync(CancellationToken token = default)
{
Response? resp = await httpClient
- .GetFromJsonAsync>(HutaoEndpoints.StatisticsOverview, token)
+ .TryCatchGetFromJsonAsync>(HutaoEndpoints.StatisticsOverview, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
diff --git a/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs b/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs
index 1fa785da..5c17c4e5 100644
--- a/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs
@@ -2,6 +2,8 @@
// Licensed under the MIT license.
using Windows.Graphics;
+using Windows.Win32.Foundation;
+using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Win32;
@@ -40,4 +42,29 @@ internal static class StructExtension
{
return sizeInt32.Width * sizeInt32.Height;
}
+
+ ///
+ /// 使用完成后自动关闭句柄
+ ///
+ /// 句柄
+ /// 用于关闭句柄的对象
+ public static IDisposable AutoClose(this HANDLE handle)
+ {
+ return new HandleCloser(handle);
+ }
+
+ private readonly struct HandleCloser : IDisposable
+ {
+ private readonly HANDLE handle;
+
+ public HandleCloser(HANDLE handle)
+ {
+ this.handle = handle;
+ }
+
+ public void Dispose()
+ {
+ CloseHandle(handle);
+ }
+ }
}
\ No newline at end of file