Compare commits

...

3 Commits

Author SHA1 Message Date
DismissedLight
331cc14532 Update Persistence.cs 2022-09-27 14:38:06 +08:00
DismissedLight
c0ddb24825 fix window issue 2022-09-27 14:36:13 +08:00
DismissedLight
e925c5909c update readme 2022-09-27 12:35:37 +08:00
17 changed files with 168 additions and 76 deletions

View File

@@ -6,7 +6,9 @@
## 安装
* 前往 [下载页面](https://go.hut.ao/archive) 下载最新版本的 `胡桃` 安装包
* 完全解压后,使用 powershell 运行 `install.ps1` 文件
* (曾启用的可以跳过此步骤)在系统设置中打开 **开发者选项** 界面,勾选 `开发人员模式``允许 PowerShell 脚本`
* 完全解压后,右键使用 powershell 运行 `install.ps1` 文件
* 安装完成后可以关闭 `允许 PowerShell 脚本`
## 特别感谢

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Core.Caching;
/// </summary>
[Injection(InjectAs.Singleton, typeof(IImageCache))]
[HttpClient(HttpClientConfigration.Default)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 20)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 16)]
public class ImageCache : CacheBase<BitmapImage>, IImageCache
{
private const string DateAccessedProperty = "System.DateAccessed";

View File

@@ -4,6 +4,7 @@
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Win32;
using System.Runtime.InteropServices;
using Windows.Graphics;
using Windows.Win32.Foundation;
@@ -25,11 +26,15 @@ internal static class Persistence
// Set first launch size.
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
SizeInt32 size = TransformSizeForWindow(new(1200, 741), hwnd);
RectInt32 rect = new(0, 0, size.Width, size.Height);
RectInt32 rect = StructMarshal.RectInt32(size);
// Make it centralized
TransformToCenterScreen(ref rect);
RectInt32 target = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
if (target.Width * target.Height < 848 * 524)
{
target = rect;
}
TransformToCenterScreen(ref target);
appWindow.MoveAndResize(target);
}

View File

@@ -77,7 +77,7 @@ public class User : Observable
/// </summary>
/// <param name="cookie">cookie的字符串形式</param>
/// <returns>包含cookie信息的字典</returns>
public static IDictionary<string, string> ParseCookie(string cookie)
public static IDictionary<string, string> MapCookie(string cookie)
{
SortedDictionary<string, string> cookieDictionary = new();
@@ -142,9 +142,9 @@ public class User : Observable
/// <param name="authClient">验证客户端</param>
/// <param name="token">取消令牌</param>
/// <returns>是否升级成功</returns>
internal async Task<bool> TryUpgradeAsync(IDictionary<string, string> addition, AuthClient authClient, CancellationToken token)
internal async Task<bool> TryUpgradeByLoginTicketAsync(IDictionary<string, string> addition, AuthClient authClient, CancellationToken token)
{
IDictionary<string, string> cookie = ParseCookie(Cookie!);
IDictionary<string, string> cookie = MapCookie(Cookie!);
if (addition.TryGetValue(CookieKeys.LOGIN_TICKET, out string? loginTicket))
{
cookie[CookieKeys.LOGIN_TICKET] = loginTicket;
@@ -155,7 +155,7 @@ public class User : Observable
cookie[CookieKeys.LOGIN_UID] = loginUid;
}
bool result = await TryAddStokenToCookieAsync(cookie, authClient, token).ConfigureAwait(false);
bool result = await TryRequestStokenAndAddToCookieAsync(cookie, authClient, token).ConfigureAwait(false);
if (result)
{
@@ -165,12 +165,31 @@ public class User : Observable
return result;
}
/// <summary>
/// 添加 Stoken
/// </summary>
/// <param name="addition">额外的cookie</param>
internal void AddStoken(IDictionary<string, string> addition)
{
IDictionary<string, string> cookie = MapCookie(Cookie!);
if (addition.TryGetValue(CookieKeys.STOKEN, out string? stoken))
{
cookie[CookieKeys.STOKEN] = stoken;
}
if (addition.TryGetValue(CookieKeys.STUID, out string? stuid))
{
cookie[CookieKeys.STUID] = stuid;
}
}
private static string ToCookieString(IDictionary<string, string> cookie)
{
return string.Join(';', cookie.Select(kvp => $"{kvp.Key}={kvp.Value}"));
}
private static async Task<bool> TryAddStokenToCookieAsync(IDictionary<string, string> cookie, AuthClient authClient, CancellationToken token)
private static async Task<bool> TryRequestStokenAndAddToCookieAsync(IDictionary<string, string> cookie, AuthClient authClient, CancellationToken token)
{
if (cookie.TryGetValue(CookieKeys.LOGIN_TICKET, out string? loginTicket))
{
@@ -225,7 +244,7 @@ public class User : Observable
return true;
}
if (await TryAddStokenToCookieAsync(cookie, authClient, token).ConfigureAwait(false))
if (await TryRequestStokenAndAddToCookieAsync(cookie, authClient, token).ConfigureAwait(false))
{
Cookie = ToCookieString(cookie);
}

View File

@@ -13,12 +13,10 @@ namespace Snap.Hutao.Model.Metadata.Converter;
internal sealed class DescParamDescriptor : ValueConverterBase<DescParam, IList<LevelParam<string, ParameterInfo>>>
{
/// <inheritdoc/>
public override IList<LevelParam<string, ParameterInfo>> Convert(DescParam rawDescParam)
public override IList<LevelParam<string, ParameterInfo>> Convert(DescParam from)
{
IList<LevelParam<string, ParameterInfo>> parameters = rawDescParam.Parameters
.Select(param => new LevelParam<string, ParameterInfo>(
param.Level.ToString(),
GetParameterInfos(rawDescParam, param.Parameters)))
IList<LevelParam<string, ParameterInfo>> parameters = from.Parameters
.Select(param => new LevelParam<string, ParameterInfo>(param.Level.ToString(), GetParameterInfos(from, param.Parameters)))
.ToList();
return parameters;

View File

@@ -9,7 +9,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.1.3.0" />
Version="1.1.4.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -91,19 +91,18 @@ internal class InfoBarService : IInfoBarService
PrepareInfoBarAndShow(InfoBarSeverity.Error, ex.GetType().Name, $"{title}\n{ex.Message}", delay);
}
[SuppressMessage("", "VSTHRD100")]
private async void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay)
private void PrepareInfoBarAndShow(InfoBarSeverity severity, string? title, string? message, int delay)
{
if (infoBarStack is null)
{
return;
}
await PrepareInfoBarAndShowInternalAsync(severity, title, message, delay).ConfigureAwait(false);
PrepareInfoBarAndShowInternalAsync(severity, title, message, delay).SafeForget();
}
/// <summary>
/// 此方法应在主线程上运行
/// 准备信息条并显示
/// </summary>
/// <param name="severity">严重程度</param>
/// <param name="title">标题</param>
@@ -122,11 +121,12 @@ internal class InfoBarService : IInfoBarService
};
infoBar.Closed += OnInfoBarClosed;
Must.NotNull(infoBarStack!).Children.Add(infoBar);
infoBarStack!.Children.Add(infoBar);
if (delay > 0)
{
await Task.Delay(delay);
await Task.Delay(delay).ConfigureAwait(true);
infoBarStack.Children.Remove(infoBar);
infoBar.IsOpen = false;
}
}
@@ -136,7 +136,7 @@ internal class InfoBarService : IInfoBarService
{
await ThreadHelper.SwitchToMainThreadAsync();
Must.NotNull(infoBarStack!).Children.Remove(sender);
infoBarStack!.Children.Remove(sender);
sender.Closed -= OnInfoBarClosed;
}
}

View File

@@ -38,10 +38,17 @@ public interface IUserService
/// <summary>
/// 尝试使用 login_ticket 升级用户
/// </summary>
/// <param name="cookie">额外的Cookie</param>
/// <param name="addiition">额外的Cookie</param>
/// <param name="token">取消令牌</param>
/// <returns>是否升级成功</returns>
Task<ValueResult<bool, string>> TryUpgradeUserAsync(IDictionary<string, string> addiition, CancellationToken token = default);
Task<ValueResult<bool, string>> TryUpgradeUserByLoginTicketAsync(IDictionary<string, string> addiition, CancellationToken token = default);
/// <summary>
/// 尝试使用 Stoken 升级用户
/// </summary>
/// <param name="stoken">stoken</param>
/// <returns>是否升级成功</returns>
Task<ValueResult<bool, string>> TryUpgradeUserByStokenAsync(IDictionary<string, string> stoken);
/// <summary>
/// 异步移除用户

View File

@@ -118,6 +118,7 @@ internal class UserService : IUserService
Verify.Operation(newUser.IsInitialized, "该用户尚未初始化");
// Sync cache
await ThreadHelper.SwitchToMainThreadAsync();
userCollection.Add(newUser);
// Sync database
@@ -180,7 +181,7 @@ internal class UserService : IUserService
}
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> TryUpgradeUserAsync(IDictionary<string, string> addition, CancellationToken token = default)
public async Task<ValueResult<bool, string>> TryUpgradeUserByLoginTicketAsync(IDictionary<string, string> addition, CancellationToken token = default)
{
Must.NotNull(userCollection!);
if (addition.TryGetValue(CookieKeys.LOGIN_UID, out string? uid))
@@ -189,15 +190,36 @@ internal class UserService : IUserService
if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is BindingUser userWithSameUid)
{
// Update user cookie here.
if (await userWithSameUid.TryUpgradeAsync(addition, authClient, token))
if (await userWithSameUid.TryUpgradeByLoginTicketAsync(addition, authClient, token))
{
appDbContext.Users.Update(userWithSameUid.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
return new(true, uid);
return new(true, userWithSameUid.UserInfo?.Nickname ?? string.Empty);
}
}
}
return new(false, string.Empty);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> TryUpgradeUserByStokenAsync(IDictionary<string, string> stoken)
{
Must.NotNull(userCollection!);
if (stoken.TryGetValue(CookieKeys.STUID, out string? uid))
{
// 查找是否有相同的uid
if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is BindingUser userWithSameUid)
{
// Update user cookie here.
userWithSameUid.AddStoken(stoken);
appDbContext.Users.Update(userWithSameUid.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
return new(true, userWithSameUid.UserInfo?.Nickname ?? string.Empty);
}
}
return new(false, string.Empty);
}
}

View File

@@ -19,12 +19,14 @@
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock
Margin="0,0,0,8"
Text="请在 成功登录米哈游通行证 后点击 [继续] 按钮"/>
<TextBlock
Text="在下方登录"
Grid.Row="0"/>
<WebView2
Grid.Row="1"
Grid.Row="2"
Margin="0,12,0,0"
Width="640"
Height="400"
x:Name="WebView"/>

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.View.Dialog;
/// </summary>
public sealed partial class UserAutoCookieDialog : ContentDialog
{
private IDictionary<string, string>? cookieString;
private IDictionary<string, string>? cookie;
/// <summary>
/// 构造一个新的用户自动Cookie对话框
@@ -32,7 +32,7 @@ public sealed partial class UserAutoCookieDialog : ContentDialog
public async Task<ValueResult<bool, IDictionary<string, string>>> GetInputCookieAsync()
{
ContentDialogResult result = await ShowAsync();
return new(result == ContentDialogResult.Primary && cookieString != null, cookieString!);
return new(result == ContentDialogResult.Primary && cookie != null, cookie!);
}
[SuppressMessage("", "VSTHRD100")]
@@ -62,8 +62,7 @@ public sealed partial class UserAutoCookieDialog : ContentDialog
{
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
cookieString = cookies.ToDictionary(c => c.Name, c => c.Value);
cookie = cookies.ToDictionary(c => c.Name, c => c.Value);
WebView.CoreWebView2.SourceChanged -= OnCoreWebView2SourceChanged;
}
catch (Exception)

View File

@@ -9,6 +9,7 @@
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shvm="using:Snap.Hutao.ViewModel"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance shvm:UserViewModel}">
<mxi:Interaction.Behaviors>
@@ -199,11 +200,11 @@
Text="Cookie"/>
<CommandBar DefaultLabelPosition="Right">
<AppBarButton
Icon="Add"
Icon="{shcm:FontIcon Glyph=&#xF4A5;}"
Label="升级Stoken"
Command="{Binding UpgradeToStokenCommand}"/>
<AppBarButton
Icon="Add"
Icon="{shcm:FontIcon Glyph=&#xE710;}"
Label="手动添加"
Command="{Binding AddUserCommand}"/>
</CommandBar>

View File

@@ -10,7 +10,6 @@ using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab;
using System.Collections.ObjectModel;
using System.Net;
using Windows.ApplicationModel.DataTransfer;
namespace Snap.Hutao.ViewModel;
@@ -21,8 +20,6 @@ namespace Snap.Hutao.ViewModel;
[Injection(InjectAs.Transient)]
internal class UserViewModel : ObservableObject
{
private const string AccountIdKey = "account_id";
private readonly IUserService userService;
private readonly IInfoBarService infoBarService;
@@ -42,7 +39,7 @@ internal class UserViewModel : ObservableObject
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
AddUserCommand = asyncRelayCommandFactory.Create(AddUserAsync);
UpgradeToStokenCommand = asyncRelayCommandFactory.Create(UpgradeToStokenAsync);
UpgradeToStokenCommand = asyncRelayCommandFactory.Create(UpgradeByLoginTicketAsync);
RemoveUserCommand = asyncRelayCommandFactory.Create<User>(RemoveUserAsync);
CopyCookieCommand = new RelayCommand<User>(CopyCookie);
}
@@ -92,35 +89,45 @@ internal class UserViewModel : ObservableObject
/// </summary>
public ICommand CopyCookieCommand { get; }
private static bool TryValidateCookie(IDictionary<string, string> map, [NotNullWhen(true)] out IDictionary<string, string>? filteredCookie)
private static (bool Valid, bool Upgrade) TryValidateCookie(IDictionary<string, string> map, out IDictionary<string, string> cookie)
{
int validFlag = 4;
int stokenFlag = 2;
SortedDictionary<string, string> filter = new();
cookie = new SortedDictionary<string, string>();
foreach ((string key, string value) in map)
{
if (key == CookieKeys.COOKIE_TOKEN || key == CookieKeys.ACCOUNT_ID || key == CookieKeys.LTOKEN || key == CookieKeys.LTUID)
switch (key)
{
validFlag--;
filter.Add(key, value);
}
else if (key == CookieKeys.STOKEN || key == CookieKeys.STUID || key == CookieKeys.LOGIN_TICKET || key == CookieKeys.LOGIN_UID)
{
filter.Add(key, value);
case CookieKeys.COOKIE_TOKEN:
case CookieKeys.ACCOUNT_ID:
case CookieKeys.LTOKEN:
case CookieKeys.LTUID:
{
validFlag--;
cookie.Add(key, value);
break;
}
case CookieKeys.STOKEN:
case CookieKeys.STUID:
{
stokenFlag--;
cookie.Add(key, value);
break;
}
case CookieKeys.LOGIN_TICKET:
case CookieKeys.LOGIN_UID:
{
cookie.Add(key, value);
break;
}
}
}
if (validFlag == 0)
{
filteredCookie = filter;
return true;
}
else
{
filteredCookie = null;
return false;
}
return (validFlag == 0, stokenFlag == 0);
}
private async Task OpenUIAsync()
@@ -138,11 +145,12 @@ internal class UserViewModel : ObservableObject
// User confirms the input
if (result.IsOk)
{
if (TryValidateCookie(User.ParseCookie(result.Value), out IDictionary<string, string>? filteredCookie))
(bool valid, bool upgradable) = TryValidateCookie(User.MapCookie(result.Value), out IDictionary<string, string> cookie);
if (valid)
{
if (await userService.CreateUserAsync(filteredCookie).ConfigureAwait(false) is User user)
if (await userService.CreateUserAsync(cookie).ConfigureAwait(false) is User user)
{
switch (await userService.TryAddUserAsync(user, filteredCookie[AccountIdKey]).ConfigureAwait(false))
switch (await userService.TryAddUserAsync(user, cookie[CookieKeys.ACCOUNT_ID]).ConfigureAwait(false))
{
case UserAddResult.Added:
infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 添加成功");
@@ -164,12 +172,28 @@ internal class UserViewModel : ObservableObject
}
else
{
infoBarService.Warning("提供的文本不是正确的 Cookie ,请重新输入");
if (upgradable)
{
(bool success, string nickname) = await userService.TryUpgradeUserByStokenAsync(cookie).ConfigureAwait(false);
if (success)
{
infoBarService.Information($"用户 [{nickname}] 的 Stoken 更新成功");
}
else
{
infoBarService.Warning($"未找到匹配的可升级用户");
}
}
else
{
infoBarService.Warning("提供的文本不是正确的 Cookie ,请重新输入");
}
}
}
}
private async Task UpgradeToStokenAsync()
private async Task UpgradeByLoginTicketAsync()
{
// Get cookie from user input
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
@@ -178,10 +202,10 @@ internal class UserViewModel : ObservableObject
// User confirms the input
if (isOk)
{
(bool isUpgradeSucceed, string uid) = await userService.TryUpgradeUserAsync(addition).ConfigureAwait(false);
if (isUpgradeSucceed)
(bool isUpgraded, string nickname) = await userService.TryUpgradeUserByLoginTicketAsync(addition).ConfigureAwait(false);
if (isUpgraded)
{
infoBarService.Information($"用户 [{uid}] 的 Cookie 已成功添加 Stoken");
infoBarService.Information($"用户 [{nickname}] 的 Cookie 已成功添加 Stoken");
}
else
{

View File

@@ -3,6 +3,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.WinUI.UI;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Intrinsic;
@@ -143,13 +144,14 @@ internal class WikiAvatarViewModel : ObservableObject
private async Task OpenUIAsync()
{
if (await metadataService.InitializeAsync())
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
IList<Avatar> avatars = await metadataService.GetAvatarsAsync();
IList<Avatar> avatars = await metadataService.GetAvatarsAsync().ConfigureAwait(false);
IOrderedEnumerable<Avatar> sorted = avatars
.OrderBy(avatar => avatar.BeginTime)
.ThenBy(avatar => avatar.Sort);
await ThreadHelper.SwitchToMainThreadAsync();
Avatars = new AdvancedCollectionView(sorted.ToList(), true);
Avatars.MoveCurrentToFirst();
}
@@ -174,7 +176,7 @@ internal class WikiAvatarViewModel : ObservableObject
.Select(e => e.Value.Value)
.ToList();
List<ItemQuality> targeQualities = FilterQualityInfos
List<ItemQuality> targetQualities = FilterQualityInfos
.Where(e => e.IsSelected)
.Select(e => e.Value.Value)
.ToList();
@@ -188,7 +190,7 @@ internal class WikiAvatarViewModel : ObservableObject
&& targetElements.Contains(avatar.FetterInfo.VisionBefore)
&& targetAssociations.Contains(avatar.FetterInfo.Association)
&& targetWeaponTypes.Contains(avatar.Weapon)
&& targeQualities.Contains(avatar.Quality)
&& targetQualities.Contains(avatar.Quality)
&& targetBodies.Contains(avatar.Body);
if (!Avatars.Contains(Selected))

View File

@@ -12,7 +12,7 @@ public class DetailedCertification : Certification
/// Id
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
public string Id { get; set; } = default!;
/// <summary>
/// 认证Id

View File

@@ -7,7 +7,7 @@ using Windows.Win32.System.Diagnostics.ToolHelp;
namespace Snap.Hutao.Win32;
/// <summary>
/// 内存拓展
/// 内存拓展 for <see cref="Memory{T}"/> <see cref="Span{T}"/>
/// </summary>
internal static class MemoryExtensions
{

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Graphics;
using Windows.Win32.System.Diagnostics.ToolHelp;
namespace Snap.Hutao.Win32;
@@ -19,6 +20,16 @@ internal static class StructMarshal
return new() { dwSize = (uint)sizeof(MODULEENTRY32) };
}
/// <summary>
/// 构造一个新的<see cref="Windows.Graphics.RectInt32"/>
/// </summary>
/// <param name="size">尺寸</param>
/// <returns>新的实例</returns>
public static RectInt32 RectInt32(SizeInt32 size)
{
return new(0, 0, size.Width, size.Height);
}
/// <summary>
/// 判断结构实例是否为默认结构
/// </summary>