prevent user service capture scoped app db context

This commit is contained in:
DismissedLight
2022-10-30 15:19:21 +08:00
parent 62d0fb5d05
commit 848392f8d4
32 changed files with 883 additions and 167 deletions

View File

@@ -21,7 +21,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -56,6 +56,11 @@ public class AppDbContext : DbContext
/// </summary>
public DbSet<AvatarInfo> AvatarInfos { get; set; } = default!;
/// <summary>
/// 游戏内账号
/// </summary>
public DbSet<GameAccount> GameAccounts { get; set; } = default!;
/// <summary>
/// 构造一个临时的应用程序数据库上下文
/// </summary>

View File

@@ -9,8 +9,9 @@ namespace Snap.Hutao.Core.Threading;
internal static class ThreadHelper
{
/// <summary>
/// 异步切换到主线程
/// 使用此静态方法以 异步切换到 主线程
/// </summary>
/// <remarks>使用 <see cref="Task.Yield"/> 异步切换到 后台线程</remarks>
/// <returns>等待体</returns>
public static DispatherQueueSwitchOperation SwitchToMainThreadAsync()
{

View File

@@ -13,7 +13,12 @@ public class HistoryWish : WishBase
/// <summary>
/// 版本
/// </summary>
public string Version { get; set; }
public string Version { get; set; } = default!;
/// <summary>
/// 卡池图片
/// </summary>
public Uri BannerImage { get; set; } = default!;
/// <summary>
/// 五星Up

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.LaunchGame;
/// <summary>
/// 服务器方案
/// </summary>
/// <summary>
/// 启动方案
/// </summary>
public class LaunchScheme
{
/// <summary>
/// 构造一个新的启动方案
/// </summary>
/// <param name="name">名称</param>
/// <param name="channel">通道</param>
/// <param name="cps">通道描述字符串</param>
/// <param name="subChannel">子通道</param>
public LaunchScheme(string name, string channel, string subChannel)
{
Name = name;
Channel = channel;
SubChannel = subChannel;
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 通道
/// </summary>
public string Channel { get; set; }
/// <summary>
/// 子通道
/// </summary>
public string SubChannel { get; set; }
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Binding.LaunchGame;
/// <summary>
/// 启动类型
/// </summary>
public enum SchemeType
{
/// <summary>
/// 国际服
/// </summary>
Mihoyo,
/// <summary>
/// 国服官服
/// </summary>
Officical,
/// <summary>
/// 渠道服
/// </summary>
Bilibili,
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Web.Hoyolab;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Snap.Hutao.Model.Entity;
/// <summary>
/// 游戏内账号
/// </summary>
[Table("game_accounts")]
public class GameAccount : ISelectable
{
/// <summary>
/// 内部Id
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <inheritdoc/>
public bool IsSelected { get; set; }
/// <summary>
/// 对应的Uid
/// </summary>
public string? AttachUid { get; set; }
/// <summary>
/// 服务器类型
/// </summary>
public SchemeType Type { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// MIHOYOSDK_ADL_PROD_CN_h3123967166
/// </summary>
public string MihoyoSDK { get; set; } = default!;
}

View File

@@ -40,4 +40,4 @@ public class User : ISelectable
{
return new() { Cookie = cookie };
}
}
}

View File

@@ -20,6 +20,11 @@ public class GachaEvent
/// </summary>
public string Version { get; set; } = default!;
/// <summary>
/// 卡池图
/// </summary>
public Uri Banner { get; set; } = default!;
/// <summary>
/// 开始时间
/// </summary>

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
namespace Snap.Hutao.Model;
/// <summary>
/// 封装带有名称描述的值
/// 在绑定枚举变量时非常有用
/// </summary>
/// <typeparam name="T">包含值的类型</typeparam>
public class NamedValue<T>
{
/// <summary>
/// 构造一个新的命名的值
/// </summary>
/// <param name="name">命名</param>
/// <param name="value">值</param>
public NamedValue(string name, T value)
{
Name = name;
Value = value;
}
/// <summary>
/// 名称
/// </summary>
public string Name { get; }
/// <summary>
/// 值
/// </summary>
public T Value { get; }
}

View File

@@ -46,4 +46,4 @@ public class Selectable<T> : ObservableObject
/// 存放的对象
/// </summary>
public T Value { get => value; set => SetProperty(ref this.value, value); }
}
}

View File

@@ -148,6 +148,7 @@ internal class HistoryWishBuilder
// fill
Version = gachaEvent.Version,
BannerImage = gachaEvent.Banner,
OrangeUpList = orangeUpCounter.ToStatisticsList(),
PurpleUpList = purpleUpCounter.ToStatisticsList(),
OrangeList = orangeCounter.ToStatisticsList(),

View File

@@ -6,10 +6,12 @@ using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.IO.Ini;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Game.Unlocker;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
@@ -27,6 +29,8 @@ internal class GameService : IGameService
private readonly IMemoryCache memoryCache;
private readonly SemaphoreSlim gameSemaphore = new(1);
private ObservableCollection<GameAccount>? gameAccounts;
/// <summary>
/// 构造一个新的游戏服务
/// </summary>
@@ -128,52 +132,78 @@ internal class GameService : IGameService
}
}
/// <inheritdoc/>
public MultiChannel GetMultiChannel()
{
string gamePath = GetGamePathSkipLocator();
string configPath = Path.Combine(gamePath, "config.ini");
using (FileStream stream = File.OpenRead(configPath))
{
List<IniElement> elements = IniSerializer.Deserialize(stream).ToList();
string? channel = elements.OfType<IniParameter>().FirstOrDefault(p => p.Key == "channel")?.Value;
string? subChannel = elements.OfType<IniParameter>().FirstOrDefault(p => p.Key == "sub_channel")?.Value;
return new(channel, subChannel);
}
}
public async Task<ObservableCollection<GameAccount>> GetGameAccountCollectionAsync()
{
if (gameAccounts == null)
{
}
return gameAccounts;
}
/// <inheritdoc/>
public async ValueTask LaunchAsync(LaunchConfiguration configuration)
{
(bool isOk, string gamePath) = await GetGamePathAsync().ConfigureAwait(false);
if (isOk)
if (gameSemaphore.CurrentCount == 0)
{
if (gameSemaphore.CurrentCount == 0)
return;
}
string gamePath = GetGamePathSkipLocator();
string commandLine = new CommandLineBuilder()
.AppendIf("-popupwindow", configuration.IsBorderless)
.Append("-screen-fullscreen", configuration.IsFullScreen ? 1 : 0)
.Append("-screen-width", configuration.ScreenWidth)
.Append("-screen-height", configuration.ScreenHeight)
.Append("-monitor", configuration.Monitor)
.Build();
Process game = new()
{
StartInfo = new()
{
return;
Arguments = commandLine,
FileName = gamePath,
UseShellExecute = true,
Verb = "runas",
WorkingDirectory = Path.GetDirectoryName(gamePath),
},
};
using (await gameSemaphore.EnterAsync().ConfigureAwait(false))
{
if (configuration.UnlockFPS)
{
IGameFpsUnlocker unlocker = new GameFpsUnlocker(game, configuration.TargetFPS);
TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100);
TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000);
TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000);
await unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay).ConfigureAwait(false);
}
string commandLine = new CommandLineBuilder()
.Append("-window-mode", configuration.WindowMode)
.Append("-screen-fullscreen", configuration.IsFullScreen ? 1 : 0)
.Append("-screen-width", configuration.ScreenWidth)
.Append("-screen-height", configuration.ScreenHeight)
.Append("-monitor", configuration.Monitor)
.Build();
Process game = new()
else
{
StartInfo = new()
if (game.Start())
{
Arguments = commandLine,
FileName = gamePath,
UseShellExecute = true,
Verb = "runas",
WorkingDirectory = Path.GetDirectoryName(gamePath),
},
};
using (await gameSemaphore.EnterAsync().ConfigureAwait(false))
{
if (configuration.UnlockFPS)
{
IGameFpsUnlocker unlocker = new GameFpsUnlocker(game, configuration.TargetFPS);
await unlocker.UnlockAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(10000), TimeSpan.FromMilliseconds(2000)).ConfigureAwait(false);
}
else
{
if (game.Start())
{
await game.WaitForExitAsync().ConfigureAwait(false);
}
await game.WaitForExitAsync().ConfigureAwait(false);
}
}
}

View File

@@ -22,6 +22,12 @@ internal interface IGameService
/// <returns>游戏路径,当路径无效时会设置并返回 <see cref="string.Empty"/></returns>
string GetGamePathSkipLocator();
/// <summary>
/// 获取多通道值
/// </summary>
/// <returns>多通道值</returns>
MultiChannel GetMultiChannel();
/// <summary>
/// 异步启动
/// </summary>

View File

@@ -14,9 +14,9 @@ internal struct LaunchConfiguration
public bool IsFullScreen { get; set; }
/// <summary>
/// Override fullscreen windowed mode. Accepted values are exclusive or borderless.
/// 是否为无边框窗口
/// </summary>
public string WindowMode { get; private set; }
public bool IsBorderless { get; private set; }
/// <summary>
/// 是否启用解锁帧率

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 多通道
/// </summary>
public struct MultiChannel
{
/// <summary>
/// 通道
/// </summary>
public string Channel;
/// <summary>
/// 子通道
/// </summary>
public string SubChannel;
/// <summary>
/// 构造一个新的多通道
/// </summary>
/// <param name="channel">通道</param>
/// <param name="subChannel">子通道</param>
public MultiChannel(string? channel, string? subChannel)
{
Channel = channel ?? string.Empty;
SubChannel = subChannel ?? string.Empty;
}
}

View File

@@ -72,4 +72,9 @@ public interface INavigationService
/// <param name="pageType">同步的页面类型</param>
/// <returns>是否同步成功</returns>
bool SyncSelectedNavigationViewItemWith(Type pageType);
/// <summary>
/// 尽可能尝试返回
/// </summary>
void GoBack();
}

View File

@@ -178,6 +178,18 @@ internal class NavigationService : INavigationService
NavigationView.IsPaneOpen = LocalSetting.Get(SettingKeys.IsNavPaneOpen, true);
}
/// <inheritdoc/>
public void GoBack()
{
bool canGoBack = Frame?.CanGoBack ?? false;
if (canGoBack)
{
Frame!.GoBack();
SyncSelectedNavigationViewItemWith(Frame.Content.GetType());
}
}
/// <summary>
/// 遍历所有子菜单项
/// </summary>

View File

@@ -2,7 +2,9 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
@@ -20,10 +22,7 @@ namespace Snap.Hutao.Service.User;
[Injection(InjectAs.Singleton, typeof(IUserService))]
internal class UserService : IUserService
{
private readonly AppDbContext appDbContext;
private readonly UserClient userClient;
private readonly BindingClient bindingClient;
private readonly AuthClient authClient;
private readonly IServiceScopeFactory scopeFactory;
private readonly IMessenger messenger;
private BindingUser? currentUser;
@@ -32,22 +31,11 @@ internal class UserService : IUserService
/// <summary>
/// 构造一个新的用户服务
/// </summary>
/// <param name="appDbContext">应用程序数据库上下文</param>
/// <param name="userClient">用户客户端</param>
/// <param name="bindingClient">角色客户端</param>
/// <param name="authClient">验证客户端</param>
/// <param name="scopeFactory">范围工厂</param>
/// <param name="messenger">消息器</param>
public UserService(
AppDbContext appDbContext,
UserClient userClient,
BindingClient bindingClient,
AuthClient authClient,
IMessenger messenger)
public UserService(IServiceScopeFactory scopeFactory, IMessenger messenger)
{
this.appDbContext = appDbContext;
this.userClient = userClient;
this.bindingClient = bindingClient;
this.authClient = authClient;
this.scopeFactory = scopeFactory;
this.messenger = messenger;
}
@@ -62,44 +50,54 @@ internal class UserService : IUserService
return;
}
// only update when not processing a deletion
if (value != null)
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// only update when not processing a deletion
if (value != null)
{
if (currentUser != null)
{
currentUser.IsSelected = false;
appDbContext.Users.Update(currentUser.Entity);
appDbContext.SaveChanges();
}
}
Message.UserChangedMessage message = new() { OldValue = currentUser, NewValue = value };
// 当删除到无用户时也能正常反应状态
currentUser = value;
if (currentUser != null)
{
currentUser.IsSelected = false;
currentUser.IsSelected = true;
appDbContext.Users.Update(currentUser.Entity);
appDbContext.SaveChanges();
}
messenger.Send(message);
}
Message.UserChangedMessage message = new() { OldValue = currentUser, NewValue = value };
// 当删除到无用户时也能正常反应状态
currentUser = value;
if (currentUser != null)
{
currentUser.IsSelected = true;
appDbContext.Users.Update(currentUser.Entity);
appDbContext.SaveChanges();
}
messenger.Send(message);
}
}
/// <inheritdoc/>
public Task RemoveUserAsync(BindingUser user)
public async Task RemoveUserAsync(BindingUser user)
{
await Task.Yield();
Must.NotNull(userCollection!);
// Sync cache
userCollection.Remove(user);
// Sync database
appDbContext.Users.Remove(user.Entity);
return appDbContext.SaveChangesAsync();
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.Users.RemoveAndSave(user.Entity);
}
}
/// <inheritdoc/>
@@ -109,21 +107,27 @@ internal class UserService : IUserService
{
List<BindingUser> users = new();
foreach (Model.Entity.User entity in appDbContext.Users)
using (IServiceScope scope = scopeFactory.CreateScope())
{
BindingUser? initialized = await BindingUser
.ResumeAsync(entity, userClient, bindingClient)
.ConfigureAwait(false);
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
UserClient userClient = scope.ServiceProvider.GetRequiredService<UserClient>();
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService<BindingClient>();
if (initialized != null)
foreach (Model.Entity.User entity in appDbContext.Users)
{
users.Add(initialized);
}
else
{
// User is unable to be initialized, remove it.
appDbContext.Users.Remove(entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
BindingUser? initialized = await BindingUser
.ResumeAsync(entity, userClient, bindingClient)
.ConfigureAwait(false);
if (initialized != null)
{
users.Add(initialized);
}
else
{
// User is unable to be initialized, remove it.
appDbContext.Users.RemoveAndSave(entity);
}
}
}
@@ -150,24 +154,27 @@ internal class UserService : IUserService
// 检查 uid 对应用户是否存在
if (UserHelper.TryGetUserByUid(userCollection, uid, out BindingUser? userWithSameUid))
{
// 检查 stoken 是否存在
if (cookie.ContainsSToken())
using (IServiceScope scope = scopeFactory.CreateScope())
{
// insert stoken
userWithSameUid.UpdateSToken(uid, cookie);
appDbContext.Users.Update(userWithSameUid.Entity);
appDbContext.SaveChanges();
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return new(UserOptionResult.Upgraded, uid);
}
// 检查 stoken 是否存在
if (cookie.ContainsSToken())
{
// insert stoken
userWithSameUid.UpdateSToken(uid, cookie);
appDbContext.Users.UpdateAndSave(userWithSameUid.Entity);
if (cookie.ContainsLTokenAndCookieToken())
{
userWithSameUid.Cookie = cookie;
appDbContext.Users.Update(userWithSameUid.Entity);
appDbContext.SaveChanges();
return new(UserOptionResult.Upgraded, uid);
}
return new(UserOptionResult.Updated, uid);
if (cookie.ContainsLTokenAndCookieToken())
{
userWithSameUid.Cookie = cookie;
appDbContext.Users.UpdateAndSave(userWithSameUid.Entity);
return new(UserOptionResult.Updated, uid);
}
}
}
else if (cookie.ContainsLTokenAndCookieToken())
@@ -184,7 +191,8 @@ internal class UserService : IUserService
if (cookie.TryGetLoginTicket(out string? loginTicket))
{
// get multitoken
Dictionary<string, string> multiToken = await authClient
Dictionary<string, string> multiToken = await Ioc.Default
.GetRequiredService<AuthClient>()
.GetMultiTokenByLoginTicketAsync(loginTicket, uid, default)
.ConfigureAwait(false);
@@ -198,22 +206,27 @@ internal class UserService : IUserService
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(ObservableCollection<BindingUser> users, Cookie cookie)
{
BindingUser? newUser = await BindingUser.CreateAsync(cookie, userClient, bindingClient).ConfigureAwait(false);
if (newUser != null)
using (IServiceScope scope = scopeFactory.CreateScope())
{
// Sync cache
await ThreadHelper.SwitchToMainThreadAsync();
users.Add(newUser);
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
UserClient userClient = scope.ServiceProvider.GetRequiredService<UserClient>();
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService<BindingClient>();
// Sync database
appDbContext.Users.Add(newUser.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
BindingUser? newUser = await BindingUser.CreateAsync(cookie, userClient, bindingClient).ConfigureAwait(false);
if (newUser != null)
{
// Sync cache
await ThreadHelper.SwitchToMainThreadAsync();
users.Add(newUser);
return new(UserOptionResult.Added, newUser.UserInfo!.Uid);
}
else
{
return new(UserOptionResult.Invalid, null!);
// Sync database
appDbContext.Users.AddAndSave(newUser.Entity);
return new(UserOptionResult.Added, newUser.UserInfo!.Uid);
}
else
{
return new(UserOptionResult.Invalid, null!);
}
}
}
}

View File

@@ -70,6 +70,8 @@
<None Remove="View\Page\GachaLogPage.xaml" />
<None Remove="View\Page\HutaoDatabasePage.xaml" />
<None Remove="View\Page\LaunchGamePage.xaml" />
<None Remove="View\Page\LoginMihoyoBBSPage.xaml" />
<None Remove="View\Page\LoginMihoyoUserPage.xaml" />
<None Remove="View\Page\SettingPage.xaml" />
<None Remove="View\Page\WikiAvatarPage.xaml" />
<None Remove="View\TitleView.xaml" />
@@ -148,6 +150,16 @@
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\LoginMihoyoBBSPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\LoginMihoyoUserPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="LaunchGameWindow.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -138,7 +138,8 @@
Style="{StaticResource SubtitleTextBlockStyle}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock
Margin="0,4,12,4"
VerticalAlignment="Bottom"
Margin="0,4,12,2"
FontFamily="Consolas"
Text="{Binding TotalCount}"
Visibility="{Binding ElementName=DetailExpander,Path=IsExpanded,Converter={StaticResource BoolToVisibilityRevertConverter}}"
@@ -318,7 +319,6 @@
</ItemsControl>
</cwucont:Case>
</cwucont:SwitchPresenter>
</ScrollViewer>
</Grid>
</Border>

View File

@@ -241,8 +241,20 @@
</ListView>
</SplitView.Pane>
<SplitView.Content>
<ScrollViewer>
<StackPanel Margin="16,0,16,0">
<Border
HorizontalAlignment="Left"
Margin="0,16,0,0"
BorderThickness="1"
BorderBrush="{StaticResource CardStrokeColorDefault}"
CornerRadius="{StaticResource CompatCornerRadius}">
<shci:CachedImage
MaxHeight="320"
Source="{Binding SelectedHistoryWish.BannerImage}"/>
</Border>
<TextBlock Text="五星" Style="{StaticResource BaseTextBlockStyle}" Margin="0,16,0,8"/>
<GridView ItemsSource="{Binding SelectedHistoryWish.OrangeList}">
<GridView.ItemTemplate>

View File

@@ -21,7 +21,7 @@
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<ScrollViewer Grid.Column="0">
<ScrollViewer Grid.Column="0" CanContentRenderOutsideBounds="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="800"/>
@@ -37,6 +37,36 @@
<ComboBox Width="120"/>
</sc:Setting.ActionContent>
</sc:Setting>
<sc:SettingExpander>
<sc:SettingExpander.Header>
<Grid Padding="0,16">
<StackPanel Orientation="Horizontal">
<FontIcon Glyph="&#xE748;"/>
<StackPanel VerticalAlignment="Center">
<TextBlock
Margin="20,0,0,0"
Text="账号"/>
<TextBlock
Opacity="0.8"
Margin="20,0,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="在游戏内切换账号,网络环境发生变化后需要重新手动检测"/>
</StackPanel>
</StackPanel>
<Button
HorizontalAlignment="Right"
Grid.Column="1"
Margin="0,0,8,0"
Width="80"
MinWidth="88"
Content="检测"/>
</Grid>
</sc:SettingExpander.Header>
</sc:SettingExpander>
</sc:SettingsGroup>
<sc:SettingsGroup Header="外观">
<sc:Setting
@@ -111,19 +141,10 @@
<Grid
Grid.Row="1"
VerticalAlignment="Bottom"
Background="{StaticResource CardBackgroundFillColorSecondary}">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<ComboBox
Header="原神账号"
Grid.Column="0"
Margin="32,24,0,24"
Width="160"/>
Background="{StaticResource SystemControlAcrylicElementMediumHighBrush}">
<Button
Grid.Column="1"
HorizontalAlignment="Right"
Grid.Column="3"
Margin="24"
Width="138"
Content="启动游戏"/>

View File

@@ -0,0 +1,31 @@
<Page
x:Class="Snap.Hutao.View.Page.LoginMihoyoBBSPage"
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"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid Loaded="OnRootLoaded">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock
Margin="12,0,0,0"
VerticalAlignment="Center"
Text="在下方登录米游社"
Grid.Row="0"/>
<Button
HorizontalAlignment="Right"
Margin="16"
Content="我已登录"
Click="CookieButtonClick"/>
<WebView2
Grid.Row="1"
Margin="0,0,0,0"
x:Name="WebView"/>
</Grid>
</Page>

View File

@@ -0,0 +1,83 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.View.Page;
/// <summary>
/// 登录米游社页面
/// </summary>
public sealed partial class LoginMihoyoBBSPage : Microsoft.UI.Xaml.Controls.Page
{
private const string CookieSite = "https://bbs.mihoyo.com";
private const string Website = "https://bbs.mihoyo.com/ys/";
/// <summary>
/// 构造一个新的登录米游社页面
/// </summary>
public LoginMihoyoBBSPage()
{
InitializeComponent();
}
[SuppressMessage("", "VSTHRD100")]
private async void OnRootLoaded(object sender, RoutedEventArgs e)
{
await WebView.EnsureCoreWebView2Async();
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync(CookieSite);
foreach (CoreWebView2Cookie item in cookies)
{
manager.DeleteCookie(item);
}
WebView.CoreWebView2.Navigate(Website);
}
private async Task HandleCurrentCookieAsync()
{
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync(CookieSite);
Cookie cookie = Cookie.FromCoreWebView2Cookies(cookies);
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
(UserOptionResult result, string nickname) = await userService.ProcessInputCookieAsync(cookie).ConfigureAwait(false);
Ioc.Default.GetRequiredService<INavigationService>().GoBack();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
switch (result)
{
case UserOptionResult.Added:
infoBarService.Success($"用户 [{nickname}] 添加成功");
break;
case UserOptionResult.Incomplete:
infoBarService.Information($"此 Cookie 不完整,操作失败");
break;
case UserOptionResult.Invalid:
infoBarService.Information($"此 Cookie 无法,操作失败");
break;
case UserOptionResult.Updated:
infoBarService.Success($"用户 [{nickname}] 更新成功");
break;
case UserOptionResult.Upgraded:
infoBarService.Information($"用户 [{nickname}] 升级成功");
break;
default:
throw Must.NeverHappen();
}
}
private void CookieButtonClick(object sender, RoutedEventArgs e)
{
HandleCurrentCookieAsync().SafeForget();
}
}

View File

@@ -0,0 +1,30 @@
<Page
x:Class="Snap.Hutao.View.Page.LoginMihoyoUserPage"
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"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid Loaded="OnRootLoaded">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock
Margin="12,0,0,0"
VerticalAlignment="Center"
Text="在下方米哈游通行证"
Grid.Row="0"/>
<Button
HorizontalAlignment="Right"
Margin="16"
Content="我已登录"
Click="CookieButtonClick"/>
<WebView2
Grid.Row="2"
Margin="0,0,0,0"
x:Name="WebView"/>
</Grid>
</Page>

View File

@@ -0,0 +1,82 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.View.Page;
/// <summary>
/// 登录米哈游通行证页面
/// </summary>
public sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.Page
{
/// <summary>
/// 构造一个新的登录米哈游通行证页面
/// </summary>
public LoginMihoyoUserPage()
{
InitializeComponent();
}
[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")
{
await HandleCurrentCookieAsync().ConfigureAwait(false);
}
}
}
private async Task HandleCurrentCookieAsync()
{
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
WebView.CoreWebView2.SourceChanged -= OnCoreWebView2SourceChanged;
Cookie cookie = Cookie.FromCoreWebView2Cookies(cookies);
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
(UserOptionResult result, string nickname) = await userService.ProcessInputCookieAsync(cookie).ConfigureAwait(false);
Ioc.Default.GetRequiredService<INavigationService>().GoBack();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
if (result == UserOptionResult.Upgraded)
{
infoBarService.Information($"用户 [{nickname}] 的 Cookie 升级成功");
}
else
{
infoBarService.Warning("请先添加对应用户的米游社Cookie");
}
}
private void CookieButtonClick(object sender, RoutedEventArgs e)
{
HandleCurrentCookieAsync().SafeForget();
}
}

View File

@@ -205,12 +205,16 @@
<TextBlock
Margin="10,6,0,6"
Style="{StaticResource BaseTextBlockStyle}"
Text="Cookie操作"/>
Text="Cookie 操作"/>
<CommandBar DefaultLabelPosition="Right">
<AppBarButton
Icon="{shcm:FontIcon Glyph=&#xF4A5;}"
Label="登录米哈游通行证"
Command="{Binding UpgradeToStokenCommand}"/>
<AppBarButton Label="网页登录" Icon="{shcm:FontIcon Glyph=&#xEB41;}">
<AppBarButton.Flyout>
<MenuFlyout>
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=&#xF4A5;}" Text="登录米游社原神社区" Command="{Binding LoginMihoyoBBSCommand}"/>
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=&#xF4A5;}" Text="登录米哈游通行证" Command="{Binding LoginMihoyoUserCommand}"/>
</MenuFlyout>
</AppBarButton.Flyout>
</AppBarButton>
<AppBarButton
Icon="{shcm:FontIcon Glyph=&#xE710;}"
Label="手动输入"

View File

@@ -3,6 +3,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Control;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game;
using System.Collections.ObjectModel;
namespace Snap.Hutao.ViewModel;
@@ -12,6 +17,148 @@ namespace Snap.Hutao.ViewModel;
[Injection(InjectAs.Scoped)]
internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
{
private readonly IGameService gameService;
private readonly List<LaunchScheme> knownSchemes = new()
{
new LaunchScheme(name: "官方服 | 天空岛", channel: "1", subChannel: "1"),
new LaunchScheme(name: "渠道服 | 世界树", channel: "14", subChannel: "0"),
// new LaunchScheme(name: "国际服 | 暂不支持", channel: "1", subChannel: "0"),
};
private LaunchScheme? selectedScheme;
private ObservableCollection<GameAccount>? gameAccounts;
private GameAccount? selectedGameAccount;
private bool isFullScreen;
private bool isBorderless;
private int screenWidth;
private int screenHeight;
private bool unlockFps;
private int targetFps;
/// <summary>
/// 构造一个新的启动游戏视图模型
/// </summary>
/// <param name="gameService">游戏服务</param>
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
public LaunchGameViewModel(IGameService gameService, IAsyncRelayCommandFactory asyncRelayCommandFactory)
{
this.gameService = gameService;
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
LaunchCommand = asyncRelayCommandFactory.Create(LaunchAsync);
DetectGameAccountCommand = asyncRelayCommandFactory.Create(DetectGameAccountAsync);
ModifyGameAccountCommand = asyncRelayCommandFactory.Create(ModifyGameAccountAsync);
RemoveGameAccountCommand = asyncRelayCommandFactory.Create(RemoveGameAccountAsync);
}
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
}
/// <summary>
/// 已知的服务器方案
/// </summary>
public List<LaunchScheme> KnownSchemes { get => knownSchemes; }
/// <summary>
/// 当前选择的服务器方案
/// </summary>
public LaunchScheme? SelectedScheme { get => selectedScheme; set => SetProperty(ref selectedScheme, value); }
/// <summary>
/// 游戏账号集合
/// </summary>
public ObservableCollection<GameAccount>? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); }
/// <summary>
/// 选中的账号
/// </summary>
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
/// <summary>
/// 全屏
/// </summary>
public bool IsFullScreen { get => isFullScreen; set => SetProperty(ref isFullScreen, value); }
/// <summary>
/// 无边框
/// </summary>
public bool IsBorderless { get => isBorderless; set => SetProperty(ref isBorderless, value); }
/// <summary>
/// 宽度
/// </summary>
public int ScreenWidth { get => screenWidth; set => SetProperty(ref screenWidth, value); }
/// <summary>
/// 高度
/// </summary>
public int ScreenHeight { get => screenHeight; set => SetProperty(ref screenHeight, value); }
/// <summary>
/// 解锁帧率
/// </summary>
public bool UnlockFps { get => unlockFps; set => SetProperty(ref unlockFps, value); }
/// <summary>
/// 目标帧率
/// </summary>
public int TargetFps { get => targetFps; set => SetProperty(ref targetFps, value); }
/// <summary>
/// 打开界面命令
/// </summary>
public ICommand OpenUICommand { get; }
/// <summary>
/// 启动游戏命令
/// </summary>
public ICommand LaunchCommand { get; }
/// <summary>
/// 检测游戏账号命令
/// </summary>
public ICommand DetectGameAccountCommand { get; }
/// <summary>
/// 修改游戏账号命令
/// </summary>
public ICommand ModifyGameAccountCommand { get; }
/// <summary>
/// 删除游戏账号命令
/// </summary>
public ICommand RemoveGameAccountCommand { get; }
private async Task OpenUIAsync()
{
(bool isOk, string gamePath) = await gameService.GetGamePathAsync().ConfigureAwait(false);
if (isOk)
{
MultiChannel multi = gameService.GetMultiChannel();
SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
}
}
private async Task LaunchAsync()
{
}
private async Task DetectGameAccountAsync()
{
}
private async Task ModifyGameAccountAsync()
{
}
private async Task RemoveGameAccountAsync()
{
}
}

View File

@@ -8,8 +8,10 @@ using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.View.Page;
using Snap.Hutao.Web.Hoyolab;
using System.Collections.ObjectModel;
@@ -40,7 +42,8 @@ internal class UserViewModel : ObservableObject
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
AddUserCommand = asyncRelayCommandFactory.Create(AddUserAsync);
UpgradeToStokenCommand = asyncRelayCommandFactory.Create(UpgradeByLoginTicketAsync);
LoginMihoyoUserCommand = new RelayCommand(LoginMihoyoUser);
LoginMihoyoBBSCommand = new RelayCommand(LoginMihoyoBBS);
RemoveUserCommand = asyncRelayCommandFactory.Create<User>(RemoveUserAsync);
CopyCookieCommand = new RelayCommand<User>(CopyCookie);
}
@@ -78,7 +81,12 @@ internal class UserViewModel : ObservableObject
/// <summary>
/// 登录米哈游通行证升级到Stoken命令
/// </summary>
public ICommand UpgradeToStokenCommand { get; }
public ICommand LoginMihoyoUserCommand { get; }
/// <summary>
/// 登录米游社命令
/// </summary>
public ICommand LoginMihoyoBBSCommand { get; }
/// <summary>
/// 移除用户命令
@@ -132,26 +140,14 @@ internal class UserViewModel : ObservableObject
}
}
private async Task UpgradeByLoginTicketAsync()
private void LoginMihoyoUser()
{
// Get cookie from user input
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
(bool isOk, Cookie addition) = await new UserAutoCookieDialog(mainWindow).GetInputCookieAsync().ConfigureAwait(false);
Ioc.Default.GetRequiredService<INavigationService>().Navigate<LoginMihoyoUserPage>(INavigationAwaiter.Default);
}
// User confirms the input
if (isOk)
{
(UserOptionResult result, string nickname) = await userService.ProcessInputCookieAsync(addition).ConfigureAwait(false);
if (result == UserOptionResult.Upgraded)
{
infoBarService.Information($"用户 [{nickname}] 的 Cookie 升级成功");
}
else
{
infoBarService.Warning("请先添加对应用户的米游社Cookie");
}
}
private void LoginMihoyoBBS()
{
Ioc.Default.GetRequiredService<INavigationService>().Navigate<LoginMihoyoBBSPage>(INavigationAwaiter.Default);
}
private async Task RemoveUserAsync(User? user)

View File

@@ -11,10 +11,10 @@ public class RankInfo
/// <summary>
/// 造成伤害
/// </summary>
public ItemRate<int, double> Damage { get; set; } = default!;
public RankValue Damage { get; set; } = default!;
/// <summary>
/// 受到伤害
/// </summary>
public ItemRate<int, double> TakeDamage { get; set; } = default!;
}
public RankValue TakeDamage { get; set; } = default!;
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 伤害值
/// </summary>
public class RankValue : ItemRate<int, double>
{
/// <summary>
/// 构造一个新的伤害值
/// </summary>
/// <param name="item">物品</param>
/// <param name="value">伤害</param>
/// <param name="rate">率</param>
/// <param name="rateOnAvatar">角色率</param>
[JsonConstructor]
public RankValue(int item, int value, double rate, double rateOnAvatar)
: base(item, rate)
{
Value = value;
RateOnAvatar = rateOnAvatar;
}
/// <summary>
/// 伤害值
/// </summary>
public int Value { get; set; }
/// <summary>
/// 角色比率
/// </summary>
public double RateOnAvatar { get; set; }
}