mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Merge pull request #1182 from DGP-Studio/develop
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
||||
@@ -20,7 +20,24 @@ namespace Snap.Hutao;
|
||||
[SuppressMessage("", "SH001")]
|
||||
public sealed partial class App : Application
|
||||
{
|
||||
private const string ConsoleBanner = """
|
||||
----------------------------------------------------------------
|
||||
_____ _ _ _
|
||||
/ ____| | | | | | |
|
||||
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
|
||||
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
|
||||
____) || | | || (_| || |_) |_ | | | || |_| || |_| (_| || (_) |
|
||||
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
|
||||
| |
|
||||
|_|
|
||||
|
||||
Snap.Hutao is a open source software developed by DGP Studio.
|
||||
Copyright (C) 2022 - 2024 DGP Studio, All Rights Reserved.
|
||||
----------------------------------------------------------------
|
||||
""";
|
||||
|
||||
private const string AppInstanceKey = "main";
|
||||
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IActivation activation;
|
||||
private readonly ILogger<App> logger;
|
||||
@@ -51,6 +68,8 @@ public sealed partial class App : Application
|
||||
|
||||
if (firstInstance.IsCurrent)
|
||||
{
|
||||
logger.LogInformation(ConsoleBanner);
|
||||
|
||||
// manually invoke
|
||||
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
|
||||
activation.InitializeWith(firstInstance);
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cwm="using:CommunityToolkit.WinUI.Media">
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<cwm:AttachedCardShadow
|
||||
x:Key="CompatCardShadow"
|
||||
BlurRadius="8"
|
||||
Opacity="0.14"
|
||||
Offset="0,4,0"/>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<cwm:AttachedCardShadow
|
||||
x:Key="CompatCardShadow"
|
||||
BlurRadius="8"
|
||||
Opacity="0.28"
|
||||
Offset="0,4,0"/>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
<Style x:Key="BorderCardStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
|
||||
@@ -14,8 +30,4 @@
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
|
||||
</Style>
|
||||
<cwm:AttachedCardShadow
|
||||
x:Key="CompatCardShadow"
|
||||
Opacity="0.1"
|
||||
Offset="0,4,0"/>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -10,25 +10,25 @@
|
||||
<x:String x:Key="Sponsor_Afadian">https://afdian.net/a/DismissedLight</x:String>
|
||||
|
||||
<!-- AvatarCard -->
|
||||
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://static.snapgenshin.com/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
|
||||
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://api.snapgenshin.com/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
|
||||
|
||||
<!-- Bg -->
|
||||
<x:String x:Key="UI_Icon_Intee_Explore_1">https://static.snapgenshin.com/Bg/UI_Icon_Intee_Explore_1.png</x:String>
|
||||
<x:String x:Key="UI_ImgSign_ItemIcon">https://static.snapgenshin.com/Bg/UI_ImgSign_ItemIcon.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_None">https://static.snapgenshin.com/Bg/UI_ItemIcon_None.png</x:String>
|
||||
<x:String x:Key="UI_MarkQuest_Events_Proce">https://static.snapgenshin.com/Bg/UI_MarkQuest_Events_Proce.png</x:String>
|
||||
<x:String x:Key="UI_MarkTower">https://static.snapgenshin.com/Bg/UI_MarkTower.png</x:String>
|
||||
<x:String x:Key="UI_Icon_Intee_Explore_1">https://api.snapgenshin.com/static/raw/Bg/UI_Icon_Intee_Explore_1.png</x:String>
|
||||
<x:String x:Key="UI_ImgSign_ItemIcon">https://api.snapgenshin.com/static/raw/Bg/UI_ImgSign_ItemIcon.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_None">https://api.snapgenshin.com/static/raw/Bg/UI_ItemIcon_None.png</x:String>
|
||||
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Bg/UI_MarkQuest_Events_Proce.png</x:String>
|
||||
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Bg/UI_MarkTower.png</x:String>
|
||||
|
||||
<!-- ItemIcon -->
|
||||
<x:String x:Key="UI_ItemIcon_201">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_201.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_204">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_204.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_210">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_210.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_220021">https://static.snapgenshin.com/ItemIcon/UI_ItemIcon_220021.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_201">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_201.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_204">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_204.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_210">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_210.png</x:String>
|
||||
<x:String x:Key="UI_ItemIcon_220021">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_220021.png</x:String>
|
||||
|
||||
<!-- EmotionIcon -->
|
||||
<x:String x:Key="UI_EmotionIcon25">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon25.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon71">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon250">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon250.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon272">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon293">https://static.snapgenshin.com/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -3,18 +3,12 @@
|
||||
|
||||
namespace Snap.Hutao.Core.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 指示该类可以解构为元组
|
||||
/// </summary>
|
||||
/// <typeparam name="T1">元组的第一个类型</typeparam>
|
||||
/// <typeparam name="T2">元组的第二个类型</typeparam>
|
||||
[HighQuality]
|
||||
internal interface IDeconstruct<T1, T2>
|
||||
{
|
||||
/// <summary>
|
||||
/// 解构
|
||||
/// </summary>
|
||||
/// <param name="t1">第一个元素</param>
|
||||
/// <param name="t2">第二个元素</param>
|
||||
void Deconstruct(out T1 t1, out T2 t2);
|
||||
}
|
||||
|
||||
internal interface IDeconstruct<T1, T2, T3>
|
||||
{
|
||||
void Deconstruct(out T1 t1, out T2 t2, out T3 t3);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ internal static class DependencyInjection
|
||||
ServiceProvider serviceProvider = new ServiceCollection()
|
||||
|
||||
// Microsoft extension
|
||||
.AddLogging(builder => builder.AddConsoleWindow())
|
||||
.AddLogging(builder => builder.AddDebug().AddConsoleWindow())
|
||||
.AddMemoryCache()
|
||||
|
||||
// Hutao extensions
|
||||
|
||||
@@ -23,13 +23,6 @@ internal static partial class EnumerableExtension
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键类型</typeparam>
|
||||
/// <typeparam name="TValue">值类型</typeparam>
|
||||
/// <param name="dict">字典</param>
|
||||
/// <param name="key">键</param>
|
||||
public static void IncreaseOne<TKey, TValue>(this Dictionary<TKey, TValue> dict, TKey key)
|
||||
where TKey : notnull
|
||||
where TValue : struct, IIncrementOperators<TValue>
|
||||
@@ -37,14 +30,6 @@ internal static partial class EnumerableExtension
|
||||
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键类型</typeparam>
|
||||
/// <typeparam name="TValue">值类型</typeparam>
|
||||
/// <param name="dict">字典</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">增加的值</param>
|
||||
public static void IncreaseValue<TKey, TValue>(this Dictionary<TKey, TValue> dict, TKey key, TValue value)
|
||||
where TKey : notnull
|
||||
where TValue : struct, IAdditionOperators<TValue, TValue, TValue>
|
||||
@@ -54,14 +39,6 @@ internal static partial class EnumerableExtension
|
||||
current += value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加计数
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">键类型</typeparam>
|
||||
/// <typeparam name="TValue">值类型</typeparam>
|
||||
/// <param name="dict">字典</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns>是否存在键值</returns>
|
||||
public static bool TryIncreaseOne<TKey, TValue>(this Dictionary<TKey, TValue> dict, TKey key)
|
||||
where TKey : notnull
|
||||
where TValue : struct, IIncrementOperators<TValue>
|
||||
@@ -76,7 +53,6 @@ internal static partial class EnumerableExtension
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey}(IEnumerable{TSource}, Func{TSource, TKey})"/>
|
||||
public static Dictionary<TKey, TSource> ToDictionaryIgnoringDuplicateKeys<TKey, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
where TKey : notnull
|
||||
{
|
||||
@@ -90,7 +66,6 @@ internal static partial class EnumerableExtension
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Enumerable.ToDictionary{TSource, TKey, TElement}(IEnumerable{TSource}, Func{TSource, TKey}, Func{TSource, TElement})"/>
|
||||
public static Dictionary<TKey, TValue> ToDictionaryIgnoringDuplicateKeys<TKey, TValue, TSource>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> elementSelector)
|
||||
where TKey : notnull
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
@@ -43,6 +44,63 @@ internal static partial class EnumerableExtension
|
||||
return first;
|
||||
}
|
||||
|
||||
public static string JoinToString<T>(this IEnumerable<T> source, char separator, Action<StringBuilder, T> selector)
|
||||
{
|
||||
StringBuilder resultBuilder = new();
|
||||
|
||||
IEnumerator<T> enumerator = source.GetEnumerator();
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
T first = enumerator.Current;
|
||||
selector(resultBuilder, first);
|
||||
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
return resultBuilder.ToString();
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
resultBuilder.Append(separator);
|
||||
selector(resultBuilder, enumerator.Current);
|
||||
}
|
||||
while (enumerator.MoveNext());
|
||||
|
||||
return resultBuilder.ToString();
|
||||
}
|
||||
|
||||
public static string JoinToString<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> source, char separator, Action<StringBuilder, TKey, TValue> selector)
|
||||
{
|
||||
StringBuilder resultBuilder = new();
|
||||
|
||||
IEnumerator<KeyValuePair<TKey, TValue>> enumerator = source.GetEnumerator();
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
KeyValuePair<TKey, TValue> first = enumerator.Current;
|
||||
selector(resultBuilder, first.Key, first.Value);
|
||||
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
return resultBuilder.ToString();
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
resultBuilder.Append(separator);
|
||||
KeyValuePair<TKey, TValue> current = enumerator.Current;
|
||||
selector(resultBuilder, current.Key, current.Value);
|
||||
}
|
||||
while (enumerator.MoveNext());
|
||||
|
||||
return resultBuilder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换到 <see cref="ObservableCollection{T}"/>
|
||||
/// </summary>
|
||||
@@ -64,7 +122,6 @@ internal static partial class EnumerableExtension
|
||||
/// <returns>Converted collection into string.</returns>
|
||||
public static string ToString<T>(this IEnumerable<T> collection, char separator)
|
||||
{
|
||||
string result = string.Join(separator, collection);
|
||||
return result.Length > 0 ? result : string.Empty;
|
||||
return string.Join(separator, collection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Entity.Extension;
|
||||
|
||||
internal static class UserExtension
|
||||
{
|
||||
public static bool TryUpdateFingerprint(this User user, string? deviceFp)
|
||||
{
|
||||
if (string.IsNullOrEmpty(deviceFp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
user.Fingerprint = deviceFp;
|
||||
user.FingerprintLastUpdateTime = DateTimeOffset.UtcNow;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ internal enum SchemeType
|
||||
/// <summary>
|
||||
/// 国际服
|
||||
/// </summary>
|
||||
Mihoyo,
|
||||
Hoyoverse,
|
||||
|
||||
/// <summary>
|
||||
/// 国服官服
|
||||
|
||||
@@ -13,6 +13,8 @@ internal sealed partial class SettingEntry
|
||||
/// </summary>
|
||||
public const string GamePath = "GamePath";
|
||||
|
||||
public const string GamePathEntries = "GamePathEntries";
|
||||
|
||||
/// <summary>
|
||||
/// PowerShell 路径
|
||||
/// </summary>
|
||||
@@ -104,6 +106,8 @@ internal sealed partial class SettingEntry
|
||||
|
||||
public const string LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled";
|
||||
|
||||
public const string LaunchIsUseCloudThirdPartyMobile = "Launch.IsUseCloudThirdPartyMobile";
|
||||
|
||||
public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics";
|
||||
|
||||
public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying";
|
||||
|
||||
@@ -2045,6 +2045,9 @@
|
||||
<data name="ViewPageLaunchGameAppearanceBorderlessHeader" xml:space="preserve">
|
||||
<value>无边框</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceCloudThirdPartyMobileDescription" xml:space="preserve">
|
||||
<value>启用内置触摸布局,不会响应键鼠输入</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameAppearanceExclusiveDescription" xml:space="preserve">
|
||||
<value>与游戏内浏览器不兼容,切屏等操作也能使游戏闪退</value>
|
||||
</data>
|
||||
@@ -2189,6 +2192,9 @@
|
||||
<data name="ViewPageOpenScreenshotFolderAction" xml:space="preserve">
|
||||
<value>打开截图文件夹</value>
|
||||
</data>
|
||||
<data name="ViewPageResetGamePathAction" xml:space="preserve">
|
||||
<value>选择游戏路径</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingAboutHeader" xml:space="preserve">
|
||||
<value>关于 胡桃</value>
|
||||
</data>
|
||||
|
||||
@@ -15,20 +15,12 @@ namespace Snap.Hutao.Service;
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed partial class AppOptions : DbStoreOptions
|
||||
{
|
||||
private string? gamePath;
|
||||
private string? powerShellPath;
|
||||
private bool? isEmptyHistoryWishVisible;
|
||||
private BackdropType? backdropType;
|
||||
private CultureInfo? currentCulture;
|
||||
private bool? isAdvancedLaunchOptionsEnabled;
|
||||
private string? geetestCustomCompositeUrl;
|
||||
|
||||
public string GamePath
|
||||
{
|
||||
get => GetOption(ref gamePath, SettingEntry.GamePath);
|
||||
set => SetOption(ref gamePath, SettingEntry.GamePath, value);
|
||||
}
|
||||
|
||||
public string PowerShellPath
|
||||
{
|
||||
get
|
||||
@@ -80,14 +72,6 @@ internal sealed partial class AppOptions : DbStoreOptions
|
||||
set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name);
|
||||
}
|
||||
|
||||
public bool IsAdvancedLaunchOptionsEnabled
|
||||
{
|
||||
// DO NOT MOVE TO OTHER CLASS
|
||||
// We use this property in SettingPage binding
|
||||
get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled);
|
||||
set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value);
|
||||
}
|
||||
|
||||
public string GeetestCustomCompositeUrl
|
||||
{
|
||||
get => GetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl);
|
||||
|
||||
@@ -3,45 +3,11 @@
|
||||
|
||||
using Snap.Hutao.Model;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service;
|
||||
|
||||
internal static class AppOptionsExtension
|
||||
{
|
||||
public static bool TryGetGameFolderAndFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
|
||||
{
|
||||
string gamePath = appOptions.GamePath;
|
||||
|
||||
gameFolder = Path.GetDirectoryName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameFolder))
|
||||
{
|
||||
gameFileName = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
gameFileName = Path.GetFileName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameFileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetGamePathAndGameFileName(this AppOptions appOptions, out string gamePath, [NotNullWhen(true)] out string? gameFileName)
|
||||
{
|
||||
gamePath = appOptions.GamePath;
|
||||
|
||||
gameFileName = Path.GetFileName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameFileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static NameValue<CultureInfo>? GetCurrentCultureForSelectionOrDefault(this AppOptions appOptions)
|
||||
{
|
||||
return appOptions.Cultures.SingleOrDefault(c => c.Value == appOptions.CurrentCulture);
|
||||
|
||||
@@ -41,8 +41,7 @@ internal static class RegistryInterop
|
||||
string base64 = Convert.ToBase64String(target);
|
||||
string path = $"HKCU:{GenshinPath}";
|
||||
string command = $"""
|
||||
$value = [Convert]::FromBase64String('{base64}');
|
||||
Set-ItemProperty -Path '{path}' -Name '{SdkChineseKey}' -Value $value -Force;
|
||||
-Command "$value = [Convert]::FromBase64String('{base64}'); Set-ItemProperty -Path '{path}' -Name '{SdkChineseKey}' -Value $value -Force;"
|
||||
""";
|
||||
|
||||
ProcessStartInfo startInfo = new()
|
||||
|
||||
@@ -13,11 +13,11 @@ namespace Snap.Hutao.Service.Game.Configuration;
|
||||
[Injection(InjectAs.Singleton, typeof(IGameChannelOptionsService))]
|
||||
internal sealed partial class GameChannelOptionsService : IGameChannelOptionsService
|
||||
{
|
||||
private readonly AppOptions appOptions;
|
||||
private readonly LaunchOptions launchOptions;
|
||||
|
||||
public ChannelOptions GetChannelOptions()
|
||||
{
|
||||
string gamePath = appOptions.GamePath;
|
||||
string gamePath = launchOptions.GamePath;
|
||||
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName);
|
||||
bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
@@ -38,7 +38,7 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
|
||||
|
||||
public bool SetChannelOptions(LaunchScheme scheme)
|
||||
{
|
||||
string gamePath = appOptions.GamePath;
|
||||
string gamePath = launchOptions.GamePath;
|
||||
string? directory = Path.GetDirectoryName(gamePath);
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
string configPath = Path.Combine(directory, ConfigFileName);
|
||||
|
||||
@@ -5,6 +5,7 @@ using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.Game.Account;
|
||||
using Snap.Hutao.Service.Game.Configuration;
|
||||
using Snap.Hutao.Service.Game.Package;
|
||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||
using Snap.Hutao.Service.Game.Process;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
@@ -6,6 +6,8 @@ using Microsoft.UI.Windowing;
|
||||
using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.Foundation;
|
||||
@@ -24,7 +26,10 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
private readonly int primaryScreenHeight;
|
||||
private readonly int primaryScreenFps;
|
||||
|
||||
private string? gamePath;
|
||||
private ImmutableList<GamePathEntry>? gamePathEntries;
|
||||
private bool? isEnabled;
|
||||
private bool? isAdvancedLaunchOptionsEnabled;
|
||||
private bool? isFullScreen;
|
||||
private bool? isBorderless;
|
||||
private bool? isExclusive;
|
||||
@@ -36,6 +41,7 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
private int? targetFps;
|
||||
private NameValue<int>? monitor;
|
||||
private bool? isMonitorEnabled;
|
||||
private bool? isUseCloudThirdPartyMobile;
|
||||
private AspectRatio? selectedAspectRatio;
|
||||
private bool? useStarwardPlayTimeStatistics;
|
||||
private bool? setDiscordActivityWhenPlaying;
|
||||
@@ -85,12 +91,32 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
}
|
||||
}
|
||||
|
||||
public string GamePath
|
||||
{
|
||||
get => GetOption(ref gamePath, SettingEntry.GamePath);
|
||||
set => SetOption(ref gamePath, SettingEntry.GamePath, value);
|
||||
}
|
||||
|
||||
public ImmutableList<GamePathEntry> GamePathEntries
|
||||
{
|
||||
// Because DbStoreOptions can't detect collection change, We use
|
||||
// ImmutableList to imply that the whole list needs to be replaced
|
||||
get => GetOption(ref gamePathEntries, SettingEntry.GamePathEntries, raw => JsonSerializer.Deserialize<ImmutableList<GamePathEntry>>(raw), []);
|
||||
set => SetOption(ref gamePathEntries, SettingEntry.GamePathEntries, value, value => JsonSerializer.Serialize(value));
|
||||
}
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true);
|
||||
set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value);
|
||||
}
|
||||
|
||||
public bool IsAdvancedLaunchOptionsEnabled
|
||||
{
|
||||
get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled);
|
||||
set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value);
|
||||
}
|
||||
|
||||
public bool IsFullScreen
|
||||
{
|
||||
get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen);
|
||||
@@ -166,6 +192,12 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
set => SetOption(ref isMonitorEnabled, SettingEntry.LaunchIsMonitorEnabled, value);
|
||||
}
|
||||
|
||||
public bool IsUseCloudThirdPartyMobile
|
||||
{
|
||||
get => GetOption(ref isUseCloudThirdPartyMobile, SettingEntry.LaunchIsUseCloudThirdPartyMobile, false);
|
||||
set => SetOption(ref isUseCloudThirdPartyMobile, SettingEntry.LaunchIsUseCloudThirdPartyMobile, value);
|
||||
}
|
||||
|
||||
public List<AspectRatio> AspectRatios { get; } =
|
||||
[
|
||||
new(2560, 1440),
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
|
||||
internal static class LaunchOptionsExtension
|
||||
{
|
||||
public static bool TryGetGameFolderAndFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
|
||||
{
|
||||
string gamePath = options.GamePath;
|
||||
|
||||
gameFolder = Path.GetDirectoryName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameFolder))
|
||||
{
|
||||
gameFileName = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
gameFileName = Path.GetFileName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameFileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetGamePathAndGameFileName(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameFileName)
|
||||
{
|
||||
gamePath = options.GamePath;
|
||||
|
||||
gameFileName = Path.GetFileName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameFileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ImmutableList<GamePathEntry> GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry)
|
||||
{
|
||||
string gamePath = options.GamePath;
|
||||
|
||||
if (string.IsNullOrEmpty(gamePath))
|
||||
{
|
||||
entry = default;
|
||||
return options.GamePathEntries;
|
||||
}
|
||||
|
||||
if (options.GamePathEntries.SingleOrDefault(entry => string.Equals(entry.Path, options.GamePath, StringComparison.OrdinalIgnoreCase)) is { } existed)
|
||||
{
|
||||
entry = existed;
|
||||
return options.GamePathEntries;
|
||||
}
|
||||
|
||||
entry = GamePathEntry.Create(options.GamePath);
|
||||
return [.. options.GamePathEntries, entry];
|
||||
}
|
||||
|
||||
public static ImmutableList<GamePathEntry> RemoveGamePathEntry(this LaunchOptions options, GamePathEntry? entry, out GamePathEntry? selected)
|
||||
{
|
||||
if (entry is not null)
|
||||
{
|
||||
if (string.Equals(options.GamePath, entry.Path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
options.GamePath = string.Empty;
|
||||
}
|
||||
|
||||
options.GamePathEntries = options.GamePathEntries.Remove(entry);
|
||||
}
|
||||
|
||||
return options.GetGamePathEntries(out selected);
|
||||
}
|
||||
|
||||
public static ImmutableList<GamePathEntry> UpdateGamePathAndRefreshEntries(this LaunchOptions options, string gamePath)
|
||||
{
|
||||
options.GamePath = gamePath;
|
||||
ImmutableList<GamePathEntry> entries = options.GetGamePathEntries(out _);
|
||||
options.GamePathEntries = entries;
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,12 @@ internal sealed partial class GamePackageService : IGamePackageService
|
||||
{
|
||||
private readonly PackageConverter packageConverter;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly LaunchOptions launchOptions;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly AppOptions appOptions;
|
||||
|
||||
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
if (!appOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
|
||||
if (!launchOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -58,7 +58,7 @@ internal sealed partial class GamePackageService : IGamePackageService
|
||||
string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName;
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
appOptions.GamePath = Path.Combine(gameFolder, exeName);
|
||||
launchOptions.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, exeName));
|
||||
}
|
||||
|
||||
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.PathAbstraction;
|
||||
|
||||
internal sealed class GamePathEntry
|
||||
{
|
||||
[JsonPropertyName("Path")]
|
||||
public string Path { get; set; } = default!;
|
||||
|
||||
[JsonIgnore]
|
||||
public GamePathKind Kind { get => GetKind(Path); }
|
||||
|
||||
public static GamePathEntry Create(string path)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Path = path,
|
||||
};
|
||||
}
|
||||
|
||||
private static GamePathKind GetKind(string path)
|
||||
{
|
||||
return GamePathKind.None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.PathAbstraction;
|
||||
|
||||
internal enum GamePathKind
|
||||
{
|
||||
None,
|
||||
ChineseClient,
|
||||
OverseaClient,
|
||||
ChineseCloud,
|
||||
OverseaCloud,
|
||||
}
|
||||
@@ -3,19 +3,19 @@
|
||||
|
||||
using Snap.Hutao.Service.Game.Locator;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
namespace Snap.Hutao.Service.Game.PathAbstraction;
|
||||
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IGamePathService))]
|
||||
internal sealed partial class GamePathService : IGamePathService
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly AppOptions appOptions;
|
||||
private readonly LaunchOptions launchOptions;
|
||||
|
||||
public async ValueTask<ValueResult<bool, string>> SilentGetGamePathAsync()
|
||||
{
|
||||
// Cannot find in setting
|
||||
if (string.IsNullOrEmpty(appOptions.GamePath))
|
||||
if (string.IsNullOrEmpty(launchOptions.GamePath))
|
||||
{
|
||||
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService<IGameLocatorFactory>();
|
||||
|
||||
@@ -40,7 +40,7 @@ internal sealed partial class GamePathService : IGamePathService
|
||||
if (isOk)
|
||||
{
|
||||
// Save result.
|
||||
appOptions.GamePath = path;
|
||||
launchOptions.UpdateGamePathAndRefreshEntries(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -48,9 +48,9 @@ internal sealed partial class GamePathService : IGamePathService
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(appOptions.GamePath))
|
||||
if (!string.IsNullOrEmpty(launchOptions.GamePath))
|
||||
{
|
||||
return new(true, appOptions.GamePath);
|
||||
return new(true, launchOptions.GamePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
namespace Snap.Hutao.Service.Game.PathAbstraction;
|
||||
|
||||
internal interface IGamePathService
|
||||
{
|
||||
@@ -21,7 +21,6 @@ internal sealed partial class GameProcessService : IGameProcessService
|
||||
private readonly IDiscordService discordService;
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
private readonly LaunchOptions launchOptions;
|
||||
private readonly AppOptions appOptions;
|
||||
|
||||
private volatile bool isGameRunning;
|
||||
|
||||
@@ -52,7 +51,7 @@ internal sealed partial class GameProcessService : IGameProcessService
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName))
|
||||
if (!launchOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName))
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(gamePath);
|
||||
return; // null check passing, actually never reach.
|
||||
@@ -73,7 +72,7 @@ internal sealed partial class GameProcessService : IGameProcessService
|
||||
await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps)
|
||||
if (runtimeOptions.IsElevated && launchOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps)
|
||||
{
|
||||
progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps));
|
||||
try
|
||||
@@ -116,6 +115,7 @@ internal sealed partial class GameProcessService : IGameProcessService
|
||||
.AppendIf("-screen-width", launchOptions.IsScreenWidthEnabled, launchOptions.ScreenWidth)
|
||||
.AppendIf("-screen-height", launchOptions.IsScreenHeightEnabled, launchOptions.ScreenHeight)
|
||||
.AppendIf("-monitor", launchOptions.IsMonitorEnabled, launchOptions.Monitor.Value)
|
||||
.AppendIf("-platform_type CLOUD_THIRD_PARTY_MOBILE", launchOptions.IsUseCloudThirdPartyMobile)
|
||||
.ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.ViewModel.User;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using System.Collections.ObjectModel;
|
||||
using BindingUser = Snap.Hutao.ViewModel.User.User;
|
||||
@@ -20,6 +19,8 @@ internal interface IUserCollectionService
|
||||
UserGameRole? GetUserGameRoleByUid(string uid);
|
||||
|
||||
ValueTask RemoveUserAsync(BindingUser user);
|
||||
ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromCookieAsync(Cookie cookie, bool isOversea);
|
||||
|
||||
ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromInputCookieAsync(InputCookie inputCookie);
|
||||
|
||||
bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user);
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
namespace Snap.Hutao.Service.User;
|
||||
|
||||
internal interface IUserInitializationService
|
||||
{
|
||||
ValueTask<ViewModel.User.User?> CreateUserFromCookieOrDefaultAsync(Cookie cookie, bool isOversea, CancellationToken token = default(CancellationToken));
|
||||
ValueTask<ViewModel.User.User?> CreateUserFromInputCookieOrDefaultAsync(InputCookie inputCookie, CancellationToken token = default(CancellationToken));
|
||||
|
||||
ValueTask<ViewModel.User.User> ResumeUserAsync(Model.Entity.User inner, CancellationToken token = default(CancellationToken));
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.ViewModel.User;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using System.Collections.ObjectModel;
|
||||
using BindingUser = Snap.Hutao.ViewModel.User.User;
|
||||
@@ -42,13 +41,7 @@ internal interface IUserService
|
||||
/// <returns>对应的角色信息</returns>
|
||||
UserGameRole? GetUserGameRoleByUid(string uid);
|
||||
|
||||
/// <summary>
|
||||
/// 尝试异步处理输入的Cookie
|
||||
/// </summary>
|
||||
/// <param name="cookie">Cookie</param>
|
||||
/// <param name="isOversea">是否为国际服</param>
|
||||
/// <returns>处理的结果</returns>
|
||||
ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie, bool isOversea);
|
||||
ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(InputCookie inputCookie);
|
||||
|
||||
ValueTask<bool> RefreshCookieTokenAsync(Model.Entity.User user);
|
||||
|
||||
|
||||
48
src/Snap.Hutao/Snap.Hutao/Service/User/InputCookie.cs
Normal file
48
src/Snap.Hutao/Snap.Hutao/Service/User/InputCookie.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
namespace Snap.Hutao.Service.User;
|
||||
|
||||
internal sealed class InputCookie : IDeconstruct<Cookie, bool, string?>
|
||||
{
|
||||
private InputCookie(Cookie cookie, bool isOversea)
|
||||
{
|
||||
Cookie = cookie;
|
||||
IsOversea = isOversea;
|
||||
cookie.TryGetDeviceFp(out string? deviceFp);
|
||||
DeviceFp = deviceFp;
|
||||
}
|
||||
|
||||
private InputCookie(Cookie cookie, bool isOversea, string? deviceFp)
|
||||
{
|
||||
Cookie = cookie;
|
||||
IsOversea = isOversea;
|
||||
DeviceFp = deviceFp;
|
||||
}
|
||||
|
||||
public Cookie Cookie { get; }
|
||||
|
||||
public bool IsOversea { get; }
|
||||
|
||||
public string? DeviceFp { get; }
|
||||
|
||||
public static InputCookie Create(Cookie cookie, bool isOversea, string? deviceFp)
|
||||
{
|
||||
return new InputCookie(cookie, isOversea, deviceFp);
|
||||
}
|
||||
|
||||
public static InputCookie CreateWithDeviceFpInference(Cookie cookie, bool isOversea)
|
||||
{
|
||||
return new InputCookie(cookie, isOversea);
|
||||
}
|
||||
|
||||
public void Deconstruct(out Cookie cookie, out bool isOversea, out string? deviceFp)
|
||||
{
|
||||
cookie = Cookie;
|
||||
isOversea = IsOversea;
|
||||
deviceFp = DeviceFp;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Message;
|
||||
using Snap.Hutao.ViewModel.User;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using System.Collections.ObjectModel;
|
||||
using BindingUser = Snap.Hutao.ViewModel.User.User;
|
||||
@@ -113,6 +112,11 @@ internal sealed partial class UserCollectionService : IUserCollectionService
|
||||
midUserMap?.Remove(user.Entity.Mid);
|
||||
}
|
||||
|
||||
foreach (UserGameRole role in user.UserGameRoles)
|
||||
{
|
||||
uidUserGameRoleMap?.Remove(role.GameUid);
|
||||
}
|
||||
|
||||
// Sync database
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
await userDbService.DeleteUserByIdAsync(user.Entity.InnerId).ConfigureAwait(false);
|
||||
@@ -146,14 +150,14 @@ internal sealed partial class UserCollectionService : IUserCollectionService
|
||||
return midUserMap.TryGetValue(mid, out user);
|
||||
}
|
||||
|
||||
public async ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromCookieAsync(Cookie cookie, bool isOversea)
|
||||
public async ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromInputCookieAsync(InputCookie inputCookie)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
BindingUser? newUser = await userInitializationService.CreateUserFromCookieOrDefaultAsync(cookie, isOversea).ConfigureAwait(false);
|
||||
BindingUser? newUser = await userInitializationService.CreateUserFromInputCookieOrDefaultAsync(inputCookie).ConfigureAwait(false);
|
||||
|
||||
if (newUser is null)
|
||||
{
|
||||
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieRequestUserInfoFailed);
|
||||
return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieRequestUserInfoFailed);
|
||||
}
|
||||
|
||||
await GetUserCollectionAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.ViewModel.User;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.PublicData.DeviceFp;
|
||||
using Snap.Hutao.Web.Response;
|
||||
@@ -21,7 +22,7 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.Entity.FingerprintLastUpdateTime >= DateTimeOffset.UtcNow - TimeSpan.FromDays(3))
|
||||
if (user.Entity.FingerprintLastUpdateTime >= DateTimeOffset.UtcNow - TimeSpan.FromDays(7))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(user.Fingerprint))
|
||||
{
|
||||
@@ -29,40 +30,62 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService
|
||||
}
|
||||
}
|
||||
|
||||
string model = Core.Random.GetUpperAndNumberString(6);
|
||||
Dictionary<string, string> extendProperties = new()
|
||||
string device = Core.Random.GetUpperAndNumberString(12);
|
||||
string product = Core.Random.GetUpperAndNumberString(6);
|
||||
Dictionary<string, object> extendProperties = new()
|
||||
{
|
||||
{ "cpuType", "arm64-v8a" },
|
||||
{ "proxyStatus", 0 },
|
||||
{ "isRoot", 0 },
|
||||
{ "romCapacity", "512" },
|
||||
{ "productName", model },
|
||||
{ "romRemain", "256" },
|
||||
{ "manufacturer", "XiaoMi" },
|
||||
{ "appMemory", "512" },
|
||||
{ "deviceName", device },
|
||||
{ "productName", product },
|
||||
{ "romRemain", "512" },
|
||||
{ "hostname", "dg02-pool03-kvm87" },
|
||||
{ "screenSize", "1080x1920" },
|
||||
{ "osVersion", "13" },
|
||||
{ "screenSize", "1440x2905" },
|
||||
{ "isTablet", 0 },
|
||||
{ "aaid", string.Empty },
|
||||
{ "vendor", "中国移动" },
|
||||
{ "accelerometer", "1.4883357x7.1712894x6.2847486" },
|
||||
{ "buildTags", "release-keys" },
|
||||
{ "model", model },
|
||||
{ "model", device },
|
||||
{ "brand", "XiaoMi" },
|
||||
{ "oaid", string.Empty },
|
||||
{ "hardware", "qcom" },
|
||||
{ "deviceType", "OP5913L1" },
|
||||
{ "devId", "REL" },
|
||||
{ "serialNumber", "unknown" },
|
||||
{ "buildTime", "1687848011000" },
|
||||
{ "buildUser", "root" },
|
||||
{ "ramCapacity", "469679" },
|
||||
{ "magnetometer", "20.081251x-27.487501x2.1937501" },
|
||||
{ "display", $"{model}_13.1.0.181(CN01)" },
|
||||
{ "ramRemain", "215344" },
|
||||
{ "deviceInfo", $@"XiaoMi/{model}/OP5913L1:13/SKQ1.221119.001/T.118e6c7-5aa23-73911:user/release-keys" },
|
||||
{ "gyroscope", "0.030226856x0.014647375x0.010652636" },
|
||||
{ "sdCapacity", 512215 },
|
||||
{ "buildTime", "1693626947000" },
|
||||
{ "buildUser", "android-build" },
|
||||
{ "simState", 5 },
|
||||
{ "ramRemain", "239814" },
|
||||
{ "appUpdateTimeDiff", 1702604034482 },
|
||||
{ "deviceInfo", $@"XiaoMi\/{product}\/OP5913L1:13\/SKQ1.221119.001\/T.118e6c7-5aa23-73911:user\/release-keys" },
|
||||
{ "vaid", string.Empty },
|
||||
{ "buildType", "user" },
|
||||
{ "sdkVersion", "33" },
|
||||
{ "sdkVersion", "34" },
|
||||
{ "ui_mode", "UI_MODE_TYPE_NORMAL" },
|
||||
{ "isMockLocation", 0 },
|
||||
{ "cpuType", "arm64-v8a" },
|
||||
{ "isAirMode", 0 },
|
||||
{ "ringMode", 2 },
|
||||
{ "chargeStatus", 1 },
|
||||
{ "manufacturer", "XiaoMi" },
|
||||
{ "emulatorStatus", 0 },
|
||||
{ "appMemory", "512" },
|
||||
{ "osVersion", "14" },
|
||||
{ "vendor", "unknown" },
|
||||
{ "accelerometer", "1.4883357x7.1712894x6.2847486" },
|
||||
{ "sdRemain", 239600 },
|
||||
{ "buildTags", "release-keys" },
|
||||
{ "packageName", "com.mihoyo.hyperion" },
|
||||
{ "networkType", "WiFi" },
|
||||
{ "oaid", string.Empty },
|
||||
{ "debugStatus", 1 },
|
||||
{ "ramCapacity", "469679" },
|
||||
{ "magnetometer", "20.081251x-27.487501x2.1937501" },
|
||||
{ "display", $"{product}_13.1.0.181(CN01)" },
|
||||
{ "appInstallTimeDiff", 1688455751496 },
|
||||
{ "packageVersion", "2.20.1" },
|
||||
{ "gyroscope", "0.030226856x0.014647375x0.010652636" },
|
||||
{ "batteryStatus", 100 },
|
||||
{ "hasKeyboard", 0 },
|
||||
{ "board", "taro" },
|
||||
};
|
||||
|
||||
@@ -79,9 +102,8 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService
|
||||
};
|
||||
|
||||
Response<DeviceFpWrapper> response = await deviceFpClient.GetFingerprintAsync(data, token).ConfigureAwait(false);
|
||||
user.Fingerprint = response.IsOk() ? response.Data.DeviceFp : string.Empty;
|
||||
user.TryUpdateFingerprint(response.IsOk() ? response.Data.DeviceFp : string.Empty);
|
||||
|
||||
user.Entity.FingerprintLastUpdateTime = DateTimeOffset.UtcNow;
|
||||
user.NeedDbUpdateAfterResume = true;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
using Snap.Hutao.Model.Entity.Extension;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Passport;
|
||||
@@ -30,14 +31,16 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
|
||||
return user;
|
||||
}
|
||||
|
||||
public async ValueTask<ViewModel.User.User?> CreateUserFromCookieOrDefaultAsync(Cookie cookie, bool isOversea, CancellationToken token = default)
|
||||
public async ValueTask<ViewModel.User.User?> CreateUserFromInputCookieOrDefaultAsync(InputCookie inputCookie, CancellationToken token = default)
|
||||
{
|
||||
// 这里只负责创建实体用户,稍后在用户服务中保存到数据库
|
||||
(Cookie cookie, bool isOversea, string? deviceFp) = inputCookie;
|
||||
Model.Entity.User entity = Model.Entity.User.From(cookie, isOversea);
|
||||
|
||||
entity.Aid = cookie.GetValueOrDefault(Cookie.STUID);
|
||||
entity.Mid = isOversea ? entity.Aid : cookie.GetValueOrDefault(Cookie.MID);
|
||||
entity.IsOversea = isOversea;
|
||||
entity.TryUpdateFingerprint(deviceFp);
|
||||
|
||||
if (entity.Aid is not null && entity.Mid is not null)
|
||||
{
|
||||
|
||||
@@ -17,15 +17,15 @@ internal enum UserOptionResult
|
||||
/// <summary>
|
||||
/// Cookie不完整
|
||||
/// </summary>
|
||||
Incomplete,
|
||||
CookieIncomplete,
|
||||
|
||||
/// <summary>
|
||||
/// Cookie信息已经失效
|
||||
/// </summary>
|
||||
Invalid,
|
||||
CookieInvalid,
|
||||
|
||||
/// <summary>
|
||||
/// 用户的Cookie成功更新
|
||||
/// </summary>
|
||||
Updated,
|
||||
CookieUpdated,
|
||||
}
|
||||
@@ -56,33 +56,36 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
|
||||
return userCollectionService.GetUserGameRoleByUid(uid);
|
||||
}
|
||||
|
||||
public async ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie, bool isOversea)
|
||||
public async ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(InputCookie inputCookie)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
string? mid = cookie.GetValueOrDefault(isOversea ? Cookie.STUID : Cookie.MID);
|
||||
(Cookie cookie, bool isOversea, string? deviceFp) = inputCookie;
|
||||
|
||||
if (string.IsNullOrEmpty(mid))
|
||||
string? midOrAid = cookie.GetValueOrDefault(isOversea ? Cookie.STUID : Cookie.MID);
|
||||
|
||||
if (string.IsNullOrEmpty(midOrAid))
|
||||
{
|
||||
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoMid);
|
||||
return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieNoMid);
|
||||
}
|
||||
|
||||
// 检查 mid 对应用户是否存在
|
||||
if (!userCollectionService.TryGetUserByMid(mid, out BindingUser? user))
|
||||
if (!userCollectionService.TryGetUserByMid(midOrAid, out BindingUser? user))
|
||||
{
|
||||
return await userCollectionService.TryCreateAndAddUserFromCookieAsync(cookie, isOversea).ConfigureAwait(false);
|
||||
return await userCollectionService.TryCreateAndAddUserFromInputCookieAsync(inputCookie).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!cookie.TryGetSToken(isOversea, out Cookie? stoken))
|
||||
{
|
||||
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken);
|
||||
return new(UserOptionResult.CookieInvalid, SH.ServiceUserProcessCookieNoSToken);
|
||||
}
|
||||
|
||||
user.SToken = stoken;
|
||||
user.LToken = cookie.TryGetLToken(out Cookie? ltoken) ? ltoken : user.LToken;
|
||||
user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken;
|
||||
user.TryUpdateFingerprint(deviceFp);
|
||||
|
||||
await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false);
|
||||
return new(UserOptionResult.Updated, mid);
|
||||
return new(UserOptionResult.CookieUpdated, midOrAid);
|
||||
}
|
||||
|
||||
public async ValueTask<bool> RefreshCookieTokenAsync(Model.Entity.User user)
|
||||
|
||||
@@ -299,13 +299,14 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
<PackageReference Include="Snap.Discord.GameSDK" Version="1.5.0" />
|
||||
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.0.1">
|
||||
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
<Image
|
||||
Width="320"
|
||||
Height="320"
|
||||
Source="{x:Bind QRCodeSource}"/>
|
||||
Source="{x:Bind QRCodeSource, Mode=OneWay}"/>
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
</ContentDialog>
|
||||
@@ -71,7 +71,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable
|
||||
private async ValueTask<ValueResult<bool, UidGameToken>> GetUidGameTokenCoreAsync()
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
await ShowAsync();
|
||||
_ = ShowAsync();
|
||||
|
||||
while (!userManualCancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="CardTemplate" x:DataType="shvcp:CardReference">
|
||||
<ItemContainer cw:Effects.Shadow="{StaticResource CompatCardShadow}" Child="{Binding Card}">
|
||||
<ItemContainer cw:Effects.Shadow="{ThemeResource CompatCardShadow}" Child="{Binding Card}">
|
||||
<ItemContainer.Resources>
|
||||
<SolidColorBrush x:Key="ItemContainerPointerOverBackground" Color="Transparent"/>
|
||||
</ItemContainer.Resources>
|
||||
|
||||
@@ -7,7 +7,6 @@ using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.Web.Bridge;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
namespace Snap.Hutao.View.Page;
|
||||
|
||||
@@ -30,11 +29,11 @@ internal interface ISupportLoginByWebView
|
||||
}
|
||||
}
|
||||
|
||||
static async ValueTask PostHandleCurrentCookieAsync(IServiceProvider serviceProvider, Cookie cookie, bool isOversea)
|
||||
static async ValueTask PostHandleCurrentCookieAsync(IServiceProvider serviceProvider, InputCookie inputCookie)
|
||||
{
|
||||
(UserOptionResult result, string nickname) = await serviceProvider
|
||||
.GetRequiredService<IUserService>()
|
||||
.ProcessInputCookieAsync(cookie, isOversea)
|
||||
.ProcessInputCookieAsync(inputCookie)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
serviceProvider.GetRequiredService<INavigationService>().GoBack();
|
||||
|
||||
@@ -38,7 +38,12 @@
|
||||
|
||||
<DataTemplate x:Key="GameAccountListTemplate">
|
||||
<Grid>
|
||||
<StackPanel Margin="0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Margin="0,12">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
<TextBlock
|
||||
Opacity="0.8"
|
||||
@@ -47,9 +52,9 @@
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
x:Name="ButtonPanel"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Visibility="Collapsed">
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
MinWidth="48"
|
||||
Margin="4,8"
|
||||
@@ -78,264 +83,293 @@
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountRemoveToolTip}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<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>
|
||||
<DataTemplate x:Key="GamePathEntryListTemplate">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Padding="0,16"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Path}"/>
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
MinWidth="48"
|
||||
Margin="4,8,0,8"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{Binding DataContext.RemoveGamePathEntryCommand, Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountRemoveToolTip}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
<Grid>
|
||||
<Grid
|
||||
Height="{StaticResource AppBarThemeCompactHeight}"
|
||||
VerticalAlignment="Top"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
IsHitTestVisible="False"/>
|
||||
<Pivot>
|
||||
<Pivot.RightHeader>
|
||||
<CommandBar DefaultLabelPosition="Right">
|
||||
<CommandBar.Content>
|
||||
<TextBlock
|
||||
Margin="12,14,12,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding LaunchStatusOptions.LaunchStatus.Description}"/>
|
||||
</CommandBar.Content>
|
||||
<AppBarButton
|
||||
Command="{Binding OpenScreenshotFolderCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewPageOpenScreenshotFolderAction}"/>
|
||||
<AppBarButton
|
||||
Command="{Binding LaunchCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewPageLaunchGameAction}"/>
|
||||
</CommandBar>
|
||||
</Pivot.RightHeader>
|
||||
<Grid Visibility="{Binding GamePathSelectedAndValid, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid
|
||||
Height="{StaticResource AppBarThemeCompactHeight}"
|
||||
VerticalAlignment="Top"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
IsHitTestVisible="False"/>
|
||||
<Pivot>
|
||||
<Pivot.RightHeader>
|
||||
<CommandBar DefaultLabelPosition="Right">
|
||||
<CommandBar.Content>
|
||||
<TextBlock
|
||||
Margin="12,14,12,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding LaunchStatusOptions.LaunchStatus.Description}"/>
|
||||
</CommandBar.Content>
|
||||
<AppBarButton
|
||||
Command="{Binding OpenScreenshotFolderCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewPageOpenScreenshotFolderAction}"/>
|
||||
<AppBarButton
|
||||
Command="{Binding ResetGamePathCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewPageResetGamePathAction}"/>
|
||||
<AppBarButton
|
||||
Command="{Binding LaunchCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewPageLaunchGameAction}"/>
|
||||
</CommandBar>
|
||||
</Pivot.RightHeader>
|
||||
|
||||
<PivotItem Header="{shcm:ResourceString Name=ViewPageLaunchGameOptionsHeader}">
|
||||
<ScrollViewer Grid.RowSpan="2">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition MaxWidth="1000"/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Margin="16" Spacing="{StaticResource SettingsCardSpacing}">
|
||||
<InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="{shcm:ResourceString Name=ViewPageLaunchGameConfigurationSaveHint}"
|
||||
Severity="Informational"/>
|
||||
|
||||
<!-- 文件 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameFileHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsEnabled="{Binding RuntimeOptions.IsElevated}">
|
||||
<cwc:SettingsCard.Description>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeDescription}"/>
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource SystemControlErrorTextForegroundBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeWarning}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard.Description>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
<shc:SizeRestrictedContentControl>
|
||||
<shccs:ComboBox2
|
||||
DisplayMemberPath="DisplayName"
|
||||
EnableMemberPath="IsNotCompatOnly"
|
||||
ItemsSource="{Binding KnownSchemes}"
|
||||
SelectedItem="{Binding SelectedScheme, Mode=TwoWay}"
|
||||
Style="{StaticResource DefaultComboBoxStyle}"/>
|
||||
</shc:SizeRestrictedContentControl>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
|
||||
<!-- 注册表 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameRegistryHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
ActionIconToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountDetectAction}"
|
||||
Command="{Binding DetectGameAccountCommand}"
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsClickEnabled="True"/>
|
||||
<Border Style="{StaticResource BorderCardStyle}">
|
||||
<ListView
|
||||
ItemTemplate="{StaticResource GameAccountListTemplate}"
|
||||
ItemsSource="{Binding GameAccounts}"
|
||||
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
|
||||
</Border>
|
||||
|
||||
<!-- 进程 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameProcessHeader}"/>
|
||||
<cwc:SettingsExpander
|
||||
shch:SettingsExpanderHelper.IsItemsEnabled="{Binding LaunchOptions.IsEnabled}"
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsEnabled, Mode=TwoWay}"/>
|
||||
<cwc:SettingsExpander.Items>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceExclusiveDescription}" Header="-window-mode exclusive">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsExclusive, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceFullscreenDescription}" Header="-screen-fullscreen">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsFullScreen, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceBorderlessDescription}" Header="-popupwindow">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsBorderless, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioHeader}">
|
||||
<shc:SizeRestrictedContentControl Margin="0,0,136,0">
|
||||
<ComboBox
|
||||
ItemsSource="{Binding LaunchOptions.AspectRatios}"
|
||||
PlaceholderText="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioPlaceHolder}"
|
||||
SelectedItem="{Binding LaunchOptions.SelectedAspectRatio, Mode=TwoWay}"/>
|
||||
</shc:SizeRestrictedContentControl>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenWidthDescription}" Header="-screen-width">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<NumberBox
|
||||
Width="156"
|
||||
Padding="12,6,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding LaunchOptions.IsScreenWidthEnabled}"
|
||||
Value="{Binding LaunchOptions.ScreenWidth, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenWidthEnabled, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenHeightDescription}" Header="-screen-height">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<NumberBox
|
||||
Width="156"
|
||||
Padding="12,6,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding LaunchOptions.IsScreenHeightEnabled}"
|
||||
Value="{Binding LaunchOptions.ScreenHeight, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenHeightEnabled, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameMonitorsDescription}" Header="-monitor">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<shc:SizeRestrictedContentControl>
|
||||
<ComboBox
|
||||
DisplayMemberPath="Name"
|
||||
IsEnabled="{Binding LaunchOptions.IsMonitorEnabled}"
|
||||
ItemsSource="{Binding LaunchOptions.Monitors}"
|
||||
SelectedItem="{Binding LaunchOptions.Monitor, Mode=TwoWay}"/>
|
||||
</shc:SizeRestrictedContentControl>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsMonitorEnabled, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
</cwc:SettingsExpander.Items>
|
||||
</cwc:SettingsExpander>
|
||||
<cwc:SettingsCard
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsEnabled="{Binding RuntimeOptions.IsElevated}"
|
||||
Visibility="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
<NumberBox
|
||||
MinWidth="156"
|
||||
Padding="10,8,0,0"
|
||||
Maximum="720"
|
||||
Minimum="60"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Value="{Binding LaunchOptions.TargetFps, Mode=TwoWay}"/>
|
||||
<ToggleSwitch
|
||||
Width="120"
|
||||
IsOn="{Binding LaunchOptions.UnlockFps, Mode=TwoWay}"
|
||||
OffContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOff}"
|
||||
OnContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOn}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
|
||||
<!-- 进程间 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameInterProcessHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.UseStarwardPlayTimeStatistics, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.SetDiscordActivityWhenPlaying, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</PivotItem>
|
||||
<PivotItem Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceHeader}">
|
||||
<Grid>
|
||||
<ScrollViewer Visibility="{Binding GameResource, Converter={StaticResource EmptyObjectToBoolConverter}}">
|
||||
<PivotItem Header="{shcm:ResourceString Name=ViewPageLaunchGameOptionsHeader}">
|
||||
<ScrollViewer Grid.RowSpan="2">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition MaxWidth="1000"/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel>
|
||||
<shvc:LaunchGameResourceExpander
|
||||
Margin="16,16,16,0"
|
||||
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
|
||||
DataContext="{Binding GameResource.PreDownloadGame.Latest, Mode=OneWay}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourcePreDownloadHeader}"
|
||||
Visibility="{Binding FallbackValue={StaticResource VisibilityCollapsed}, Converter={StaticResource EmptyObjectToVisibilityConverter}}"/>
|
||||
<ItemsControl
|
||||
Margin="0,0,0,0"
|
||||
ItemTemplate="{StaticResource GameResourceTemplate}"
|
||||
ItemsSource="{Binding GameResource.PreDownloadGame.Diffs, Mode=OneWay}"/>
|
||||
<shvc:LaunchGameResourceExpander
|
||||
Margin="16,16,16,0"
|
||||
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
|
||||
DataContext="{Binding GameResource.Game.Latest, Mode=OneWay}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceLatestHeader}"/>
|
||||
<ItemsControl
|
||||
Margin="0,0,0,16"
|
||||
ItemTemplate="{StaticResource GameResourceTemplate}"
|
||||
ItemsSource="{Binding GameResource.Game.Diffs, Mode=OneWay}"/>
|
||||
<StackPanel Margin="16" Spacing="{StaticResource SettingsCardSpacing}">
|
||||
<InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="{shcm:ResourceString Name=ViewPageLaunchGameConfigurationSaveHint}"
|
||||
Severity="Informational"/>
|
||||
|
||||
<!-- 文件 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameFileHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsEnabled="{Binding RuntimeOptions.IsElevated}">
|
||||
<cwc:SettingsCard.Description>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeDescription}"/>
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource SystemControlErrorTextForegroundBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeWarning}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard.Description>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
<shc:SizeRestrictedContentControl>
|
||||
<shccs:ComboBox2
|
||||
DisplayMemberPath="DisplayName"
|
||||
EnableMemberPath="IsNotCompatOnly"
|
||||
ItemsSource="{Binding KnownSchemes}"
|
||||
SelectedItem="{Binding SelectedScheme, Mode=TwoWay}"
|
||||
Style="{StaticResource DefaultComboBoxStyle}"/>
|
||||
</shc:SizeRestrictedContentControl>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
|
||||
<!-- 注册表 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameRegistryHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
ActionIconToolTip="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountDetectAction}"
|
||||
Command="{Binding DetectGameAccountCommand}"
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameSwitchAccountHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsClickEnabled="True"/>
|
||||
<Border Style="{StaticResource BorderCardStyle}">
|
||||
<ListView
|
||||
ItemTemplate="{StaticResource GameAccountListTemplate}"
|
||||
ItemsSource="{Binding GameAccounts}"
|
||||
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
|
||||
</Border>
|
||||
|
||||
<!-- 进程 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameProcessHeader}"/>
|
||||
<cwc:SettingsExpander
|
||||
shch:SettingsExpanderHelper.IsItemsEnabled="{Binding LaunchOptions.IsEnabled}"
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsEnabled, Mode=TwoWay}"/>
|
||||
<cwc:SettingsExpander.Items>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceExclusiveDescription}" Header="-window-mode exclusive">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsExclusive, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceFullscreenDescription}" Header="-screen-fullscreen">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsFullScreen, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceBorderlessDescription}" Header="-popupwindow">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsBorderless, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceCloudThirdPartyMobileDescription}" Header="-platform_type CLOUD_THIRD_PARTY_MOBILE">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsUseCloudThirdPartyMobile, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioHeader}">
|
||||
<shc:SizeRestrictedContentControl Margin="0,0,136,0">
|
||||
<ComboBox
|
||||
ItemsSource="{Binding LaunchOptions.AspectRatios}"
|
||||
PlaceholderText="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioPlaceHolder}"
|
||||
SelectedItem="{Binding LaunchOptions.SelectedAspectRatio, Mode=TwoWay}"/>
|
||||
</shc:SizeRestrictedContentControl>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenWidthDescription}" Header="-screen-width">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<NumberBox
|
||||
Width="156"
|
||||
Padding="12,6,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding LaunchOptions.IsScreenWidthEnabled}"
|
||||
Value="{Binding LaunchOptions.ScreenWidth, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenWidthEnabled, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenHeightDescription}" Header="-screen-height">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<NumberBox
|
||||
Width="156"
|
||||
Padding="12,6,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding LaunchOptions.IsScreenHeightEnabled}"
|
||||
Value="{Binding LaunchOptions.ScreenHeight, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenHeightEnabled, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameMonitorsDescription}" Header="-monitor">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<shc:SizeRestrictedContentControl>
|
||||
<ComboBox
|
||||
DisplayMemberPath="Name"
|
||||
IsEnabled="{Binding LaunchOptions.IsMonitorEnabled}"
|
||||
ItemsSource="{Binding LaunchOptions.Monitors}"
|
||||
SelectedItem="{Binding LaunchOptions.Monitor, Mode=TwoWay}"/>
|
||||
</shc:SizeRestrictedContentControl>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsMonitorEnabled, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
</cwc:SettingsExpander.Items>
|
||||
</cwc:SettingsExpander>
|
||||
<cwc:SettingsCard
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsEnabled="{Binding RuntimeOptions.IsElevated}"
|
||||
Visibility="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
<NumberBox
|
||||
MinWidth="156"
|
||||
Padding="10,8,0,0"
|
||||
Maximum="720"
|
||||
Minimum="60"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Value="{Binding LaunchOptions.TargetFps, Mode=TwoWay}"/>
|
||||
<ToggleSwitch
|
||||
Width="120"
|
||||
IsOn="{Binding LaunchOptions.UnlockFps, Mode=TwoWay}"
|
||||
OffContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOff}"
|
||||
OnContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOn}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
|
||||
<!-- 进程间 -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageLaunchGameInterProcessHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.UseStarwardPlayTimeStatistics, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard
|
||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityDescription}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}">
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.SetDiscordActivityWhenPlaying, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<shvc:LoadingView IsLoading="{Binding GameResource, Converter={StaticResource EmptyObjectToBoolRevertConverter}}"/>
|
||||
</Grid>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
</PivotItem>
|
||||
<PivotItem Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceHeader}">
|
||||
<Grid>
|
||||
<ScrollViewer Visibility="{Binding GameResource, Converter={StaticResource EmptyObjectToBoolConverter}}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition MaxWidth="1000"/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel>
|
||||
<shvc:LaunchGameResourceExpander
|
||||
Margin="16,16,16,0"
|
||||
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
|
||||
DataContext="{Binding GameResource.PreDownloadGame.Latest, Mode=OneWay}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourcePreDownloadHeader}"
|
||||
Visibility="{Binding FallbackValue={StaticResource VisibilityCollapsed}, Converter={StaticResource EmptyObjectToVisibilityConverter}}"/>
|
||||
<ItemsControl
|
||||
Margin="0,0,0,0"
|
||||
ItemTemplate="{StaticResource GameResourceTemplate}"
|
||||
ItemsSource="{Binding GameResource.PreDownloadGame.Diffs, Mode=OneWay}"/>
|
||||
<shvc:LaunchGameResourceExpander
|
||||
Margin="16,16,16,0"
|
||||
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
|
||||
DataContext="{Binding GameResource.Game.Latest, Mode=OneWay}"
|
||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameResourceLatestHeader}"/>
|
||||
<ItemsControl
|
||||
Margin="0,0,0,16"
|
||||
ItemTemplate="{StaticResource GameResourceTemplate}"
|
||||
ItemsSource="{Binding GameResource.Game.Diffs, Mode=OneWay}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<shvc:LoadingView IsLoading="{Binding GameResource, Converter={StaticResource EmptyObjectToBoolRevertConverter}}"/>
|
||||
</Grid>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
</Grid>
|
||||
<Grid Visibility="{Binding GamePathSelectedAndValid, Converter={StaticResource BoolToVisibilityRevertConverter}}">
|
||||
<StackPanel
|
||||
MaxWidth="600"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="3">
|
||||
<Border Style="{ThemeResource BorderCardStyle}">
|
||||
<ListView
|
||||
ItemTemplate="{StaticResource GamePathEntryListTemplate}"
|
||||
ItemsSource="{Binding GamePathEntries}"
|
||||
SelectedItem="{Binding SelectedGamePathEntry, Mode=TwoWay}"/>
|
||||
</Border>
|
||||
<cwc:SettingsCard
|
||||
ActionIcon="{shcm:FontIcon Glyph=}"
|
||||
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingSetGamePathAction}"
|
||||
Command="{Binding SetGamePathCommand}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingSetGamePathHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsClickEnabled="True">
|
||||
<cwc:SettingsCard.Description>
|
||||
<StackPanel>
|
||||
<TextBlock Foreground="{ThemeResource SystemErrorTextColor}" Text="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard.Description>
|
||||
</cwc:SettingsCard>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</shc:ScopedPage>
|
||||
@@ -4,6 +4,7 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.Web;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
@@ -88,7 +89,7 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
|
||||
Cookie stokenV1 = Cookie.FromSToken(uid, multiTokenMap[Cookie.STOKEN]);
|
||||
|
||||
await ISupportLoginByWebView
|
||||
.PostHandleCurrentCookieAsync(serviceProvider, stokenV1, true)
|
||||
.PostHandleCurrentCookieAsync(serviceProvider, InputCookie.Create(stokenV1, true, default))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Passport;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
@@ -75,7 +76,7 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
|
||||
Cookie stokenV2 = Cookie.FromLoginResult(loginResultResponse.Data);
|
||||
|
||||
await ISupportLoginByWebView
|
||||
.PostHandleCurrentCookieAsync(serviceProvider, stokenV2, false)
|
||||
.PostHandleCurrentCookieAsync(serviceProvider, InputCookie.Create(stokenV2, false, default))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,20 +307,6 @@
|
||||
</cwc:SettingsExpander>
|
||||
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingGameHeader}"/>
|
||||
<cwc:SettingsCard
|
||||
ActionIcon="{shcm:FontIcon Glyph=}"
|
||||
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingSetGamePathAction}"
|
||||
Command="{Binding SetGamePathCommand}"
|
||||
Header="{shcm:ResourceString Name=ViewPageSettingSetGamePathHeader}"
|
||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||
IsClickEnabled="True">
|
||||
<cwc:SettingsCard.Description>
|
||||
<StackPanel>
|
||||
<TextBlock Foreground="{ThemeResource SystemErrorTextColor}" Text="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}"/>
|
||||
<TextBlock Text="{Binding AppOptions.GamePath}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard.Description>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard
|
||||
ActionIcon="{shcm:FontIcon Glyph=}"
|
||||
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingSetGamePathAction}"
|
||||
@@ -418,13 +404,13 @@
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
Text="{shcm:ResourceString Name=ViewPageSettingFeaturesDangerousHint}"
|
||||
Visibility="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
<TextBlock Text="{shcm:ResourceString Name=ViewPageSettingIsAdvancedLaunchOptionsEnabledDescription}" Visibility="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
Visibility="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
<TextBlock Text="{shcm:ResourceString Name=ViewPageSettingIsAdvancedLaunchOptionsEnabledDescription}" Visibility="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard.Description>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<shvc:Elevation Visibility="{Binding HutaoOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Mode=TwoWay}"/>
|
||||
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard
|
||||
@@ -435,10 +421,7 @@
|
||||
IsClickEnabled="True">
|
||||
<cwc:SettingsCard.Description>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
Text="{shcm:ResourceString Name=ViewPageSettingDangerousHint}"
|
||||
Visibility="{Binding AppOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
<TextBlock Foreground="{ThemeResource SystemFillColorCriticalBrush}" Text="{shcm:ResourceString Name=ViewPageSettingDangerousHint}"/>
|
||||
<TextBlock Text="{shcm:ResourceString Name=ViewPageSettingDeleteUserDescription}"/>
|
||||
</StackPanel>
|
||||
</cwc:SettingsCard.Description>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core;
|
||||
@@ -11,13 +12,15 @@ using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service;
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Service.Game.Configuration;
|
||||
using Snap.Hutao.Service.Game.Locator;
|
||||
using Snap.Hutao.Service.Game.Package;
|
||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
|
||||
@@ -38,7 +41,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
|
||||
private readonly IContentDialogFactory contentDialogFactory;
|
||||
private readonly LaunchStatusOptions launchStatusOptions;
|
||||
private readonly INavigationService navigationService;
|
||||
private readonly IGameLocatorFactory gameLocatorFactory;
|
||||
private readonly IProgressFactory progressFactory;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly ResourceClient resourceClient;
|
||||
@@ -54,6 +57,9 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
private ObservableCollection<GameAccount>? gameAccounts;
|
||||
private GameAccount? selectedGameAccount;
|
||||
private GameResource? gameResource;
|
||||
private bool gamePathSelectedAndValid;
|
||||
private ImmutableList<GamePathEntry> gamePathEntries;
|
||||
private GamePathEntry? selectedGamePathEntry;
|
||||
|
||||
public List<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Get();
|
||||
|
||||
@@ -99,68 +105,132 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
|
||||
public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); }
|
||||
|
||||
protected override async ValueTask<bool> InitializeUIAsync()
|
||||
public bool GamePathSelectedAndValid
|
||||
{
|
||||
if (File.Exists(AppOptions.GamePath))
|
||||
get => gamePathSelectedAndValid;
|
||||
set
|
||||
{
|
||||
try
|
||||
if (SetProperty(ref gamePathSelectedAndValid, value) && value)
|
||||
{
|
||||
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
|
||||
InitializeUICoreAsync().SafeForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ImmutableList<GamePathEntry> GamePathEntries { get => gamePathEntries; set => SetProperty(ref gamePathEntries, value); }
|
||||
|
||||
public GamePathEntry? SelectedGamePathEntry
|
||||
{
|
||||
get => selectedGamePathEntry;
|
||||
set => UpdateSelectedGamePathEntry(value, true);
|
||||
}
|
||||
|
||||
protected override ValueTask<bool> InitializeUIAsync()
|
||||
{
|
||||
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
|
||||
SelectedGamePathEntry = entry;
|
||||
return ValueTask.FromResult(true);
|
||||
}
|
||||
|
||||
private async ValueTask InitializeUICoreAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
|
||||
{
|
||||
ChannelOptions options = gameService.GetChannelOptions();
|
||||
if (string.IsNullOrEmpty(options.ConfigFilePath))
|
||||
{
|
||||
ChannelOptions options = gameService.GetChannelOptions();
|
||||
if (string.IsNullOrEmpty(options.ConfigFilePath))
|
||||
try
|
||||
{
|
||||
try
|
||||
SelectedScheme = KnownSchemes.Single(scheme => scheme.Equals(options));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
if (!IgnoredInvalidChannelOptions.Contains(options))
|
||||
{
|
||||
SelectedScheme = KnownSchemes.Single(scheme => scheme.Equals(options));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
if (!IgnoredInvalidChannelOptions.Contains(options))
|
||||
{
|
||||
// 后台收集
|
||||
throw new NotSupportedException($"不支持的 MultiChannel: {options}");
|
||||
}
|
||||
// 后台收集
|
||||
throw new NotSupportedException($"不支持的 MultiChannel: {options}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath));
|
||||
}
|
||||
|
||||
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
GameAccounts = accounts;
|
||||
|
||||
// Sync uid
|
||||
if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
|
||||
{
|
||||
SelectedGameAccount = GameAccounts.FirstOrDefault(g => g.AttachUid == uid);
|
||||
}
|
||||
|
||||
// Try set to the current account.
|
||||
SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
|
||||
}
|
||||
}
|
||||
catch (UserdataCorruptedException ex)
|
||||
{
|
||||
infoBarService.Error(ex);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
else
|
||||
{
|
||||
infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath));
|
||||
}
|
||||
|
||||
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
GameAccounts = accounts;
|
||||
|
||||
// Sync uid
|
||||
if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
|
||||
{
|
||||
SelectedGameAccount = GameAccounts.FirstOrDefault(g => g.AttachUid == uid);
|
||||
}
|
||||
|
||||
// Try set to the current account.
|
||||
SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (UserdataCorruptedException ex)
|
||||
{
|
||||
infoBarService.Warning(SH.ViewModelLaunchGamePathInvalid);
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
await navigationService
|
||||
.NavigateAsync<View.Page.SettingPage>(INavigationAwaiter.Default, true)
|
||||
.ConfigureAwait(false);
|
||||
infoBarService.Error(ex);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSelectedGamePathEntry(GamePathEntry? value, bool setBack)
|
||||
{
|
||||
if (SetProperty(ref selectedGamePathEntry, value, nameof(SelectedGamePathEntry)) && setBack)
|
||||
{
|
||||
if (IsViewDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
launchOptions.GamePath = value?.Path ?? string.Empty;
|
||||
GamePathSelectedAndValid = File.Exists(launchOptions.GamePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("SetGamePathCommand")]
|
||||
private async Task SetGamePathAsync()
|
||||
{
|
||||
IGameLocator locator = gameLocatorFactory.Create(GameLocationSource.Manual);
|
||||
|
||||
(bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false);
|
||||
if (!isOk)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
return true;
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
try
|
||||
{
|
||||
GamePathEntries = launchOptions.UpdateGamePathAndRefreshEntries(path);
|
||||
}
|
||||
catch (SqliteException ex)
|
||||
{
|
||||
// 文件夹权限不足,无法写入数据库
|
||||
infoBarService.Error(ex, SH.ViewModelSettingSetGamePathDatabaseFailedTitle);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("ResetGamePathCommand")]
|
||||
private void ResetGamePath()
|
||||
{
|
||||
SelectedGamePathEntry = default;
|
||||
}
|
||||
|
||||
[Command("RemoveGamePathEntryCommand")]
|
||||
private void RemoveGamePathEntry(GamePathEntry? entry)
|
||||
{
|
||||
GamePathEntries = launchOptions.RemoveGamePathEntry(entry, out GamePathEntry? selected);
|
||||
SelectedGamePathEntry = selected;
|
||||
}
|
||||
|
||||
[Command("LaunchCommand")]
|
||||
@@ -187,6 +257,12 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
|
||||
UpdateSelectedGamePathEntry(entry, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (SelectedGameAccount is not null)
|
||||
@@ -265,7 +341,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
[Command("OpenScreenshotFolderCommand")]
|
||||
private async Task OpenScreenshotFolderAsync()
|
||||
{
|
||||
string game = appOptions.GamePath;
|
||||
string game = LaunchOptions.GamePath;
|
||||
string? directory = Path.GetDirectoryName(game);
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
string screenshot = Path.Combine(directory, "ScreenShot");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core;
|
||||
@@ -16,6 +15,7 @@ using Snap.Hutao.Factory.Picker;
|
||||
using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Service;
|
||||
using Snap.Hutao.Service.GachaLog.QueryProvider;
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Service.Game.Locator;
|
||||
using Snap.Hutao.Service.Hutao;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
@@ -53,6 +53,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
|
||||
private readonly HutaoUserOptions hutaoUserOptions;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
private readonly LaunchOptions launchOptions;
|
||||
private readonly HotKeyOptions hotKeyOptions;
|
||||
private readonly IUserService userService;
|
||||
private readonly ITaskContext taskContext;
|
||||
@@ -74,6 +75,8 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
|
||||
|
||||
public HotKeyOptions HotKeyOptions { get => hotKeyOptions; }
|
||||
|
||||
public LaunchOptions LaunchOptions { get => launchOptions; }
|
||||
|
||||
public HutaoPassportViewModel Passport { get => hutaoPassportViewModel; }
|
||||
|
||||
public NameValue<BackdropType>? SelectedBackdropType
|
||||
@@ -157,27 +160,6 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
|
||||
await Launcher.LaunchUriAsync(new("ms-windows-store://pdp/?productid=9PH4NXJ2JN52"));
|
||||
}
|
||||
|
||||
[Command("SetGamePathCommand")]
|
||||
private async Task SetGamePathAsync()
|
||||
{
|
||||
IGameLocator locator = gameLocatorFactory.Create(GameLocationSource.Manual);
|
||||
|
||||
(bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false);
|
||||
if (isOk)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
try
|
||||
{
|
||||
AppOptions.GamePath = path;
|
||||
}
|
||||
catch (SqliteException ex)
|
||||
{
|
||||
// 文件夹权限不足,无法写入数据库
|
||||
infoBarService.Error(ex, SH.ViewModelSettingSetGamePathDatabaseFailedTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Command("SetPowerShellPathCommand")]
|
||||
private async Task SetPowerShellPathAsync()
|
||||
{
|
||||
@@ -193,7 +175,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
|
||||
[Command("DeleteGameWebCacheCommand")]
|
||||
private void DeleteGameWebCache()
|
||||
{
|
||||
string gamePath = AppOptions.GamePath;
|
||||
string gamePath = launchOptions.GamePath;
|
||||
|
||||
if (!string.IsNullOrEmpty(gamePath))
|
||||
{
|
||||
|
||||
@@ -55,7 +55,7 @@ internal sealed class User : ObservableObject, IEntityOnly<EntityUser>, IMapping
|
||||
set => SetSelectedUserGameRole(value);
|
||||
}
|
||||
|
||||
public string? Fingerprint { get => inner.Fingerprint; set => inner.Fingerprint = value; }
|
||||
public string? Fingerprint { get => inner.Fingerprint; }
|
||||
|
||||
public Guid InnerId { get => inner.InnerId; }
|
||||
|
||||
@@ -111,4 +111,4 @@ internal sealed class User : ObservableObject, IEntityOnly<EntityUser>, IMapping
|
||||
messenger.Send(Message.UserChangedMessage.CreateOnlyRoleChanged(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserExtension.cs
Normal file
14
src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserExtension.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity.Extension;
|
||||
|
||||
namespace Snap.Hutao.ViewModel.User;
|
||||
|
||||
internal static class UserExtension
|
||||
{
|
||||
public static bool TryUpdateFingerprint(this User user, string? deviceFp)
|
||||
{
|
||||
return user.Entity.TryUpdateFingerprint(deviceFp);
|
||||
}
|
||||
}
|
||||
@@ -52,8 +52,11 @@ internal sealed partial class UserViewModel : ObservableObject
|
||||
get => selectedUser ??= userService.Current;
|
||||
set
|
||||
{
|
||||
// Pre select the chosen role to avoid multiple UserChangedMessage
|
||||
value?.SetSelectedUserGameRole(value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen), false);
|
||||
if (value is { SelectedUserGameRole: null })
|
||||
{
|
||||
// Pre select the chosen role to avoid multiple UserChangedMessage
|
||||
value.SetSelectedUserGameRole(value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen), false);
|
||||
}
|
||||
|
||||
if (SetProperty(ref selectedUser, value))
|
||||
{
|
||||
@@ -87,13 +90,13 @@ internal sealed partial class UserViewModel : ObservableObject
|
||||
|
||||
infoBarService.Success(SH.FormatViewModelUserAdded(uid));
|
||||
break;
|
||||
case UserOptionResult.Incomplete:
|
||||
case UserOptionResult.CookieIncomplete:
|
||||
infoBarService.Information(SH.ViewModelUserIncomplete);
|
||||
break;
|
||||
case UserOptionResult.Invalid:
|
||||
case UserOptionResult.CookieInvalid:
|
||||
infoBarService.Information(SH.ViewModelUserInvalid);
|
||||
break;
|
||||
case UserOptionResult.Updated:
|
||||
case UserOptionResult.CookieUpdated:
|
||||
infoBarService.Success(SH.FormatViewModelUserUpdated(uid));
|
||||
break;
|
||||
default:
|
||||
@@ -118,16 +121,16 @@ internal sealed partial class UserViewModel : ObservableObject
|
||||
[Command("AddUserCommand")]
|
||||
private Task AddUserAsync()
|
||||
{
|
||||
return AddUserCoreAsync(false).AsTask();
|
||||
return AddUserByManualInputCookieAsync(false).AsTask();
|
||||
}
|
||||
|
||||
[Command("AddOverseaUserCommand")]
|
||||
private Task AddOverseaUserAsync()
|
||||
{
|
||||
return AddUserCoreAsync(true).AsTask();
|
||||
return AddUserByManualInputCookieAsync(true).AsTask();
|
||||
}
|
||||
|
||||
private async ValueTask AddUserCoreAsync(bool isOversea)
|
||||
private async ValueTask AddUserByManualInputCookieAsync(bool isOversea)
|
||||
{
|
||||
// ContentDialog must be created by main thread.
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
@@ -140,9 +143,7 @@ internal sealed partial class UserViewModel : ObservableObject
|
||||
if (result.TryGetValue(out string rawCookie))
|
||||
{
|
||||
Cookie cookie = Cookie.Parse(rawCookie);
|
||||
|
||||
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(cookie, isOversea).ConfigureAwait(false);
|
||||
|
||||
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(InputCookie.CreateWithDeviceFpInference(cookie, isOversea)).ConfigureAwait(false);
|
||||
await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -150,22 +151,21 @@ internal sealed partial class UserViewModel : ObservableObject
|
||||
[Command("LoginMihoyoUserCommand")]
|
||||
private void LoginMihoyoUser()
|
||||
{
|
||||
if (runtimeOptions.IsWebView2Supported)
|
||||
{
|
||||
navigationService.Navigate<LoginMihoyoUserPage>(INavigationAwaiter.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning(SH.CoreWebView2HelperVersionUndetected);
|
||||
}
|
||||
NavigateToLoginPage<LoginMihoyoUserPage>();
|
||||
}
|
||||
|
||||
[Command("LoginHoyoverseUserCommand")]
|
||||
private void LoginHoyoverseUser()
|
||||
{
|
||||
NavigateToLoginPage<LoginHoyoverseUserPage>();
|
||||
}
|
||||
|
||||
private void NavigateToLoginPage<TPage>()
|
||||
where TPage : Page
|
||||
{
|
||||
if (runtimeOptions.IsWebView2Supported)
|
||||
{
|
||||
navigationService.Navigate<LoginHoyoverseUserPage>(INavigationAwaiter.Default);
|
||||
navigationService.Navigate<TPage>(INavigationAwaiter.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -192,9 +192,7 @@ internal sealed partial class UserViewModel : ObservableObject
|
||||
if (sTokenResponse.IsOk())
|
||||
{
|
||||
Cookie stokenV2 = Cookie.FromLoginResult(sTokenResponse.Data);
|
||||
|
||||
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(stokenV2, false).ConfigureAwait(false);
|
||||
|
||||
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(InputCookie.CreateWithDeviceFpInference(stokenV2, false)).ConfigureAwait(false);
|
||||
await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -202,17 +200,19 @@ internal sealed partial class UserViewModel : ObservableObject
|
||||
[Command("RemoveUserCommand")]
|
||||
private async Task RemoveUserAsync(User? user)
|
||||
{
|
||||
if (user is not null)
|
||||
if (user is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await userService.RemoveUserAsync(user).ConfigureAwait(false);
|
||||
infoBarService.Success(SH.FormatViewModelUserRemoved(user.UserInfo?.Nickname));
|
||||
}
|
||||
catch (UserdataCorruptedException ex)
|
||||
{
|
||||
infoBarService.Error(ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await userService.RemoveUserAsync(user).ConfigureAwait(false);
|
||||
infoBarService.Success(SH.FormatViewModelUserRemoved(user.UserInfo?.Nickname));
|
||||
}
|
||||
catch (UserdataCorruptedException ex)
|
||||
{
|
||||
infoBarService.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,44 +243,42 @@ internal sealed partial class UserViewModel : ObservableObject
|
||||
[Command("RefreshCookieTokenCommand")]
|
||||
private async Task RefreshCookieTokenAsync()
|
||||
{
|
||||
if (SelectedUser is not null)
|
||||
if (SelectedUser is null)
|
||||
{
|
||||
if (await userService.RefreshCookieTokenAsync(SelectedUser).ConfigureAwait(false))
|
||||
{
|
||||
infoBarService.Success(SH.ViewUserRefreshCookieTokenSuccess);
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning(SH.ViewUserRefreshCookieTokenWarning);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (await userService.RefreshCookieTokenAsync(SelectedUser).ConfigureAwait(false))
|
||||
{
|
||||
infoBarService.Success(SH.ViewUserRefreshCookieTokenSuccess);
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning(SH.ViewUserRefreshCookieTokenWarning);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("ClaimSignInRewardCommand")]
|
||||
private async Task ClaimSignInRewardAsync(AppBarButton? appBarButton)
|
||||
{
|
||||
if (SelectedUser is not null)
|
||||
if (!UserAndUid.TryFromUser(SelectedUser, out UserAndUid? userAndUid))
|
||||
{
|
||||
if (UserAndUid.TryFromUser(SelectedUser, out UserAndUid? userAndUid))
|
||||
{
|
||||
(bool isOk, string message) = await signInService.ClaimRewardAsync(userAndUid).ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
infoBarService.Success(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
FlyoutBase.ShowAttachedFlyout(appBarButton);
|
||||
infoBarService.Warning(message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning(SH.MustSelectUserAndUid);
|
||||
}
|
||||
infoBarService.Warning(SH.MustSelectUserAndUid);
|
||||
return;
|
||||
}
|
||||
|
||||
(bool isOk, string message) = await signInService.ClaimRewardAsync(userAndUid).ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
infoBarService.Success(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Manual webview
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
FlyoutBase.ShowAttachedFlyout(appBarButton);
|
||||
infoBarService.Warning(message);
|
||||
}
|
||||
|
||||
[Command("OpenDocumentationCommand")]
|
||||
|
||||
@@ -120,9 +120,11 @@ internal static class ApiEndpoints
|
||||
/// <returns>游戏记录实时便笺字符串</returns>
|
||||
public static string GameRecordDailyNote(in PlayerUid uid)
|
||||
{
|
||||
return $"{ApiTakumiRecordApi}/dailyNote?server={uid.Region}&role_id={uid.Value}";
|
||||
return $"{GameRecordDailyNotePath}?server={uid.Region}&role_id={uid.Value}";
|
||||
}
|
||||
|
||||
public const string GameRecordDailyNotePath = $"{ApiTakumiRecordApi}/dailyNote";
|
||||
|
||||
/// <summary>
|
||||
/// 游戏记录主页
|
||||
/// </summary>
|
||||
@@ -130,9 +132,11 @@ internal static class ApiEndpoints
|
||||
/// <returns>游戏记录主页字符串</returns>
|
||||
public static string GameRecordIndex(in PlayerUid uid)
|
||||
{
|
||||
return $"{ApiTakumiRecordApi}/index?server={uid.Region}&role_id={uid.Value}";
|
||||
return $"{GameRecordIndexPath}?server={uid.Region}&role_id={uid.Value}";
|
||||
}
|
||||
|
||||
public const string GameRecordIndexPath = $"{ApiTakumiRecordApi}/index";
|
||||
|
||||
/// <summary>
|
||||
/// 深渊信息
|
||||
/// </summary>
|
||||
@@ -141,8 +145,10 @@ internal static class ApiEndpoints
|
||||
/// <returns>深渊信息字符串</returns>
|
||||
public static string GameRecordSpiralAbyss(Hoyolab.Takumi.GameRecord.SpiralAbyssSchedule scheduleType, in PlayerUid uid)
|
||||
{
|
||||
return $"{ApiTakumiRecordApi}/spiralAbyss?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}";
|
||||
return $"{GameRecordSpiralAbyssPath}?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}";
|
||||
}
|
||||
|
||||
public const string GameRecordSpiralAbyssPath = $"{ApiTakumiRecordApi}/spiralAbyss";
|
||||
#endregion
|
||||
|
||||
#region ApiTakumiEventCalculate
|
||||
|
||||
@@ -14,19 +14,12 @@ internal sealed partial class Cookie
|
||||
{
|
||||
private readonly SortedDictionary<string, string> inner;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个空白的Cookie
|
||||
/// </summary>
|
||||
public Cookie()
|
||||
: this([])
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的Cookie
|
||||
/// </summary>
|
||||
/// <param name="dict">源</param>
|
||||
private Cookie(SortedDictionary<string, string> dict)
|
||||
public Cookie(SortedDictionary<string, string> dict)
|
||||
{
|
||||
inner = dict;
|
||||
}
|
||||
@@ -37,11 +30,6 @@ internal sealed partial class Cookie
|
||||
set => inner[key] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析Cookie字符串
|
||||
/// </summary>
|
||||
/// <param name="cookieString">cookie字符串</param>
|
||||
/// <returns>新的Cookie对象</returns>
|
||||
public static Cookie Parse(string cookieString)
|
||||
{
|
||||
SortedDictionary<string, string> cookieMap = [];
|
||||
@@ -69,11 +57,6 @@ internal sealed partial class Cookie
|
||||
return new(cookieMap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从登录结果创建 Cookie
|
||||
/// </summary>
|
||||
/// <param name="loginResult">登录结果</param>
|
||||
/// <returns>Cookie</returns>
|
||||
public static Cookie FromLoginResult(LoginResult? loginResult)
|
||||
{
|
||||
if (loginResult is null)
|
||||
@@ -91,12 +74,6 @@ internal sealed partial class Cookie
|
||||
return new(cookieMap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 SToken 创建 Cookie
|
||||
/// </summary>
|
||||
/// <param name="stuid">stuid</param>
|
||||
/// <param name="stoken">stoken</param>
|
||||
/// <returns>Cookie</returns>
|
||||
public static Cookie FromSToken(string stuid, string stoken)
|
||||
{
|
||||
SortedDictionary<string, string> cookieMap = new()
|
||||
@@ -113,119 +90,18 @@ internal sealed partial class Cookie
|
||||
return inner.Count <= 0;
|
||||
}
|
||||
|
||||
public bool TryGetLoginTicket([NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
if (TryGetValue(LOGIN_TICKET, out string? loginTicket) && TryGetValue(LOGIN_UID, out string? loginUid))
|
||||
{
|
||||
cookie = new Cookie(new()
|
||||
{
|
||||
[LOGIN_TICKET] = loginTicket,
|
||||
[LOGIN_UID] = loginUid,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cookie = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetSToken(bool isOversea, [NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
return isOversea ? TryGetLegacySToken(out cookie) : TryGetSToken(out cookie);
|
||||
}
|
||||
|
||||
public bool TryGetLToken([NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
if (TryGetValue(LTOKEN, out string? ltoken) && TryGetValue(LTUID, out string? ltuid))
|
||||
{
|
||||
cookie = new Cookie(new()
|
||||
{
|
||||
[LTOKEN] = ltoken,
|
||||
[LTUID] = ltuid,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cookie = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetCookieToken([NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
if (TryGetValue(ACCOUNT_ID, out string? accountId) && TryGetValue(COOKIE_TOKEN, out string? cookieToken))
|
||||
{
|
||||
cookie = new Cookie(new()
|
||||
{
|
||||
[ACCOUNT_ID] = accountId,
|
||||
[COOKIE_TOKEN] = cookieToken,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cookie = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Dictionary{TKey, TValue}.TryGetValue(TKey, out TValue)"/>
|
||||
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
return inner.TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取值
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns>值或默认值</returns>
|
||||
public string? GetValueOrDefault(string key)
|
||||
{
|
||||
return inner.GetValueOrDefault(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为Cookie的字符串表示
|
||||
/// </summary>
|
||||
/// <returns>Cookie的字符串表示</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Join(';', inner.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
||||
}
|
||||
|
||||
private bool TryGetSToken([NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
if (TryGetValue(MID, out string? mid) && TryGetValue(STOKEN, out string? stoken) && TryGetValue(STUID, out string? stuid))
|
||||
{
|
||||
cookie = new Cookie(new()
|
||||
{
|
||||
[MID] = mid,
|
||||
[STOKEN] = stoken,
|
||||
[STUID] = stuid,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cookie = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetLegacySToken([NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
if (TryGetValue(STOKEN, out string? stoken) && TryGetValue(STUID, out string? stuid))
|
||||
{
|
||||
cookie = new Cookie(new()
|
||||
{
|
||||
[STOKEN] = stoken,
|
||||
[STUID] = stuid,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cookie = null;
|
||||
return false;
|
||||
return inner.JoinToString(';', (builder, key, value) => builder.Append(key).Append('=').Append(value));
|
||||
}
|
||||
}
|
||||
68
src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/CookieExtension.cs
Normal file
68
src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/CookieExtension.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
[SuppressMessage("", "SA1310")]
|
||||
internal static class CookieExtension
|
||||
{
|
||||
private const string LOGIN_TICKET = "login_ticket";
|
||||
private const string LOGIN_UID = "login_uid";
|
||||
private const string ACCOUNT_ID = "account_id";
|
||||
private const string COOKIE_TOKEN = "cookie_token";
|
||||
private const string LTOKEN = "ltoken";
|
||||
private const string LTUID = "ltuid";
|
||||
private const string MID = "mid";
|
||||
private const string STOKEN = "stoken";
|
||||
private const string STUID = "stuid";
|
||||
private const string DEVICEFP = "DEVICEFP";
|
||||
|
||||
public static bool TryGetLoginTicket(this Cookie source, [NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
return source.TryGetValuesToCookie([LOGIN_TICKET, LOGIN_UID], out cookie);
|
||||
}
|
||||
|
||||
public static bool TryGetSToken(this Cookie source, bool isOversea, [NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
return isOversea
|
||||
? source.TryGetValuesToCookie([STOKEN, STUID], out cookie)
|
||||
: source.TryGetValuesToCookie([MID, STOKEN, STUID], out cookie);
|
||||
}
|
||||
|
||||
public static bool TryGetLToken(this Cookie source, [NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
return source.TryGetValuesToCookie([LTOKEN, LTUID], out cookie);
|
||||
}
|
||||
|
||||
public static bool TryGetCookieToken(this Cookie source, [NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
return source.TryGetValuesToCookie([ACCOUNT_ID, COOKIE_TOKEN], out cookie);
|
||||
}
|
||||
|
||||
public static bool TryGetDeviceFp(this Cookie source, [NotNullWhen(true)] out string? deviceFp)
|
||||
{
|
||||
return source.TryGetValue(DEVICEFP, out deviceFp);
|
||||
}
|
||||
|
||||
private static bool TryGetValuesToCookie(this Cookie source, in ReadOnlySpan<string> keys, [NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
Must.Range(keys.Length > 0, "Empty keys is not supported");
|
||||
SortedDictionary<string, string> cookieMap = [];
|
||||
|
||||
foreach (string key in keys)
|
||||
{
|
||||
if (source.TryGetValue(key, out string? value))
|
||||
{
|
||||
cookieMap.TryAdd(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
cookie = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
cookie = new(cookieMap);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ internal static class HoyolabOptions
|
||||
/// </summary>
|
||||
public const string MobileUserAgentOversea = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBSOversea/{SaltConstants.OSVersion}";
|
||||
|
||||
public const string ToolVersion = "v4.2.2-ys";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社设备Id
|
||||
/// </summary>
|
||||
|
||||
@@ -5,7 +5,6 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Web.Hoyolab.Annotation;
|
||||
using Snap.Hutao.Web.Hoyolab.DataSigning;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using Snap.Hutao.Web.Response;
|
||||
|
||||
@@ -25,17 +25,13 @@ internal sealed partial class CardClient
|
||||
private readonly ILogger<CardClient> logger;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// 注册验证码
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>注册结果</returns>
|
||||
public async ValueTask<Response<VerificationRegistration>> CreateVerificationAsync(User user, CancellationToken token)
|
||||
public async ValueTask<Response<VerificationRegistration>> CreateVerificationAsync(User user, CardVerifiationHeaders headers, CancellationToken token)
|
||||
{
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.CardCreateVerification(true))
|
||||
.SetUserCookieAndFpHeader(user, CookieType.LToken)
|
||||
.SetHeader("x-rpc-challenge_game", $"{headers.ChallengeGame}")
|
||||
.SetHeader("x-rpc-challenge_path", headers.ChallengePath)
|
||||
.Get();
|
||||
|
||||
await builder.SignDataAsync(DataSignAlgorithmVersion.Gen2, SaltType.X4, false).ConfigureAwait(false);
|
||||
@@ -47,17 +43,12 @@ internal sealed partial class CardClient
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 二次验证
|
||||
/// </summary>
|
||||
/// <param name="challenge">流水号</param>
|
||||
/// <param name="validate">验证</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>验证结果</returns>
|
||||
public async ValueTask<Response<VerificationResult>> VerifyVerificationAsync(string challenge, string validate, CancellationToken token)
|
||||
public async ValueTask<Response<VerificationResult>> VerifyVerificationAsync(CardVerifiationHeaders headers, string challenge, string validate, CancellationToken token)
|
||||
{
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.CardVerifyVerification)
|
||||
.SetHeader("x-rpc-challenge_game", $"{headers.ChallengeGame}")
|
||||
.SetHeader("x-rpc-challenge_path", headers.ChallengePath)
|
||||
.PostJson(new VerificationData(challenge, validate));
|
||||
|
||||
await builder.SignDataAsync(DataSignAlgorithmVersion.Gen2, SaltType.X4, false).ConfigureAwait(false);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
|
||||
internal sealed class CardVerifiationHeaders
|
||||
{
|
||||
public int ChallengeGame { get; private set; }
|
||||
|
||||
public string ChallengePath { get; private set; } = string.Empty;
|
||||
|
||||
public string Page { get; private set; } = string.Empty;
|
||||
|
||||
public static CardVerifiationHeaders CreateForDailyNote()
|
||||
{
|
||||
return Create(ApiEndpoints.GameRecordDailyNotePath);
|
||||
}
|
||||
|
||||
public static CardVerifiationHeaders CreateForIndex()
|
||||
{
|
||||
return Create(ApiEndpoints.GameRecordIndexPath);
|
||||
}
|
||||
|
||||
public static CardVerifiationHeaders CreateForSpiralAbyss()
|
||||
{
|
||||
return Create(ApiEndpoints.GameRecordSpiralAbyssPath);
|
||||
}
|
||||
|
||||
public static CardVerifiationHeaders CreateForCharacter()
|
||||
{
|
||||
return Create(ApiEndpoints.GameRecordCharacter, $"{HoyolabOptions.ToolVersion}_#/ys/role/all");
|
||||
}
|
||||
|
||||
private static CardVerifiationHeaders Create(string path, string page = $"{HoyolabOptions.ToolVersion}_#/ys")
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ChallengeGame = 2,
|
||||
ChallengePath = path,
|
||||
Page = page,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,12 +28,6 @@ internal sealed partial class GameRecordClient : IGameRecordClient
|
||||
private readonly ILogger<GameRecordClient> logger;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取实时便笺
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>实时便笺</returns>
|
||||
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.X4)]
|
||||
public async ValueTask<Response<DailyNote.DailyNote>> GetDailyNoteAsync(UserAndUid userAndUid, CancellationToken token = default)
|
||||
{
|
||||
@@ -54,9 +48,11 @@ internal sealed partial class GameRecordClient : IGameRecordClient
|
||||
{
|
||||
// Replace message
|
||||
resp.Message = SH.WebDailyNoteVerificationFailed;
|
||||
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
|
||||
|
||||
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is { } challenge)
|
||||
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
|
||||
CardVerifiationHeaders headers = CardVerifiationHeaders.CreateForDailyNote();
|
||||
|
||||
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, headers, token).ConfigureAwait(false) is { } challenge)
|
||||
{
|
||||
HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.GameRecordDailyNote(userAndUid.Uid))
|
||||
@@ -76,19 +72,12 @@ internal sealed partial class GameRecordClient : IGameRecordClient
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取玩家基础信息
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>玩家的基础信息</returns>
|
||||
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.X4)]
|
||||
public async ValueTask<Response<PlayerInfo>> GetPlayerInfoAsync(UserAndUid userAndUid, CancellationToken token = default)
|
||||
{
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.GameRecordIndex(userAndUid.Uid))
|
||||
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
|
||||
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
|
||||
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
|
||||
.Get();
|
||||
|
||||
@@ -103,14 +92,15 @@ internal sealed partial class GameRecordClient : IGameRecordClient
|
||||
{
|
||||
// Replace message
|
||||
resp.Message = SH.WebIndexOrSpiralAbyssVerificationFailed;
|
||||
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
|
||||
|
||||
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is { } challenge)
|
||||
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
|
||||
CardVerifiationHeaders headers = CardVerifiationHeaders.CreateForIndex();
|
||||
|
||||
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, headers, token).ConfigureAwait(false) is { } challenge)
|
||||
{
|
||||
HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.GameRecordIndex(userAndUid.Uid))
|
||||
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
|
||||
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
|
||||
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
|
||||
.SetXrpcChallenge(challenge)
|
||||
.Get();
|
||||
@@ -139,7 +129,6 @@ internal sealed partial class GameRecordClient : IGameRecordClient
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid))
|
||||
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
|
||||
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
|
||||
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
|
||||
.Get();
|
||||
|
||||
@@ -154,14 +143,15 @@ internal sealed partial class GameRecordClient : IGameRecordClient
|
||||
{
|
||||
// Replace message
|
||||
resp.Message = SH.WebIndexOrSpiralAbyssVerificationFailed;
|
||||
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
|
||||
|
||||
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is { } challenge)
|
||||
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
|
||||
CardVerifiationHeaders headers = CardVerifiationHeaders.CreateForSpiralAbyss();
|
||||
|
||||
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, headers, token).ConfigureAwait(false) is { } challenge)
|
||||
{
|
||||
HttpRequestMessageBuilder verifiedbuilder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid))
|
||||
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
|
||||
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
|
||||
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
|
||||
.SetXrpcChallenge(challenge)
|
||||
.Get();
|
||||
@@ -189,7 +179,6 @@ internal sealed partial class GameRecordClient : IGameRecordClient
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.GameRecordRoleBasicInfo(userAndUid.Uid))
|
||||
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
|
||||
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
|
||||
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
|
||||
.Get();
|
||||
|
||||
@@ -215,7 +204,6 @@ internal sealed partial class GameRecordClient : IGameRecordClient
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.GameRecordCharacter)
|
||||
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
|
||||
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
|
||||
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
|
||||
.PostJson(new CharacterData(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id)));
|
||||
|
||||
@@ -230,14 +218,15 @@ internal sealed partial class GameRecordClient : IGameRecordClient
|
||||
{
|
||||
// Replace message
|
||||
resp.Message = SH.WebIndexOrSpiralAbyssVerificationFailed;
|
||||
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
|
||||
|
||||
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is { } challenge)
|
||||
IGeetestCardVerifier verifier = serviceProvider.GetRequiredKeyedService<IGeetestCardVerifier>(GeetestCardVerifierType.Custom);
|
||||
CardVerifiationHeaders headers = CardVerifiationHeaders.CreateForCharacter();
|
||||
|
||||
if (await verifier.TryValidateXrpcChallengeAsync(userAndUid.User, headers, token).ConfigureAwait(false) is { } challenge)
|
||||
{
|
||||
HttpRequestMessageBuilder verifiedBuilder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.GameRecordCharacter)
|
||||
.SetUserCookieAndFpHeader(userAndUid, CookieType.Cookie)
|
||||
.SetHeader("x-rpc-page", "v4.2.2-ys_#/ys/daily")
|
||||
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
|
||||
.SetXrpcChallenge(challenge)
|
||||
.PostJson(new CharacterData(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id)));
|
||||
|
||||
@@ -13,9 +13,9 @@ internal sealed partial class HomaGeetestCardVerifier : IGeetestCardVerifier
|
||||
private readonly CardClient cardClient;
|
||||
private readonly HomaGeetestClient homaGeetestClient;
|
||||
|
||||
public async ValueTask<string?> TryValidateXrpcChallengeAsync(User user, CancellationToken token)
|
||||
public async ValueTask<string?> TryValidateXrpcChallengeAsync(User user, CardVerifiationHeaders headers, CancellationToken token)
|
||||
{
|
||||
Response.Response<VerificationRegistration> registrationResponse = await cardClient.CreateVerificationAsync(user, token).ConfigureAwait(false);
|
||||
Response.Response<VerificationRegistration> registrationResponse = await cardClient.CreateVerificationAsync(user, headers, token).ConfigureAwait(false);
|
||||
if (registrationResponse.IsOk())
|
||||
{
|
||||
VerificationRegistration registration = registrationResponse.Data;
|
||||
@@ -24,7 +24,7 @@ internal sealed partial class HomaGeetestCardVerifier : IGeetestCardVerifier
|
||||
|
||||
if (response is { Code: 0, Data.Validate: string validate })
|
||||
{
|
||||
Response.Response<VerificationResult> verifyResponse = await cardClient.VerifyVerificationAsync(registration.Challenge, validate, token).ConfigureAwait(false);
|
||||
Response.Response<VerificationResult> verifyResponse = await cardClient.VerifyVerificationAsync(headers, registration.Challenge, validate, token).ConfigureAwait(false);
|
||||
if (verifyResponse.IsOk())
|
||||
{
|
||||
VerificationResult result = verifyResponse.Data;
|
||||
|
||||
@@ -7,5 +7,5 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Verification;
|
||||
|
||||
internal interface IGeetestCardVerifier
|
||||
{
|
||||
ValueTask<string?> TryValidateXrpcChallengeAsync(User user, CancellationToken token);
|
||||
ValueTask<string?> TryValidateXrpcChallengeAsync(User user, CardVerifiationHeaders headers, CancellationToken token);
|
||||
}
|
||||
@@ -9,8 +9,14 @@ namespace Snap.Hutao.Web.Response;
|
||||
[HighQuality]
|
||||
internal enum KnownReturnCode
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户不存在
|
||||
/// </summary>
|
||||
UserNotExist = -20001,
|
||||
|
||||
/// <summary>
|
||||
/// 无效请求
|
||||
/// 因战绩功能服务优化升级,V2.10及以下版本将无法正常使用战绩功能,请更新米游社至最新版本再进行使用。
|
||||
/// </summary>
|
||||
InvalidRequest = -10001,
|
||||
|
||||
@@ -120,22 +126,32 @@ internal enum KnownReturnCode
|
||||
LoginStateInvalid = 1004,
|
||||
|
||||
/// <summary>
|
||||
/// 账号有风险
|
||||
/// 实时便笺 账号有风险
|
||||
/// </summary>
|
||||
CODE1034 = 1034,
|
||||
|
||||
/// <summary>
|
||||
/// 请登录
|
||||
/// 实时便笺 当前账号存在风险,暂无数据
|
||||
/// </summary>
|
||||
CODE5003 = 5003,
|
||||
|
||||
/// <summary>
|
||||
/// 请登录 登录后可查看战绩信息
|
||||
/// </summary>
|
||||
PleaseLogin = 10001,
|
||||
|
||||
/// <summary>
|
||||
/// 原神战绩 查看他人战绩次数过多,请休息一会儿再试
|
||||
/// </summary>
|
||||
CODE10101 = 10101,
|
||||
|
||||
/// <summary>
|
||||
/// 数据未公开
|
||||
/// </summary>
|
||||
DataIsNotPublicForTheUser = 10102,
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺
|
||||
/// 实时便笺 你的账号已被封禁,无法查看
|
||||
/// </summary>
|
||||
CODE10103 = 10103,
|
||||
|
||||
@@ -143,4 +159,4 @@ internal enum KnownReturnCode
|
||||
/// 实时便笺
|
||||
/// </summary>
|
||||
CODE10104 = 10104,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user