mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
improve welcome download experience
This commit is contained in:
25
src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs
Normal file
25
src/Snap.Hutao/Snap.Hutao.Test/CSharpLanguageFeatureTest.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Snap.Hutao.Test;
|
||||
|
||||
[TestClass]
|
||||
public class CSharpLanguageFeatureTest
|
||||
{
|
||||
[TestMethod]
|
||||
public unsafe void NullStringFixedAlsoNullPointer()
|
||||
{
|
||||
string testStr = null!;
|
||||
fixed(char* pStr = testStr)
|
||||
{
|
||||
Assert.IsTrue(pStr == null);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public unsafe void EmptyStringFixedIsNullTerminator()
|
||||
{
|
||||
string testStr = string.Empty;
|
||||
fixed (char* pStr = testStr)
|
||||
{
|
||||
Assert.IsTrue(*pStr == '\0');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Test;
|
||||
public class DependencyInjectionTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void OriginalTypeDiscoverable()
|
||||
public void OriginalTypeNotDiscoverable()
|
||||
{
|
||||
IServiceProvider services = new ServiceCollection()
|
||||
.AddSingleton<IService, ServiceA>()
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,8 +10,6 @@ using Snap.Hutao.Win32;
|
||||
using System.IO;
|
||||
using Windows.Graphics;
|
||||
using Windows.UI;
|
||||
using Windows.Win32.Foundation;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
@@ -23,34 +21,22 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMessage>, IRecipient<FlyoutOpenCloseMessage>
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
{
|
||||
private readonly HWND hwnd;
|
||||
private readonly AppWindow appWindow;
|
||||
|
||||
private readonly TWindow window;
|
||||
private readonly FrameworkElement titleBar;
|
||||
private readonly WindowOptions<TWindow> options;
|
||||
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ILogger<ExtendedWindow<TWindow>> logger;
|
||||
private readonly WindowSubclassManager<TWindow> subclassManager;
|
||||
|
||||
private readonly bool useLegacyDragBar;
|
||||
private readonly WindowSubclass<TWindow> subclass;
|
||||
|
||||
private SystemBackdrop? systemBackdrop;
|
||||
|
||||
private ExtendedWindow(TWindow window, FrameworkElement titleBar, IServiceProvider serviceProvider)
|
||||
{
|
||||
this.window = window;
|
||||
this.titleBar = titleBar;
|
||||
options = new(window, titleBar);
|
||||
subclass = new(options);
|
||||
|
||||
logger = serviceProvider.GetRequiredService<ILogger<ExtendedWindow<TWindow>>>();
|
||||
this.serviceProvider = serviceProvider;
|
||||
|
||||
hwnd = (HWND)WindowNative.GetWindowHandle(window);
|
||||
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
|
||||
appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
|
||||
useLegacyDragBar = !AppWindowTitleBar.IsCustomizationSupported();
|
||||
subclassManager = new(window, hwnd, useLegacyDragBar);
|
||||
|
||||
InitializeWindow();
|
||||
}
|
||||
|
||||
@@ -79,63 +65,63 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
|
||||
/// <inheritdoc/>
|
||||
public void Receive(FlyoutOpenCloseMessage message)
|
||||
{
|
||||
UpdateDragRectangles(appWindow.TitleBar, message.IsOpen);
|
||||
UpdateDragRectangles(options.AppWindow.TitleBar, message.IsOpen);
|
||||
}
|
||||
|
||||
private void InitializeWindow()
|
||||
{
|
||||
appWindow.Title = string.Format(SH.AppNameAndVersion, CoreEnvironment.Version);
|
||||
appWindow.SetIcon(Path.Combine(CoreEnvironment.InstalledLocation, "Assets/Logo.ico"));
|
||||
options.AppWindow.Title = string.Format(SH.AppNameAndVersion, CoreEnvironment.Version);
|
||||
options.AppWindow.SetIcon(Path.Combine(CoreEnvironment.InstalledLocation, "Assets/Logo.ico"));
|
||||
ExtendsContentIntoTitleBar();
|
||||
|
||||
Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize);
|
||||
Persistence.RecoverOrInit(options);
|
||||
|
||||
// appWindow.Show(true);
|
||||
// appWindow.Show can't bring window to top.
|
||||
window.Activate();
|
||||
options.Window.Activate();
|
||||
|
||||
systemBackdrop = new(window);
|
||||
systemBackdrop = new(options.Window);
|
||||
bool micaApplied = systemBackdrop.TryApply();
|
||||
logger.LogInformation("Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
|
||||
|
||||
bool subClassApplied = subclassManager.TrySetWindowSubclass();
|
||||
logger.LogInformation("Apply {name} : {result}", nameof(WindowSubclassManager<TWindow>), subClassApplied ? "succeed" : "failed");
|
||||
bool subClassApplied = subclass.Initialize();
|
||||
logger.LogInformation("Apply {name} : {result}", nameof(WindowSubclass<TWindow>), subClassApplied ? "succeed" : "failed");
|
||||
|
||||
IMessenger messenger = Ioc.Default.GetRequiredService<IMessenger>();
|
||||
IMessenger messenger = serviceProvider.GetRequiredService<IMessenger>();
|
||||
messenger.Register<BackdropTypeChangedMessage>(this);
|
||||
messenger.Register<FlyoutOpenCloseMessage>(this);
|
||||
|
||||
window.Closed += OnWindowClosed;
|
||||
options.Window.Closed += OnWindowClosed;
|
||||
}
|
||||
|
||||
private void OnWindowClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
if (window.PersistSize)
|
||||
if (options.Window.PersistSize)
|
||||
{
|
||||
Persistence.Save(appWindow);
|
||||
Persistence.Save(options);
|
||||
}
|
||||
|
||||
subclassManager?.Dispose();
|
||||
subclass?.Dispose();
|
||||
}
|
||||
|
||||
private void ExtendsContentIntoTitleBar()
|
||||
{
|
||||
if (useLegacyDragBar)
|
||||
if (options.UseLegacyDragBarImplementation)
|
||||
{
|
||||
// use normal Window method to extend.
|
||||
window.ExtendsContentIntoTitleBar = true;
|
||||
window.SetTitleBar(titleBar);
|
||||
options.Window.ExtendsContentIntoTitleBar = true;
|
||||
options.Window.SetTitleBar(options.TitleBar);
|
||||
}
|
||||
else
|
||||
{
|
||||
AppWindowTitleBar appTitleBar = appWindow.TitleBar;
|
||||
AppWindowTitleBar appTitleBar = options.AppWindow.TitleBar;
|
||||
appTitleBar.IconShowOptions = IconShowOptions.HideIconAndSystemMenu;
|
||||
appTitleBar.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
UpdateTitleButtonColor(appTitleBar);
|
||||
UpdateDragRectangles(appTitleBar);
|
||||
titleBar.ActualThemeChanged += (s, e) => UpdateTitleButtonColor(appTitleBar);
|
||||
titleBar.SizeChanged += (s, e) => UpdateDragRectangles(appTitleBar);
|
||||
options.TitleBar.ActualThemeChanged += (s, e) => UpdateTitleButtonColor(appTitleBar);
|
||||
options.TitleBar.SizeChanged += (s, e) => UpdateDragRectangles(appTitleBar);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,20 +154,20 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
|
||||
if (isFlyoutOpened)
|
||||
{
|
||||
// set to 0
|
||||
appTitleBar.SetDragRectangles(default(RectInt32).Enumerate().ToArray());
|
||||
appTitleBar.SetDragRectangles(default(RectInt32).ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
double scale = Persistence.GetScaleForWindowHandle(hwnd);
|
||||
double scale = Persistence.GetScaleForWindowHandle(options.Hwnd);
|
||||
|
||||
// 48 is the navigation button leftInset
|
||||
RectInt32 dragRect = StructMarshal.RectInt32(new(48, 0), titleBar.ActualSize).Scale(scale);
|
||||
appTitleBar.SetDragRectangles(dragRect.Enumerate().ToArray());
|
||||
RectInt32 dragRect = StructMarshal.RectInt32(new(48, 0), options.TitleBar.ActualSize).Scale(scale);
|
||||
appTitleBar.SetDragRectangles(dragRect.ToArray());
|
||||
|
||||
// workaround for https://github.com/microsoft/WindowsAppSDK/issues/2976
|
||||
SizeInt32 size = appWindow.ClientSize;
|
||||
SizeInt32 size = options.AppWindow.ClientSize;
|
||||
size.Height -= (int)(31 * scale);
|
||||
appWindow.ResizeClient(size);
|
||||
options.AppWindow.ResizeClient(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Win32;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
@@ -22,43 +22,44 @@ internal static class Persistence
|
||||
/// <summary>
|
||||
/// 设置窗体位置
|
||||
/// </summary>
|
||||
/// <param name="appWindow">应用窗体</param>
|
||||
/// <param name="persistSize">持久化尺寸</param>
|
||||
/// <param name="initialSize">初始尺寸</param>
|
||||
public static unsafe void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 initialSize)
|
||||
/// <param name="options">选项</param>
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
public static void RecoverOrInit<TWindow>(WindowOptions<TWindow> options)
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
{
|
||||
// Set first launch size.
|
||||
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
|
||||
SizeInt32 transformedSize = TransformSizeForWindow(initialSize, hwnd);
|
||||
double scale = GetScaleForWindowHandle(options.Hwnd);
|
||||
SizeInt32 transformedSize = options.Window.InitSize.Scale(scale);
|
||||
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
|
||||
|
||||
if (persistSize)
|
||||
if (options.Window.PersistSize)
|
||||
{
|
||||
RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (persistedRect.Size() >= initialSize.Size())
|
||||
if (persistedRect.Size() >= options.Window.InitSize.Size())
|
||||
{
|
||||
rect = persistedRect;
|
||||
}
|
||||
}
|
||||
|
||||
TransformToCenterScreen(ref rect);
|
||||
appWindow.MoveAndResize(rect);
|
||||
options.AppWindow.MoveAndResize(rect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存状态的位置
|
||||
/// 保存窗体的位置
|
||||
/// </summary>
|
||||
/// <param name="appWindow">应用窗体</param>
|
||||
public static void Save(AppWindow appWindow)
|
||||
/// <param name="options">选项</param>
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
public static void Save<TWindow>(WindowOptions<TWindow> options)
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
{
|
||||
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
|
||||
WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT();
|
||||
GetWindowPlacement(hwnd, ref windowPlacement);
|
||||
GetWindowPlacement(options.Hwnd, ref windowPlacement);
|
||||
|
||||
// prevent save value when we are maximized.
|
||||
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)appWindow.GetRect());
|
||||
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)options.AppWindow.GetRect());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,13 +71,7 @@ internal static class Persistence
|
||||
public static double GetScaleForWindowHandle(HWND hwnd)
|
||||
{
|
||||
uint dpi = GetDpiForWindow(hwnd);
|
||||
return Math.Round(dpi / 96d, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static SizeInt32 TransformSizeForWindow(SizeInt32 size, HWND hwnd)
|
||||
{
|
||||
double scale = GetScaleForWindowHandle(hwnd);
|
||||
return new((int)(size.Width * scale), (int)(size.Height * scale));
|
||||
return Math.Round(dpi / 96D, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static void TransformToCenterScreen(ref RectInt32 rect)
|
||||
@@ -88,60 +83,41 @@ internal static class Persistence
|
||||
rect.Y = workAreaRect.Y + ((workAreaRect.Height - rect.Height) / 2);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private struct CompactRect
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public short X;
|
||||
|
||||
[FieldOffset(2)]
|
||||
public short Y;
|
||||
|
||||
[FieldOffset(4)]
|
||||
public short Width;
|
||||
|
||||
[FieldOffset(6)]
|
||||
public short Height;
|
||||
|
||||
[FieldOffset(0)]
|
||||
public ulong Value;
|
||||
|
||||
private CompactRect(int x, int y, int width, int height)
|
||||
{
|
||||
Value = 0;
|
||||
X = (short)x;
|
||||
Y = (short)y;
|
||||
Width = (short)width;
|
||||
Height = (short)height;
|
||||
}
|
||||
|
||||
private CompactRect(ulong value)
|
||||
{
|
||||
X = 0;
|
||||
Y = 0;
|
||||
Width = 0;
|
||||
Height = 0;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator RectInt32(CompactRect rect)
|
||||
{
|
||||
return new(rect.X, rect.Y, rect.Width, rect.Height);
|
||||
}
|
||||
|
||||
public static explicit operator CompactRect(ulong value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
public static explicit operator CompactRect(RectInt32 rect)
|
||||
{
|
||||
return new(rect.X, rect.Y, rect.Width, rect.Height);
|
||||
}
|
||||
|
||||
public static implicit operator ulong(CompactRect rect)
|
||||
public static unsafe explicit operator CompactRect(ulong value)
|
||||
{
|
||||
return rect.Value;
|
||||
Unsafe.SkipInit(out CompactRect rect);
|
||||
*(ulong*)&rect = value;
|
||||
return rect;
|
||||
}
|
||||
|
||||
public static unsafe implicit operator ulong(CompactRect rect)
|
||||
{
|
||||
return *(ulong*)▭
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Snap.Hutao.Control.Theme;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Service;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.System;
|
||||
using WinRT;
|
||||
@@ -34,9 +35,7 @@ internal sealed class SystemBackdrop
|
||||
this.window = window;
|
||||
using (IServiceScope scope = Ioc.Default.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.SystemBackdropType, BackdropType.Mica.ToString());
|
||||
BackdropType = Enum.Parse<BackdropType>(entry.Value!);
|
||||
BackdropType = scope.ServiceProvider.GetRequiredService<AppOptions>().BackdropType;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
53
src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowOptions.cs
Normal file
53
src/Snap.Hutao/Snap.Hutao/Core/Windowing/WindowOptions.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Win32.Foundation;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
/// <summary>
|
||||
/// Window 选项
|
||||
/// </summary>
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
internal readonly struct WindowOptions<TWindow>
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
{
|
||||
/// <summary>
|
||||
/// 窗体句柄
|
||||
/// </summary>
|
||||
public readonly HWND Hwnd;
|
||||
|
||||
/// <summary>
|
||||
/// AppWindow
|
||||
/// </summary>
|
||||
public readonly AppWindow AppWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 窗体
|
||||
/// </summary>
|
||||
public readonly TWindow Window;
|
||||
|
||||
/// <summary>
|
||||
/// 标题栏元素
|
||||
/// </summary>
|
||||
public readonly FrameworkElement TitleBar;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用 Win UI 3 自带的拓展标题栏实现
|
||||
/// </summary>
|
||||
public readonly bool UseLegacyDragBarImplementation = !AppWindowTitleBar.IsCustomizationSupported();
|
||||
|
||||
public WindowOptions(TWindow window, FrameworkElement titleBar)
|
||||
{
|
||||
Window = window;
|
||||
Hwnd = (HWND)WindowNative.GetWindowHandle(window);
|
||||
WindowId windowId = Win32Interop.GetWindowIdFromWindow(Hwnd);
|
||||
AppWindow = AppWindow.GetFromWindowId(windowId);
|
||||
|
||||
TitleBar = titleBar;
|
||||
}
|
||||
}
|
||||
@@ -14,16 +14,13 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
/// </summary>
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
[HighQuality]
|
||||
internal sealed class WindowSubclassManager<TWindow> : IDisposable
|
||||
internal sealed class WindowSubclass<TWindow> : IDisposable
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
{
|
||||
private const int WindowSubclassId = 101;
|
||||
private const int DragBarSubclassId = 102;
|
||||
|
||||
private readonly TWindow window;
|
||||
private readonly HWND hwnd;
|
||||
private readonly bool isLegacyDragBar;
|
||||
private HWND hwndDragBar;
|
||||
private readonly WindowOptions<TWindow> options;
|
||||
|
||||
// We have to explicitly hold a reference to SUBCLASSPROC
|
||||
private SUBCLASSPROC? windowProc;
|
||||
@@ -32,32 +29,28 @@ internal sealed class WindowSubclassManager<TWindow> : IDisposable
|
||||
/// <summary>
|
||||
/// 构造一个新的窗体子类管理器
|
||||
/// </summary>
|
||||
/// <param name="window">窗体实例</param>
|
||||
/// <param name="hwnd">窗体句柄</param>
|
||||
/// <param name="isLegacyDragBar">是否为经典标题栏区域</param>
|
||||
public WindowSubclassManager(TWindow window, HWND hwnd, bool isLegacyDragBar)
|
||||
/// <param name="options">选项</param>
|
||||
public WindowSubclass(WindowOptions<TWindow> options)
|
||||
{
|
||||
this.window = window;
|
||||
this.hwnd = hwnd;
|
||||
this.isLegacyDragBar = isLegacyDragBar;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试设置窗体子类
|
||||
/// </summary>
|
||||
/// <returns>是否设置成功</returns>
|
||||
public unsafe bool TrySetWindowSubclass()
|
||||
public bool Initialize()
|
||||
{
|
||||
windowProc = new(OnSubclassProcedure);
|
||||
bool windowHooked = SetWindowSubclass(hwnd, windowProc, WindowSubclassId, 0);
|
||||
bool windowHooked = SetWindowSubclass(options.Hwnd, windowProc, WindowSubclassId, 0);
|
||||
|
||||
bool titleBarHooked = true;
|
||||
|
||||
// only hook up drag bar proc when not use legacy Window.ExtendsContentIntoTitleBar
|
||||
if (isLegacyDragBar)
|
||||
// only hook up drag bar proc when use legacy Window.ExtendsContentIntoTitleBar
|
||||
if (options.UseLegacyDragBarImplementation)
|
||||
{
|
||||
titleBarHooked = false;
|
||||
hwndDragBar = FindWindowEx(hwnd, default, "DRAG_BAR_WINDOW_CLASS", string.Empty);
|
||||
HWND hwndDragBar = FindWindowEx(options.Hwnd, default, "DRAG_BAR_WINDOW_CLASS", default);
|
||||
|
||||
if (!hwndDragBar.IsNull)
|
||||
{
|
||||
@@ -72,14 +65,14 @@ internal sealed class WindowSubclassManager<TWindow> : IDisposable
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
RemoveWindowSubclass(hwnd, windowProc, WindowSubclassId);
|
||||
if (isLegacyDragBar)
|
||||
{
|
||||
RemoveWindowSubclass(hwnd, dragBarProc, DragBarSubclassId);
|
||||
}
|
||||
|
||||
RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId);
|
||||
windowProc = null;
|
||||
dragBarProc = null;
|
||||
|
||||
if (options.UseLegacyDragBarImplementation)
|
||||
{
|
||||
RemoveWindowSubclass(options.Hwnd, dragBarProc, DragBarSubclassId);
|
||||
dragBarProc = null;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe LRESULT OnSubclassProcedure(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam, nuint uIdSubclass, nuint dwRefData)
|
||||
@@ -89,14 +82,14 @@ internal sealed class WindowSubclassManager<TWindow> : IDisposable
|
||||
case WM_GETMINMAXINFO:
|
||||
{
|
||||
double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd);
|
||||
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
|
||||
options.Window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_NCRBUTTONDOWN:
|
||||
case WM_NCRBUTTONUP:
|
||||
{
|
||||
return new(0);
|
||||
return (LRESULT)0; // WM_NULL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +103,7 @@ internal sealed class WindowSubclassManager<TWindow> : IDisposable
|
||||
case WM_NCRBUTTONDOWN:
|
||||
case WM_NCRBUTTONUP:
|
||||
{
|
||||
return new(0);
|
||||
return (LRESULT)0; // WM_NULL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ internal static class MemoryCacheExtension
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>是否移除成功</returns>
|
||||
public static bool TryRemove(this IMemoryCache memoryCache, string key, [NotNullWhen(true)] out object? value)
|
||||
public static bool TryRemove(this IMemoryCache memoryCache, string key, out object? value)
|
||||
{
|
||||
if (memoryCache.TryGetValue(key, out value))
|
||||
{
|
||||
|
||||
23
src/Snap.Hutao/Snap.Hutao/Extension/ObjectExtension.cs
Normal file
23
src/Snap.Hutao/Snap.Hutao/Extension/ObjectExtension.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
/// <summary>
|
||||
/// 对象拓展
|
||||
/// </summary>
|
||||
internal static class ObjectExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 转换到只有1长度的数组
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型</typeparam>
|
||||
/// <param name="source">源</param>
|
||||
/// <returns>数组</returns>
|
||||
public static T[] ToArray<T>(this T source)
|
||||
{
|
||||
return new[] { source };
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
|
||||
"allowMarshaling": true,
|
||||
"public": true
|
||||
"public": true,
|
||||
"useSafeHandles": false
|
||||
}
|
||||
@@ -3,6 +3,7 @@ INFINITE
|
||||
WM_GETMINMAXINFO
|
||||
WM_NCRBUTTONDOWN
|
||||
WM_NCRBUTTONUP
|
||||
WM_NULL
|
||||
|
||||
// Type & Enum definition
|
||||
CWMO_FLAGS
|
||||
|
||||
@@ -64,7 +64,20 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
memory = new byte[entry.modBaseSize];
|
||||
fixed (byte* lpBuffer = memory)
|
||||
{
|
||||
return ReadProcessMemory(process.SafeHandle, entry.modBaseAddr, lpBuffer, entry.modBaseSize, null);
|
||||
bool hProcessAddRef = false;
|
||||
try
|
||||
{
|
||||
process.SafeHandle.DangerousAddRef(ref hProcessAddRef);
|
||||
HANDLE hProcessLocal = (HANDLE)process.SafeHandle.DangerousGetHandle();
|
||||
return ReadProcessMemory(hProcessLocal, entry.modBaseAddr, lpBuffer, entry.modBaseSize, null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (hProcessAddRef)
|
||||
{
|
||||
process.SafeHandle.DangerousRelease();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +85,20 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||
{
|
||||
int* lpBuffer = &write;
|
||||
|
||||
return WriteProcessMemory(process.SafeHandle, (void*)baseAddress, lpBuffer, sizeof(int), null);
|
||||
bool hProcessAddRef = false;
|
||||
try
|
||||
{
|
||||
process.SafeHandle.DangerousAddRef(ref hProcessAddRef);
|
||||
HANDLE hProcessLocal = (HANDLE)process.SafeHandle.DangerousGetHandle();
|
||||
return WriteProcessMemory(hProcessLocal, (void*)baseAddress, lpBuffer, sizeof(int), null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (hProcessAddRef)
|
||||
{
|
||||
process.SafeHandle.DangerousRelease();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe MODULEENTRY32 UnsafeFindModule(int processId, ReadOnlySpan<byte> moduleName)
|
||||
|
||||
@@ -35,15 +35,16 @@
|
||||
<ItemsControl Margin="0,0,0,32" ItemsSource="{Binding DownloadSummaries}">
|
||||
<ItemsControl.ItemContainerTransitions>
|
||||
<TransitionCollection>
|
||||
<EntranceThemeTransition IsStaggeringEnabled="False"/>
|
||||
<AddDeleteThemeTransition/>
|
||||
<RepositionThemeTransition IsStaggeringEnabled="False"/>
|
||||
<ContentThemeTransition/>
|
||||
<ReorderThemeTransition/>
|
||||
<EntranceThemeTransition IsStaggeringEnabled="False"/>
|
||||
</TransitionCollection>
|
||||
</ItemsControl.ItemContainerTransitions>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Style="{StaticResource BorderCardStyle}">
|
||||
<StackPanel Margin="0,8,0,0">
|
||||
<Border Margin="0,4,0,0" Style="{StaticResource BorderCardStyle}">
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="{Binding DisplayName}"/>
|
||||
<ProgressBar
|
||||
Width="240"
|
||||
|
||||
@@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI.Notifications;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -56,8 +57,10 @@ internal sealed class WelcomeViewModel : ObservableObject
|
||||
|
||||
await Parallel.ForEachAsync(downloadSummaries, async (summary, token) =>
|
||||
{
|
||||
await summary.DownloadAndExtractAsync().ConfigureAwait(false);
|
||||
ThreadHelper.InvokeOnMainThread(() => DownloadSummaries.Remove(summary));
|
||||
if (await summary.DownloadAndExtractAsync().ConfigureAwait(false))
|
||||
{
|
||||
ThreadHelper.InvokeOnMainThread(() => DownloadSummaries.Remove(summary));
|
||||
}
|
||||
}).ConfigureAwait(true);
|
||||
|
||||
serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
|
||||
@@ -128,6 +131,7 @@ internal sealed class WelcomeViewModel : ObservableObject
|
||||
private readonly Progress<StreamCopyState> progress;
|
||||
private string description = SH.ViewModelWelcomeDownloadSummaryDefault;
|
||||
private double progressValue;
|
||||
private long updateCount;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的下载信息
|
||||
@@ -137,6 +141,8 @@ internal sealed class WelcomeViewModel : ObservableObject
|
||||
public DownloadSummary(IServiceProvider serviceProvider, string fileName)
|
||||
{
|
||||
httpClient = serviceProvider.GetRequiredService<HttpClient>();
|
||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Core.CoreEnvironment.CommonUA);
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
|
||||
DisplayName = fileName;
|
||||
@@ -171,7 +177,7 @@ internal sealed class WelcomeViewModel : ObservableObject
|
||||
/// 异步下载并解压
|
||||
/// </summary>
|
||||
/// <returns>任务</returns>
|
||||
public async Task DownloadAndExtractAsync()
|
||||
public async Task<bool> DownloadAndExtractAsync()
|
||||
{
|
||||
ILogger<DownloadSummary> logger = serviceProvider.GetRequiredService<ILogger<DownloadSummary>>();
|
||||
try
|
||||
@@ -189,6 +195,7 @@ internal sealed class WelcomeViewModel : ObservableObject
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
ProgressValue = 1;
|
||||
Description = SH.ViewModelWelcomeDownloadSummaryComplete;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,13 +204,17 @@ internal sealed class WelcomeViewModel : ObservableObject
|
||||
logger.LogError(ex, "Download Static Zip failed");
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
Description = SH.ViewModelWelcomeDownloadSummaryException;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateProgressStatus(StreamCopyState status)
|
||||
{
|
||||
Description = $"{Converters.ToFileSizeString(status.BytesCopied)}/{Converters.ToFileSizeString(status.TotalBytes)}";
|
||||
ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesCopied / status.TotalBytes;
|
||||
if (Interlocked.Increment(ref updateCount) % 40 == 0)
|
||||
{
|
||||
Description = $"{Converters.ToFileSizeString(status.BytesCopied)}/{Converters.ToFileSizeString(status.TotalBytes)}";
|
||||
ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesCopied / status.TotalBytes;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractFiles(Stream stream)
|
||||
|
||||
75
src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/EndIds.cs
Normal file
75
src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/EndIds.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao.GachaLog;
|
||||
|
||||
/// <summary>
|
||||
/// 末尾Id 字典
|
||||
/// </summary>
|
||||
internal sealed class EndIds
|
||||
{
|
||||
/// <summary>
|
||||
/// 新手祈愿
|
||||
/// </summary>
|
||||
[JsonPropertyName("100")]
|
||||
public long NoviceWish { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 常驻祈愿
|
||||
/// </summary>
|
||||
[JsonPropertyName("200")]
|
||||
public long StandardWish { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色活动祈愿
|
||||
/// </summary>
|
||||
[JsonPropertyName("301")]
|
||||
public long AvatarEventWish { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 武器活动祈愿
|
||||
/// </summary>
|
||||
[JsonPropertyName("302")]
|
||||
public long WeaponEventWish { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Last Id
|
||||
/// </summary>
|
||||
/// <param name="type">类型</param>
|
||||
/// <returns>Last Id</returns>
|
||||
public long this[GachaConfigType type]
|
||||
{
|
||||
get
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
GachaConfigType.NoviceWish => NoviceWish,
|
||||
GachaConfigType.StandardWish => StandardWish,
|
||||
GachaConfigType.AvatarEventWish => AvatarEventWish,
|
||||
GachaConfigType.WeaponEventWish => WeaponEventWish,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case GachaConfigType.NoviceWish:
|
||||
NoviceWish = value;
|
||||
break;
|
||||
case GachaConfigType.StandardWish:
|
||||
StandardWish = value;
|
||||
break;
|
||||
case GachaConfigType.AvatarEventWish:
|
||||
AvatarEventWish = value;
|
||||
break;
|
||||
case GachaConfigType.WeaponEventWish:
|
||||
WeaponEventWish = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/GachaItem.cs
Normal file
39
src/Snap.Hutao/Snap.Hutao/Web/Hutao/GachaLog/GachaItem.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao.GachaLog;
|
||||
|
||||
/// <summary>
|
||||
/// 服务器接口使用的祈愿记录物品
|
||||
/// </summary>
|
||||
internal sealed class GachaItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 祈愿记录分类
|
||||
/// </summary>
|
||||
public GachaConfigType GachaType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿记录查询分类
|
||||
/// 合并保底的卡池使用此属性
|
||||
/// 仅4种(不含400)
|
||||
/// </summary>
|
||||
public GachaConfigType QueryType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 物品Id
|
||||
/// </summary>
|
||||
public int ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取时间
|
||||
/// </summary>
|
||||
public DateTimeOffset Time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
}
|
||||
145
src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaGachaLogClient.cs
Normal file
145
src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaGachaLogClient.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Service.Hutao;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
using Snap.Hutao.Web.Hutao.GachaLog;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃祈愿记录API客户端
|
||||
/// </summary>
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class HomaGachaLogClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<HomaGachaLogClient> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃祈愿记录API客户端
|
||||
/// </summary>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public HomaGachaLogClient(HttpClient httpClient, IServiceProvider serviceProvider)
|
||||
{
|
||||
options = serviceProvider.GetRequiredService<JsonSerializerOptions>();
|
||||
logger = serviceProvider.GetRequiredService<ILogger<HomaGachaLogClient>>();
|
||||
|
||||
this.httpClient = httpClient;
|
||||
|
||||
HutaoUserOptions hutaoUserOptions = serviceProvider.GetRequiredService<HutaoUserOptions>();
|
||||
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", hutaoUserOptions.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取 Uid 列表
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>Uid 列表</returns>
|
||||
public async Task<Response<List<string>>> GetUidsAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<List<string>>? resp = await httpClient
|
||||
.TryCatchGetFromJsonAsync<Response<List<string>>>(HutaoEndpoints.GachaLogUids, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取末尾 Id
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>末尾Id</returns>
|
||||
public async Task<Response<EndIds>> GetEndIdsAsync(PlayerUid uid, CancellationToken token = default)
|
||||
{
|
||||
Response<EndIds>? resp = await httpClient
|
||||
.TryCatchGetFromJsonAsync<Response<EndIds>>(HutaoEndpoints.GachaLogEndIds(uid.Value), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取云端祈愿记录
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="endIds">末尾 Id</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>云端祈愿记录</returns>
|
||||
public async Task<Response<List<GachaItem>>> RetrieveGachaItemsAsync(PlayerUid uid, EndIds endIds, CancellationToken token = default)
|
||||
{
|
||||
UidAndEndIds uidAndEndIds = new(uid, endIds);
|
||||
|
||||
Response<List<GachaItem>>? resp = await httpClient
|
||||
.TryCatchPostAsJsonAsync<UidAndEndIds, Response<List<GachaItem>>>(HutaoEndpoints.GachaLogRetrieve, uidAndEndIds, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步上传祈愿记录物品
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="gachaItems">祈愿记录</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>响应</returns>
|
||||
public async Task<Response.Response> UploadGachaItemsAsync(PlayerUid uid, List<GachaItem> gachaItems, CancellationToken token = default)
|
||||
{
|
||||
UidAndItems uidAndItems = new(uid, gachaItems);
|
||||
|
||||
Response.Response? resp = await httpClient
|
||||
.TryCatchPostAsJsonAsync<UidAndItems, Response<List<GachaItem>>>(HutaoEndpoints.GachaLogUpload, uidAndItems, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步删除祈愿记录
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>响应</returns>
|
||||
public async Task<Response.Response> DeleteGachaItemsAsync(PlayerUid uid, CancellationToken token = default)
|
||||
{
|
||||
Response.Response? resp = await httpClient
|
||||
.TryCatchGetFromJsonAsync<Response<List<GachaItem>>>(HutaoEndpoints.GachaLogDelete(uid), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
private sealed class UidAndEndIds
|
||||
{
|
||||
public UidAndEndIds(PlayerUid uid, EndIds endIds)
|
||||
{
|
||||
Uid = uid.Value;
|
||||
EndIds = endIds;
|
||||
}
|
||||
|
||||
public string Uid { get; }
|
||||
|
||||
public EndIds EndIds { get; }
|
||||
}
|
||||
|
||||
private sealed class UidAndItems
|
||||
{
|
||||
public UidAndItems(PlayerUid uid, List<GachaItem> gachaItems)
|
||||
{
|
||||
Uid = uid.Value;
|
||||
Items = gachaItems;
|
||||
}
|
||||
|
||||
public string Uid { get; set; } = default!;
|
||||
|
||||
public List<GachaItem> Items { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
namespace Snap.Hutao.Web;
|
||||
|
||||
/// <summary>
|
||||
@@ -16,11 +18,48 @@ internal static class HutaoEndpoints
|
||||
/// </summary>
|
||||
public const string StaticHutao = "static.hut.ao";
|
||||
|
||||
#region Passport
|
||||
#region GachaLog
|
||||
|
||||
/// <summary>
|
||||
/// 获取注册验证码
|
||||
/// 获取末尾Id
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>获取末尾Id Url</returns>
|
||||
public static string GachaLogEndIds(PlayerUid uid)
|
||||
{
|
||||
return $"{HomaSnapGenshinApi}/GachaLog/EndIds?Uid={uid.Value}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取祈愿记录
|
||||
/// </summary>
|
||||
public const string GachaLogRetrieve = $"{HomaSnapGenshinApi}/GachaLog/Retrieve";
|
||||
|
||||
/// <summary>
|
||||
/// 上传祈愿记录
|
||||
/// </summary>
|
||||
public const string GachaLogUpload = $"{HomaSnapGenshinApi}/GachaLog/Upload";
|
||||
|
||||
/// <summary>
|
||||
/// 获取Uid列表
|
||||
/// </summary>
|
||||
public const string GachaLogUids = $"{HomaSnapGenshinApi}/GachaLog/Uids";
|
||||
|
||||
/// <summary>
|
||||
/// 删除祈愿记录
|
||||
/// </summary>
|
||||
/// <returns>删除祈愿记录 Url</returns>
|
||||
public static string GachaLogDelete(PlayerUid uid)
|
||||
{
|
||||
return $"{HomaSnapGenshinApi}/GachaLog/Delete?Uid={uid.Value}";
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Passport
|
||||
|
||||
/// <summary>
|
||||
/// 获取注册验证码
|
||||
/// </summary>
|
||||
public const string PassportVerify = $"{HomaSnapGenshinApi}/Passport/Verify";
|
||||
|
||||
/// <summary>
|
||||
@@ -121,14 +160,6 @@ internal static class HutaoEndpoints
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Patcher
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃检查更新
|
||||
/// </summary>
|
||||
public const string PatcherHutaoStable = $"{PatcherDGPStudioApi}/hutao/stable";
|
||||
#endregion
|
||||
|
||||
#region Static & Zip
|
||||
|
||||
/// <summary>
|
||||
@@ -170,7 +201,6 @@ internal static class HutaoEndpoints
|
||||
|
||||
private const string HomaSnapGenshinApi = "https://homa.snapgenshin.com";
|
||||
private const string HutaoMetadataSnapGenshinApi = "https://hutao-metadata.snapgenshin.com";
|
||||
private const string PatcherDGPStudioApi = "https://patcher.dgp-studio.cn";
|
||||
private const string StaticSnapGenshinApi = "https://static.snapgenshin.com";
|
||||
private const string StaticZipSnapGenshinApi = "https://static-zip.snapgenshin.com";
|
||||
}
|
||||
@@ -32,6 +32,17 @@ internal static class StructExtension
|
||||
return new((int)(rectInt32.X * scale), (int)(rectInt32.Y * scale), (int)(rectInt32.Width * scale), (int)(rectInt32.Height * scale));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比例缩放
|
||||
/// </summary>
|
||||
/// <param name="sizeInt32">源</param>
|
||||
/// <param name="scale">比例</param>
|
||||
/// <returns>结果</returns>
|
||||
public static SizeInt32 Scale(this SizeInt32 sizeInt32, double scale)
|
||||
{
|
||||
return new((int)(sizeInt32.Width * scale), (int)(sizeInt32.Height * scale));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尺寸
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user