mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Merge pull request #1085 from DGP-Studio/develop
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,8 @@ namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
internal interface ICurrentWindowReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Only set in WindowController
|
||||
/// </summary>
|
||||
public Window Window { get; set; }
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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*)¤t);
|
||||
|
||||
UnregisterForCurrentWindow();
|
||||
RegisterForCurrentWindow();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
20
src/Snap.Hutao/Snap.Hutao/Extension/UnsafeDateTimeOffset.cs
Normal file
20
src/Snap.Hutao/Snap.Hutao/Extension/UnsafeDateTimeOffset.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
13
src/Snap.Hutao/Snap.Hutao/Model/CollectionsNameValue.cs
Normal file
13
src/Snap.Hutao/Snap.Hutao/Model/CollectionsNameValue.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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=""
|
||||
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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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=}">
|
||||
<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=}"
|
||||
IsClickEnabled="True"/>
|
||||
|
||||
<cwc:SettingsCard
|
||||
Margin="0,3,0,0"
|
||||
ActionIcon="{shcm:FontIcon Glyph=}"
|
||||
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}"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Core.Setting;
|
||||
|
||||
namespace Snap.Hutao.ViewModel;
|
||||
namespace Snap.Hutao.ViewModel.Setting;
|
||||
|
||||
internal sealed class HomeCardOptions
|
||||
{
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
26
src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSBridge.cs
Normal file
26
src/Snap.Hutao/Snap.Hutao/Web/Bridge/SignInJSBridge.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
/// 类型标签
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
18
src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUidExtension.cs
Normal file
18
src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUidExtension.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user