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
|
public class DependencyInjectionTest
|
||||||
{
|
{
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void OriginalTypeDiscoverable()
|
public void OriginalTypeNotDiscoverable()
|
||||||
{
|
{
|
||||||
IServiceProvider services = new ServiceCollection()
|
IServiceProvider services = new ServiceCollection()
|
||||||
.AddSingleton<IService, ServiceA>()
|
.AddSingleton<IService, ServiceA>()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<IsTestProject>true</IsTestProject>
|
<IsTestProject>true</IsTestProject>
|
||||||
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ using Snap.Hutao.Win32;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using Windows.Graphics;
|
using Windows.Graphics;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
using Windows.Win32.Foundation;
|
|
||||||
using WinRT.Interop;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.Windowing;
|
namespace Snap.Hutao.Core.Windowing;
|
||||||
|
|
||||||
@@ -23,34 +21,22 @@ namespace Snap.Hutao.Core.Windowing;
|
|||||||
internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMessage>, IRecipient<FlyoutOpenCloseMessage>
|
internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMessage>, IRecipient<FlyoutOpenCloseMessage>
|
||||||
where TWindow : Window, IExtendedWindowSource
|
where TWindow : Window, IExtendedWindowSource
|
||||||
{
|
{
|
||||||
private readonly HWND hwnd;
|
private readonly WindowOptions<TWindow> options;
|
||||||
private readonly AppWindow appWindow;
|
|
||||||
|
|
||||||
private readonly TWindow window;
|
|
||||||
private readonly FrameworkElement titleBar;
|
|
||||||
|
|
||||||
private readonly IServiceProvider serviceProvider;
|
private readonly IServiceProvider serviceProvider;
|
||||||
private readonly ILogger<ExtendedWindow<TWindow>> logger;
|
private readonly ILogger<ExtendedWindow<TWindow>> logger;
|
||||||
private readonly WindowSubclassManager<TWindow> subclassManager;
|
private readonly WindowSubclass<TWindow> subclass;
|
||||||
|
|
||||||
private readonly bool useLegacyDragBar;
|
|
||||||
|
|
||||||
private SystemBackdrop? systemBackdrop;
|
private SystemBackdrop? systemBackdrop;
|
||||||
|
|
||||||
private ExtendedWindow(TWindow window, FrameworkElement titleBar, IServiceProvider serviceProvider)
|
private ExtendedWindow(TWindow window, FrameworkElement titleBar, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
this.window = window;
|
options = new(window, titleBar);
|
||||||
this.titleBar = titleBar;
|
subclass = new(options);
|
||||||
|
|
||||||
logger = serviceProvider.GetRequiredService<ILogger<ExtendedWindow<TWindow>>>();
|
logger = serviceProvider.GetRequiredService<ILogger<ExtendedWindow<TWindow>>>();
|
||||||
this.serviceProvider = serviceProvider;
|
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();
|
InitializeWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,63 +65,63 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Receive(FlyoutOpenCloseMessage message)
|
public void Receive(FlyoutOpenCloseMessage message)
|
||||||
{
|
{
|
||||||
UpdateDragRectangles(appWindow.TitleBar, message.IsOpen);
|
UpdateDragRectangles(options.AppWindow.TitleBar, message.IsOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeWindow()
|
private void InitializeWindow()
|
||||||
{
|
{
|
||||||
appWindow.Title = string.Format(SH.AppNameAndVersion, CoreEnvironment.Version);
|
options.AppWindow.Title = string.Format(SH.AppNameAndVersion, CoreEnvironment.Version);
|
||||||
appWindow.SetIcon(Path.Combine(CoreEnvironment.InstalledLocation, "Assets/Logo.ico"));
|
options.AppWindow.SetIcon(Path.Combine(CoreEnvironment.InstalledLocation, "Assets/Logo.ico"));
|
||||||
ExtendsContentIntoTitleBar();
|
ExtendsContentIntoTitleBar();
|
||||||
|
|
||||||
Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize);
|
Persistence.RecoverOrInit(options);
|
||||||
|
|
||||||
// appWindow.Show(true);
|
// appWindow.Show(true);
|
||||||
// appWindow.Show can't bring window to top.
|
// appWindow.Show can't bring window to top.
|
||||||
window.Activate();
|
options.Window.Activate();
|
||||||
|
|
||||||
systemBackdrop = new(window);
|
systemBackdrop = new(options.Window);
|
||||||
bool micaApplied = systemBackdrop.TryApply();
|
bool micaApplied = systemBackdrop.TryApply();
|
||||||
logger.LogInformation("Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
|
logger.LogInformation("Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
|
||||||
|
|
||||||
bool subClassApplied = subclassManager.TrySetWindowSubclass();
|
bool subClassApplied = subclass.Initialize();
|
||||||
logger.LogInformation("Apply {name} : {result}", nameof(WindowSubclassManager<TWindow>), subClassApplied ? "succeed" : "failed");
|
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<BackdropTypeChangedMessage>(this);
|
||||||
messenger.Register<FlyoutOpenCloseMessage>(this);
|
messenger.Register<FlyoutOpenCloseMessage>(this);
|
||||||
|
|
||||||
window.Closed += OnWindowClosed;
|
options.Window.Closed += OnWindowClosed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWindowClosed(object sender, WindowEventArgs args)
|
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()
|
private void ExtendsContentIntoTitleBar()
|
||||||
{
|
{
|
||||||
if (useLegacyDragBar)
|
if (options.UseLegacyDragBarImplementation)
|
||||||
{
|
{
|
||||||
// use normal Window method to extend.
|
// use normal Window method to extend.
|
||||||
window.ExtendsContentIntoTitleBar = true;
|
options.Window.ExtendsContentIntoTitleBar = true;
|
||||||
window.SetTitleBar(titleBar);
|
options.Window.SetTitleBar(options.TitleBar);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AppWindowTitleBar appTitleBar = appWindow.TitleBar;
|
AppWindowTitleBar appTitleBar = options.AppWindow.TitleBar;
|
||||||
appTitleBar.IconShowOptions = IconShowOptions.HideIconAndSystemMenu;
|
appTitleBar.IconShowOptions = IconShowOptions.HideIconAndSystemMenu;
|
||||||
appTitleBar.ExtendsContentIntoTitleBar = true;
|
appTitleBar.ExtendsContentIntoTitleBar = true;
|
||||||
|
|
||||||
UpdateTitleButtonColor(appTitleBar);
|
UpdateTitleButtonColor(appTitleBar);
|
||||||
UpdateDragRectangles(appTitleBar);
|
UpdateDragRectangles(appTitleBar);
|
||||||
titleBar.ActualThemeChanged += (s, e) => UpdateTitleButtonColor(appTitleBar);
|
options.TitleBar.ActualThemeChanged += (s, e) => UpdateTitleButtonColor(appTitleBar);
|
||||||
titleBar.SizeChanged += (s, e) => UpdateDragRectangles(appTitleBar);
|
options.TitleBar.SizeChanged += (s, e) => UpdateDragRectangles(appTitleBar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,20 +154,20 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
|
|||||||
if (isFlyoutOpened)
|
if (isFlyoutOpened)
|
||||||
{
|
{
|
||||||
// set to 0
|
// set to 0
|
||||||
appTitleBar.SetDragRectangles(default(RectInt32).Enumerate().ToArray());
|
appTitleBar.SetDragRectangles(default(RectInt32).ToArray());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
double scale = Persistence.GetScaleForWindowHandle(hwnd);
|
double scale = Persistence.GetScaleForWindowHandle(options.Hwnd);
|
||||||
|
|
||||||
// 48 is the navigation button leftInset
|
// 48 is the navigation button leftInset
|
||||||
RectInt32 dragRect = StructMarshal.RectInt32(new(48, 0), titleBar.ActualSize).Scale(scale);
|
RectInt32 dragRect = StructMarshal.RectInt32(new(48, 0), options.TitleBar.ActualSize).Scale(scale);
|
||||||
appTitleBar.SetDragRectangles(dragRect.Enumerate().ToArray());
|
appTitleBar.SetDragRectangles(dragRect.ToArray());
|
||||||
|
|
||||||
// workaround for https://github.com/microsoft/WindowsAppSDK/issues/2976
|
// workaround for https://github.com/microsoft/WindowsAppSDK/issues/2976
|
||||||
SizeInt32 size = appWindow.ClientSize;
|
SizeInt32 size = options.AppWindow.ClientSize;
|
||||||
size.Height -= (int)(31 * scale);
|
size.Height -= (int)(31 * scale);
|
||||||
appWindow.ResizeClient(size);
|
options.AppWindow.ResizeClient(size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Microsoft.UI;
|
|
||||||
using Microsoft.UI.Windowing;
|
using Microsoft.UI.Windowing;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
using Snap.Hutao.Core.Setting;
|
using Snap.Hutao.Core.Setting;
|
||||||
using Snap.Hutao.Win32;
|
using Snap.Hutao.Win32;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Windows.Graphics;
|
using Windows.Graphics;
|
||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
using Windows.Win32.UI.WindowsAndMessaging;
|
using Windows.Win32.UI.WindowsAndMessaging;
|
||||||
@@ -22,43 +22,44 @@ internal static class Persistence
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置窗体位置
|
/// 设置窗体位置
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="appWindow">应用窗体</param>
|
/// <param name="options">选项</param>
|
||||||
/// <param name="persistSize">持久化尺寸</param>
|
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||||
/// <param name="initialSize">初始尺寸</param>
|
public static void RecoverOrInit<TWindow>(WindowOptions<TWindow> options)
|
||||||
public static unsafe void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 initialSize)
|
where TWindow : Window, IExtendedWindowSource
|
||||||
{
|
{
|
||||||
// Set first launch size.
|
// Set first launch size.
|
||||||
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
|
double scale = GetScaleForWindowHandle(options.Hwnd);
|
||||||
SizeInt32 transformedSize = TransformSizeForWindow(initialSize, hwnd);
|
SizeInt32 transformedSize = options.Window.InitSize.Scale(scale);
|
||||||
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
|
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
|
||||||
|
|
||||||
if (persistSize)
|
if (options.Window.PersistSize)
|
||||||
{
|
{
|
||||||
RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||||
if (persistedRect.Size() >= initialSize.Size())
|
if (persistedRect.Size() >= options.Window.InitSize.Size())
|
||||||
{
|
{
|
||||||
rect = persistedRect;
|
rect = persistedRect;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TransformToCenterScreen(ref rect);
|
TransformToCenterScreen(ref rect);
|
||||||
appWindow.MoveAndResize(rect);
|
options.AppWindow.MoveAndResize(rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 保存状态的位置
|
/// 保存窗体的位置
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="appWindow">应用窗体</param>
|
/// <param name="options">选项</param>
|
||||||
public static void Save(AppWindow appWindow)
|
/// <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();
|
WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT();
|
||||||
GetWindowPlacement(hwnd, ref windowPlacement);
|
GetWindowPlacement(options.Hwnd, ref windowPlacement);
|
||||||
|
|
||||||
// prevent save value when we are maximized.
|
// prevent save value when we are maximized.
|
||||||
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
|
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)
|
public static double GetScaleForWindowHandle(HWND hwnd)
|
||||||
{
|
{
|
||||||
uint dpi = GetDpiForWindow(hwnd);
|
uint dpi = GetDpiForWindow(hwnd);
|
||||||
return Math.Round(dpi / 96d, 2, MidpointRounding.AwayFromZero);
|
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TransformToCenterScreen(ref RectInt32 rect)
|
private static void TransformToCenterScreen(ref RectInt32 rect)
|
||||||
@@ -88,60 +83,41 @@ internal static class Persistence
|
|||||||
rect.Y = workAreaRect.Y + ((workAreaRect.Height - rect.Height) / 2);
|
rect.Y = workAreaRect.Y + ((workAreaRect.Height - rect.Height) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Explicit)]
|
|
||||||
private struct CompactRect
|
private struct CompactRect
|
||||||
{
|
{
|
||||||
[FieldOffset(0)]
|
|
||||||
public short X;
|
public short X;
|
||||||
|
|
||||||
[FieldOffset(2)]
|
|
||||||
public short Y;
|
public short Y;
|
||||||
|
|
||||||
[FieldOffset(4)]
|
|
||||||
public short Width;
|
public short Width;
|
||||||
|
|
||||||
[FieldOffset(6)]
|
|
||||||
public short Height;
|
public short Height;
|
||||||
|
|
||||||
[FieldOffset(0)]
|
|
||||||
public ulong Value;
|
|
||||||
|
|
||||||
private CompactRect(int x, int y, int width, int height)
|
private CompactRect(int x, int y, int width, int height)
|
||||||
{
|
{
|
||||||
Value = 0;
|
|
||||||
X = (short)x;
|
X = (short)x;
|
||||||
Y = (short)y;
|
Y = (short)y;
|
||||||
Width = (short)width;
|
Width = (short)width;
|
||||||
Height = (short)height;
|
Height = (short)height;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompactRect(ulong value)
|
|
||||||
{
|
|
||||||
X = 0;
|
|
||||||
Y = 0;
|
|
||||||
Width = 0;
|
|
||||||
Height = 0;
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator RectInt32(CompactRect rect)
|
public static implicit operator RectInt32(CompactRect rect)
|
||||||
{
|
{
|
||||||
return new(rect.X, rect.Y, rect.Width, rect.Height);
|
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)
|
public static explicit operator CompactRect(RectInt32 rect)
|
||||||
{
|
{
|
||||||
return new(rect.X, rect.Y, rect.Width, rect.Height);
|
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.Core.Database;
|
||||||
using Snap.Hutao.Model.Entity;
|
using Snap.Hutao.Model.Entity;
|
||||||
using Snap.Hutao.Model.Entity.Database;
|
using Snap.Hutao.Model.Entity.Database;
|
||||||
|
using Snap.Hutao.Service;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Windows.System;
|
using Windows.System;
|
||||||
using WinRT;
|
using WinRT;
|
||||||
@@ -34,9 +35,7 @@ internal sealed class SystemBackdrop
|
|||||||
this.window = window;
|
this.window = window;
|
||||||
using (IServiceScope scope = Ioc.Default.CreateScope())
|
using (IServiceScope scope = Ioc.Default.CreateScope())
|
||||||
{
|
{
|
||||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
BackdropType = scope.ServiceProvider.GetRequiredService<AppOptions>().BackdropType;
|
||||||
SettingEntry entry = appDbContext.Settings.SingleOrAdd(SettingEntry.SystemBackdropType, BackdropType.Mica.ToString());
|
|
||||||
BackdropType = Enum.Parse<BackdropType>(entry.Value!);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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>
|
/// </summary>
|
||||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||||
[HighQuality]
|
[HighQuality]
|
||||||
internal sealed class WindowSubclassManager<TWindow> : IDisposable
|
internal sealed class WindowSubclass<TWindow> : IDisposable
|
||||||
where TWindow : Window, IExtendedWindowSource
|
where TWindow : Window, IExtendedWindowSource
|
||||||
{
|
{
|
||||||
private const int WindowSubclassId = 101;
|
private const int WindowSubclassId = 101;
|
||||||
private const int DragBarSubclassId = 102;
|
private const int DragBarSubclassId = 102;
|
||||||
|
|
||||||
private readonly TWindow window;
|
private readonly WindowOptions<TWindow> options;
|
||||||
private readonly HWND hwnd;
|
|
||||||
private readonly bool isLegacyDragBar;
|
|
||||||
private HWND hwndDragBar;
|
|
||||||
|
|
||||||
// We have to explicitly hold a reference to SUBCLASSPROC
|
// We have to explicitly hold a reference to SUBCLASSPROC
|
||||||
private SUBCLASSPROC? windowProc;
|
private SUBCLASSPROC? windowProc;
|
||||||
@@ -32,32 +29,28 @@ internal sealed class WindowSubclassManager<TWindow> : IDisposable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造一个新的窗体子类管理器
|
/// 构造一个新的窗体子类管理器
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="window">窗体实例</param>
|
/// <param name="options">选项</param>
|
||||||
/// <param name="hwnd">窗体句柄</param>
|
public WindowSubclass(WindowOptions<TWindow> options)
|
||||||
/// <param name="isLegacyDragBar">是否为经典标题栏区域</param>
|
|
||||||
public WindowSubclassManager(TWindow window, HWND hwnd, bool isLegacyDragBar)
|
|
||||||
{
|
{
|
||||||
this.window = window;
|
this.options = options;
|
||||||
this.hwnd = hwnd;
|
|
||||||
this.isLegacyDragBar = isLegacyDragBar;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 尝试设置窗体子类
|
/// 尝试设置窗体子类
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>是否设置成功</returns>
|
/// <returns>是否设置成功</returns>
|
||||||
public unsafe bool TrySetWindowSubclass()
|
public bool Initialize()
|
||||||
{
|
{
|
||||||
windowProc = new(OnSubclassProcedure);
|
windowProc = new(OnSubclassProcedure);
|
||||||
bool windowHooked = SetWindowSubclass(hwnd, windowProc, WindowSubclassId, 0);
|
bool windowHooked = SetWindowSubclass(options.Hwnd, windowProc, WindowSubclassId, 0);
|
||||||
|
|
||||||
bool titleBarHooked = true;
|
bool titleBarHooked = true;
|
||||||
|
|
||||||
// only hook up drag bar proc when not use legacy Window.ExtendsContentIntoTitleBar
|
// only hook up drag bar proc when use legacy Window.ExtendsContentIntoTitleBar
|
||||||
if (isLegacyDragBar)
|
if (options.UseLegacyDragBarImplementation)
|
||||||
{
|
{
|
||||||
titleBarHooked = false;
|
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)
|
if (!hwndDragBar.IsNull)
|
||||||
{
|
{
|
||||||
@@ -72,14 +65,14 @@ internal sealed class WindowSubclassManager<TWindow> : IDisposable
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
RemoveWindowSubclass(hwnd, windowProc, WindowSubclassId);
|
RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId);
|
||||||
if (isLegacyDragBar)
|
|
||||||
{
|
|
||||||
RemoveWindowSubclass(hwnd, dragBarProc, DragBarSubclassId);
|
|
||||||
}
|
|
||||||
|
|
||||||
windowProc = null;
|
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)
|
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:
|
case WM_GETMINMAXINFO:
|
||||||
{
|
{
|
||||||
double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd);
|
double scalingFactor = Persistence.GetScaleForWindowHandle(hwnd);
|
||||||
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
|
options.Window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case WM_NCRBUTTONDOWN:
|
case WM_NCRBUTTONDOWN:
|
||||||
case WM_NCRBUTTONUP:
|
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_NCRBUTTONDOWN:
|
||||||
case WM_NCRBUTTONUP:
|
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="key">键</param>
|
||||||
/// <param name="value">值</param>
|
/// <param name="value">值</param>
|
||||||
/// <returns>是否移除成功</returns>
|
/// <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))
|
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,
|
"allowMarshaling": true,
|
||||||
"public": true
|
"public": true,
|
||||||
|
"useSafeHandles": false
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ INFINITE
|
|||||||
WM_GETMINMAXINFO
|
WM_GETMINMAXINFO
|
||||||
WM_NCRBUTTONDOWN
|
WM_NCRBUTTONDOWN
|
||||||
WM_NCRBUTTONUP
|
WM_NCRBUTTONUP
|
||||||
|
WM_NULL
|
||||||
|
|
||||||
// Type & Enum definition
|
// Type & Enum definition
|
||||||
CWMO_FLAGS
|
CWMO_FLAGS
|
||||||
|
|||||||
@@ -64,7 +64,20 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
|||||||
memory = new byte[entry.modBaseSize];
|
memory = new byte[entry.modBaseSize];
|
||||||
fixed (byte* lpBuffer = memory)
|
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;
|
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)
|
private static unsafe MODULEENTRY32 UnsafeFindModule(int processId, ReadOnlySpan<byte> moduleName)
|
||||||
|
|||||||
@@ -35,15 +35,16 @@
|
|||||||
<ItemsControl Margin="0,0,0,32" ItemsSource="{Binding DownloadSummaries}">
|
<ItemsControl Margin="0,0,0,32" ItemsSource="{Binding DownloadSummaries}">
|
||||||
<ItemsControl.ItemContainerTransitions>
|
<ItemsControl.ItemContainerTransitions>
|
||||||
<TransitionCollection>
|
<TransitionCollection>
|
||||||
<EntranceThemeTransition IsStaggeringEnabled="False"/>
|
|
||||||
<AddDeleteThemeTransition/>
|
<AddDeleteThemeTransition/>
|
||||||
<RepositionThemeTransition IsStaggeringEnabled="False"/>
|
<ContentThemeTransition/>
|
||||||
|
<ReorderThemeTransition/>
|
||||||
|
<EntranceThemeTransition IsStaggeringEnabled="False"/>
|
||||||
</TransitionCollection>
|
</TransitionCollection>
|
||||||
</ItemsControl.ItemContainerTransitions>
|
</ItemsControl.ItemContainerTransitions>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Border Style="{StaticResource BorderCardStyle}">
|
<Border Margin="0,4,0,0" Style="{StaticResource BorderCardStyle}">
|
||||||
<StackPanel Margin="0,8,0,0">
|
<StackPanel Margin="8">
|
||||||
<TextBlock Text="{Binding DisplayName}"/>
|
<TextBlock Text="{Binding DisplayName}"/>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
Width="240"
|
Width="240"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using CommunityToolkit.WinUI.Notifications;
|
using CommunityToolkit.WinUI.Notifications;
|
||||||
using Snap.Hutao.Core.Caching;
|
using Snap.Hutao.Core.Caching;
|
||||||
|
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||||
using Snap.Hutao.Core.IO;
|
using Snap.Hutao.Core.IO;
|
||||||
using Snap.Hutao.Core.Setting;
|
using Snap.Hutao.Core.Setting;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
@@ -56,8 +57,10 @@ internal sealed class WelcomeViewModel : ObservableObject
|
|||||||
|
|
||||||
await Parallel.ForEachAsync(downloadSummaries, async (summary, token) =>
|
await Parallel.ForEachAsync(downloadSummaries, async (summary, token) =>
|
||||||
{
|
{
|
||||||
await summary.DownloadAndExtractAsync().ConfigureAwait(false);
|
if (await summary.DownloadAndExtractAsync().ConfigureAwait(false))
|
||||||
ThreadHelper.InvokeOnMainThread(() => DownloadSummaries.Remove(summary));
|
{
|
||||||
|
ThreadHelper.InvokeOnMainThread(() => DownloadSummaries.Remove(summary));
|
||||||
|
}
|
||||||
}).ConfigureAwait(true);
|
}).ConfigureAwait(true);
|
||||||
|
|
||||||
serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
|
serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
|
||||||
@@ -128,6 +131,7 @@ internal sealed class WelcomeViewModel : ObservableObject
|
|||||||
private readonly Progress<StreamCopyState> progress;
|
private readonly Progress<StreamCopyState> progress;
|
||||||
private string description = SH.ViewModelWelcomeDownloadSummaryDefault;
|
private string description = SH.ViewModelWelcomeDownloadSummaryDefault;
|
||||||
private double progressValue;
|
private double progressValue;
|
||||||
|
private long updateCount;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造一个新的下载信息
|
/// 构造一个新的下载信息
|
||||||
@@ -137,6 +141,8 @@ internal sealed class WelcomeViewModel : ObservableObject
|
|||||||
public DownloadSummary(IServiceProvider serviceProvider, string fileName)
|
public DownloadSummary(IServiceProvider serviceProvider, string fileName)
|
||||||
{
|
{
|
||||||
httpClient = serviceProvider.GetRequiredService<HttpClient>();
|
httpClient = serviceProvider.GetRequiredService<HttpClient>();
|
||||||
|
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Core.CoreEnvironment.CommonUA);
|
||||||
|
|
||||||
this.serviceProvider = serviceProvider;
|
this.serviceProvider = serviceProvider;
|
||||||
|
|
||||||
DisplayName = fileName;
|
DisplayName = fileName;
|
||||||
@@ -171,7 +177,7 @@ internal sealed class WelcomeViewModel : ObservableObject
|
|||||||
/// 异步下载并解压
|
/// 异步下载并解压
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>任务</returns>
|
/// <returns>任务</returns>
|
||||||
public async Task DownloadAndExtractAsync()
|
public async Task<bool> DownloadAndExtractAsync()
|
||||||
{
|
{
|
||||||
ILogger<DownloadSummary> logger = serviceProvider.GetRequiredService<ILogger<DownloadSummary>>();
|
ILogger<DownloadSummary> logger = serviceProvider.GetRequiredService<ILogger<DownloadSummary>>();
|
||||||
try
|
try
|
||||||
@@ -189,6 +195,7 @@ internal sealed class WelcomeViewModel : ObservableObject
|
|||||||
await ThreadHelper.SwitchToMainThreadAsync();
|
await ThreadHelper.SwitchToMainThreadAsync();
|
||||||
ProgressValue = 1;
|
ProgressValue = 1;
|
||||||
Description = SH.ViewModelWelcomeDownloadSummaryComplete;
|
Description = SH.ViewModelWelcomeDownloadSummaryComplete;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,13 +204,17 @@ internal sealed class WelcomeViewModel : ObservableObject
|
|||||||
logger.LogError(ex, "Download Static Zip failed");
|
logger.LogError(ex, "Download Static Zip failed");
|
||||||
await ThreadHelper.SwitchToMainThreadAsync();
|
await ThreadHelper.SwitchToMainThreadAsync();
|
||||||
Description = SH.ViewModelWelcomeDownloadSummaryException;
|
Description = SH.ViewModelWelcomeDownloadSummaryException;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateProgressStatus(StreamCopyState status)
|
private void UpdateProgressStatus(StreamCopyState status)
|
||||||
{
|
{
|
||||||
Description = $"{Converters.ToFileSizeString(status.BytesCopied)}/{Converters.ToFileSizeString(status.TotalBytes)}";
|
if (Interlocked.Increment(ref updateCount) % 40 == 0)
|
||||||
ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesCopied / status.TotalBytes;
|
{
|
||||||
|
Description = $"{Converters.ToFileSizeString(status.BytesCopied)}/{Converters.ToFileSizeString(status.TotalBytes)}";
|
||||||
|
ProgressValue = status.TotalBytes == 0 ? 0 : (double)status.BytesCopied / status.TotalBytes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExtractFiles(Stream stream)
|
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.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Web.Hoyolab;
|
||||||
|
|
||||||
namespace Snap.Hutao.Web;
|
namespace Snap.Hutao.Web;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -16,11 +18,48 @@ internal static class HutaoEndpoints
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const string StaticHutao = "static.hut.ao";
|
public const string StaticHutao = "static.hut.ao";
|
||||||
|
|
||||||
#region Passport
|
#region GachaLog
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取注册验证码
|
/// 获取末尾Id
|
||||||
/// </summary>
|
/// </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";
|
public const string PassportVerify = $"{HomaSnapGenshinApi}/Passport/Verify";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -121,14 +160,6 @@ internal static class HutaoEndpoints
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Patcher
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 胡桃检查更新
|
|
||||||
/// </summary>
|
|
||||||
public const string PatcherHutaoStable = $"{PatcherDGPStudioApi}/hutao/stable";
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static & Zip
|
#region Static & Zip
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -170,7 +201,6 @@ internal static class HutaoEndpoints
|
|||||||
|
|
||||||
private const string HomaSnapGenshinApi = "https://homa.snapgenshin.com";
|
private const string HomaSnapGenshinApi = "https://homa.snapgenshin.com";
|
||||||
private const string HutaoMetadataSnapGenshinApi = "https://hutao-metadata.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 StaticSnapGenshinApi = "https://static.snapgenshin.com";
|
||||||
private const string StaticZipSnapGenshinApi = "https://static-zip.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));
|
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>
|
||||||
/// 尺寸
|
/// 尺寸
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user