Merge pull request #1085 from DGP-Studio/develop

This commit is contained in:
DismissedLight
2023-11-10 15:41:33 +08:00
committed by GitHub
65 changed files with 1067 additions and 396 deletions

View File

@@ -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;

View File

@@ -1,4 +1,5 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<CornerRadius x:Key="ControlCornerRadiusTop">4,4,0,0</CornerRadius>
<CornerRadius x:Key="ControlCornerRadiusBottom">0,0,4,4</CornerRadius>
<CornerRadius x:Key="ControlCornerRadiusTopRightAndBottomLeft">0,4,0,4</CornerRadius>
</ResourceDictionary>

View File

@@ -4,7 +4,6 @@
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
</Style>
<Style

View File

@@ -36,7 +36,7 @@ internal static partial class IocHttpClientConfiguration
}
/// <summary>
/// 对于需要添加动态密钥的客户端使用此配置
/// 对于需要添加动态密钥1的客户端使用此配置
/// </summary>
/// <param name="client">配置后的客户端</param>
private static void XRpcConfiguration(HttpClient client)
@@ -50,7 +50,7 @@ internal static partial class IocHttpClientConfiguration
}
/// <summary>
/// 对于需要添加动态密钥的客户端使用此配置
/// 对于需要添加动态密钥2的客户端使用此配置
/// </summary>
/// <param name="client">配置后的客户端</param>
private static void XRpc2Configuration(HttpClient client)
@@ -64,11 +64,11 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2");
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0");
}
/// <summary>
/// 对于需要添加动态密钥的客户端使用此配置
/// 对于需要添加动态密钥1的客户端使用此配置
/// HoYoLAB app
/// </summary>
/// <param name="client">配置后的客户端</param>
@@ -84,7 +84,7 @@ internal static partial class IocHttpClientConfiguration
}
/// <summary>
/// 对于需要添加动态密钥的客户端使用此配置
/// 对于需要添加动态密钥2的客户端使用此配置
/// HoYoLAB web
/// </summary>
/// <param name="client">配置后的客户端</param>

View File

@@ -7,6 +7,8 @@ namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 实现日期的转换
/// 此转换器无法实现无损往返
/// 必须在反序列化后调整 Offset
/// </summary>
[HighQuality]
internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
@@ -18,7 +20,10 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
if (reader.GetString() is { } dataTimeString)
{
return DateTimeOffset.ParseExact(dataTimeString, Format, CultureInfo.CurrentCulture);
// By doing so, the DateTimeOffset parsed out will be a
// no offset datetime, and need to be adjusted later
DateTime dateTime = DateTime.ParseExact(dataTimeString, Format, CultureInfo.InvariantCulture);
return new DateTimeOffset(dateTime, default);
}
return default;
@@ -27,6 +32,6 @@ internal class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(Format, CultureInfo.CurrentCulture));
writer.WriteStringValue(value.DateTime.ToString(Format, CultureInfo.InvariantCulture));
}
}

View File

@@ -163,7 +163,7 @@ internal sealed partial class Activation : IActivation
{
await taskContext.SwitchToMainThreadAsync();
currentWindowReference.Window = serviceProvider.GetRequiredService<MainWindow>();
serviceProvider.GetRequiredService<MainWindow>();
serviceProvider
.GetRequiredService<IMetadataService>()
@@ -270,7 +270,7 @@ internal sealed partial class Activation : IActivation
if (currentWindowReference.Window is null)
{
currentWindowReference.Window = serviceProvider.GetRequiredService<LaunchGameWindow>();
serviceProvider.GetRequiredService<LaunchGameWindow>();
}
else
{

View File

@@ -2,6 +2,9 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Windows.Win32.Foundation;
using WinRT.Interop;
namespace Snap.Hutao.Core.LifeCycle;
@@ -11,4 +14,11 @@ internal static class CurrentWindowReferenceExtension
{
return reference.Window.Content.XamlRoot;
}
public static HWND GetWindowHandle(this ICurrentWindowReference reference)
{
return reference.Window is IWindowOptionsSource optionsSource
? optionsSource.WindowOptions.Hwnd
: (HWND)WindowNative.GetWindowHandle(reference.Window);
}
}

View File

@@ -7,5 +7,8 @@ namespace Snap.Hutao.Core.LifeCycle;
internal interface ICurrentWindowReference
{
/// <summary>
/// Only set in WindowController
/// </summary>
public Window Window { get; set; }
}

View File

@@ -81,4 +81,6 @@ internal static class SettingKeys
public const string IsHomeCardGachaStatisticsPresented = "IsHomeCardGachaStatisticsPresented";
public const string IsHomeCardAchievementPresented = "IsHomeCardAchievementPresented";
public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented";
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Dispatching;
namespace Snap.Hutao.Core.Threading;
internal sealed class DispatcherQueueSynchronizationContextSendSupport : SynchronizationContext
{
private readonly DispatcherQueue dispatcherQueue;
public DispatcherQueueSynchronizationContextSendSupport(DispatcherQueue dispatcherQueue)
{
this.dispatcherQueue = dispatcherQueue;
}
public override void Post(SendOrPostCallback d, object? state)
{
ArgumentNullException.ThrowIfNull(d);
dispatcherQueue.TryEnqueue(() => d(state));
}
public override void Send(SendOrPostCallback d, object? state)
{
ArgumentNullException.ThrowIfNull(d);
dispatcherQueue.Invoke(() => d(state));
}
public override SynchronizationContext CreateCopy()
{
return new DispatcherQueueSynchronizationContextSendSupport(dispatcherQueue);
}
}

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core.Threading;
[Injection(InjectAs.Singleton, typeof(ITaskContext))]
internal sealed class TaskContext : ITaskContext
{
private readonly DispatcherQueueSynchronizationContextSendSupport synchronizationContext;
private readonly DispatcherQueueSynchronizationContext synchronizationContext;
private readonly DispatcherQueue dispatcherQueue;
/// <summary>

View File

@@ -0,0 +1,306 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Model;
using System.Text;
using Windows.System;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Windowing.HotKey;
[SuppressMessage("", "SA1124")]
internal sealed class HotKeyCombination : ObservableObject
{
private readonly ICurrentWindowReference currentWindowReference;
private readonly RuntimeOptions runtimeOptions;
private readonly string settingKey;
private readonly int hotKeyId;
private readonly HotKeyParameter defaultHotKeyParameter;
private bool registered;
private bool modifierHasWindows;
private bool modifierHasControl;
private bool modifierHasShift;
private bool modifierHasAlt;
private NameValue<VirtualKey> keyNameValue;
private HOT_KEY_MODIFIERS modifiers;
private VirtualKey key;
private bool isEnabled;
public HotKeyCombination(IServiceProvider serviceProvider, string settingKey, int hotKeyId, HOT_KEY_MODIFIERS defaultModifiers, VirtualKey defaultKey)
{
currentWindowReference = serviceProvider.GetRequiredService<ICurrentWindowReference>();
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
this.settingKey = settingKey;
this.hotKeyId = hotKeyId;
defaultHotKeyParameter = new(defaultModifiers, defaultKey);
// Initialize Property backing fields
{
// Retrieve from LocalSetting
isEnabled = LocalSetting.Get($"{settingKey}.IsEnabled", true);
HotKeyParameter actual = LocalSettingGetHotKeyParameter();
modifiers = actual.Modifiers;
InitializeModifiersComposeFields();
key = actual.Key;
keyNameValue = VirtualKeys.GetList().Single(v => v.Value == key);
}
}
#region Binding Property
public bool ModifierHasWindows
{
get => modifierHasWindows;
set
{
if (SetProperty(ref modifierHasWindows, value))
{
UpdateModifiers();
}
}
}
public bool ModifierHasControl
{
get => modifierHasControl;
set
{
if (SetProperty(ref modifierHasControl, value))
{
UpdateModifiers();
}
}
}
public bool ModifierHasShift
{
get => modifierHasShift;
set
{
if (SetProperty(ref modifierHasShift, value))
{
UpdateModifiers();
}
}
}
public bool ModifierHasAlt
{
get => modifierHasAlt;
set
{
if (SetProperty(ref modifierHasAlt, value))
{
UpdateModifiers();
}
}
}
public NameValue<VirtualKey> KeyNameValue
{
get => keyNameValue;
set
{
if (value is null)
{
return;
}
if (SetProperty(ref keyNameValue, value))
{
Key = value.Value;
}
}
}
#endregion
public HOT_KEY_MODIFIERS Modifiers
{
get => modifiers;
private set
{
if (SetProperty(ref modifiers, value))
{
OnPropertyChanged(nameof(DisplayName));
LocalSettingSetHotKeyParameterAndRefresh();
}
}
}
public VirtualKey Key
{
get => key;
private set
{
if (SetProperty(ref key, value))
{
OnPropertyChanged(nameof(DisplayName));
LocalSettingSetHotKeyParameterAndRefresh();
}
}
}
public bool IsEnabled
{
get => isEnabled;
set
{
if (SetProperty(ref isEnabled, value))
{
LocalSetting.Set($"{settingKey}.IsEnabled", value);
_ = (value, registered) switch
{
(true, false) => RegisterForCurrentWindow(),
(false, true) => UnregisterForCurrentWindow(),
_ => false,
};
}
}
}
public string DisplayName { get => ToString(); }
public bool RegisterForCurrentWindow()
{
if (!runtimeOptions.IsElevated || !IsEnabled)
{
return false;
}
if (registered)
{
return true;
}
HWND hwnd = currentWindowReference.GetWindowHandle();
BOOL result = RegisterHotKey(hwnd, hotKeyId, Modifiers, (uint)Key);
registered = result;
return result;
}
public bool UnregisterForCurrentWindow()
{
if (!runtimeOptions.IsElevated)
{
return false;
}
if (!registered)
{
return true;
}
HWND hwnd = currentWindowReference.GetWindowHandle();
BOOL result = UnregisterHotKey(hwnd, hotKeyId);
registered = !result;
return result;
}
public override string ToString()
{
StringBuilder stringBuilder = new();
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_WIN))
{
stringBuilder.Append("Win").Append(" + ");
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_CONTROL))
{
stringBuilder.Append("Ctrl").Append(" + ");
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_SHIFT))
{
stringBuilder.Append("Shift").Append(" + ");
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_ALT))
{
stringBuilder.Append("Alt").Append(" + ");
}
stringBuilder.Append(Key);
return stringBuilder.ToString();
}
private void UpdateModifiers()
{
HOT_KEY_MODIFIERS modifiers = default;
if (ModifierHasWindows)
{
modifiers |= HOT_KEY_MODIFIERS.MOD_WIN;
}
if (ModifierHasControl)
{
modifiers |= HOT_KEY_MODIFIERS.MOD_CONTROL;
}
if (ModifierHasShift)
{
modifiers |= HOT_KEY_MODIFIERS.MOD_SHIFT;
}
if (ModifierHasAlt)
{
modifiers |= HOT_KEY_MODIFIERS.MOD_ALT;
}
Modifiers = modifiers;
}
private void InitializeModifiersComposeFields()
{
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_WIN))
{
modifierHasWindows = true;
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_CONTROL))
{
modifierHasControl = true;
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_SHIFT))
{
modifierHasShift = true;
}
if (Modifiers.HasFlag(HOT_KEY_MODIFIERS.MOD_ALT))
{
modifierHasAlt = true;
}
}
private unsafe HotKeyParameter LocalSettingGetHotKeyParameter()
{
fixed (HotKeyParameter* pDefaultHotKey = &defaultHotKeyParameter)
{
int value = LocalSetting.Get(settingKey, *(int*)pDefaultHotKey);
return *(HotKeyParameter*)&value;
}
}
private unsafe void LocalSettingSetHotKeyParameterAndRefresh()
{
HotKeyParameter current = new(Modifiers, Key);
LocalSetting.Set(settingKey, *(int*)&current);
UnregisterForCurrentWindow();
RegisterForCurrentWindow();
}
}

View File

@@ -2,69 +2,38 @@
// Licensed under the MIT license.
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Windowing.HotKey;
[SuppressMessage("", "CA1001")]
internal sealed class HotKeyController : IHotKeyController
[ConstructorGenerated]
internal sealed partial class HotKeyController : IHotKeyController
{
private const int DefaultId = 100000;
private static readonly WaitCallback RunMouseClickRepeatForever = MouseClickRepeatForever;
private readonly object locker = new();
private readonly WaitCallback runMouseClickRepeatForever;
private readonly HotKeyOptions hotKeyOptions;
private readonly RuntimeOptions runtimeOptions;
private volatile CancellationTokenSource? cancellationTokenSource;
public HotKeyController(IServiceProvider serviceProvider)
public void RegisterAll()
{
hotKeyOptions = serviceProvider.GetRequiredService<HotKeyOptions>();
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
runMouseClickRepeatForever = MouseClickRepeatForever;
hotKeyOptions.MouseClickRepeatForeverKeyCombination.RegisterForCurrentWindow();
}
public bool Register(in HWND hwnd)
public void UnregisterAll()
{
if (runtimeOptions.IsElevated)
{
return RegisterHotKey(hwnd, DefaultId, default, (uint)VIRTUAL_KEY.VK_F8);
}
return false;
}
public bool Unregister(in HWND hwnd)
{
if (runtimeOptions.IsElevated)
{
return UnregisterHotKey(hwnd, DefaultId);
}
return false;
hotKeyOptions.MouseClickRepeatForeverKeyCombination.UnregisterForCurrentWindow();
}
public void OnHotKeyPressed(in HotKeyParameter parameter)
{
if (parameter is { Key: VIRTUAL_KEY.VK_F8, NativeModifier: 0 })
if (parameter.Equals(hotKeyOptions.MouseClickRepeatForeverKeyCombination))
{
lock (locker)
{
if (hotKeyOptions.IsMouseClickRepeatForeverOn)
{
cancellationTokenSource?.Cancel();
cancellationTokenSource = default;
hotKeyOptions.IsMouseClickRepeatForeverOn = false;
}
else
{
cancellationTokenSource = new();
ThreadPool.QueueUserWorkItem(runMouseClickRepeatForever, cancellationTokenSource.Token);
hotKeyOptions.IsMouseClickRepeatForeverOn = true;
}
}
ToggleMouseClickRepeatForever();
}
}
@@ -76,7 +45,7 @@ internal sealed class HotKeyController : IHotKeyController
}
[SuppressMessage("", "SH007")]
private unsafe void MouseClickRepeatForever(object? state)
private static unsafe void MouseClickRepeatForever(object? state)
{
CancellationToken token = (CancellationToken)state!;
@@ -102,4 +71,25 @@ internal sealed class HotKeyController : IHotKeyController
Thread.Sleep(Random.Shared.Next(100, 150));
}
}
private void ToggleMouseClickRepeatForever()
{
lock (locker)
{
if (hotKeyOptions.IsMouseClickRepeatForeverOn)
{
// Turn off
cancellationTokenSource?.Cancel();
cancellationTokenSource = default;
hotKeyOptions.IsMouseClickRepeatForeverOn = false;
}
else
{
// Turn on
cancellationTokenSource = new();
ThreadPool.QueueUserWorkItem(RunMouseClickRepeatForever, cancellationTokenSource.Token);
hotKeyOptions.IsMouseClickRepeatForeverOn = true;
}
}
}
}

View File

@@ -2,13 +2,34 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Model;
using Windows.System;
namespace Snap.Hutao.Core.Windowing.HotKey;
[Injection(InjectAs.Singleton)]
internal sealed class HotKeyOptions : ObservableObject
internal sealed partial class HotKeyOptions : ObservableObject
{
private bool isVirtualKeyF8Pressed;
private bool isMouseClickRepeatForeverOn;
private HotKeyCombination mouseClickRepeatForeverKeyCombination;
public bool IsMouseClickRepeatForeverOn { get => isVirtualKeyF8Pressed; set => SetProperty(ref isVirtualKeyF8Pressed, value); }
public HotKeyOptions(IServiceProvider serviceProvider)
{
mouseClickRepeatForeverKeyCombination = new(serviceProvider, SettingKeys.HotKeyMouseClickRepeatForever, 100000, default, VirtualKey.F8);
}
public List<NameValue<VirtualKey>> VirtualKeys { get; } = HotKey.VirtualKeys.GetList();
public bool IsMouseClickRepeatForeverOn
{
get => isMouseClickRepeatForeverOn;
set => SetProperty(ref isMouseClickRepeatForeverOn, value);
}
public HotKeyCombination MouseClickRepeatForeverKeyCombination
{
get => mouseClickRepeatForeverKeyCombination;
set => SetProperty(ref mouseClickRepeatForeverKeyCombination, value);
}
}

View File

@@ -1,17 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.System;
using Windows.Win32.UI.Input.KeyboardAndMouse;
namespace Snap.Hutao.Core.Windowing.HotKey;
internal readonly struct HotKeyParameter
/// <summary>
/// HotKeyParameter
/// The size of this struct must be sizeof(LPARAM) or 4
/// </summary>
internal readonly struct HotKeyParameter : IEquatable<HotKeyCombination>
{
public readonly ushort NativeModifier;
public readonly VIRTUAL_KEY Key;
public readonly ushort NativeModifiers;
public readonly VIRTUAL_KEY NativeKey;
public readonly HOT_KEY_MODIFIERS Modifier
public HotKeyParameter(HOT_KEY_MODIFIERS modifiers, VirtualKey key)
{
get => (HOT_KEY_MODIFIERS)NativeModifier;
NativeModifiers = (ushort)modifiers;
NativeKey = (VIRTUAL_KEY)key;
}
public readonly HOT_KEY_MODIFIERS Modifiers
{
get => (HOT_KEY_MODIFIERS)NativeModifiers;
}
public readonly VirtualKey Key
{
get => (VirtualKey)NativeKey;
}
public bool Equals(HotKeyCombination? other)
{
if (other is null)
{
return false;
}
return Modifiers == other.Modifiers && Key == other.Key;
}
}

View File

@@ -1,15 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Win32.Foundation;
namespace Snap.Hutao.Core.Windowing.HotKey;
internal interface IHotKeyController
{
void OnHotKeyPressed(in HotKeyParameter parameter);
bool Register(in HWND hwnd);
void RegisterAll();
bool Unregister(in HWND hwnd);
void UnregisterAll();
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model;
using Windows.System;
namespace Snap.Hutao.Core.Windowing.HotKey;
internal static class VirtualKeys
{
private static readonly List<NameValue<VirtualKey>> Values = CollectionsNameValue.ListFromEnum<VirtualKey>();
public static List<NameValue<VirtualKey>> GetList()
{
return Values;
}
}

View File

@@ -6,6 +6,7 @@ using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service;
using System.IO;
@@ -32,8 +33,9 @@ internal sealed class WindowController
this.options = options;
this.serviceProvider = serviceProvider;
// Window reference must be set before Window Subclass created
serviceProvider.GetRequiredService<ICurrentWindowReference>().Window = window;
subclass = new(window, options, serviceProvider);
InitializeCore();
}
@@ -78,7 +80,7 @@ internal sealed class WindowController
private void RecoverOrInitWindowSize()
{
// Set first launch size
double scale = options.GetWindowScale();
double scale = options.GetRasterizationScale();
SizeInt32 scaledSize = options.InitSize.Scale(scale);
RectInt32 rect = StructMarshal.RectInt32(scaledSize);
@@ -108,14 +110,14 @@ internal sealed class WindowController
// prevent save value when we are maximized.
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
{
double scale = 1 / options.GetWindowScale();
double scale = 1.0 / options.GetRasterizationScale();
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)window.AppWindow.GetRect().Scale(scale));
}
}
private void OnOptionsPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(AppOptions.BackdropType))
if (e.PropertyName is nameof(AppOptions.BackdropType))
{
if (sender is AppOptions options)
{
@@ -198,7 +200,7 @@ internal sealed class WindowController
{
AppWindowTitleBar appTitleBar = window.AppWindow.TitleBar;
double scale = options.GetWindowScale();
double scale = options.GetRasterizationScale();
// 48 is the navigation button leftInset
RectInt32 dragRect = StructMarshal.RectInt32(48, 0, options.TitleBar.ActualSize).Scale(scale);

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Input;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.Graphics;
@@ -20,6 +21,11 @@ internal readonly struct WindowOptions
/// </summary>
public readonly HWND Hwnd;
/// <summary>
/// 非客户端区域指针源
/// </summary>
public readonly InputNonClientPointerSource InputNonClientPointerSource;
/// <summary>
/// 标题栏元素
/// </summary>
@@ -50,6 +56,7 @@ internal readonly struct WindowOptions
public WindowOptions(Window window, FrameworkElement titleBar, SizeInt32 initSize, bool persistSize = false)
{
Hwnd = (HWND)WindowNative.GetWindowHandle(window);
InputNonClientPointerSource = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id);
TitleBar = titleBar;
InitSize = initSize;
PersistSize = persistSize;
@@ -59,7 +66,7 @@ internal readonly struct WindowOptions
/// 获取窗体当前的DPI缩放比
/// </summary>
/// <returns>缩放比</returns>
public double GetWindowScale()
public double GetRasterizationScale()
{
uint dpi = GetDpiForWindow(Hwnd);
return Math.Round(dpi / 96D, 2, MidpointRounding.AwayFromZero);

View File

@@ -45,7 +45,7 @@ internal sealed class WindowSubclass : IDisposable
{
windowProc = OnSubclassProcedure;
bool windowHooked = SetWindowSubclass(options.Hwnd, windowProc, WindowSubclassId, 0);
hotKeyController.Register(options.Hwnd);
hotKeyController.RegisterAll();
bool titleBarHooked = true;
@@ -72,7 +72,7 @@ internal sealed class WindowSubclass : IDisposable
/// <inheritdoc/>
public void Dispose()
{
hotKeyController.Unregister(options.Hwnd);
hotKeyController.UnregisterAll();
RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId);
windowProc = null;
@@ -93,7 +93,7 @@ internal sealed class WindowSubclass : IDisposable
{
if (window is IMinMaxInfoHandler handler)
{
handler.HandleMinMaxInfo(ref *(MINMAXINFO*)lParam.Value, options.GetWindowScale());
handler.HandleMinMaxInfo(ref *(MINMAXINFO*)lParam.Value, options.GetRasterizationScale());
}
break;

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Extension;
internal struct UnsafeDateTimeOffset
{
private DateTime dateTime;
private short offsetMinutes;
public DateTime DateTime { readonly get => dateTime; set => dateTime = value; }
[SuppressMessage("", "SH002")]
public static unsafe DateTimeOffset AdjustOffsetOnly(DateTimeOffset dateTimeOffset, in TimeSpan offset)
{
UnsafeDateTimeOffset* pUnsafe = (UnsafeDateTimeOffset*)&dateTimeOffset;
pUnsafe->offsetMinutes = (short)(offset.Ticks / TimeSpan.TicksPerMinute);
return dateTimeOffset;
}
}

View File

@@ -5,6 +5,7 @@ using Snap.Hutao.Core;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Windowing;
using Windows.Storage.Pickers;
using Windows.Win32.Foundation;
using WinRT.Interop;
namespace Snap.Hutao.Factory.Picker;
@@ -79,10 +80,8 @@ internal sealed partial class PickerFactory : IPickerFactory
{
// Create a folder picker.
T picker = new();
nint hwnd = currentWindowReference.Window is IWindowOptionsSource optionsSource
? (nint)optionsSource.WindowOptions.Hwnd
: WindowNative.GetWindowHandle(currentWindowReference.Window);
HWND hwnd = currentWindowReference.GetWindowHandle();
InitializeWithWindow.Initialize(picker, hwnd);
return picker;

View File

@@ -1,4 +1,7 @@
namespace Snap.Hutao.Factory.Progress;
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Factory.Progress;
internal interface IProgressFactory
{

View File

@@ -1,4 +1,7 @@
using System;
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model;
internal static class CollectionsNameValue
{
public static List<NameValue<T>> ListFromEnum<T>()
where T : struct, Enum
{
return Enum.GetValues<T>().Select(x => new NameValue<T>(x.ToString(), x)).ToList();
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Model.InterChange.GachaLog;
@@ -10,12 +11,12 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
/// https://uigf.org/standards/UIGF.html
/// </summary>
[HighQuality]
internal sealed class UIGF
internal sealed class UIGF : IJsonOnSerializing, IJsonOnDeserialized
{
/// <summary>
/// 当前版本
/// </summary>
public const string CurrentVersion = "v2.3";
public const string CurrentVersion = "v2.4";
/// <summary>
/// 信息
@@ -30,11 +31,27 @@ internal sealed class UIGF
[JsonPropertyName("list")]
public List<UIGFItem> List { get; set; } = default!;
/// <summary>
/// 确认当前UIGF对象的版本是否受支持
/// </summary>
/// <param name="version">版本</param>
/// <returns>当前UIAF对象是否受支持</returns>
public void OnSerializing()
{
TimeSpan offset = GetRegionTimeZoneUtcOffset();
foreach (UIGFItem item in List)
{
item.Time = item.Time.ToOffset(offset);
}
}
public void OnDeserialized()
{
// Adjust items timezone
TimeSpan offset = GetRegionTimeZoneUtcOffset();
foreach (UIGFItem item in List)
{
item.Time = UnsafeDateTimeOffset.AdjustOffsetOnly(item.Time, offset);
}
}
public bool IsCurrentVersionSupported(out UIGFVersion version)
{
version = Info.UIGFVersion switch
@@ -42,17 +59,13 @@ internal sealed class UIGF
"v2.1" => UIGFVersion.Major2Minor2OrLower,
"v2.2" => UIGFVersion.Major2Minor2OrLower,
"v2.3" => UIGFVersion.Major2Minor3OrHigher,
"v2.4" => UIGFVersion.Major2Minor3OrHigher,
_ => UIGFVersion.NotSupported,
};
return version != UIGFVersion.NotSupported;
}
/// <summary>
/// 列表物品是否正常
/// </summary>
/// <param name="id">首个出错的Id</param>
/// <returns>是否正常</returns>
public bool IsMajor2Minor2OrLowerListValid([NotNullWhen(false)] out long id)
{
foreach (ref readonly UIGFItem item in CollectionsMarshal.AsSpan(List))
@@ -82,4 +95,14 @@ internal sealed class UIGF
id = 0;
return true;
}
private TimeSpan GetRegionTimeZoneUtcOffset()
{
if (Info.RegionTimeZone is int offsetHours)
{
return new TimeSpan(offsetHours, 0, 0);
}
return PlayerUid.GetRegionTimeZoneUtcOffset(Info.Uid);
}
}

View File

@@ -4,6 +4,7 @@
using Snap.Hutao.Core;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Model.InterChange.GachaLog;
@@ -58,6 +59,12 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, Metadata
[JsonPropertyName("uigf_version")]
public string UIGFVersion { get; set; } = default!;
/// <summary>
/// 时区偏移
/// </summary>
[JsonPropertyName("region_time_zone")]
public int? RegionTimeZone { get; set; } = default!;
public static UIGFInfo From(RuntimeOptions runtimeOptions, MetadataOptions metadataOptions, string uid)
{
return new()
@@ -68,6 +75,7 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, Metadata
ExportApp = SH.AppName,
ExportAppVersion = runtimeOptions.Version.ToString(),
UIGFVersion = UIGF.CurrentVersion,
RegionTimeZone = PlayerUid.GetRegionTimeZoneUtcOffset(uid).Hours,
};
}
}

View File

@@ -6,6 +6,8 @@ namespace Snap.Hutao.Model;
/// <summary>
/// 封装带有名称描述的值
/// 在绑定枚举变量时非常有用
/// https://github.com/microsoft/microsoft-ui-xaml/issues/4266
/// 直接绑定枚举变量会显示 Windows.Foundation.IReference{T}
/// </summary>
/// <typeparam name="T">包含值的类型</typeparam>
[HighQuality]

View File

@@ -2300,6 +2300,15 @@
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
<value>Enable Advanced Features</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
<value>Change Auto Click Shortcut</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
<value>Auto Click</value>
</data>
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>Shortcut Keys</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>Official Website</value>
</data>
@@ -2526,7 +2535,7 @@
<value>Current user</value>
</data>
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
<value>My Characters</value>
<value>Official Tools</value>
</data>
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
<value>Web Login</value>
@@ -2741,6 +2750,12 @@
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
<value>Weapon Event Wish</value>
</data>
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>Copy Link Successful</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>Invalid UID</value>
</data>
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
<value>Verification failed. Please verify manually or check MiHoYo BBS - My Characters page</value>
</data>

View File

@@ -2022,7 +2022,7 @@
<value>ファイル</value>
</data>
<data name="ViewPageLaunchGameInterProcessHeader" xml:space="preserve">
<value>进程间</value>
<value>インタープロセス</value>
</data>
<data name="ViewPageLaunchGameMonitorsDescription" xml:space="preserve">
<value>指定したディスプレイで実行</value>
@@ -2040,10 +2040,10 @@
<value>ゲームオプション</value>
</data>
<data name="ViewPageLaunchGamePlayTimeDescription" xml:space="preserve">
<value>在游戏启动后尝试启动并使用 Starward 进行游戏时长统计</value>
<value>ゲームの開始後にStarward ランチャーを起動し、プレイ時間の統計を確認してみてください。</value>
</data>
<data name="ViewPageLaunchGamePlayTimeHeader" xml:space="preserve">
<value>时长统计</value>
<value>プレイ時間</value>
</data>
<data name="ViewPageLaunchGameProcessHeader" xml:space="preserve">
<value>プロセス</value>
@@ -2300,6 +2300,15 @@
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
<value>上級者向け設定を有効にする</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
<value>オートクリック機能のショートカットキーを変更します</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
<value>オートクリック</value>
</data>
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>ショートカットキー</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>公式サイト</value>
</data>
@@ -2526,7 +2535,7 @@
<value>現在のユーザー</value>
</data>
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
<value>マイ キャラクター</value>
<value>旅行ツール</value>
</data>
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
<value>ウェブ上でログイン</value>
@@ -2741,6 +2750,12 @@
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
<value>イベント祈願・武器</value>
</data>
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>ダウンロードリンクのコピーに成功しました</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>无效的 UID</value>
</data>
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
<value>認証に失敗しました。 手動で認証するか、MiHoYo BBS - 戦績 を確認してください。</value>
</data>

View File

@@ -2300,6 +2300,15 @@
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
<value>고급 기능 활성화</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
<value>更改自动连点功能的快捷键</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
<value>自动连点</value>
</data>
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>快捷键</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>공식 홈페이지로 이동</value>
</data>
@@ -2526,7 +2535,7 @@
<value>当前用户</value>
</data>
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
<value>我的角色</value>
<value>旅行工具</value>
</data>
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
<value>웹 로그인</value>
@@ -2741,6 +2750,12 @@
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
<value>무기 이벤트 기원</value>
</data>
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>下载链接复制成功</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>无效的 UID</value>
</data>
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
<value>验证失败,请手动验证或前往「米游社-我的角色」页面查看</value>
</data>

View File

@@ -2300,6 +2300,15 @@
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
<value>启动高级功能</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
<value>更改自动连点功能的快捷键</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
<value>自动连点</value>
</data>
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>快捷键</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>前往官网</value>
</data>
@@ -2526,7 +2535,7 @@
<value>当前用户</value>
</data>
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
<value>我的角色</value>
<value>旅行工具</value>
</data>
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
<value>网页登录</value>
@@ -2744,6 +2753,9 @@
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>下载链接复制成功</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>无效的 UID</value>
</data>
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
<value>验证失败,请手动验证或前往「米游社-我的角色」页面查看</value>
</data>

View File

@@ -2300,6 +2300,15 @@
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
<value>啟動高級功能</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
<value>更改自动连点功能的快捷键</value>
</data>
<data name="ViewPageSettingKeyShortcutAutoClickingHeader" xml:space="preserve">
<value>自动连点</value>
</data>
<data name="ViewPageSettingKeyShortcutHeader" xml:space="preserve">
<value>快捷键</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>前往官網</value>
</data>
@@ -2526,7 +2535,7 @@
<value>當前用戶</value>
</data>
<data name="ViewUserCookieOperationGameRecordIndexAction" xml:space="preserve">
<value>我的角色</value>
<value>旅行工具</value>
</data>
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
<value>網頁登陸</value>
@@ -2741,6 +2750,12 @@
<data name="WebGachaConfigTypeWeaponEventWish" xml:space="preserve">
<value>武器活動祈願</value>
</data>
<data name="WebGameResourcePathCopySucceed" xml:space="preserve">
<value>下载链接复制成功</value>
</data>
<data name="WebHoyolabInvalidUid" xml:space="preserve">
<value>无效的 UID</value>
</data>
<data name="WebIndexOrSpiralAbyssVerificationFailed" xml:space="preserve">
<value>验证失败,请手动验证或前往「米游社-我的角色」页面查看</value>
</data>

View File

@@ -19,12 +19,7 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Singleton)]
internal sealed partial class AppOptions : DbStoreOptions
{
private readonly List<NameValue<BackdropType>> supportedBackdropTypesInner = new()
{
new("Acrylic", BackdropType.Acrylic),
new("Mica", BackdropType.Mica),
new("MicaAlt", BackdropType.MicaAlt),
};
private readonly List<NameValue<BackdropType>> supportedBackdropTypesInner = CollectionsNameValue.ListFromEnum<BackdropType>();
private readonly List<NameValue<string>> supportedCulturesInner = new()
{

View File

@@ -104,7 +104,7 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
{
ReadOnlySpan<byte> span = stream.ToArray();
ReadOnlySpan<byte> match = isOversea
? "https://webstatic-sea.hoyoverse.com/genshin/event/e20190909gacha-v2/index.html"u8
? "https://gs.hoyoverse.com/genshin/event/e20190909gacha-v2/index.html"u8
: "https://webstatic.mihoyo.com/hk4e/event/e20190909gacha-v2/index.html"u8;
int index = span.LastIndexOf(match);

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Service.Game.Scheme;
/// 启动方案
/// </summary>
[HighQuality]
internal class LaunchScheme
internal class LaunchScheme : IEquatable<ChannelOptions>
{
/// <summary>
/// 显示名称
@@ -67,14 +67,10 @@ internal class LaunchScheme
};
}
/// <summary>
/// 多通道相等
/// </summary>
/// <param name="multiChannel">多通道</param>
/// <returns>是否相等</returns>
public bool MultiChannelEqual(in ChannelOptions multiChannel)
[SuppressMessage("", "SH002")]
public bool Equals(ChannelOptions other)
{
return Channel == multiChannel.Channel && SubChannel == multiChannel.SubChannel;
return Channel == other.Channel && SubChannel == other.SubChannel;
}
public bool ExecutableMatches(string gameFileName)

View File

@@ -83,6 +83,11 @@ internal sealed partial class MetadataService : IMetadataService, IMetadataServi
return false;
}
}
catch (JsonException ex)
{
infoBarService.Error(ex, SH.ServiceMetadataRequestFailed);
return false;
}
catch (HttpRequestException ex)
{
if (ex.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.NotFound)

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.View.Control;
internal interface IWebViewerSource
{
MiHoYoJSInterface CreateJsInterface(IServiceProvider serviceProvider, CoreWebView2 coreWebView2, UserAndUid userAndUid);
MiHoYoJSBridge CreateJSBridge(IServiceProvider serviceProvider, CoreWebView2 coreWebView2, UserAndUid userAndUid);
string GetSource(UserAndUid userAndUid);
}

View File

@@ -12,9 +12,9 @@ namespace Snap.Hutao.View.Control;
[DependencyProperty("OverseaSource", typeof(string))]
internal sealed partial class StaticWebview2ViewerSource : DependencyObject, IWebViewerSource
{
public MiHoYoJSInterface CreateJsInterface(IServiceProvider serviceProvider, CoreWebView2 coreWebView2, UserAndUid userAndUid)
public MiHoYoJSBridge CreateJSBridge(IServiceProvider serviceProvider, CoreWebView2 coreWebView2, UserAndUid userAndUid)
{
return serviceProvider.CreateInstance<MiHoYoJSInterface>(coreWebView2, userAndUid);
return serviceProvider.CreateInstance<MiHoYoJSBridge>(coreWebView2, userAndUid);
}
public string GetSource(UserAndUid userAndUid)

View File

@@ -6,5 +6,43 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Transitions="{ThemeResource EntranceThemeTransitions}"
mc:Ignorable="d">
<WebView2 x:Name="WebView" DefaultBackgroundColor="Transparent"/>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Button
MinWidth="42"
Command="{x:Bind GoBackCommand}"
FontSize="12"
IsEnabled="{x:Bind WebView.CanGoBack, Mode=OneWay}"
Style="{StaticResource NavigationBackButtonSmallStyle}"/>
<Button
MinWidth="42"
Command="{x:Bind RefreshCommand}"
Content="&#xE72C;"
FontSize="12"
Style="{StaticResource NavigationBackButtonSmallStyle}"/>
<TextBlock
MaxWidth="240"
Margin="6"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{x:Bind DocumentTitle, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"/>
</StackPanel>
<WebView2
x:Name="WebView"
Grid.Row="1"
DefaultBackgroundColor="Transparent"/>
<Rectangle
Grid.Row="2"
Height="8"
Fill="{ThemeResource SystemControlAccentAcrylicElementAccentMediumHighBrush}"/>
</Grid>
</UserControl>

View File

@@ -10,19 +10,22 @@ using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Bridge;
using Windows.Foundation;
using WinRT;
using WinRT.Interop;
namespace Snap.Hutao.View.Control;
[DependencyProperty("SourceProvider", typeof(IWebViewerSource))]
[DependencyProperty("DocumentTitle", typeof(string))]
internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
{
private readonly IServiceProvider serviceProvider;
private readonly IInfoBarService infoBarService;
private readonly RoutedEventHandler loadEventHandler;
private readonly TypedEventHandler<CoreWebView2, object> documentTitleChangedEventHander;
private MiHoYoJSInterface? jsInterface;
private MiHoYoJSBridge? jsBridge;
private bool isInitializingOrInitialized;
public WebViewer()
@@ -34,6 +37,7 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
serviceProvider.GetRequiredService<IMessenger>().Register(this);
loadEventHandler = OnLoaded;
documentTitleChangedEventHander = OnDocumentTitleChanged;
Loaded += loadEventHandler;
}
@@ -44,6 +48,21 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
taskContext.InvokeOnMainThread(RefreshWebview2Content);
}
[Command("GoBackCommand")]
private void GoBack()
{
if (WebView.CanGoBack)
{
WebView.GoBack();
}
}
[Command("RefreshCommand")]
private void Refresh()
{
WebView.Reload();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
InitializeAsync().SafeForget();
@@ -60,9 +79,15 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
await WebView.EnsureCoreWebView2Async();
WebView.CoreWebView2.DisableDevToolsForReleaseBuild();
WebView.CoreWebView2.DocumentTitleChanged += documentTitleChangedEventHander;
RefreshWebview2Content();
}
private void OnDocumentTitleChanged(CoreWebView2 sender, object args)
{
DocumentTitle = sender.DocumentTitle;
}
private async void RefreshWebview2Content()
{
User? user = serviceProvider.GetRequiredService<IUserService>().Current;
@@ -104,8 +129,8 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
coreWebView2
.SetCookie(user.CookieToken, user.LToken, userAndUid.IsOversea)
.SetMobileUserAgent(userAndUid.IsOversea);
jsInterface?.Detach();
jsInterface = SourceProvider.CreateJsInterface(serviceProvider, coreWebView2, userAndUid);
jsBridge?.Detach();
jsBridge = SourceProvider.CreateJSBridge(serviceProvider, coreWebView2, userAndUid);
await navigator.NavigateAsync(source).ConfigureAwait(true);
}

View File

@@ -272,39 +272,36 @@
<Grid>
<Pivot Visibility="{Binding CultivateEntries.Count, Converter={StaticResource Int32ToVisibilityConverter}}">
<PivotItem Header="{shcm:ResourceString Name=ViewPageCultivationCultivateEntry}">
<ItemsView
Padding="16,0"
IsItemInvokedEnabled="False"
ItemTemplate="{StaticResource CultivateEntryTemplate}"
ItemsSource="{Binding CultivateEntries}"
SelectionMode="None">
<ItemsView.ItemTransitionProvider>
<shcl:DefaultItemCollectionTransitionProvider/>
</ItemsView.ItemTransitionProvider>
<ItemsView.Layout>
<shcl:UniformStaggeredLayout
MinColumnSpacing="12"
MinItemWidth="300"
MinRowSpacing="-4"/>
</ItemsView.Layout>
</ItemsView>
<ScrollView Padding="16,0">
<ItemsRepeater
Margin="0,16,0,0"
ItemTemplate="{StaticResource CultivateEntryTemplate}"
ItemsSource="{Binding CultivateEntries}">
<ItemsRepeater.Layout>
<shcl:UniformStaggeredLayout
MinColumnSpacing="12"
MinItemWidth="300"
MinRowSpacing="-4"/>
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollView>
</PivotItem>
<PivotItem Header="{shcm:ResourceString Name=ViewPageCultivationMaterialStatistics}">
<ItemsView
Padding="16,0"
IsItemInvokedEnabled="False"
ItemTemplate="{StaticResource StatisticsItemTemplate}"
ItemsSource="{Binding StatisticsItems}"
SelectionMode="None">
<ItemsView.Layout>
<UniformGridLayout
ItemsJustification="Start"
ItemsStretch="Fill"
MinColumnSpacing="12"
MinItemWidth="300"
MinRowSpacing="-4"/>
</ItemsView.Layout>
</ItemsView>
<ScrollView Padding="16,0">
<ItemsRepeater
Margin="0,16,0,0"
ItemTemplate="{StaticResource StatisticsItemTemplate}"
ItemsSource="{Binding StatisticsItems}">
<ItemsRepeater.Layout>
<UniformGridLayout
ItemsJustification="Start"
ItemsStretch="Fill"
MinColumnSpacing="12"
MinItemWidth="300"
MinRowSpacing="-4"/>
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollView>
</PivotItem>
</Pivot>
<StackPanel

View File

@@ -212,7 +212,7 @@
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="#80000000"
CornerRadius="0,6,0,6">
CornerRadius="{ThemeResource ControlCornerRadiusTopRightAndBottomLeft}">
<TextBlock
Margin="6,0,6,2"
Foreground="#FFFFFFFF"

View File

@@ -195,6 +195,46 @@
SelectedItem="{Binding SelectedBackdropType, Mode=TwoWay}"/>
</cwc:SettingsCard>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingKeyShortcutHeader}"/>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE92E;}">
<StackPanel Orientation="Horizontal" Spacing="16">
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Win"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasWindows, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Ctrl"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasControl, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Shift"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasShift, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Alt"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasAlt, Mode=TwoWay}"/>
<ComboBox
VerticalAlignment="Center"
DisplayMemberPath="Name"
ItemsSource="{Binding HotKeyOptions.VirtualKeys}"
SelectedItem="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.KeyNameValue, Mode=TwoWay}"
Style="{StaticResource DefaultComboBoxStyle}"/>
<ToggleSwitch
MinWidth="120"
VerticalAlignment="Center"
IsOn="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.IsEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewpageSettingHomeHeader}"/>
<cwc:SettingsExpander
Description="{shcm:ResourceString Name=ViewpageSettingHomeCardDescription}"
@@ -295,9 +335,7 @@
Header="{shcm:ResourceString Name=ViewPageSettingSetDataFolderHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE8DE;}"
IsClickEnabled="True"/>
<cwc:SettingsCard
Margin="0,3,0,0"
ActionIcon="{shcm:FontIcon Glyph=&#xE76C;}"
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingStorageOpenAction}"
Command="{Binding OpenCacheFolderCommand}"
@@ -318,7 +356,6 @@
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageSettingDangerousHeader}"/>
<InfoBar
IsClosable="False"
IsOpen="{Binding Options.IsAdvancedLaunchOptionsEnabled}"

View File

@@ -22,25 +22,28 @@
Text="{x:Bind Title}"
TextWrapping="NoWrap"/>
<ToggleButton
<StackPanel
Grid.Column="1"
Margin="0,0,6,0"
VerticalAlignment="Center"
IsChecked="{x:Bind HotKeyOptions.IsMouseClickRepeatForeverOn, Mode=OneWay}"
IsHitTestVisible="False"
Visibility="{x:Bind RuntimeOptions.IsElevated}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
Orientation="Horizontal"
Spacing="6"
Visibility="{x:Bind RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityConverter}}">
<ToggleButton
Margin="0,0,6,0"
VerticalAlignment="Center"
IsChecked="{x:Bind HotKeyOptions.IsMouseClickRepeatForeverOn, Mode=OneWay}"
IsHitTestVisible="False"
Visibility="{x:Bind HotKeyOptions.MouseClickRepeatForeverKeyCombination.IsEnabled, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{x:Bind HotKeyOptions.MouseClickRepeatForeverKeyCombination.DisplayName, Mode=OneWay}"/>
<TextBlock Grid.Column="1" Text="{shcm:ResourceString Name=ViewTitleAutoClicking}"/>
</Grid>
</ToggleButton>
</StackPanel>
<TextBlock Grid.Column="0" Text="F8"/>
<TextBlock
Grid.Column="1"
Margin="6,0,0,0"
Text="{shcm:ResourceString Name=ViewTitleAutoClicking}"/>
</Grid>
</ToggleButton>
</Grid>
</UserControl>

View File

@@ -216,15 +216,10 @@
Style="{StaticResource DefaultAppBarButtonStyle}">
<FlyoutBase.AttachedFlyout>
<Flyout
FlyoutPresenterStyle="{StaticResource WebViewerFlyoutPresenterStyle}"
LightDismissOverlayMode="On"
Placement="Full"
ShouldConstrainToRootBounds="False">
<Flyout.FlyoutPresenterStyle>
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
</Style>
</Flyout.FlyoutPresenterStyle>
<Grid>
<shvc:WebViewer>
<shvc:WebViewer.SourceProvider>

View File

@@ -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<MiHoYoJSInterface>(coreWebView2, userAndUid);
return serviceProvider.CreateInstance<MiHoYoJSBridge>(coreWebView2, userAndUid);
}
public string GetSource(UserAndUid userAndUid)

View File

@@ -36,7 +36,7 @@ internal sealed class SummaryItem : Item
/// </summary>
public string TimeFormatted
{
get => $"{Time:yyy.MM.dd HH:mm:ss}";
get => $"{Time.ToLocalTime():yyy.MM.dd HH:mm:ss}";
}
/// <summary>

View File

@@ -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)
{

View File

@@ -3,7 +3,7 @@
using Snap.Hutao.Core.Setting;
namespace Snap.Hutao.ViewModel;
namespace Snap.Hutao.ViewModel.Setting;
internal sealed class HomeCardOptions
{

View File

@@ -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
/// </summary>
public AppOptions Options { get => appOptions; }
/// <summary>
/// 胡桃选项
/// </summary>
public RuntimeOptions HutaoOptions { get => runtimeOptions; }
/// <summary>
/// 胡桃用户选项
/// </summary>
public HutaoUserOptions UserOptions { get => hutaoUserOptions; }
public HomeCardOptions HomeCardOptions { get => homeCardOptions; }
public HotKeyOptions HotKeyOptions { get => hotKeyOptions; }
public HutaoPassportViewModel Passport { get => hutaoPassportViewModel; }
/// <summary>

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.ViewModel.SpiralAbyss;
internal sealed class MonsterView : INameIcon, IMappingFrom<MonsterView, TowerMonster, Model.Metadata.Monster.Monster>
{
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<MonsterView, TowerMo
AttackMonolith = towerMonster.AttackMonolith;
}
public static MonsterView Default(MonsterRelationshipId id)
{
return new(id);
}
public string Name { get; }
public Uri Icon { get; }
@@ -44,6 +39,11 @@ internal sealed class MonsterView : INameIcon, IMappingFrom<MonsterView, TowerMo
public bool AttackMonolith { get; }
public static MonsterView Default(in MonsterRelationshipId id)
{
return new(id);
}
public static MonsterView From(TowerMonster tower, Model.Metadata.Monster.Monster meta)
{
return new MonsterView(tower, meta);

View File

@@ -10,11 +10,11 @@ namespace Snap.Hutao.ViewModel.User;
internal sealed class SignInWebViewerSouce : DependencyObject, IWebViewerSource
{
public MiHoYoJSInterface CreateJsInterface(IServiceProvider serviceProvider, CoreWebView2 coreWebView2, UserAndUid userAndUid)
public MiHoYoJSBridge CreateJSBridge(IServiceProvider serviceProvider, CoreWebView2 coreWebView2, UserAndUid userAndUid)
{
return userAndUid.User.IsOversea
? serviceProvider.CreateInstance<SignInJSInterfaceOversea>(coreWebView2, userAndUid)
: serviceProvider.CreateInstance<SignInJSInterface>(coreWebView2, userAndUid);
? serviceProvider.CreateInstance<SignInJSBridgeOversea>(coreWebView2, userAndUid)
: serviceProvider.CreateInstance<SignInJSBridge>(coreWebView2, userAndUid);
}
public string GetSource(UserAndUid userAndUid)

View File

@@ -321,19 +321,6 @@ internal static class ApiEndpoints
public const string AnnContent = $"{Hk4eApiAnnouncementApi}/getAnnContent?{AnnouncementQuery}";
#endregion
#region Hk4eApiGachaInfoApi
/// <summary>
/// 获取祈愿记录
/// </summary>
/// <param name="query">query string</param>
/// <returns>祈愿记录信息Url</returns>
public static string GachaInfoGetGachaLog(string query)
{
return $"{Hk4eApiGachaInfoApi}/getGachaLog?{query}";
}
#endregion
#region PassportApi | PassportApiV4
/// <summary>
@@ -380,6 +367,19 @@ internal static class ApiEndpoints
}
#endregion
#region PublicOperationHk4eGachaInfoApi
/// <summary>
/// 获取祈愿记录
/// </summary>
/// <param name="query">query string</param>
/// <returns>祈愿记录信息Url</returns>
public static string GachaInfoGetGachaLog(string query)
{
return $"{PublicOperationHk4eGachaInfoApi}/getGachaLog?{query}";
}
#endregion
#region SdkStaticLauncherApi
/// <summary>
@@ -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";

View File

@@ -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";

View File

@@ -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<MiHoYoJSInterface> logger;
private readonly ILogger<MiHoYoJSBridge> logger;
private readonly TypedEventHandler<CoreWebView2, CoreWebView2WebMessageReceivedEventArgs> webMessageReceivedEventHandler;
private readonly TypedEventHandler<CoreWebView2, CoreWebView2DOMContentLoadedEventArgs> 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<ITaskContext>();
logger = serviceProvider.GetRequiredService<ILogger<MiHoYoJSInterface>>();
logger = serviceProvider.GetRequiredService<ILogger<MiHoYoJSBridge>>();
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!;
}
/// <summary>
/// 关闭
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
public virtual async ValueTask<IJsResult?> ClosePageAsync(JsParam param)
protected virtual async ValueTask<IJsResult?> ClosePageAsync(JsParam param)
{
await taskContext.SwitchToMainThreadAsync();
if (coreWebView2.CanGoBack)
@@ -99,7 +107,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
public virtual IJsResult? ConfigureShare(JsParam param)
protected virtual IJsResult? ConfigureShare(JsParam param)
{
return null;
}
@@ -109,7 +117,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="jsParam">参数</param>
/// <returns>响应</returns>
public virtual async ValueTask<IJsResult?> GetActionTicketAsync(JsParam<ActionTypePayload> jsParam)
protected virtual async ValueTask<IJsResult?> GetActionTicketAsync(JsParam<ActionTypePayload> jsParam)
{
return await serviceProvider
.GetRequiredService<AuthClient>()
@@ -122,7 +130,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
public virtual JsResult<Dictionary<string, string>> GetCookieInfo(JsParam param)
protected virtual JsResult<Dictionary<string, string>> GetCookieInfo(JsParam param)
{
ArgumentNullException.ThrowIfNull(userAndUid.User.LToken);
@@ -142,7 +150,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
public virtual async ValueTask<JsResult<Dictionary<string, string>>> GetCookieTokenAsync(JsParam<CookieTokenPayload> param)
protected virtual async ValueTask<JsResult<Dictionary<string, string>>> GetCookieTokenAsync(JsParam<CookieTokenPayload> param)
{
IUserService userService = serviceProvider.GetRequiredService<IUserService>();
if (param.Payload.ForceRefresh)
@@ -162,7 +170,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">param</param>
/// <returns>语言与时区</returns>
public virtual JsResult<Dictionary<string, string>> GetCurrentLocale(JsParam<PushPagePayload> param)
protected virtual JsResult<Dictionary<string, string>> GetCurrentLocale(JsParam<PushPagePayload> param)
{
MetadataOptions metadataOptions = serviceProvider.GetRequiredService<MetadataOptions>();
@@ -181,7 +189,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
public virtual JsResult<Dictionary<string, string>> GetDynamicSecrectV1(JsParam param)
protected virtual JsResult<Dictionary<string, string>> GetDynamicSecrectV1(JsParam param)
{
string salt = HoyolabOptions.Salts[SaltType.LK2];
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
@@ -212,7 +220,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
public virtual JsResult<Dictionary<string, string>> GetDynamicSecrectV2(JsParam<DynamicSecrect2Playload> param)
protected virtual JsResult<Dictionary<string, string>> GetDynamicSecrectV2(JsParam<DynamicSecrect2Playload> param)
{
string salt = HoyolabOptions.Salts[SaltType.X4];
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
@@ -235,25 +243,45 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">参数</param>
/// <returns>Http请求头</returns>
public virtual JsResult<Dictionary<string, string>> GetHttpRequestHeader(JsParam param)
protected virtual JsResult<Dictionary<string, string>> GetHttpRequestHeader(JsParam param)
{
Dictionary<string, string> 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<string, string>()
{
{ "x-rpc-client_type", "5" },
{ "x-rpc-device_id", HoyolabOptions.DeviceId },
{ "x-rpc-app_version", SaltConstants.CNVersion },
},
Data = headers,
};
}
protected virtual void GetHttpRequestHeaderCore(Dictionary<string, string> headers)
{
}
/// <summary>
/// 获取状态栏高度
/// </summary>
/// <param name="param">参数</param>
/// <returns>结果</returns>
public virtual JsResult<Dictionary<string, object>> GetStatusBarHeight(JsParam param)
protected virtual JsResult<Dictionary<string, object>> GetStatusBarHeight(JsParam param)
{
return new() { Data = new() { ["statusBarHeight"] = 0 } };
}
@@ -263,7 +291,7 @@ internal class MiHoYoJSInterface
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
public virtual async ValueTask<JsResult<Dictionary<string, object>>> GetUserInfoAsync(JsParam param)
protected virtual async ValueTask<JsResult<Dictionary<string, object>>> GetUserInfoAsync(JsParam param)
{
Response<UserFullInfoWrapper> response = await serviceProvider
.GetRequiredService<IOverseaSupportFactory<IUserClient>>()
@@ -292,7 +320,7 @@ internal class MiHoYoJSInterface
}
}
public virtual async ValueTask<IJsResult?> PushPageAsync(JsParam<PushPagePayload> param)
protected virtual async ValueTask<IJsResult?> PushPageAsync(JsParam<PushPagePayload> param)
{
const string bbsSchema = "mihoyobbs://";
string pageUrl = param.Payload.Page;
@@ -316,7 +344,7 @@ internal class MiHoYoJSInterface
return null;
}
public virtual IJsResult? Share(JsParam<SharePayload> param)
protected virtual IJsResult? Share(JsParam<SharePayload> param)
{
return new JsResult<Dictionary<string, string>>()
{
@@ -327,57 +355,53 @@ internal class MiHoYoJSInterface
};
}
public virtual ValueTask<IJsResult?> ShowAlertDialogAsync(JsParam param)
protected virtual ValueTask<IJsResult?> ShowAlertDialogAsync(JsParam param)
{
return ValueTask.FromException<IJsResult?>(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<IJsResult?> GetNotificationSettingsAsync(JsParam param)
protected virtual ValueTask<IJsResult?> 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<string> 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);
}

View File

@@ -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;
/// <summary>
/// 签到页面JS桥
/// </summary>
[HighQuality]
internal sealed class SignInJSBridge : MiHoYoJSBridge
{
public SignInJSBridge(CoreWebView2 webView, UserAndUid userAndUid)
: base(webView, userAndUid)
{
}
protected override void GetHttpRequestHeaderCore(Dictionary<string, string> headers)
{
headers["x-rpc-client_type"] = "2";
}
}

View File

@@ -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;
/// <summary>
/// HoYoLAB 签到页面JS桥
/// </summary>
[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<string, string> headers)
{
headers["x-rpc-client_type"] = "2";
}
protected override void DOMContentLoaded(CoreWebView2 coreWebView2)
{
coreWebView2.ExecuteScriptAsync(RemoveRotationWarningScript).AsTask().SafeForget();
}
}

View File

@@ -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;
/// <summary>
/// 签到页面JS桥
/// </summary>
[HighQuality]
internal sealed class SignInJSInterface : MiHoYoJSInterface
{
/// <inheritdoc cref="MiHoYoJSInterface(IServiceProvider, CoreWebView2)"/>
public SignInJSInterface(CoreWebView2 webView, IServiceProvider serviceProvider, UserAndUid userAndUid)
: base(webView, userAndUid)
{
}
/// <inheritdoc/>
public override JsResult<Dictionary<string, string>> GetHttpRequestHeader(JsParam param)
{
return new()
{
Data = new Dictionary<string, string>()
{
{ "x-rpc-client_type", "2" },
{ "x-rpc-device_id", HoyolabOptions.DeviceId },
{ "x-rpc-app_version", SaltConstants.CNVersion },
},
};
}
}

View File

@@ -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;
/// <summary>
/// HoYoLAB 签到页面JS桥
/// </summary>
[HighQuality]
internal sealed class SignInJSInterfaceOversea : MiHoYoJSInterface
{
private const string RemoveRotationWarningScript = """
let landscape = document.getElementById('mihoyo_landscape');
landscape.remove();
""";
private readonly ILogger<MiHoYoJSInterface> logger;
/// <inheritdoc cref="MiHoYoJSInterface(IServiceProvider, CoreWebView2)"/>
public SignInJSInterfaceOversea(CoreWebView2 webView, IServiceProvider serviceProvider, UserAndUid userAndUid)
: base(webView, userAndUid)
{
logger = serviceProvider.GetRequiredService<ILogger<MiHoYoJSInterface>>();
webView.DOMContentLoaded += OnDOMContentLoaded;
}
/// <inheritdoc/>
public override JsResult<Dictionary<string, string>> GetHttpRequestHeader(JsParam param)
{
return new()
{
Data = new Dictionary<string, string>()
{
{ "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);
}
}

View File

@@ -7,8 +7,11 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
/// 公告
/// </summary>
[HighQuality]
[SuppressMessage("", "SA1124")]
internal sealed class Announcement : AnnouncementContent
{
#region Binding
/// <summary>
/// 是否应展示时间
/// </summary>
@@ -81,6 +84,7 @@ internal sealed class Announcement : AnnouncementContent
{
get => $"{StartTime:yyyy.MM.dd HH:mm} - {EndTime:yyyy.MM.dd HH:mm}";
}
#endregion
/// <summary>
/// 类型标签

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
/// 公告包装器
/// </summary>
[HighQuality]
internal sealed class AnnouncementWrapper : ListWrapper<AnnouncementListWrapper>
internal sealed class AnnouncementWrapper : ListWrapper<AnnouncementListWrapper>, IJsonOnDeserialized
{
/// <summary>
/// 总数
@@ -46,4 +46,18 @@ internal sealed class AnnouncementWrapper : ListWrapper<AnnouncementListWrapper>
/// </summary>
[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);
}
}
}
}

View File

@@ -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
/// </summary>
[HighQuality]
internal readonly struct PlayerUid
internal readonly partial struct PlayerUid
{
/// <summary>
/// UID 的实际值
@@ -28,52 +28,63 @@ internal readonly struct PlayerUid
/// <param name="region">服务器,当提供该参数时会无条件信任</param>
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);
}
/// <summary>
/// 判断是否为国际服
/// We make this a static method rather than property,
/// to avoid unnecessary memory allocation.
/// </summary>
/// <param name="uid">uid</param>
/// <returns>是否为国际服</returns>
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,
};
}
/// <inheritdoc/>
public override string ToString()
{
return Value;
}
/// <summary>
/// 转换到查询字符串
/// </summary>
/// <returns>查询字符串</returns>
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();
}

View File

@@ -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;
}
}

View File

@@ -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);
/// <summary>
/// UTC-05
/// </summary>
public static TimeSpan AmericaServerOffset { get => AmericaOffsetValue; }
/// <summary>
/// UTC+01
/// </summary>
public static TimeSpan EuropeServerOffset { get => EuropeOffsetValue; }
/// <summary>
/// UTC+08
/// </summary>
public static TimeSpan CommonOffset { get => CommonOffsetValue; }
}