diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs
index 43d82481..ce1b24f9 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/UniformStaggeredLayout.cs
@@ -128,8 +128,13 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
UniformStaggeredItem item = state.GetItemAt(i);
if (item.Height == 0)
{
+ // https://github.com/DGP-Studio/Snap.Hutao/issues/1079
+ // The first element must be force refreshed otherwise
+ // it will use the old one realized
+ ElementRealizationOptions options = i == 0 ? ElementRealizationOptions.ForceCreate : ElementRealizationOptions.None;
+
// Item has not been measured yet. Get the element and store the values
- UIElement element = context.GetOrCreateElementAt(i);
+ UIElement element = context.GetOrCreateElementAt(i, options);
element.Measure(new Size(state.ColumnWidth, availableHeight));
item.Height = element.DesiredSize.Height;
item.Element = element;
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/CornerRadius.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/CornerRadius.xaml
index f2ebb9f5..4dcfd7d4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/CornerRadius.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/CornerRadius.xaml
@@ -1,4 +1,5 @@
4,4,0,0
0,0,4,4
+ 0,4,0,4
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/FlyoutStyle.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/FlyoutStyle.xaml
index b2da9a2e..955fffba 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/FlyoutStyle.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/FlyoutStyle.xaml
@@ -4,7 +4,6 @@
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
-
-
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteWebViewerSource.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteWebViewerSource.cs
index c8e648da..830c060b 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteWebViewerSource.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNote/DailyNoteWebViewerSource.cs
@@ -5,15 +5,16 @@ using Microsoft.Web.WebView2.Core;
using Snap.Hutao.View.Control;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Bridge;
+using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Request.QueryString;
namespace Snap.Hutao.ViewModel.DailyNote;
internal sealed class DailyNoteWebViewerSource : IWebViewerSource
{
- public MiHoYoJSInterface CreateJsInterface(IServiceProvider serviceProvider, CoreWebView2 coreWebView2, UserAndUid userAndUid)
+ public MiHoYoJSBridge CreateJSBridge(IServiceProvider serviceProvider, CoreWebView2 coreWebView2, UserAndUid userAndUid)
{
- return serviceProvider.CreateInstance(coreWebView2, userAndUid);
+ return serviceProvider.CreateInstance(coreWebView2, userAndUid);
}
public string GetSource(UserAndUid userAndUid)
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/SummaryItem.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/SummaryItem.cs
index e25c02f0..21f31527 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/SummaryItem.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/SummaryItem.cs
@@ -36,7 +36,7 @@ internal sealed class SummaryItem : Item
///
public string TimeFormatted
{
- get => $"{Time:yyy.MM.dd HH:mm:ss}";
+ get => $"{Time.ToLocalTime():yyy.MM.dd HH:mm:ss}";
}
///
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs
index 4fd85dcf..02a5e442 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs
@@ -125,7 +125,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
{
SelectedScheme = KnownSchemes
.Where(scheme => scheme.IsOversea == options.IsOversea)
- .Single(scheme => scheme.MultiChannelEqual(options));
+ .Single(scheme => scheme.Equals(options));
}
catch (InvalidOperationException)
{
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/HomeCardOptions.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HomeCardOptions.cs
similarity index 96%
rename from src/Snap.Hutao/Snap.Hutao/ViewModel/HomeCardOptions.cs
rename to src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HomeCardOptions.cs
index e5807644..6316ac4e 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/HomeCardOptions.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/HomeCardOptions.cs
@@ -3,7 +3,7 @@
using Snap.Hutao.Core.Setting;
-namespace Snap.Hutao.ViewModel;
+namespace Snap.Hutao.ViewModel.Setting;
internal sealed class HomeCardOptions
{
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs
index 131190c7..b57ee8d9 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs
@@ -11,6 +11,7 @@ using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Shell;
using Snap.Hutao.Core.Windowing;
+using Snap.Hutao.Core.Windowing.HotKey;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Picker;
using Snap.Hutao.Model;
@@ -51,6 +52,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
private readonly IInfoBarService infoBarService;
private readonly RuntimeOptions runtimeOptions;
private readonly IPickerFactory pickerFactory;
+ private readonly HotKeyOptions hotKeyOptions;
private readonly IUserService userService;
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
@@ -63,18 +65,14 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
///
public AppOptions Options { get => appOptions; }
- ///
- /// 胡桃选项
- ///
public RuntimeOptions HutaoOptions { get => runtimeOptions; }
- ///
- /// 胡桃用户选项
- ///
public HutaoUserOptions UserOptions { get => hutaoUserOptions; }
public HomeCardOptions HomeCardOptions { get => homeCardOptions; }
+ public HotKeyOptions HotKeyOptions { get => hotKeyOptions; }
+
public HutaoPassportViewModel Passport { get => hutaoPassportViewModel; }
///
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/MonsterView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/MonsterView.cs
index b7f6bf8a..d9a47f8c 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/MonsterView.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/SpiralAbyss/MonsterView.cs
@@ -11,7 +11,7 @@ namespace Snap.Hutao.ViewModel.SpiralAbyss;
internal sealed class MonsterView : INameIcon, IMappingFrom
{
- private MonsterView(MonsterRelationshipId id)
+ private MonsterView(in MonsterRelationshipId id)
{
Name = $"Unknown {id}";
Icon = Web.HutaoEndpoints.UIIconNone;
@@ -27,11 +27,6 @@ internal sealed class MonsterView : INameIcon, IMappingFrom(coreWebView2, userAndUid)
- : serviceProvider.CreateInstance(coreWebView2, userAndUid);
+ ? serviceProvider.CreateInstance(coreWebView2, userAndUid)
+ : serviceProvider.CreateInstance(coreWebView2, userAndUid);
}
public string GetSource(UserAndUid userAndUid)
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs
index 2f1f87c4..5ea90cb8 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs
@@ -321,19 +321,6 @@ internal static class ApiEndpoints
public const string AnnContent = $"{Hk4eApiAnnouncementApi}/getAnnContent?{AnnouncementQuery}";
#endregion
- #region Hk4eApiGachaInfoApi
-
- ///
- /// 获取祈愿记录
- ///
- /// query string
- /// 祈愿记录信息Url
- public static string GachaInfoGetGachaLog(string query)
- {
- return $"{Hk4eApiGachaInfoApi}/getGachaLog?{query}";
- }
- #endregion
-
#region PassportApi | PassportApiV4
///
@@ -380,6 +367,19 @@ internal static class ApiEndpoints
}
#endregion
+ #region PublicOperationHk4eGachaInfoApi
+
+ ///
+ /// 获取祈愿记录
+ ///
+ /// query string
+ /// 祈愿记录信息Url
+ public static string GachaInfoGetGachaLog(string query)
+ {
+ return $"{PublicOperationHk4eGachaInfoApi}/getGachaLog?{query}";
+ }
+ #endregion
+
#region SdkStaticLauncherApi
///
@@ -425,7 +425,6 @@ internal static class ApiEndpoints
private const string Hk4eApi = "https://hk4e-api.mihoyo.com";
private const string Hk4eApiAnnouncementApi = $"{Hk4eApi}/common/hk4e_cn/announcement/api";
- private const string Hk4eApiGachaInfoApi = $"{Hk4eApi}/event/gacha_info/api";
private const string PassportApi = "https://passport-api.mihoyo.com";
private const string PassportApiAuthApi = $"{PassportApi}/account/auth/api";
@@ -434,6 +433,9 @@ internal static class ApiEndpoints
private const string PublicDataApi = "https://public-data-api.mihoyo.com";
private const string PublicDataApiDeviceFpApi = $"{PublicDataApi}/device-fp/api";
+ private const string PublicOperationHk4e = "https://public-operation-hk4e.mihoyo.com";
+ private const string PublicOperationHk4eGachaInfoApi = $"{PublicOperationHk4e}/gacha_info/api";
+
private const string SdkStatic = "https://sdk-static.mihoyo.com";
private const string SdkStaticLauncherApi = $"{SdkStatic}/hk4e_cn/mdk/launcher/api";
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
index 676fb754..26cbc006 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
@@ -307,7 +307,7 @@ internal static class ApiOsEndpoints
private const string BbsApiOsGameRecordAppApi = $"{BbsApiOs}/game_record/app/genshin/api";
private const string Hk4eApiOs = "https://hk4e-api-os.hoyoverse.com";
- private const string Hk4eApiOsGachaInfoApi = $"{Hk4eApiOs}/event/gacha_info/api";
+ private const string Hk4eApiOsGachaInfoApi = $"{Hk4eApiOs}/gacha_info/api";
private const string SdkOsStatic = "https://sdk-os-static.mihoyo.com";
private const string SdkOsStaticLauncherApi = $"{SdkOsStatic}/hk4e_global/mdk/launcher/api";
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs
similarity index 83%
rename from src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs
rename to src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs
index 644a0f7e..2630976f 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs
@@ -24,7 +24,7 @@ namespace Snap.Hutao.Web.Bridge;
[HighQuality]
[SuppressMessage("", "CA1001")]
[SuppressMessage("", "CA1308")]
-internal class MiHoYoJSInterface
+internal class MiHoYoJSBridge
{
private const string InitializeJsInterfaceScript2 = """
window.MiHoYoJSInterface = {
@@ -45,7 +45,7 @@ internal class MiHoYoJSInterface
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
- private readonly ILogger logger;
+ private readonly ILogger logger;
private readonly TypedEventHandler webMessageReceivedEventHandler;
private readonly TypedEventHandler domContentLoadedEventHandler;
@@ -53,7 +53,7 @@ internal class MiHoYoJSInterface
private CoreWebView2 coreWebView2;
- public MiHoYoJSInterface(CoreWebView2 coreWebView2, UserAndUid userAndUid)
+ public MiHoYoJSBridge(CoreWebView2 coreWebView2, UserAndUid userAndUid)
{
// 由于Webview2 的作用域特殊性,我们在此处直接使用根服务
serviceProvider = Ioc.Default;
@@ -61,7 +61,7 @@ internal class MiHoYoJSInterface
this.userAndUid = userAndUid;
taskContext = serviceProvider.GetRequiredService();
- logger = serviceProvider.GetRequiredService>();
+ logger = serviceProvider.GetRequiredService>();
webMessageReceivedEventHandler = OnWebMessageReceived;
domContentLoadedEventHandler = OnDOMContentLoaded;
@@ -74,12 +74,20 @@ internal class MiHoYoJSInterface
public event Action? ClosePageRequested;
+ public void Detach()
+ {
+ coreWebView2.WebMessageReceived -= webMessageReceivedEventHandler;
+ coreWebView2.DOMContentLoaded -= domContentLoadedEventHandler;
+ coreWebView2.NavigationStarting -= navigationStartingEventHandler;
+ coreWebView2 = default!;
+ }
+
///
/// 关闭
///
/// 参数
/// 响应
- public virtual async ValueTask ClosePageAsync(JsParam param)
+ protected virtual async ValueTask ClosePageAsync(JsParam param)
{
await taskContext.SwitchToMainThreadAsync();
if (coreWebView2.CanGoBack)
@@ -99,7 +107,7 @@ internal class MiHoYoJSInterface
///
/// 参数
/// 响应
- public virtual IJsResult? ConfigureShare(JsParam param)
+ protected virtual IJsResult? ConfigureShare(JsParam param)
{
return null;
}
@@ -109,7 +117,7 @@ internal class MiHoYoJSInterface
///
/// 参数
/// 响应
- public virtual async ValueTask GetActionTicketAsync(JsParam jsParam)
+ protected virtual async ValueTask GetActionTicketAsync(JsParam jsParam)
{
return await serviceProvider
.GetRequiredService()
@@ -122,7 +130,7 @@ internal class MiHoYoJSInterface
///
/// 参数
/// 响应
- public virtual JsResult> GetCookieInfo(JsParam param)
+ protected virtual JsResult> GetCookieInfo(JsParam param)
{
ArgumentNullException.ThrowIfNull(userAndUid.User.LToken);
@@ -142,7 +150,7 @@ internal class MiHoYoJSInterface
///
/// 参数
/// 响应
- public virtual async ValueTask>> GetCookieTokenAsync(JsParam param)
+ protected virtual async ValueTask>> GetCookieTokenAsync(JsParam param)
{
IUserService userService = serviceProvider.GetRequiredService();
if (param.Payload.ForceRefresh)
@@ -162,7 +170,7 @@ internal class MiHoYoJSInterface
///
/// param
/// 语言与时区
- public virtual JsResult> GetCurrentLocale(JsParam param)
+ protected virtual JsResult> GetCurrentLocale(JsParam param)
{
MetadataOptions metadataOptions = serviceProvider.GetRequiredService();
@@ -181,7 +189,7 @@ internal class MiHoYoJSInterface
///
/// 参数
/// 响应
- public virtual JsResult> GetDynamicSecrectV1(JsParam param)
+ protected virtual JsResult> GetDynamicSecrectV1(JsParam param)
{
string salt = HoyolabOptions.Salts[SaltType.LK2];
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
@@ -212,7 +220,7 @@ internal class MiHoYoJSInterface
///
/// 参数
/// 响应
- public virtual JsResult> GetDynamicSecrectV2(JsParam param)
+ protected virtual JsResult> GetDynamicSecrectV2(JsParam param)
{
string salt = HoyolabOptions.Salts[SaltType.X4];
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
@@ -235,25 +243,45 @@ internal class MiHoYoJSInterface
///
/// 参数
/// Http请求头
- public virtual JsResult> GetHttpRequestHeader(JsParam param)
+ protected virtual JsResult> GetHttpRequestHeader(JsParam param)
{
+ Dictionary headers = new()
+ {
+ // Skip x-rpc-device_name
+ // Skip x-rpc-device_model
+ // Skip x-rpc-sys_version
+ // Skip x-rpc-game_biz
+ // Skip x-rpc-lifecycle_id
+ { "x-rpc-app_id", "bll8iq97cem8" },
+ { "x-rpc-client_type", "5" },
+ { "x-rpc-device_id", HoyolabOptions.DeviceId },
+ { "x-rpc-app_version", userAndUid.IsOversea ? SaltConstants.OSVersion : SaltConstants.CNVersion },
+ { "x-rpc-sdk_version", "2.16.0" },
+ };
+
+ if (!userAndUid.IsOversea)
+ {
+ headers.Add("x-rpc-device_fp", userAndUid.User.Fingerprint ?? string.Empty);
+ }
+
+ GetHttpRequestHeaderCore(headers);
+
return new()
{
- Data = new Dictionary()
- {
- { "x-rpc-client_type", "5" },
- { "x-rpc-device_id", HoyolabOptions.DeviceId },
- { "x-rpc-app_version", SaltConstants.CNVersion },
- },
+ Data = headers,
};
}
+ protected virtual void GetHttpRequestHeaderCore(Dictionary headers)
+ {
+ }
+
///
/// 获取状态栏高度
///
/// 参数
/// 结果
- public virtual JsResult> GetStatusBarHeight(JsParam param)
+ protected virtual JsResult> GetStatusBarHeight(JsParam param)
{
return new() { Data = new() { ["statusBarHeight"] = 0 } };
}
@@ -263,7 +291,7 @@ internal class MiHoYoJSInterface
///
/// 参数
/// 响应
- public virtual async ValueTask>> GetUserInfoAsync(JsParam param)
+ protected virtual async ValueTask>> GetUserInfoAsync(JsParam param)
{
Response response = await serviceProvider
.GetRequiredService>()
@@ -292,7 +320,7 @@ internal class MiHoYoJSInterface
}
}
- public virtual async ValueTask PushPageAsync(JsParam param)
+ protected virtual async ValueTask PushPageAsync(JsParam param)
{
const string bbsSchema = "mihoyobbs://";
string pageUrl = param.Payload.Page;
@@ -316,7 +344,7 @@ internal class MiHoYoJSInterface
return null;
}
- public virtual IJsResult? Share(JsParam param)
+ protected virtual IJsResult? Share(JsParam param)
{
return new JsResult>()
{
@@ -327,57 +355,53 @@ internal class MiHoYoJSInterface
};
}
- public virtual ValueTask ShowAlertDialogAsync(JsParam param)
+ protected virtual ValueTask ShowAlertDialogAsync(JsParam param)
{
return ValueTask.FromException(new NotSupportedException());
}
- public virtual IJsResult? StartRealPersonValidation(JsParam param)
+ protected virtual IJsResult? StartRealPersonValidation(JsParam param)
{
throw new NotImplementedException();
}
- public virtual IJsResult? StartRealnameAuth(JsParam param)
+ protected virtual IJsResult? StartRealnameAuth(JsParam param)
{
throw new NotImplementedException();
}
- public virtual IJsResult? GenAuthKey(JsParam param)
+ protected virtual IJsResult? GenAuthKey(JsParam param)
{
throw new NotImplementedException();
}
- public virtual IJsResult? GenAppAuthKey(JsParam param)
+ protected virtual IJsResult? GenAppAuthKey(JsParam param)
{
throw new NotImplementedException();
}
- public virtual IJsResult? OpenSystemBrowser(JsParam param)
+ protected virtual IJsResult? OpenSystemBrowser(JsParam param)
{
throw new NotImplementedException();
}
- public virtual IJsResult? SaveLoginTicket(JsParam param)
+ protected virtual IJsResult? SaveLoginTicket(JsParam param)
{
throw new NotImplementedException();
}
- public virtual ValueTask GetNotificationSettingsAsync(JsParam param)
+ protected virtual ValueTask GetNotificationSettingsAsync(JsParam param)
{
throw new NotImplementedException();
}
- public virtual IJsResult? ShowToast(JsParam param)
+ protected virtual IJsResult? ShowToast(JsParam param)
{
throw new NotImplementedException();
}
- public void Detach()
+ protected virtual void DOMContentLoaded(CoreWebView2 coreWebView2)
{
- coreWebView2.WebMessageReceived -= webMessageReceivedEventHandler;
- coreWebView2.DOMContentLoaded -= domContentLoadedEventHandler;
- coreWebView2.NavigationStarting -= navigationStartingEventHandler;
- coreWebView2 = default!;
}
private async ValueTask ExecuteCallbackScriptAsync(string callback, string? payload = null)
@@ -478,6 +502,7 @@ internal class MiHoYoJSInterface
private void OnDOMContentLoaded(CoreWebView2 coreWebView2, CoreWebView2DOMContentLoadedEventArgs args)
{
+ DOMContentLoaded(coreWebView2);
coreWebView2.ExecuteScriptAsync(HideScrollBarScript).AsTask().SafeForget(logger);
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSBridge.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSBridge.cs
new file mode 100644
index 00000000..a96061ca
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSBridge.cs
@@ -0,0 +1,26 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.Web.WebView2.Core;
+using Snap.Hutao.ViewModel.User;
+using Snap.Hutao.Web.Bridge.Model;
+using Snap.Hutao.Web.Hoyolab;
+
+namespace Snap.Hutao.Web.Bridge;
+
+///
+/// 签到页面JS桥
+///
+[HighQuality]
+internal sealed class SignInJSBridge : MiHoYoJSBridge
+{
+ public SignInJSBridge(CoreWebView2 webView, UserAndUid userAndUid)
+ : base(webView, userAndUid)
+ {
+ }
+
+ protected override void GetHttpRequestHeaderCore(Dictionary headers)
+ {
+ headers["x-rpc-client_type"] = "2";
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSBridgeOversea.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSBridgeOversea.cs
new file mode 100644
index 00000000..bf172a7f
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSBridgeOversea.cs
@@ -0,0 +1,37 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.Web.WebView2.Core;
+using Snap.Hutao.ViewModel.User;
+using Snap.Hutao.Web.Bridge.Model;
+using Snap.Hutao.Web.Hoyolab;
+
+namespace Snap.Hutao.Web.Bridge;
+
+///
+/// HoYoLAB 签到页面JS桥
+///
+[HighQuality]
+internal sealed class SignInJSBridgeOversea : MiHoYoJSBridge
+{
+ // 移除 请旋转手机 提示所在的HTML元素
+ private const string RemoveRotationWarningScript = """
+ let landscape = document.getElementById('mihoyo_landscape');
+ landscape.remove();
+ """;
+
+ public SignInJSBridgeOversea(CoreWebView2 webView, UserAndUid userAndUid)
+ : base(webView, userAndUid)
+ {
+ }
+
+ protected override void GetHttpRequestHeaderCore(Dictionary headers)
+ {
+ headers["x-rpc-client_type"] = "2";
+ }
+
+ protected override void DOMContentLoaded(CoreWebView2 coreWebView2)
+ {
+ coreWebView2.ExecuteScriptAsync(RemoveRotationWarningScript).AsTask().SafeForget();
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSInterface.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSInterface.cs
deleted file mode 100644
index 52c03502..00000000
--- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSInterface.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (c) DGP Studio. All rights reserved.
-// Licensed under the MIT license.
-
-using Microsoft.Web.WebView2.Core;
-using Snap.Hutao.ViewModel.User;
-using Snap.Hutao.Web.Bridge.Model;
-using Snap.Hutao.Web.Hoyolab;
-
-namespace Snap.Hutao.Web.Bridge;
-
-///
-/// 签到页面JS桥
-///
-[HighQuality]
-internal sealed class SignInJSInterface : MiHoYoJSInterface
-{
- ///
- public SignInJSInterface(CoreWebView2 webView, IServiceProvider serviceProvider, UserAndUid userAndUid)
- : base(webView, userAndUid)
- {
- }
-
- ///
- public override JsResult> GetHttpRequestHeader(JsParam param)
- {
- return new()
- {
- Data = new Dictionary()
- {
- { "x-rpc-client_type", "2" },
- { "x-rpc-device_id", HoyolabOptions.DeviceId },
- { "x-rpc-app_version", SaltConstants.CNVersion },
- },
- };
- }
-}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSInterfaceOversea.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSInterfaceOversea.cs
deleted file mode 100644
index 4321bec2..00000000
--- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSInterfaceOversea.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (c) DGP Studio. All rights reserved.
-// Licensed under the MIT license.
-
-using Microsoft.Web.WebView2.Core;
-using Snap.Hutao.ViewModel.User;
-using Snap.Hutao.Web.Bridge.Model;
-using Snap.Hutao.Web.Hoyolab;
-
-namespace Snap.Hutao.Web.Bridge;
-
-///
-/// HoYoLAB 签到页面JS桥
-///
-[HighQuality]
-internal sealed class SignInJSInterfaceOversea : MiHoYoJSInterface
-{
- private const string RemoveRotationWarningScript = """
- let landscape = document.getElementById('mihoyo_landscape');
- landscape.remove();
- """;
-
- private readonly ILogger logger;
-
- ///
- public SignInJSInterfaceOversea(CoreWebView2 webView, IServiceProvider serviceProvider, UserAndUid userAndUid)
- : base(webView, userAndUid)
- {
- logger = serviceProvider.GetRequiredService>();
- webView.DOMContentLoaded += OnDOMContentLoaded;
- }
-
- ///
- public override JsResult> GetHttpRequestHeader(JsParam param)
- {
- return new()
- {
- Data = new Dictionary()
- {
- { "x-rpc-client_type", "2" },
- { "x-rpc-device_id", HoyolabOptions.DeviceId },
- { "x-rpc-app_version", SaltConstants.OSVersion },
- },
- };
- }
-
- private void OnDOMContentLoaded(CoreWebView2 coreWebView2, CoreWebView2DOMContentLoadedEventArgs args)
- {
- // 移除“请旋转手机”提示所在的HTML元素
- coreWebView2.ExecuteScriptAsync(RemoveRotationWarningScript).AsTask().SafeForget(logger);
- }
-}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/Announcement.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/Announcement.cs
index 68b20263..cc3429fd 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/Announcement.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/Announcement.cs
@@ -7,8 +7,11 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
/// 公告
///
[HighQuality]
+[SuppressMessage("", "SA1124")]
internal sealed class Announcement : AnnouncementContent
{
+ #region Binding
+
///
/// 是否应展示时间
///
@@ -81,6 +84,7 @@ internal sealed class Announcement : AnnouncementContent
{
get => $"{StartTime:yyyy.MM.dd HH:mm} - {EndTime:yyyy.MM.dd HH:mm}";
}
+ #endregion
///
/// 类型标签
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementWrapper.cs
index e27a29da..ce675358 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementWrapper.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementWrapper.cs
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
/// 公告包装器
///
[HighQuality]
-internal sealed class AnnouncementWrapper : ListWrapper
+internal sealed class AnnouncementWrapper : ListWrapper, IJsonOnDeserialized
{
///
/// 总数
@@ -46,4 +46,18 @@ internal sealed class AnnouncementWrapper : ListWrapper
///
[JsonPropertyName("t")]
public string TimeStamp { get; set; } = default!;
+
+ public void OnDeserialized()
+ {
+ TimeSpan offset = new(TimeZone, 0, 0);
+
+ foreach (AnnouncementListWrapper wrapper in List)
+ {
+ foreach (Announcement item in wrapper.List)
+ {
+ item.StartTime = UnsafeDateTimeOffset.AdjustOffsetOnly(item.StartTime, offset);
+ item.EndTime = UnsafeDateTimeOffset.AdjustOffsetOnly(item.EndTime, offset);
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs
index 2b9c9bce..9dbe74ee 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs
@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-using Snap.Hutao.Web.Request.QueryString;
+using System.Text.RegularExpressions;
namespace Snap.Hutao.Web.Hoyolab;
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Web.Hoyolab;
/// 玩家 Uid
///
[HighQuality]
-internal readonly struct PlayerUid
+internal readonly partial struct PlayerUid
{
///
/// UID 的实际值
@@ -28,52 +28,63 @@ internal readonly struct PlayerUid
/// 服务器,当提供该参数时会无条件信任
public PlayerUid(string value, string? region = default)
{
- Must.Argument(value.Length == 9, "uid 应为9位数字");
+ Must.Argument(UidRegex().IsMatch(value), SH.WebHoyolabInvalidUid);
Value = value;
Region = region ?? EvaluateRegion(value.AsSpan()[0]);
}
public static implicit operator PlayerUid(string source)
{
- return new(source);
+ return FromUidString(source);
+ }
+
+ public static PlayerUid FromUidString(string uid)
+ {
+ return new(uid);
}
///
/// 判断是否为国际服
- /// We make this a static method rather than property,
- /// to avoid unnecessary memory allocation.
///
/// uid
/// 是否为国际服
public static bool IsOversea(string uid)
{
- return uid[0] switch
+ // We make this a static method rather than property,
+ // to avoid unnecessary memory allocation (Region field).
+ Must.Argument(UidRegex().IsMatch(uid), SH.WebHoyolabInvalidUid);
+
+ return uid.AsSpan()[0] switch
{
>= '1' and <= '5' => false,
_ => true,
};
}
+ public static TimeSpan GetRegionTimeZoneUtcOffset(string uid)
+ {
+ // We make this a static method rather than property,
+ // to avoid unnecessary memory allocation (Region field).
+ Must.Argument(UidRegex().IsMatch(uid), SH.WebHoyolabInvalidUid);
+
+ // 美服 UTC-05
+ // 欧服 UTC+01
+ // 其他 UTC+08
+ return uid.AsSpan()[0] switch
+ {
+ '6' => ServerRegionTimeZone.AmericaServerOffset,
+ '7' => ServerRegionTimeZone.EuropeServerOffset,
+ _ => ServerRegionTimeZone.CommonOffset,
+ };
+ }
+
///
public override string ToString()
{
return Value;
}
- ///
- /// 转换到查询字符串
- ///
- /// 查询字符串
- public QueryString ToQueryString()
- {
- QueryString queryString = new();
- queryString.Set("role_id", Value);
- queryString.Set("server", Region);
-
- return queryString;
- }
-
- private static string EvaluateRegion(char first)
+ private static string EvaluateRegion(in char first)
{
return first switch
{
@@ -89,4 +100,7 @@ internal readonly struct PlayerUid
_ => throw Must.NeverHappen(),
};
}
+
+ [GeneratedRegex("^[1-9][0-9]{8}$")]
+ private static partial Regex UidRegex();
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUidExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUidExtension.cs
new file mode 100644
index 00000000..4dcb9eda
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUidExtension.cs
@@ -0,0 +1,18 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Web.Request.QueryString;
+
+namespace Snap.Hutao.Web.Hoyolab;
+
+internal static class PlayerUidExtension
+{
+ public static QueryString ToQueryString(this in PlayerUid playerUid)
+ {
+ QueryString queryString = new();
+ queryString.Set("role_id", playerUid.Value);
+ queryString.Set("server", playerUid.Region);
+
+ return queryString;
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ServerRegionTimeZone.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ServerRegionTimeZone.cs
new file mode 100644
index 00000000..39bed842
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/ServerRegionTimeZone.cs
@@ -0,0 +1,26 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Web.Hoyolab;
+
+internal static class ServerRegionTimeZone
+{
+ private static readonly TimeSpan AmericaOffsetValue = new(-05, 0, 0);
+ private static readonly TimeSpan EuropeOffsetValue = new(+01, 0, 0);
+ private static readonly TimeSpan CommonOffsetValue = new(+08, 0, 0);
+
+ ///
+ /// UTC-05
+ ///
+ public static TimeSpan AmericaServerOffset { get => AmericaOffsetValue; }
+
+ ///
+ /// UTC+01
+ ///
+ public static TimeSpan EuropeServerOffset { get => EuropeOffsetValue; }
+
+ ///
+ /// UTC+08
+ ///
+ public static TimeSpan CommonOffset { get => CommonOffsetValue; }
+}
\ No newline at end of file