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
{
[TestMethod]
public void OriginalTypeDiscoverable()
public void OriginalTypeNotDiscoverable()
{
IServiceProvider services = new ServiceCollection()
.AddSingleton<IService, ServiceA>()

View File

@@ -6,6 +6,7 @@
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

View File

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

View File

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

View File

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

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

View File

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

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,
"public": true
"public": true,
"useSafeHandles": false
}

View File

@@ -3,6 +3,7 @@ INFINITE
WM_GETMINMAXINFO
WM_NCRBUTTONDOWN
WM_NCRBUTTONUP
WM_NULL
// Type & Enum definition
CWMO_FLAGS

View File

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

View File

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

View File

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

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.
// 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";
}

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