improve welcome download experience

This commit is contained in:
Lightczx
2023-04-06 15:12:51 +08:00
parent cad1182ade
commit e5012d9051
20 changed files with 548 additions and 152 deletions

View 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');
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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*)&rect;
} }
} }
} }

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View 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!;
}
}

View File

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

View File

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