Compare commits

..

1 Commits

Author SHA1 Message Date
DismissedLight
9e344f56e0 LaunchGame 2022-11-03 18:19:36 +08:00
42 changed files with 1373 additions and 316 deletions

View File

@@ -15,9 +15,6 @@ namespace Snap.Hutao.Core;
/// </summary> /// </summary>
internal static class CoreEnvironment internal static class CoreEnvironment
{ {
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
private const string MachineGuidValue = "MachineGuid";
// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd // 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// <summary> /// <summary>
@@ -71,6 +68,9 @@ internal static class CoreEnvironment
WriteIndented = true, WriteIndented = true,
}; };
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
private const string MachineGuidValue = "MachineGuid";
static CoreEnvironment() static CoreEnvironment()
{ {
Version = Package.Current.Id.Version.ToVersion(); Version = Package.Current.Id.Version.ToVersion();

View File

@@ -26,7 +26,6 @@ internal class DbCurrent<TEntity, TMessage>
/// </summary> /// </summary>
/// <param name="dbSet">数据集</param> /// <param name="dbSet">数据集</param>
/// <param name="messenger">消息器</param> /// <param name="messenger">消息器</param>
///
public DbCurrent(DbSet<TEntity> dbSet, IMessenger messenger) public DbCurrent(DbSet<TEntity> dbSet, IMessenger messenger)
{ {
this.dbSet = dbSet; this.dbSet = dbSet;

View File

@@ -3,6 +3,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Snap.Hutao.Model.Entity;
namespace Snap.Hutao.Core.Database; namespace Snap.Hutao.Core.Database;

View File

@@ -0,0 +1,95 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 设置帮助类
/// </summary>
public static class SettingEntryHelper
{
/// <summary>
/// 获取或添加一个对应的设置
/// </summary>
/// <param name="dbSet">设置集</param>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <returns>设置</returns>
public static SettingEntry SingleOrAdd(this DbSet<SettingEntry> dbSet, string key, string value)
{
SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key);
if (entry == null)
{
entry = new(key, value);
dbSet.Add(entry);
dbSet.Context().SaveChanges();
}
return entry;
}
/// <summary>
/// 获取或添加一个对应的设置
/// </summary>
/// <param name="dbSet">设置集</param>
/// <param name="key">键</param>
/// <param name="valueFactory">值工厂</param>
/// <returns>设置</returns>
public static SettingEntry SingleOrAdd(this DbSet<SettingEntry> dbSet, string key, Func<string> valueFactory)
{
SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key);
if (entry == null)
{
entry = new(key, valueFactory());
dbSet.Add(entry);
dbSet.Context().SaveChanges();
}
return entry;
}
/// <summary>
/// 获取 Boolean 值
/// </summary>
/// <param name="entry">设置</param>
/// <returns>值</returns>
public static bool GetBoolean(this SettingEntry entry)
{
return bool.Parse(entry.Value!);
}
/// <summary>
/// 设置 Boolean 值
/// </summary>
/// <param name="entry">设置</param>
/// <param name="value">值</param>
public static void SetBoolean(this SettingEntry entry, bool value)
{
entry.Value = value.ToString();
}
/// <summary>
/// 获取 Int32 值
/// </summary>
/// <param name="entry">设置</param>
/// <returns>值</returns>
public static int GetInt32(this SettingEntry entry)
{
return int.Parse(entry.Value!);
}
/// <summary>
/// 设置 Int32 值
/// </summary>
/// <param name="entry">设置</param>
/// <param name="value">值</param>
public static void SetInt32(this SettingEntry entry, int value)
{
entry.Value = value.ToString();
}
}

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core.IO.Ini;
internal static class IniSerializer internal static class IniSerializer
{ {
/// <summary> /// <summary>
/// 异步反序列化 /// 反序列化
/// </summary> /// </summary>
/// <param name="fileStream">文件流</param> /// <param name="fileStream">文件流</param>
/// <returns>Ini 元素集合</returns> /// <returns>Ini 元素集合</returns>
@@ -44,4 +44,20 @@ internal static class IniSerializer
} }
} }
} }
/// <summary>
/// 序列化
/// </summary>
/// <param name="fileStream">写入的流</param>
/// <param name="elements">元素</param>
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
{
using (TextWriter writer = new StreamWriter(fileStream))
{
foreach (IniElement element in elements)
{
writer.WriteLine(element.ToString());
}
}
}
} }

View File

@@ -5,6 +5,7 @@ using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.Threading; using Snap.Hutao.Core.Threading;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Navigation;
using System.Security.Principal;
namespace Snap.Hutao.Core.LifeCycle; namespace Snap.Hutao.Core.LifeCycle;
@@ -20,6 +21,19 @@ internal static class Activation
private static readonly SemaphoreSlim ActivateSemaphore = new(1); private static readonly SemaphoreSlim ActivateSemaphore = new(1);
/// <summary>
/// 获取是否提升了权限
/// </summary>
/// <returns>是否提升了权限</returns>
public static bool GetElevated()
{
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
/// <summary> /// <summary>
/// 响应激活事件 /// 响应激活事件
/// 激活事件一般不会在UI线程上触发 /// 激活事件一般不会在UI线程上触发
@@ -70,6 +84,18 @@ internal static class Activation
case LaunchGame: case LaunchGame:
{ {
await ThreadHelper.SwitchToMainThreadAsync();
if (!MainWindow.IsPresent)
{
_ = Ioc.Default.GetRequiredService<LaunchGameWindow>();
}
else
{
await Ioc.Default
.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.LaunchGamePage>(INavigationAwaiter.Default, true).ConfigureAwait(false);
}
break; break;
} }
} }

View File

@@ -18,16 +18,18 @@ namespace Snap.Hutao.Core.Windowing;
/// 窗口管理器 /// 窗口管理器
/// 主要包含了针对窗体的 P/Inoke 逻辑 /// 主要包含了针对窗体的 P/Inoke 逻辑
/// </summary> /// </summary>
internal sealed class ExtendedWindow /// <typeparam name="TWindow">窗体类型</typeparam>
internal sealed class ExtendedWindow<TWindow>
where TWindow : Window, IExtendedWindowSource
{ {
private readonly HWND handle; private readonly HWND handle;
private readonly AppWindow appWindow; private readonly AppWindow appWindow;
private readonly Window window; private readonly TWindow window;
private readonly FrameworkElement titleBar; private readonly FrameworkElement titleBar;
private readonly ILogger<ExtendedWindow> logger; private readonly ILogger<ExtendedWindow<TWindow>> logger;
private readonly WindowSubclassManager subclassManager; private readonly WindowSubclassManager<TWindow> subclassManager;
private readonly bool useLegacyDragBar; private readonly bool useLegacyDragBar;
@@ -36,11 +38,11 @@ internal sealed class ExtendedWindow
/// </summary> /// </summary>
/// <param name="window">窗口</param> /// <param name="window">窗口</param>
/// <param name="titleBar">充当标题栏的元素</param> /// <param name="titleBar">充当标题栏的元素</param>
private ExtendedWindow(Window window, FrameworkElement titleBar) private ExtendedWindow(TWindow window, FrameworkElement titleBar)
{ {
this.window = window; this.window = window;
this.titleBar = titleBar; this.titleBar = titleBar;
logger = Ioc.Default.GetRequiredService<ILogger<ExtendedWindow>>(); logger = Ioc.Default.GetRequiredService<ILogger<ExtendedWindow<TWindow>>>();
handle = (HWND)WindowNative.GetWindowHandle(window); handle = (HWND)WindowNative.GetWindowHandle(window);
@@ -48,7 +50,7 @@ internal sealed class ExtendedWindow
appWindow = AppWindow.GetFromWindowId(windowId); appWindow = AppWindow.GetFromWindowId(windowId);
useLegacyDragBar = !AppWindowTitleBar.IsCustomizationSupported(); useLegacyDragBar = !AppWindowTitleBar.IsCustomizationSupported();
subclassManager = new(handle, useLegacyDragBar); subclassManager = new(window, handle, useLegacyDragBar);
InitializeWindow(); InitializeWindow();
} }
@@ -57,11 +59,10 @@ internal sealed class ExtendedWindow
/// 初始化 /// 初始化
/// </summary> /// </summary>
/// <param name="window">窗口</param> /// <param name="window">窗口</param>
/// <param name="titleBar">标题栏</param>
/// <returns>实例</returns> /// <returns>实例</returns>
public static ExtendedWindow Initialize(Window window, FrameworkElement titleBar) public static ExtendedWindow<TWindow> Initialize(TWindow window)
{ {
return new(window, titleBar); return new(window, window.TitleBar);
} }
private static void UpdateTitleButtonColor(AppWindowTitleBar appTitleBar) private static void UpdateTitleButtonColor(AppWindowTitleBar appTitleBar)
@@ -103,7 +104,8 @@ internal sealed class ExtendedWindow
appWindow.Title = "胡桃"; appWindow.Title = "胡桃";
ExtendsContentIntoTitleBar(); ExtendsContentIntoTitleBar();
Persistence.RecoverOrInit(appWindow);
Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize);
// Log basic window state here. // Log basic window state here.
(string pos, string size) = GetPostionAndSize(appWindow); (string pos, string size) = GetPostionAndSize(appWindow);
@@ -115,14 +117,18 @@ internal sealed class ExtendedWindow
logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed"); logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
bool subClassApplied = subclassManager.TrySetWindowSubclass(); bool subClassApplied = subclassManager.TrySetWindowSubclass();
logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager), subClassApplied ? "succeed" : "failed"); logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager<TWindow>), subClassApplied ? "succeed" : "failed");
window.Closed += OnWindowClosed; window.Closed += OnWindowClosed;
} }
private void OnWindowClosed(object sender, WindowEventArgs args) private void OnWindowClosed(object sender, WindowEventArgs args)
{
if (window.PersistSize)
{ {
Persistence.Save(appWindow); Persistence.Save(appWindow);
}
subclassManager?.Dispose(); subclassManager?.Dispose();
} }

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Windows.Graphics;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 为扩展窗体提供必要的选项
/// </summary>
/// <typeparam name="TWindow">窗体类型</typeparam>
internal interface IExtendedWindowSource
{
/// <summary>
/// 提供的标题栏
/// </summary>
FrameworkElement TitleBar { get; }
/// <summary>
/// 是否持久化尺寸
/// </summary>
bool PersistSize { get; }
/// <summary>
/// 初始大小
/// </summary>
SizeInt32 InitSize { get; }
/// <summary>
/// 处理最大最小信息
/// </summary>
/// <param name="pInfo">信息指针</param>
/// <param name="scalingFactor">缩放比</param>
unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor);
}

View File

@@ -21,21 +21,26 @@ internal static class Persistence
/// 设置窗体位置 /// 设置窗体位置
/// </summary> /// </summary>
/// <param name="appWindow">应用窗体</param> /// <param name="appWindow">应用窗体</param>
public static void RecoverOrInit(AppWindow appWindow) /// <param name="persistSize">持久化尺寸</param>
/// <param name="size">初始尺寸</param>
public static void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 size)
{ {
// Set first launch size. // Set first launch size.
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id); HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
SizeInt32 size = TransformSizeForWindow(new(1200, 741), hwnd); SizeInt32 transformedSize = TransformSizeForWindow(size, hwnd);
RectInt32 rect = StructMarshal.RectInt32(size); RectInt32 rect = StructMarshal.RectInt32(transformedSize);
RectInt32 target = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect); if (persistSize)
if (target.Width * target.Height < 848 * 524)
{ {
target = rect; RectInt32 persistedSize = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
if (persistedSize.Width * persistedSize.Height > 848 * 524)
{
rect = persistedSize;
}
} }
TransformToCenterScreen(ref target); TransformToCenterScreen(ref rect);
appWindow.MoveAndResize(target); appWindow.MoveAndResize(rect);
} }
/// <summary> /// <summary>

View File

@@ -1,6 +1,7 @@
// 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.Xaml;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell; using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging; using Windows.Win32.UI.WindowsAndMessaging;
@@ -11,14 +12,14 @@ namespace Snap.Hutao.Core.Windowing;
/// <summary> /// <summary>
/// 窗体子类管理器 /// 窗体子类管理器
/// </summary> /// </summary>
internal class WindowSubclassManager : IDisposable /// <typeparam name="TWindow">窗体类型</typeparam>
internal class WindowSubclassManager<TWindow> : IDisposable
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 const int MinWidth = 848; private readonly TWindow window;
private const int MinHeight = 524;
private readonly HWND hwnd; private readonly HWND hwnd;
private readonly bool isLegacyDragBar; private readonly bool isLegacyDragBar;
private HWND hwndDragBar; private HWND hwndDragBar;
@@ -30,12 +31,13 @@ internal class WindowSubclassManager : IDisposable
/// <summary> /// <summary>
/// 构造一个新的窗体子类管理器 /// 构造一个新的窗体子类管理器
/// </summary> /// </summary>
/// <param name="window">窗体实例</param>
/// <param name="hwnd">窗体句柄</param> /// <param name="hwnd">窗体句柄</param>
/// <param name="isLegacyDragBar">是否为经典标题栏区域</param> /// <param name="isLegacyDragBar">是否为经典标题栏区域</param>
public WindowSubclassManager(HWND hwnd, bool isLegacyDragBar) public WindowSubclassManager(TWindow window, HWND hwnd, bool isLegacyDragBar)
{ {
Must.NotNull(hwnd); this.window = window;
this.hwnd = hwnd; this.hwnd = Must.NotNull(hwnd);
this.isLegacyDragBar = isLegacyDragBar; this.isLegacyDragBar = isLegacyDragBar;
} }
@@ -85,9 +87,7 @@ internal class WindowSubclassManager : IDisposable
case WM_GETMINMAXINFO: case WM_GETMINMAXINFO:
{ {
double scalingFactor = Persistence.GetScaleForWindow(hwnd); double scalingFactor = Persistence.GetScaleForWindow(hwnd);
MINMAXINFO* info = (MINMAXINFO*)lParam.Value; window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
info->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, info->ptMinTrackSize.X);
info->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, info->ptMinTrackSize.Y);
break; break;
} }
} }

View File

@@ -1,12 +1,63 @@
<Window <Window
x:Class="Snap.Hutao.LaunchGameWindow" x:Class="Snap.Hutao.LaunchGameWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Snap.Hutao"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shv="using:Snap.Hutao.ViewModel"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid>
<Grid
Name="RootGrid"
d:DataContext="{d:DesignInstance shv:LaunchGameViewModel}">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid
x:Name="DragableGrid"
Grid.Row="0"
Height="32">
<TextBlock
Text="选择账号并启动"
TextWrapping="NoWrap"
Style="{StaticResource CaptionTextBlockStyle}"
VerticalAlignment="Center"
Margin="12,0,0,0"/>
</Grid>
<ListView
Grid.Row="1"
ItemsSource="{Binding GameAccounts}"
SelectedItem="{Binding SelectedGameAccount,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<StackPanel Margin="0,12">
<TextBlock Text="{Binding Name}"/>
<TextBlock
Opacity="0.8"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding AttachUid,TargetNullValue=该账号尚未绑定 UID}"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button
Margin="16"
Grid.Row="2"
HorizontalAlignment="Stretch"
Content="启动游戏"
Command="{Binding LaunchCommand}"/>
</Grid> </Grid>
</Window> </Window>

View File

@@ -1,20 +1,63 @@
// 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.Extensions.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.ViewModel;
using Windows.Graphics;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao; namespace Snap.Hutao;
/// <summary> /// <summary>
/// 启动游戏窗口 /// 启动游戏窗口
/// </summary> /// </summary>
public sealed partial class LaunchGameWindow : Window [Injection(InjectAs.Singleton)]
public sealed partial class LaunchGameWindow : Window, IDisposable, IExtendedWindowSource
{ {
private const int MinWidth = 240;
private const int MinHeight = 240;
private const int MaxWidth = 320;
private const int MaxHeight = 320;
private readonly IServiceScope scope;
/// <summary> /// <summary>
/// 构造一个新的启动游戏窗口 /// 构造一个新的启动游戏窗口
/// </summary> /// </summary>
public LaunchGameWindow() /// <param name="scopeFactory">范围工厂</param>
public LaunchGameWindow(IServiceScopeFactory scopeFactory)
{ {
InitializeComponent(); InitializeComponent();
ExtendedWindow<LaunchGameWindow>.Initialize(this);
scope = scopeFactory.CreateScope();
RootGrid.DataContext = scope.ServiceProvider.GetRequiredService<LaunchGameViewModel>();
}
/// <inheritdoc/>
public FrameworkElement TitleBar { get => DragableGrid; }
/// <inheritdoc/>
public bool PersistSize { get => false; }
/// <inheritdoc/>
public SizeInt32 InitSize { get => new(320, 320); }
/// <inheritdoc/>
public void Dispose()
{
scope.Dispose();
}
/// <inheritdoc/>
public unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor)
{
pInfo->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo->ptMinTrackSize.X);
pInfo->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo->ptMinTrackSize.Y);
pInfo->ptMaxTrackSize.X = (int)Math.Min(MaxWidth * scalingFactor, pInfo->ptMaxTrackSize.X);
pInfo->ptMaxTrackSize.Y = (int)Math.Min(MaxHeight * scalingFactor, pInfo->ptMaxTrackSize.Y);
} }
} }

View File

@@ -3,6 +3,8 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing; using Snap.Hutao.Core.Windowing;
using Windows.Graphics;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao; namespace Snap.Hutao;
@@ -11,14 +13,40 @@ namespace Snap.Hutao;
/// </summary> /// </summary>
[Injection(InjectAs.Singleton)] [Injection(InjectAs.Singleton)]
[SuppressMessage("", "CA1001")] [SuppressMessage("", "CA1001")]
public sealed partial class MainWindow : Window public sealed partial class MainWindow : Window, IExtendedWindowSource
{ {
private const int MinWidth = 848;
private const int MinHeight = 524;
/// <summary> /// <summary>
/// 构造一个新的主窗体 /// 构造一个新的主窗体
/// </summary> /// </summary>
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
ExtendedWindow.Initialize(this, TitleBarView.DragArea); ExtendedWindow<MainWindow>.Initialize(this);
IsPresent = true;
Closed += (s, e) => IsPresent = false;
}
/// <summary>
/// 是否打开
/// </summary>
public static bool IsPresent { get; private set; }
/// <inheritdoc/>
public FrameworkElement TitleBar { get => TitleBarView.DragArea; }
/// <inheritdoc/>
public bool PersistSize { get => true; }
/// <inheritdoc/>
public SizeInt32 InitSize { get => new(1200, 741); }
/// <inheritdoc/>
public unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor)
{
pInfo->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo->ptMinTrackSize.X);
pInfo->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo->ptMinTrackSize.Y);
} }
} }

View File

@@ -0,0 +1,215 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Context.Database;
#nullable disable
namespace Snap.Hutao.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20221031104940_GameAccount")]
partial class GameAccount
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("Current")
.HasColumnType("INTEGER");
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("gacha_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("GachaType")
.HasColumnType("INTEGER");
b.Property<long>("Id")
.HasColumnType("INTEGER");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("QueryType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Cookie")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("users");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,34 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
public partial class GameAccount : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "game_accounts",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
AttachUid = table.Column<string>(type: "TEXT", nullable: true),
Type = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
MihoyoSDK = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_game_accounts", x => x.InnerId);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "game_accounts");
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{ {
@@ -131,6 +131,31 @@ namespace Snap.Hutao.Migrations
b.ToTable("gacha_items"); b.ToTable("gacha_items");
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{ {
b.Property<string>("Key") b.Property<string>("Key")

View File

@@ -56,7 +56,7 @@ public class User : ObservableObject
} }
/// <inheritdoc cref="EntityUser.Cookie"/> /// <inheritdoc cref="EntityUser.Cookie"/>
public Cookie Cookie public Cookie? Cookie
{ {
get => inner.Cookie; get => inner.Cookie;
set set
@@ -71,7 +71,7 @@ public class User : ObservableObject
/// </summary> /// </summary>
public bool HasSToken public bool HasSToken
{ {
get => inner.Cookie.ContainsSToken(); get => inner.Cookie!.ContainsSToken();
} }
/// <summary> /// <summary>
@@ -84,17 +84,6 @@ public class User : ObservableObject
/// </summary> /// </summary>
public bool IsInitialized { get => isInitialized; } public bool IsInitialized { get => isInitialized; }
/// <summary>
/// 更新SToken
/// </summary>
/// <param name="uid">uid</param>
/// <param name="cookie">cookie</param>
internal void UpdateSToken(string uid, Cookie cookie)
{
Cookie.InsertSToken(uid, cookie);
OnPropertyChanged(nameof(HasSToken));
}
/// <summary> /// <summary>
/// 从数据库恢复用户 /// 从数据库恢复用户
/// </summary> /// </summary>
@@ -125,6 +114,17 @@ public class User : ObservableObject
return successful ? user : null; return successful ? user : null;
} }
/// <summary>
/// 更新SToken
/// </summary>
/// <param name="uid">uid</param>
/// <param name="cookie">cookie</param>
internal void UpdateSToken(string uid, Cookie cookie)
{
Cookie!.InsertSToken(uid, cookie);
OnPropertyChanged(nameof(HasSToken));
}
private async Task<bool> InitializeCoreAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token = default) private async Task<bool> InitializeCoreAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token = default)
{ {
if (isInitialized) if (isInitialized)

View File

@@ -1,9 +1,7 @@
// 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.Core.Database;
using Snap.Hutao.Model.Binding.LaunchGame; using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Web.Hoyolab;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
@@ -13,8 +11,11 @@ namespace Snap.Hutao.Model.Entity;
/// 游戏内账号 /// 游戏内账号
/// </summary> /// </summary>
[Table("game_accounts")] [Table("game_accounts")]
public class GameAccount : ISelectable public class GameAccount : INotifyPropertyChanged
{ {
/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary> /// <summary>
/// 内部Id /// 内部Id
/// </summary> /// </summary>
@@ -22,9 +23,6 @@ public class GameAccount : ISelectable
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; } public Guid InnerId { get; set; }
/// <inheritdoc/>
public bool IsSelected { get; set; }
/// <summary> /// <summary>
/// 对应的Uid /// 对应的Uid
/// </summary> /// </summary>
@@ -41,7 +39,43 @@ public class GameAccount : ISelectable
public string Name { get; set; } = default!; public string Name { get; set; } = default!;
/// <summary> /// <summary>
/// MIHOYOSDK_ADL_PROD_CN_h3123967166 /// [MIHOYOSDK_ADL_PROD_CN_h3123967166]
/// see <see cref="Service.Game.GameAccountRegistryInterop.SdkKey"/>
/// </summary> /// </summary>
public string MihoyoSDK { get; set; } = default!; public string MihoyoSDK { get; set; } = default!;
/// <summary>
/// 构造一个新的游戏内账号
/// </summary>
/// <param name="name">名称</param>
/// <param name="sdk">sdk</param>
/// <returns>游戏内账号</returns>
public static GameAccount Create(string name, string sdk)
{
return new()
{
Name = name,
MihoyoSDK = sdk,
};
}
/// <summary>
/// 更新绑定的Uid
/// </summary>
/// <param name="uid">uid</param>
public void UpdateAttachUid(string? uid)
{
AttachUid = uid;
PropertyChanged?.Invoke(this, new(nameof(AttachUid)));
}
/// <summary>
/// 更新名称
/// </summary>
/// <param name="name">新名称</param>
public void UpdateName(string name)
{
Name = name;
PropertyChanged?.Invoke(this, new(nameof(Name)));
}
} }

View File

@@ -22,6 +22,36 @@ public class SettingEntry
/// </summary> /// </summary>
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible"; public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
/// <summary>
/// 启动游戏 全屏
/// </summary>
public const string LaunchIsFullScreen = "Launch.IsFullScreen";
/// <summary>
/// 启动游戏 无边框
/// </summary>
public const string LaunchIsBorderless = "Launch.IsBorderless";
/// <summary>
/// 启动游戏 宽度
/// </summary>
public const string LaunchScreenWidth = "Launch.ScreenWidth";
/// <summary>
/// 启动游戏 高度
/// </summary>
public const string LaunchScreenHeight = "Launch.ScreenHeight";
/// <summary>
/// 启动游戏 解锁帧率
/// </summary>
public const string LaunchUnlockFps = "Launch.UnlockFps";
/// <summary>
/// 启动游戏 目标帧率
/// </summary>
public const string LaunchTargetFps = "Launch.TargetFps";
/// <summary> /// <summary>
/// 构造一个新的设置入口 /// 构造一个新的设置入口
/// </summary> /// </summary>

View File

@@ -29,7 +29,7 @@ public class User : ISelectable
/// <summary> /// <summary>
/// 用户的Cookie /// 用户的Cookie
/// </summary> /// </summary>
public Cookie Cookie { get; set; } = default!; public Cookie? Cookie { get; set; }
/// <summary> /// <summary>
/// 创建一个新的用户 /// 创建一个新的用户

View File

@@ -2,19 +2,22 @@
<Package <Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap"> IgnorableNamespaces="uap desktop6 rescap">
<Identity <Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d" Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio" Publisher="CN=DGP Studio"
Version="1.1.14.0" /> Version="1.1.18.0" />
<Properties> <Properties>
<DisplayName>胡桃</DisplayName> <DisplayName>胡桃</DisplayName>
<PublisherDisplayName>DGP Studio</PublisherDisplayName> <PublisherDisplayName>DGP Studio</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo> <Logo>Assets\StoreLogo.png</Logo>
<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
</Properties> </Properties>
<Dependencies> <Dependencies>
@@ -51,5 +54,6 @@
<Capabilities> <Capabilities>
<rescap:Capability Name="runFullTrust"/> <rescap:Capability Name="runFullTrust"/>
<rescap:Capability Name="unvirtualizedResources"/>
</Capabilities> </Capabilities>
</Package> </Package>

View File

@@ -49,7 +49,6 @@ public static class LogHelper
if (frames.Length > 0 && frames[0].HasNativeImage()) if (frames.Length > 0 && frames[0].HasNativeImage())
{ {
} }
return current; return current;

View File

@@ -54,59 +54,34 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
{ {
using (FileStream fileStream = new(tempFile.Path, FileMode.Open, FileAccess.Read, FileShare.Read)) using (FileStream fileStream = new(tempFile.Path, FileMode.Open, FileAccess.Read, FileShare.Read))
{ {
using (BinaryReader reader = new(fileStream)) using (MemoryStream memoryStream = new())
{ {
string url = string.Empty; await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
while (!reader.EndOfStream()) string? result = Match(memoryStream);
{ return new(!string.IsNullOrEmpty(result), result!);
uint test = reader.ReadUInt32();
if (test == 0x2F302F31)
{
byte[] chars = ReadBytesUntilZero(reader);
string result = Encoding.UTF8.GetString(chars.AsSpan());
if (result.Contains("&auth_appid=webview_gacha"))
{
url = result;
}
// align up
long offset = reader.BaseStream.Position % 128;
reader.BaseStream.Position += 128 - offset;
}
}
return new(!string.IsNullOrEmpty(url), url);
} }
} }
} }
} }
else else
{ {
return new(false, $"未正确提供原神路径,或当前设置的路径不正确"); return new(false, "未正确提供原神路径,或当前设置的路径不正确");
} }
} }
private static byte[] ReadBytesUntilZero(BinaryReader binaryReader) private static string? Match(MemoryStream stream)
{ {
return ReadByteEnumerableUntilZero(binaryReader).ToArray(); ReadOnlySpan<byte> span = stream.ToArray();
ReadOnlySpan<byte> match = Encoding.UTF8.GetBytes("https://webstatic.mihoyo.com/hk4e/event/e20190909gacha-v2/index.html");
ReadOnlySpan<byte> zero = Encoding.UTF8.GetBytes("\0");
int index = span.LastIndexOf(match);
if (index >= 0)
{
int length = span[index..].IndexOf(zero);
return Encoding.UTF8.GetString(span.Slice(index, length));
} }
private static IEnumerable<byte> ReadByteEnumerableUntilZero(BinaryReader binaryReader) return null;
{
while (binaryReader.BaseStream.Position < binaryReader.BaseStream.Length)
{
byte b = binaryReader.ReadByte();
if (b == 0x00)
{
yield break;
}
else
{
yield return b;
}
}
} }
} }

View File

@@ -0,0 +1,49 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Model.Entity;
using System.Text;
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 定义了对注册表的操作
/// </summary>
internal static class GameAccountRegistryInterop
{
private const string GenshinKey = @"HKEY_CURRENT_USER\Software\miHoYo\原神";
private const string SdkKey = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
/// <summary>
/// 设置键值
/// </summary>
/// <param name="account">账户</param>
/// <returns>账号是否设置</returns>
public static bool Set(GameAccount? account)
{
if (account != null)
{
Registry.SetValue(GenshinKey, SdkKey, Encoding.UTF8.GetBytes(account.MihoyoSDK));
return true;
}
return false;
}
/// <summary>
/// 在注册表中获取账号信息
/// </summary>
/// <returns>当前注册表中的信息</returns>
public static string? Get()
{
object? sdk = Registry.GetValue(GenshinKey, SdkKey, Array.Empty<byte>());
if (sdk is byte[] bytes)
{
return Encoding.UTF8.GetString(bytes);
}
return null;
}
}

View File

@@ -8,9 +8,11 @@ using Snap.Hutao.Core;
using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.IO.Ini; using Snap.Hutao.Core.IO.Ini;
using Snap.Hutao.Core.Threading; using Snap.Hutao.Core.Threading;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Locator; using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Game.Unlocker; using Snap.Hutao.Service.Game.Unlocker;
using Snap.Hutao.View.Dialog;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@@ -24,6 +26,7 @@ namespace Snap.Hutao.Service.Game;
internal class GameService : IGameService internal class GameService : IGameService
{ {
private const string GamePathKey = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}"; private const string GamePathKey = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}";
private const string ConfigFile = "config.ini";
private readonly IServiceScopeFactory scopeFactory; private readonly IServiceScopeFactory scopeFactory;
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;
@@ -136,7 +139,7 @@ internal class GameService : IGameService
public MultiChannel GetMultiChannel() public MultiChannel GetMultiChannel()
{ {
string gamePath = GetGamePathSkipLocator(); string gamePath = GetGamePathSkipLocator();
string configPath = Path.Combine(gamePath, "config.ini"); string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile);
using (FileStream stream = File.OpenRead(configPath)) using (FileStream stream = File.OpenRead(configPath))
{ {
@@ -148,11 +151,61 @@ internal class GameService : IGameService
} }
} }
public async Task<ObservableCollection<GameAccount>> GetGameAccountCollectionAsync() /// <inheritdoc/>
public void SetMultiChannel(LaunchScheme scheme)
{
string gamePath = GetGamePathSkipLocator();
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile);
List<IniElement> elements;
using (FileStream readStream = File.OpenRead(configPath))
{
elements = IniSerializer.Deserialize(readStream).ToList();
}
foreach (IniElement element in elements)
{
if (element is IniParameter parameter)
{
if (parameter.Key == "channel")
{
parameter.Value = scheme.Channel;
}
if (parameter.Key == "sub_channel")
{
parameter.Value = scheme.SubChannel;
}
}
}
using (FileStream writeStream = File.Create(configPath))
{
IniSerializer.Serialize(writeStream, elements);
}
}
/// <inheritdoc/>
public bool IsGameRunning()
{
if (gameSemaphore.CurrentCount == 0)
{
return true;
}
return Process.GetProcessesByName("YuanShen.exe").Any();
}
/// <inheritdoc/>
public ObservableCollection<GameAccount> GetGameAccountCollection()
{ {
if (gameAccounts == null) if (gameAccounts == null)
{ {
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
gameAccounts = new(appDbContext.GameAccounts.ToList());
}
} }
return gameAccounts; return gameAccounts;
@@ -161,19 +214,19 @@ internal class GameService : IGameService
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask LaunchAsync(LaunchConfiguration configuration) public async ValueTask LaunchAsync(LaunchConfiguration configuration)
{ {
if (gameSemaphore.CurrentCount == 0) if (IsGameRunning())
{ {
return; return;
} }
string gamePath = GetGamePathSkipLocator(); string gamePath = GetGamePathSkipLocator();
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
string commandLine = new CommandLineBuilder() string commandLine = new CommandLineBuilder()
.AppendIf("-popupwindow", configuration.IsBorderless) .AppendIf("-popupwindow", configuration.IsBorderless)
.Append("-screen-fullscreen", configuration.IsFullScreen ? 1 : 0) .Append("-screen-fullscreen", configuration.IsFullScreen ? 1 : 0)
.Append("-screen-width", configuration.ScreenWidth) .Append("-screen-width", configuration.ScreenWidth)
.Append("-screen-height", configuration.ScreenHeight) .Append("-screen-height", configuration.ScreenHeight)
.Append("-monitor", configuration.Monitor)
.Build(); .Build();
Process game = new() Process game = new()
@@ -189,6 +242,8 @@ internal class GameService : IGameService
}; };
using (await gameSemaphore.EnterAsync().ConfigureAwait(false)) using (await gameSemaphore.EnterAsync().ConfigureAwait(false))
{
try
{ {
if (configuration.UnlockFPS) if (configuration.UnlockFPS)
{ {
@@ -197,8 +252,11 @@ internal class GameService : IGameService
TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100); TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100);
TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000); TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000);
TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000); TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000);
if (game.Start())
{
await unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay).ConfigureAwait(false); await unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay).ConfigureAwait(false);
} }
}
else else
{ {
if (game.Start()) if (game.Start())
@@ -207,5 +265,100 @@ internal class GameService : IGameService
} }
} }
} }
catch (Win32Exception)
{
}
}
}
/// <inheritdoc/>
public async ValueTask DetectGameAccountAsync()
{
Must.NotNull(gameAccounts!);
string? registrySdk = GameAccountRegistryInterop.Get();
if (!string.IsNullOrEmpty(registrySdk))
{
GameAccount? account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
if (account == null)
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
(bool isOk, string name) = await new GameAccountNameDialog(mainWindow).GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
account = GameAccount.Create(name, registrySdk);
// sync cache
await ThreadHelper.SwitchToMainThreadAsync();
gameAccounts.Add(GameAccount.Create(name, registrySdk));
// sync database
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.GameAccounts.AddAndSave(account);
}
}
}
}
}
/// <inheritdoc/>
public bool SetGameAccount(GameAccount account)
{
return GameAccountRegistryInterop.Set(account);
}
/// <inheritdoc/>
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IQueryable<GameAccount> oldAccounts = appDbContext.GameAccounts.Where(a => a.AttachUid == uid);
foreach (GameAccount account in oldAccounts)
{
account.UpdateAttachUid(null);
appDbContext.GameAccounts.UpdateAndSave(account);
}
gameAccount.UpdateAttachUid(uid);
appDbContext.GameAccounts.UpdateAndSave(gameAccount);
}
}
/// <inheritdoc/>
public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
(bool isOk, string name) = await new GameAccountNameDialog(mainWindow).GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
gameAccount.UpdateName(name);
// sync database
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.GameAccounts.UpdateAndSave(gameAccount);
}
}
}
/// <inheritdoc/>
public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
{
Must.NotNull(gameAccounts!).Remove(gameAccount);
await Task.Yield();
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.GameAccounts.RemoveAndSave(gameAccount);
}
} }
} }

View File

@@ -2,6 +2,9 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.Threading; using Snap.Hutao.Core.Threading;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game; namespace Snap.Hutao.Service.Game;
@@ -10,6 +13,26 @@ namespace Snap.Hutao.Service.Game;
/// </summary> /// </summary>
internal interface IGameService internal interface IGameService
{ {
/// <summary>
/// 将账号绑定到对应的Uid
/// 清除老账号的绑定状态
/// </summary>
/// <param name="gameAccount">游戏内账号</param>
/// <param name="uid">uid</param>
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
/// <summary>
/// 检测并尝试添加游戏内账户
/// </summary>
/// <returns>任务</returns>
ValueTask DetectGameAccountAsync();
/// <summary>
/// 获取游戏内账号集合
/// </summary>
/// <returns>游戏内账号集合</returns>
ObservableCollection<GameAccount> GetGameAccountCollection();
/// <summary> /// <summary>
/// 异步获取游戏路径 /// 异步获取游戏路径
/// </summary> /// </summary>
@@ -28,6 +51,12 @@ internal interface IGameService
/// <returns>多通道值</returns> /// <returns>多通道值</returns>
MultiChannel GetMultiChannel(); MultiChannel GetMultiChannel();
/// <summary>
/// 游戏是否正在运行
/// </summary>
/// <returns>是否正在运行</returns>
bool IsGameRunning();
/// <summary> /// <summary>
/// 异步启动 /// 异步启动
/// </summary> /// </summary>
@@ -35,9 +64,36 @@ internal interface IGameService
/// <returns>任务</returns> /// <returns>任务</returns>
ValueTask LaunchAsync(LaunchConfiguration configuration); ValueTask LaunchAsync(LaunchConfiguration configuration);
/// <summary>
/// 异步修改游戏账号名称
/// </summary>
/// <param name="gameAccount">游戏账号</param>
/// <returns>任务</returns>
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
/// <summary> /// <summary>
/// 重写游戏路径 /// 重写游戏路径
/// </summary> /// </summary>
/// <param name="path">路径</param> /// <param name="path">路径</param>
void OverwriteGamePath(string path); void OverwriteGamePath(string path);
/// <summary>
/// 异步尝试移除账号
/// </summary>
/// <param name="gameAccount">账号</param>
/// <returns>任务</returns>
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
/// <summary>
/// 修改注册表中的账号信息
/// </summary>
/// <param name="account">账号</param>
/// <returns>是否设置成功</returns>
bool SetGameAccount(GameAccount account);
/// <summary>
/// 设置多通道值
/// </summary>
/// <param name="scheme">方案</param>
void SetMultiChannel(LaunchScheme scheme);
} }

View File

@@ -6,40 +6,55 @@ namespace Snap.Hutao.Service.Game;
/// <summary> /// <summary>
/// 启动游戏配置 /// 启动游戏配置
/// </summary> /// </summary>
internal struct LaunchConfiguration internal readonly struct LaunchConfiguration
{ {
/// <summary> /// <summary>
/// 是否全屏,全屏时无边框设置将被覆盖 /// 是否全屏,全屏时无边框设置将被覆盖
/// </summary> /// </summary>
public bool IsFullScreen { get; set; } public readonly bool IsFullScreen;
/// <summary> /// <summary>
/// 是否为无边框窗口 /// 是否为无边框窗口
/// </summary> /// </summary>
public bool IsBorderless { get; private set; } public readonly bool IsBorderless;
/// <summary>
/// 是否启用解锁帧率
/// </summary>
public bool UnlockFPS { get; private set; }
/// <summary>
/// 目标帧率
/// </summary>
public int TargetFPS { get; private set; }
/// <summary> /// <summary>
/// 窗口宽度 /// 窗口宽度
/// </summary> /// </summary>
public int ScreenWidth { get; private set; } public readonly int ScreenWidth;
/// <summary> /// <summary>
/// 窗口高度 /// 窗口高度
/// </summary> /// </summary>
public int ScreenHeight { get; private set; } public readonly int ScreenHeight;
/// <summary> /// <summary>
/// 显示器编号 /// 是否启用解锁帧率
/// </summary> /// </summary>
public int Monitor { get; private set; } public readonly bool UnlockFPS;
/// <summary>
/// 目标帧率
/// </summary>
public readonly int TargetFPS;
/// <summary>
/// 构造一个新的启动配置
/// </summary>
/// <param name="isFullScreen">全屏</param>
/// <param name="isBorderless">无边框</param>
/// <param name="screenWidth">宽度</param>
/// <param name="screenHeight">高度</param>
/// <param name="unlockFps">解锁帧率</param>
/// <param name="targetFps">目标帧率</param>
public LaunchConfiguration(bool isFullScreen, bool isBorderless, int screenWidth, int screenHeight, bool unlockFps, int targetFps)
{
IsFullScreen = isFullScreen;
IsBorderless = isBorderless;
ScreenHeight = screenHeight;
ScreenWidth = screenWidth;
ScreenHeight = screenHeight;
UnlockFPS = unlockFps;
TargetFPS = targetFps;
}
} }

View File

@@ -180,6 +180,8 @@ internal class NavigationService : INavigationService
/// <inheritdoc/> /// <inheritdoc/>
public void GoBack() public void GoBack()
{
Program.DispatcherQueue!.TryEnqueue(() =>
{ {
bool canGoBack = Frame?.CanGoBack ?? false; bool canGoBack = Frame?.CanGoBack ?? false;
@@ -188,6 +190,7 @@ internal class NavigationService : INavigationService
Frame!.GoBack(); Frame!.GoBack();
SyncSelectedNavigationViewItemWith(Frame.Content.GetType()); SyncSelectedNavigationViewItemWith(Frame.Content.GetType());
} }
});
} }
/// <summary> /// <summary>

View File

@@ -95,7 +95,6 @@ internal class UserService : IUserService
using (IServiceScope scope = scopeFactory.CreateScope()) using (IServiceScope scope = scopeFactory.CreateScope())
{ {
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.Users.RemoveAndSave(user.Entity); appDbContext.Users.RemoveAndSave(user.Entity);
} }
} }

View File

@@ -60,7 +60,7 @@
<None Remove="View\Dialog\GachaLogImportDialog.xaml" /> <None Remove="View\Dialog\GachaLogImportDialog.xaml" />
<None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" /> <None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" />
<None Remove="View\Dialog\GachaLogUrlDialog.xaml" /> <None Remove="View\Dialog\GachaLogUrlDialog.xaml" />
<None Remove="View\Dialog\UserAutoCookieDialog.xaml" /> <None Remove="View\Dialog\GameAccountNameDialog.xaml" />
<None Remove="View\Dialog\UserDialog.xaml" /> <None Remove="View\Dialog\UserDialog.xaml" />
<None Remove="View\MainView.xaml" /> <None Remove="View\MainView.xaml" />
<None Remove="View\Page\AchievementPage.xaml" /> <None Remove="View\Page\AchievementPage.xaml" />
@@ -150,6 +150,11 @@
<ItemGroup> <ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" /> <None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\GameAccountNameDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="View\Page\LoginMihoyoBBSPage.xaml"> <Page Update="View\Page\LoginMihoyoBBSPage.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@@ -190,11 +195,6 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\UserAutoCookieDialog.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="View\Dialog\AchievementArchiveCreateDialog.xaml"> <Page Update="View\Dialog\AchievementArchiveCreateDialog.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>

View File

@@ -0,0 +1,21 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.GameAccountNameDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="为账号命名"
DefaultButton="Primary"
PrimaryButtonText="确认"
CloseButtonText="取消"
Style="{StaticResource DefaultContentDialogStyle}">
<Grid>
<TextBox
Margin="0,0,0,0"
x:Name="InputText"
PlaceholderText="在此处输入"
VerticalAlignment="Top"/>
</Grid>
</ContentDialog>

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.Threading;
namespace Snap.Hutao.View.Dialog;
/// <summary>
/// 游戏账号命名对话框
/// </summary>
public sealed partial class GameAccountNameDialog : ContentDialog
{
/// <summary>
/// 构造一个新的游戏账号命名对话框
/// </summary>
/// <param name="window">窗体</param>
public GameAccountNameDialog(Window window)
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
}
/// <summary>
/// 获取输入的Cookie
/// </summary>
/// <returns>输入的结果</returns>
public async Task<ValueResult<bool, string>> GetInputNameAsync()
{
ContentDialogResult result = await ShowAsync();
string text = InputText.Text;
return new(result == ContentDialogResult.Primary && (!string.IsNullOrEmpty(text)), text);
}
}

View File

@@ -1,33 +0,0 @@
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.UserAutoCookieDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="登录米哈游通行证"
DefaultButton="Primary"
PrimaryButtonText="继续"
CloseButtonText="取消"
Style="{StaticResource DefaultContentDialogStyle}">
<ContentDialog.Resources>
<x:Double x:Key="ContentDialogMaxWidth">1600</x:Double>
<x:Double x:Key="ContentDialogMinHeight">200</x:Double>
<x:Double x:Key="ContentDialogMaxHeight">1200</x:Double>
</ContentDialog.Resources>
<Grid Loaded="OnRootLoaded">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock
Text="在下方登录"
Grid.Row="0"/>
<WebView2
Grid.Row="2"
Margin="0,12,0,0"
Width="640"
Height="400"
x:Name="WebView"/>
</Grid>
</ContentDialog>

View File

@@ -1,69 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.View.Dialog;
/// <summary>
/// 用户自动Cookie对话框
/// </summary>
public sealed partial class UserAutoCookieDialog : ContentDialog
{
private Cookie? cookie;
/// <summary>
/// 构造一个新的用户自动Cookie对话框
/// </summary>
/// <param name="window">依赖窗口</param>
public UserAutoCookieDialog(Window window)
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
}
/// <summary>
/// 获取输入的Cookie
/// </summary>
/// <returns>输入的结果</returns>
public async Task<ValueResult<bool, Cookie>> GetInputCookieAsync()
{
ContentDialogResult result = await ShowAsync();
return new(result == ContentDialogResult.Primary && cookie != null, cookie!);
}
[SuppressMessage("", "VSTHRD100")]
private async void OnRootLoaded(object sender, RoutedEventArgs e)
{
await WebView.EnsureCoreWebView2Async();
WebView.CoreWebView2.SourceChanged += OnCoreWebView2SourceChanged;
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
foreach (CoreWebView2Cookie item in cookies)
{
manager.DeleteCookie(item);
}
WebView.CoreWebView2.Navigate("https://user.mihoyo.com/#/login/password");
}
[SuppressMessage("", "VSTHRD100")]
private async void OnCoreWebView2SourceChanged(CoreWebView2 sender, CoreWebView2SourceChangedEventArgs args)
{
if (sender != null)
{
if (sender.Source.ToString() == "https://user.mihoyo.com/#/account/home")
{
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
cookie = Cookie.FromCoreWebView2Cookies(cookies);
WebView.CoreWebView2.SourceChanged -= OnCoreWebView2SourceChanged;
}
}
}
}

View File

@@ -7,7 +7,7 @@
xmlns:settings="using:SettingsUI.Controls" xmlns:settings="using:SettingsUI.Controls"
mc:Ignorable="d" mc:Ignorable="d"
IsPrimaryButtonEnabled="False" IsPrimaryButtonEnabled="False"
Title="设置米游社Cookie" Title="设置 Cookie"
DefaultButton="Primary" DefaultButton="Primary"
PrimaryButtonText="请输入Cookie" PrimaryButtonText="请输入Cookie"
CloseButtonText="取消" CloseButtonText="取消"
@@ -20,17 +20,19 @@
TextChanged="InputTextChanged" TextChanged="InputTextChanged"
PlaceholderText="在此处输入" PlaceholderText="在此处输入"
VerticalAlignment="Top"/> VerticalAlignment="Top"/>
<TextBlock Margin="0,4,0,0" Text="接受包含 Cookie / LoginTicket / Stoken 的字符串"/>
<settings:SettingsGroup Margin="0,-48,0,0"> <settings:SettingsGroup Margin="0,-48,0,0">
<settings:Setting <settings:Setting
Icon="&#xEB41;" Icon="&#xEB41;"
Header="操作文档" Header="操作文档"
Description="进入我们的文档页面并按指示操作" Description="进入文档页面并按指示操作"
HorizontalAlignment="Stretch"> HorizontalAlignment="Stretch">
<HyperlinkButton <HyperlinkButton
Margin="12,0,0,0" Margin="12,0,0,0"
Padding="6" Padding="6"
Content="立即前往" Content="立即前往"
NavigateUri="https://www.snapgenshin.com/documents/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie"/> NavigateUri="https://hut.ao/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie"/>
</settings:Setting> </settings:Setting>
</settings:SettingsGroup> </settings:SettingsGroup>
</StackPanel> </StackPanel>

View File

@@ -1,19 +1,32 @@
<control:ScopedPage <shc:ScopedPage
x:Class="Snap.Hutao.View.Page.LaunchGamePage" x:Class="Snap.Hutao.View.Page.LaunchGamePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
xmlns:mxim="using:Microsoft.Xaml.Interactions.Media"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:sc="using:SettingsUI.Controls" xmlns:sc="using:SettingsUI.Controls"
xmlns:control="using:Snap.Hutao.Control" xmlns:shc="using:Snap.Hutao.Control"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shv="using:Snap.Hutao.ViewModel"
mc:Ignorable="d" mc:Ignorable="d"
d:DataContext="{d:DesignInstance shv:LaunchGameViewModel}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Page.Resources> <Page.Resources>
<shc:BindingProxy x:Key="BindingProxy" DataContext="{Binding}"/>
<Style TargetType="Button" BasedOn="{StaticResource SettingButtonStyle}"> <Style TargetType="Button" BasedOn="{StaticResource SettingButtonStyle}">
<Setter Property="MinWidth" Value="120"/> <Setter Property="MinWidth" Value="160"/>
</Style> </Style>
<Style TargetType="HyperlinkButton" BasedOn="{StaticResource HyperlinkButtonStyle}"> <Style TargetType="HyperlinkButton" BasedOn="{StaticResource HyperlinkButtonStyle}">
<Setter Property="MinWidth" Value="120"/> <Setter Property="MinWidth" Value="160"/>
</Style> </Style>
</Page.Resources> </Page.Resources>
<Grid> <Grid>
@@ -34,10 +47,14 @@
Header="服务器" Header="服务器"
Description="切换游戏服务器B服用户需要自备额外的 PCGameSDK.dll 文件"> Description="切换游戏服务器B服用户需要自备额外的 PCGameSDK.dll 文件">
<sc:Setting.ActionContent> <sc:Setting.ActionContent>
<ComboBox Width="120"/> <ComboBox
Width="160"
ItemsSource="{Binding KnownSchemes}"
SelectedItem="{Binding SelectedScheme,Mode=TwoWay}"
DisplayMemberPath="Name"/>
</sc:Setting.ActionContent> </sc:Setting.ActionContent>
</sc:Setting> </sc:Setting>
<sc:SettingExpander> <sc:SettingExpander IsExpanded="True">
<sc:SettingExpander.Header> <sc:SettingExpander.Header>
<Grid Padding="0,16"> <Grid Padding="0,16">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
@@ -45,7 +62,6 @@
<StackPanel VerticalAlignment="Center"> <StackPanel VerticalAlignment="Center">
<TextBlock <TextBlock
Margin="20,0,0,0" Margin="20,0,0,0"
Text="账号"/> Text="账号"/>
<TextBlock <TextBlock
Opacity="0.8" Opacity="0.8"
@@ -53,19 +69,103 @@
Style="{StaticResource CaptionTextBlockStyle}" Style="{StaticResource CaptionTextBlockStyle}"
Text="在游戏内切换账号,网络环境发生变化后需要重新手动检测"/> Text="在游戏内切换账号,网络环境发生变化后需要重新手动检测"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<Button <Button
HorizontalAlignment="Right" HorizontalAlignment="Right"
Command="{Binding DetectGameAccountCommand}"
Grid.Column="1" Grid.Column="1"
Margin="0,0,8,0" Margin="0,0,8,0"
Width="80" Width="128"
MinWidth="88" MinWidth="128"
Content="检测"/> Content="检测"/>
</Grid> </Grid>
</sc:SettingExpander.Header> </sc:SettingExpander.Header>
<ListView
ItemsSource="{Binding GameAccounts}"
SelectedItem="{Binding SelectedGameAccount,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<StackPanel Margin="0,12">
<TextBlock Text="{Binding Name}"/>
<TextBlock
Opacity="0.8"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding AttachUid,TargetNullValue=该账号尚未绑定 UID}"/>
</StackPanel>
<StackPanel
x:Name="ButtonPanel"
Visibility="Collapsed"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="4,8"
MinWidth="48"
VerticalAlignment="Stretch"
ToolTipService.ToolTip="绑定当前用户角色"
Content="&#xE723;"
Command="{Binding DataContext.AttachGameAccountCommand,Source={StaticResource BindingProxy}}"
CommandParameter="{Binding}"
FontFamily="{StaticResource SymbolThemeFontFamily}"/>
<Button
Margin="4,8"
MinWidth="48"
VerticalAlignment="Stretch"
ToolTipService.ToolTip="重命名"
Content="&#xE8AC;"
Command="{Binding DataContext.ModifyGameAccountCommand,Source={StaticResource BindingProxy}}"
CommandParameter="{Binding}"
FontFamily="{StaticResource SymbolThemeFontFamily}"/>
<Button
Margin="4,8"
MinWidth="48"
VerticalAlignment="Stretch"
ToolTipService.ToolTip="删除"
Content="&#xE74D;"
Command="{Binding DataContext.RemoveGameAccountCommand,Source={StaticResource BindingProxy}}"
CommandParameter="{Binding}"
FontFamily="{StaticResource SymbolThemeFontFamily}"/>
</StackPanel>
<Grid.Resources>
<Storyboard x:Name="ButtonPanelVisibleStoryboard">
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ButtonPanel"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Name="ButtonPanelCollapsedStoryboard">
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ButtonPanel"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</Grid.Resources>
<mxi:Interaction.Behaviors>
<mxic:EventTriggerBehavior EventName="PointerEntered">
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelVisibleStoryboard}"/>
</mxic:EventTriggerBehavior>
<mxic:EventTriggerBehavior EventName="PointerExited">
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelCollapsedStoryboard}"/>
</mxic:EventTriggerBehavior>
</mxi:Interaction.Behaviors>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</sc:SettingExpander> </sc:SettingExpander>
</sc:SettingsGroup> </sc:SettingsGroup>
<sc:SettingsGroup Header="外观"> <sc:SettingsGroup Header="外观">
@@ -75,6 +175,7 @@
Description="覆盖默认的全屏状态"> Description="覆盖默认的全屏状态">
<sc:Setting.ActionContent> <sc:Setting.ActionContent>
<ToggleSwitch <ToggleSwitch
IsOn="{Binding IsFullScreen,Mode=TwoWay}"
Style="{StaticResource ToggleSwitchSettingStyle}" Style="{StaticResource ToggleSwitchSettingStyle}"
Width="120"/> Width="120"/>
</sc:Setting.ActionContent> </sc:Setting.ActionContent>
@@ -85,6 +186,7 @@
Description="将窗口创建为弹出窗口,不带框架"> Description="将窗口创建为弹出窗口,不带框架">
<sc:Setting.ActionContent> <sc:Setting.ActionContent>
<ToggleSwitch <ToggleSwitch
IsOn="{Binding IsBorderless,Mode=TwoWay}"
Style="{StaticResource ToggleSwitchSettingStyle}" Style="{StaticResource ToggleSwitchSettingStyle}"
Width="120"/> Width="120"/>
</sc:Setting.ActionContent> </sc:Setting.ActionContent>
@@ -97,7 +199,8 @@
Description="覆盖默认屏幕宽度"> Description="覆盖默认屏幕宽度">
<sc:Setting.ActionContent> <sc:Setting.ActionContent>
<NumberBox <NumberBox
Width="120"/> Value="{Binding ScreenWidth,Mode=TwoWay}"
Width="160"/>
</sc:Setting.ActionContent> </sc:Setting.ActionContent>
</sc:Setting> </sc:Setting>
<sc:Setting <sc:Setting
@@ -106,18 +209,20 @@
Description="覆盖默认屏幕高度"> Description="覆盖默认屏幕高度">
<sc:Setting.ActionContent> <sc:Setting.ActionContent>
<NumberBox <NumberBox
Width="120"/> Value="{Binding ScreenHeight,Mode=TwoWay}"
Width="160"/>
</sc:Setting.ActionContent> </sc:Setting.ActionContent>
</sc:Setting> </sc:Setting>
</sc:SettingsGroup> </sc:SettingsGroup>
<sc:SettingsGroup Header="Dangerous feature"> <sc:SettingsGroup Header="Dangerous Feature" IsEnabled="{Binding IsElevated}">
<sc:Setting <sc:Setting
Icon="&#xE785;" Icon="&#xE785;"
Header="Unlock frame rate limit" Header="Unlock frame rate limit"
Description="Requires administrator privilege.&#10;Otherwise the option does not take effect."> Description="Requires administrator privilege. Otherwise the option will be disabled.">
<sc:Setting.ActionContent> <sc:Setting.ActionContent>
<ToggleSwitch <ToggleSwitch
IsOn="{Binding UnlockFps,Mode=TwoWay}"
OnContent="Enable" OnContent="Enable"
OffContent="Disable" OffContent="Disable"
Style="{StaticResource ToggleSwitchSettingStyle}" Style="{StaticResource ToggleSwitchSettingStyle}"
@@ -126,9 +231,10 @@
</sc:Setting> </sc:Setting>
<sc:Setting <sc:Setting
Header="Set frame rate" Header="Set frame rate"
Description="60"> Description="{Binding TargetFps}">
<sc:Setting.ActionContent> <sc:Setting.ActionContent>
<Slider <Slider
Value="{Binding TargetFps,Mode=TwoWay}"
Minimum="60" Minimum="60"
Maximum="360" Maximum="360"
Width="400"/> Width="400"/>
@@ -143,6 +249,7 @@
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Background="{StaticResource SystemControlAcrylicElementMediumHighBrush}"> Background="{StaticResource SystemControlAcrylicElementMediumHighBrush}">
<Button <Button
Command="{Binding LaunchCommand}"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Grid.Column="3" Grid.Column="3"
Margin="24" Margin="24"
@@ -150,4 +257,4 @@
Content="启动游戏"/> Content="启动游戏"/>
</Grid> </Grid>
</Grid> </Grid>
</control:ScopedPage> </shc:ScopedPage>

View File

@@ -63,7 +63,7 @@ public sealed partial class LoginMihoyoBBSPage : Microsoft.UI.Xaml.Controls.Page
infoBarService.Information($"此 Cookie 不完整,操作失败"); infoBarService.Information($"此 Cookie 不完整,操作失败");
break; break;
case UserOptionResult.Invalid: case UserOptionResult.Invalid:
infoBarService.Information($"此 Cookie 无,操作失败"); infoBarService.Information($"此 Cookie 无,操作失败");
break; break;
case UserOptionResult.Updated: case UserOptionResult.Updated:
infoBarService.Success($"用户 [{nickname}] 更新成功"); infoBarService.Success($"用户 [{nickname}] 更新成功");

View File

@@ -28,7 +28,6 @@ public sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.Pag
private async void OnRootLoaded(object sender, RoutedEventArgs e) private async void OnRootLoaded(object sender, RoutedEventArgs e)
{ {
await WebView.EnsureCoreWebView2Async(); await WebView.EnsureCoreWebView2Async();
WebView.CoreWebView2.SourceChanged += OnCoreWebView2SourceChanged;
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager; CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com"); IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
@@ -40,23 +39,10 @@ public sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.Pag
WebView.CoreWebView2.Navigate("https://user.mihoyo.com/#/login/password"); WebView.CoreWebView2.Navigate("https://user.mihoyo.com/#/login/password");
} }
[SuppressMessage("", "VSTHRD100")]
private async void OnCoreWebView2SourceChanged(CoreWebView2 sender, CoreWebView2SourceChangedEventArgs args)
{
if (sender != null)
{
if (sender.Source.ToString() == "https://user.mihoyo.com/#/account/home")
{
await HandleCurrentCookieAsync().ConfigureAwait(false);
}
}
}
private async Task HandleCurrentCookieAsync() private async Task HandleCurrentCookieAsync()
{ {
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager; CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com"); IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
WebView.CoreWebView2.SourceChanged -= OnCoreWebView2SourceChanged;
Cookie cookie = Cookie.FromCoreWebView2Cookies(cookies); Cookie cookie = Cookie.FromCoreWebView2Cookies(cookies);
IUserService userService = Ioc.Default.GetRequiredService<IUserService>(); IUserService userService = Ioc.Default.GetRequiredService<IUserService>();

View File

@@ -22,7 +22,7 @@
Background="Transparent" Background="Transparent"
BorderBrush="{x:Null}" BorderBrush="{x:Null}"
Grid.Column="2" Grid.Column="2"
Margin="4"> Margin="4,4,4,6">
<Button.Resources> <Button.Resources>
<shc:BindingProxy <shc:BindingProxy
x:Key="ViewModelBindingProxy" x:Key="ViewModelBindingProxy"
@@ -210,8 +210,8 @@
<AppBarButton Label="网页登录" Icon="{shcm:FontIcon Glyph=&#xEB41;}"> <AppBarButton Label="网页登录" Icon="{shcm:FontIcon Glyph=&#xEB41;}">
<AppBarButton.Flyout> <AppBarButton.Flyout>
<MenuFlyout> <MenuFlyout>
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=&#xF4A5;}" Text="登录米游社原神社区" Command="{Binding LoginMihoyoBBSCommand}"/> <MenuFlyoutItem Icon="{shcm:FontIcon Glyph=&#xF4A5;}" Text="添加 Cookie" Command="{Binding LoginMihoyoBBSCommand}"/>
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=&#xF4A5;}" Text="登录米哈游通行证" Command="{Binding LoginMihoyoUserCommand}"/> <MenuFlyoutItem Icon="{shcm:FontIcon Glyph=&#xF4A5;}" Text="升级 Stoken" Command="{Binding LoginMihoyoUserCommand}"/>
</MenuFlyout> </MenuFlyout>
</AppBarButton.Flyout> </AppBarButton.Flyout>
</AppBarButton> </AppBarButton>

View File

@@ -2,11 +2,19 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Control; using Snap.Hutao.Control;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Factory.Abstraction; using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.LaunchGame; using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Game; using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Snap.Hutao.ViewModel; namespace Snap.Hutao.ViewModel;
@@ -17,7 +25,11 @@ namespace Snap.Hutao.ViewModel;
[Injection(InjectAs.Scoped)] [Injection(InjectAs.Scoped)]
internal class LaunchGameViewModel : ObservableObject, ISupportCancellation internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
{ {
private static readonly string TrueString = true.ToString();
private static readonly string FalseString = false.ToString();
private readonly IGameService gameService; private readonly IGameService gameService;
private readonly AppDbContext appDbContext;
private readonly List<LaunchScheme> knownSchemes = new() private readonly List<LaunchScheme> knownSchemes = new()
{ {
@@ -41,16 +53,19 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
/// 构造一个新的启动游戏视图模型 /// 构造一个新的启动游戏视图模型
/// </summary> /// </summary>
/// <param name="gameService">游戏服务</param> /// <param name="gameService">游戏服务</param>
/// <param name="appDbContext">数据库上下文</param>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param> /// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public LaunchGameViewModel(IGameService gameService, IAsyncRelayCommandFactory asyncRelayCommandFactory) public LaunchGameViewModel(IGameService gameService, AppDbContext appDbContext, IAsyncRelayCommandFactory asyncRelayCommandFactory)
{ {
this.gameService = gameService; this.gameService = gameService;
this.appDbContext = appDbContext;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync); OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
LaunchCommand = asyncRelayCommandFactory.Create(LaunchAsync); LaunchCommand = asyncRelayCommandFactory.Create(LaunchAsync);
DetectGameAccountCommand = asyncRelayCommandFactory.Create(DetectGameAccountAsync); DetectGameAccountCommand = asyncRelayCommandFactory.Create(DetectGameAccountAsync);
ModifyGameAccountCommand = asyncRelayCommandFactory.Create(ModifyGameAccountAsync); ModifyGameAccountCommand = asyncRelayCommandFactory.Create<GameAccount>(ModifyGameAccountAsync);
RemoveGameAccountCommand = asyncRelayCommandFactory.Create(RemoveGameAccountAsync); RemoveGameAccountCommand = asyncRelayCommandFactory.Create<GameAccount>(RemoveGameAccountAsync);
AttachGameAccountCommand = new RelayCommand<GameAccount>(AttachGameAccountToCurrentUserGameRole);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -106,6 +121,12 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
/// </summary> /// </summary>
public int TargetFps { get => targetFps; set => SetProperty(ref targetFps, value); } public int TargetFps { get => targetFps; set => SetProperty(ref targetFps, value); }
/// <summary>
/// 是否提权
/// </summary>
[SuppressMessage("Performance", "CA1822")]
public bool IsElevated { get => Activation.GetElevated(); }
/// <summary> /// <summary>
/// 打开界面命令 /// 打开界面命令
/// </summary> /// </summary>
@@ -131,6 +152,11 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
/// </summary> /// </summary>
public ICommand RemoveGameAccountCommand { get; } public ICommand RemoveGameAccountCommand { get; }
/// <summary>
/// 绑定到Uid命令
/// </summary>
public ICommand AttachGameAccountCommand { get; }
private async Task OpenUIAsync() private async Task OpenUIAsync()
{ {
(bool isOk, string gamePath) = await gameService.GetGamePathAsync().ConfigureAwait(false); (bool isOk, string gamePath) = await gameService.GetGamePathAsync().ConfigureAwait(false);
@@ -139,26 +165,110 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
{ {
MultiChannel multi = gameService.GetMultiChannel(); MultiChannel multi = gameService.GetMultiChannel();
SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel); SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
GameAccounts = gameService.GetGameAccountCollection();
// Sync from Settings
RetiveSetting();
} }
} }
private void RetiveSetting()
{
DbSet<SettingEntry> settings = appDbContext.Settings;
isFullScreen = settings.SingleOrAdd(SettingEntry.LaunchIsFullScreen, TrueString).GetBoolean();
OnPropertyChanged(nameof(IsFullScreen));
isBorderless = settings.SingleOrAdd(SettingEntry.LaunchIsBorderless, FalseString).GetBoolean();
OnPropertyChanged(nameof(IsBorderless));
screenWidth = settings.SingleOrAdd(SettingEntry.LaunchScreenWidth, "1920").GetInt32();
OnPropertyChanged(nameof(ScreenWidth));
screenHeight = settings.SingleOrAdd(SettingEntry.LaunchScreenHeight, "1080").GetInt32();
OnPropertyChanged(nameof(ScreenHeight));
unlockFps = settings.SingleOrAdd(SettingEntry.LaunchUnlockFps, FalseString).GetBoolean();
OnPropertyChanged(nameof(UnlockFps));
targetFps = settings.SingleOrAdd(SettingEntry.LaunchTargetFps, "60").GetInt32();
OnPropertyChanged(nameof(TargetFps));
}
private void SaveSetting()
{
DbSet<SettingEntry> settings = appDbContext.Settings;
settings.SingleOrAdd(SettingEntry.LaunchIsFullScreen, TrueString).SetBoolean(IsFullScreen);
settings.SingleOrAdd(SettingEntry.LaunchIsBorderless, FalseString).SetBoolean(IsBorderless);
settings.SingleOrAdd(SettingEntry.LaunchScreenWidth, "1920").SetInt32(ScreenWidth);
settings.SingleOrAdd(SettingEntry.LaunchScreenHeight, "1080").SetInt32(ScreenHeight);
settings.SingleOrAdd(SettingEntry.LaunchUnlockFps, FalseString).SetBoolean(UnlockFps);
settings.SingleOrAdd(SettingEntry.LaunchTargetFps, "60").SetInt32(TargetFps);
appDbContext.SaveChanges();
}
private async Task LaunchAsync() private async Task LaunchAsync()
{ {
if (gameService.IsGameRunning())
{
return;
}
if (SelectedScheme != null)
{
gameService.SetMultiChannel(SelectedScheme);
}
if (SelectedGameAccount != null)
{
if (!gameService.SetGameAccount(SelectedGameAccount))
{
Ioc.Default.GetRequiredService<IInfoBarService>().Warning("切换账号失败");
}
}
SaveSetting();
LaunchConfiguration configuration = new(IsFullScreen, IsBorderless, ScreenWidth, ScreenHeight, IsElevated && UnlockFps, TargetFps);
await gameService.LaunchAsync(configuration).ConfigureAwait(false);
} }
private async Task DetectGameAccountAsync() private async Task DetectGameAccountAsync()
{ {
await gameService.DetectGameAccountAsync().ConfigureAwait(false);
} }
private async Task ModifyGameAccountAsync() private void AttachGameAccountToCurrentUserGameRole(GameAccount? gameAccount)
{ {
if (gameAccount != null)
}
private async Task RemoveGameAccountAsync()
{ {
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
if (userService.Current?.SelectedUserGameRole is UserGameRole role)
{
gameService.AttachGameAccountToUid(gameAccount, role.GameUid);
}
else
{
infoBarService.Warning("当前未选择角色");
}
}
}
private async Task ModifyGameAccountAsync(GameAccount? gameAccount)
{
if (gameAccount != null)
{
await gameService.ModifyGameAccountAsync(gameAccount).ConfigureAwait(false);
}
}
private async Task RemoveGameAccountAsync(GameAccount? gameAccount)
{
if (gameAccount != null)
{
await gameService.RemoveGameAccountAsync(gameAccount).ConfigureAwait(false);
}
} }
} }

View File

@@ -126,7 +126,7 @@ internal class UserViewModel : ObservableObject
infoBarService.Information($"此 Cookie 不完整,操作失败"); infoBarService.Information($"此 Cookie 不完整,操作失败");
break; break;
case UserOptionResult.Invalid: case UserOptionResult.Invalid:
infoBarService.Information($"此 Cookie 无,操作失败"); infoBarService.Information($"此 Cookie 无,操作失败");
break; break;
case UserOptionResult.Updated: case UserOptionResult.Updated:
infoBarService.Success($"用户 [{uid}] 更新成功"); infoBarService.Success($"用户 [{uid}] 更新成功");
@@ -162,7 +162,7 @@ internal class UserViewModel : ObservableObject
Verify.Operation(user != null, "待复制 Cookie 的用户不应为 null"); Verify.Operation(user != null, "待复制 Cookie 的用户不应为 null");
try try
{ {
Clipboard.SetText(user.Cookie.ToString()); Clipboard.SetText(user.Cookie!.ToString());
infoBarService.Success($"{user.UserInfo!.Nickname} 的 Cookie 复制成功"); infoBarService.Success($"{user.UserInfo!.Nickname} 的 Cookie 复制成功");
} }
catch (Exception e) catch (Exception e)

View File

@@ -68,7 +68,7 @@ internal static class HttpClientExtensions
/// <returns>客户端</returns> /// <returns>客户端</returns>
internal static HttpClient SetUser(this HttpClient httpClient, User user) internal static HttpClient SetUser(this HttpClient httpClient, User user)
{ {
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie.ToString()); httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie!.ToString());
return httpClient; return httpClient;
} }