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