refactor options

This commit is contained in:
Lightczx
2023-12-06 15:41:13 +08:00
parent 8d7373c6cb
commit a97aa26d79
23 changed files with 360 additions and 486 deletions

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Options;
using Microsoft.Web.WebView2.Core;
using Microsoft.Win32;
using Snap.Hutao.Core.Setting;
@@ -12,28 +11,16 @@ using Windows.Storage;
namespace Snap.Hutao.Core;
/// <summary>
/// 存储环境相关的选项
/// 运行时运算得到的选项,无数据库交互
/// </summary>
[Injection(InjectAs.Singleton)]
internal sealed class RuntimeOptions : IOptions<RuntimeOptions>
internal sealed class RuntimeOptions
{
private readonly ILogger<RuntimeOptions> logger;
private readonly bool isWebView2Supported;
private readonly string webView2Version = SH.CoreWebView2HelperVersionUndetected;
private bool? isElevated;
/// <summary>
/// 构造一个新的胡桃选项
/// </summary>
/// <param name="logger">日志器</param>
public RuntimeOptions(ILogger<RuntimeOptions> logger)
{
this.logger = logger;
AppLaunchTime = DateTimeOffset.UtcNow;
DataFolder = GetDataFolderPath();
@@ -45,117 +32,95 @@ internal sealed class RuntimeOptions : IOptions<RuntimeOptions>
UserAgent = $"Snap Hutao/{Version}";
DeviceId = GetUniqueUserId();
DetectWebView2Environment(ref webView2Version, ref isWebView2Supported);
DetectWebView2Environment(logger, out webView2Version, out isWebView2Supported);
static string GetDataFolderPath()
{
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
if (!string.IsNullOrEmpty(preferredPath))
{
Directory.CreateDirectory(preferredPath);
return preferredPath;
}
// Fallback to MyDocuments
string myDocuments = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
string path = Path.GetFullPath(Path.Combine(myDocuments, folderName));
Directory.CreateDirectory(path);
return path;
}
static string GetUniqueUserId()
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
return Convert.ToMd5HexString($"{userName}{machineGuid}");
}
static void DetectWebView2Environment(ILogger<RuntimeOptions> logger, out string webView2Version, out bool isWebView2Supported)
{
try
{
webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString();
isWebView2Supported = true;
}
catch (FileNotFoundException ex)
{
webView2Version = SH.CoreWebView2HelperVersionUndetected;
isWebView2Supported = false;
logger.LogError(ex, "WebView2 Runtime not installed.");
}
}
}
/// <summary>
/// 当前版本
/// </summary>
public Version Version { get; }
/// <summary>
/// 标准UA
/// </summary>
public string UserAgent { get; }
/// <summary>
/// 安装位置
/// </summary>
public string InstalledLocation { get; }
/// <summary>
/// 数据文件夹路径
/// </summary>
public string DataFolder { get; }
/// <summary>
/// 本地缓存
/// </summary>
public string LocalCache { get; }
/// <summary>
/// 包家族名称
/// </summary>
public string FamilyName { get; }
/// <summary>
/// 设备Id
/// </summary>
public string DeviceId { get; }
/// <summary>
/// WebView2 版本
/// </summary>
public string WebView2Version { get => webView2Version; }
/// <summary>
/// 是否支持 WebView2
/// </summary>
public bool IsWebView2Supported { get => isWebView2Supported; }
/// <summary>
/// 是否为提升的权限
/// </summary>
public bool IsElevated { get => isElevated ??= GetElevated(); }
public bool IsElevated
{
get
{
return isElevated ??= GetElevated();
static bool GetElevated()
{
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
{
return true;
}
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
}
}
public DateTimeOffset AppLaunchTime { get; }
/// <inheritdoc/>
public RuntimeOptions Value { get => this; }
private static string GetDataFolderPath()
{
string preferredPath = LocalSetting.Get(SettingKeys.DataFolderPath, string.Empty);
if (!string.IsNullOrEmpty(preferredPath) && Directory.Exists(preferredPath))
{
return preferredPath;
}
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE
// 将测试版与正式版的文件目录分离
string folderName = Package.Current.PublisherDisplayName == "DGP Studio CI" ? "HutaoAlpha" : "Hutao";
#else
// 使得迁移能正常生成
string folderName = "Hutao";
#endif
string path = Path.GetFullPath(Path.Combine(myDocument, folderName));
Directory.CreateDirectory(path);
return path;
}
private static string GetUniqueUserId()
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\", "MachineGuid", userName);
return Convert.ToMd5HexString($"{userName}{machineGuid}");
}
private static bool GetElevated()
{
if (LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false))
{
return true;
}
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
private void DetectWebView2Environment(ref string webView2Version, ref bool isWebView2Supported)
{
try
{
webView2Version = CoreWebView2Environment.GetAvailableBrowserVersionString();
isWebView2Supported = true;
}
catch (FileNotFoundException ex)
{
logger.LogError(ex, "WebView2 Runtime not installed.");
}
}
}

View File

@@ -11,35 +11,27 @@ internal static class DateTimeOffsetExtension
{
public static readonly DateTimeOffset DatebaseDefaultTime = new(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0));
/// <summary>
/// 从Unix时间戳转换
/// </summary>
/// <param name="timestamp">时间戳</param>
/// <param name="defaultValue">默认值</param>
/// <returns>转换的时间</returns>
public static DateTimeOffset FromUnixTime(long? timestamp, in DateTimeOffset defaultValue)
public static DateTimeOffset UnsafeRelaxedFromUnixTime(long? timestamp, in DateTimeOffset defaultValue)
{
if (timestamp is { } value)
{
try
{
return DateTimeOffset.FromUnixTimeSeconds(value);
}
catch (ArgumentOutOfRangeException)
{
try
{
return DateTimeOffset.FromUnixTimeMilliseconds(value);
}
catch (ArgumentOutOfRangeException)
{
return defaultValue;
}
}
}
else
if (timestamp is not { } value)
{
return defaultValue;
}
try
{
return DateTimeOffset.FromUnixTimeSeconds(value);
}
catch (ArgumentOutOfRangeException)
{
try
{
return DateTimeOffset.FromUnixTimeMilliseconds(value);
}
catch (ArgumentOutOfRangeException)
{
return defaultValue;
}
}
}
}

View File

@@ -17,19 +17,6 @@ internal static partial class EnumerableExtension
return source.ElementAtOrDefault(index) ?? source.LastOrDefault();
}
/// <summary>
/// 如果传入集合不为空则原路返回,
/// 如果传入集合为空返回一个集合的空集
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>源集合或空集</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEnumerable<TSource> EmptyIfNull<TSource>(this IEnumerable<TSource>? source)
{
return source ?? Enumerable.Empty<TSource>();
}
/// <summary>
/// 寻找枚举中唯一的值,找不到时
/// 回退到首个或默认值

View File

@@ -17,4 +17,22 @@ internal static class NullableExtension
value = default;
return false;
}
public static string ToStringOrEmpty<T>(this in T? nullable)
where T : struct
{
string? result = default;
if (nullable.HasValue)
{
result = nullable.Value.ToString();
}
if (string.IsNullOrEmpty(result))
{
result = string.Empty;
}
return result;
}
}

View File

@@ -18,9 +18,9 @@ internal static class SpanExtension
/// <param name="span">Span</param>
/// <returns>最大值的下标</returns>
public static int IndexOfMax<T>(this in ReadOnlySpan<T> span)
where T : INumber<T>
where T : INumber<T>, IMinMaxValue<T>
{
T max = T.Zero;
T max = T.MinValue;
int maxIndex = 0;
for (int i = 0; i < span.Length; i++)
{

View File

@@ -30,7 +30,7 @@ internal sealed class UIAFInfo : IMappingFrom<UIAFInfo, RuntimeOptions>
[JsonIgnore]
public DateTimeOffset ExportDateTime
{
get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
}
/// <summary>

View File

@@ -38,7 +38,7 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, Metadata
[JsonIgnore]
public DateTimeOffset ExportDateTime
{
get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
}
/// <summary>

View File

@@ -35,7 +35,7 @@ internal sealed class UIIFInfo
[JsonIgnore]
public DateTimeOffset ExportDateTime
{
get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
get => DateTimeOffsetExtension.UnsafeRelaxedFromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
}
/// <summary>

View File

@@ -2250,7 +2250,7 @@
<value>设备 ID</value>
</data>
<data name="ViewPageSettingDeviceIpDescription" xml:space="preserve">
<value>IP{0}归属:{1}</value>
<value>IP{0} 归属服务器{1}</value>
</data>
<data name="ViewPageSettingDeviceIpHeader" xml:space="preserve">
<value>设备 IP</value>

View File

@@ -11,25 +11,10 @@ using System.IO;
namespace Snap.Hutao.Service;
/// <summary>
/// 应用程序选项
/// 存储服务相关的选项
/// </summary>
[ConstructorGenerated(CallBaseConstructor = true)]
[Injection(InjectAs.Singleton)]
internal sealed partial class AppOptions : DbStoreOptions
{
private readonly List<NameValue<BackdropType>> supportedBackdropTypesInner = CollectionsNameValue.FromEnum<BackdropType>();
private readonly List<NameValue<CultureInfo>> supportedCulturesInner =
[
ToNameValue(CultureInfo.GetCultureInfo("zh-Hans")),
ToNameValue(CultureInfo.GetCultureInfo("zh-Hant")),
ToNameValue(CultureInfo.GetCultureInfo("en")),
ToNameValue(CultureInfo.GetCultureInfo("ko")),
ToNameValue(CultureInfo.GetCultureInfo("ja")),
];
private string? gamePath;
private string? powerShellPath;
private bool? isEmptyHistoryWishVisible;
@@ -38,75 +23,67 @@ internal sealed partial class AppOptions : DbStoreOptions
private bool? isAdvancedLaunchOptionsEnabled;
private string? geetestCustomCompositeUrl;
/// <summary>
/// 游戏路径
/// </summary>
public string GamePath
{
get => GetOption(ref gamePath, SettingEntry.GamePath);
set => SetOption(ref gamePath, SettingEntry.GamePath, value);
}
/// <summary>
/// PowerShell 路径
/// </summary>
public string PowerShellPath
{
get => GetOption(ref powerShellPath, SettingEntry.PowerShellPath, GetPowerShellLocationOrEmpty);
get
{
return GetOption(ref powerShellPath, SettingEntry.PowerShellPath, GetDefaultPowerShellLocationOrEmpty);
static string GetDefaultPowerShellLocationOrEmpty()
{
string? paths = Environment.GetEnvironmentVariable("Path");
if (!string.IsNullOrEmpty(paths))
{
foreach (StringSegment path in new StringTokenizer(paths, [';']))
{
if (path is { HasValue: true, Length: > 0 })
{
if (path.Value.Contains("WindowsPowerShell", StringComparison.OrdinalIgnoreCase))
{
return Path.Combine(path.Value, "powershell.exe");
}
}
}
}
return string.Empty;
}
}
set => SetOption(ref powerShellPath, SettingEntry.PowerShellPath, value);
}
/// <summary>
/// 游戏路径
/// </summary>
public bool IsEmptyHistoryWishVisible
{
get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible);
set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value);
}
/// <summary>
/// 所有支持的背景样式
/// </summary>
public List<NameValue<BackdropType>> BackdropTypes { get => supportedBackdropTypesInner; }
public List<NameValue<BackdropType>> BackdropTypes { get; } = CollectionsNameValue.FromEnum<BackdropType>();
/// <summary>
/// 背景类型 默认 Mica
/// </summary>
public BackdropType BackdropType
{
get => GetOption(ref backdropType, SettingEntry.SystemBackdropType, v => Enum.Parse<BackdropType>(v), BackdropType.Mica).Value;
set => SetOption(ref backdropType, SettingEntry.SystemBackdropType, value, value => value.ToString()!);
set => SetOption(ref backdropType, SettingEntry.SystemBackdropType, value, value => value.ToStringOrEmpty());
}
/// <summary>
/// 所有支持的语言
/// </summary>
public List<NameValue<CultureInfo>> Cultures { get => supportedCulturesInner; }
public List<NameValue<CultureInfo>> Cultures { get; } = SupportedCultures.Get();
/// <summary>
/// 初始化前的语言
/// 通过设置与获取此属性,就可以获取到与系统同步的语言
/// </summary>
public CultureInfo PreviousCulture { get; set; } = default!;
/// <summary>
/// 当前语言
/// 默认为系统语言
/// </summary>
public CultureInfo CurrentCulture
{
get => GetOption(ref currentCulture, SettingEntry.Culture, CultureInfo.GetCultureInfo, CultureInfo.CurrentCulture);
set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name);
}
/// <summary>
/// 是否启用高级功能
/// DO NOT MOVE TO OTHER CLASS
/// We are binding this property in SettingPage
/// </summary>
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);
}
@@ -117,28 +94,5 @@ internal sealed partial class AppOptions : DbStoreOptions
set => SetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl, value);
}
private static NameValue<CultureInfo> ToNameValue(CultureInfo info)
{
return new(info.NativeName, info);
}
private static string GetPowerShellLocationOrEmpty()
{
string? paths = Environment.GetEnvironmentVariable("Path");
if (!string.IsNullOrEmpty(paths))
{
foreach (StringSegment path in new StringTokenizer(paths, [';']))
{
if (path is { HasValue: true, Length: > 0 })
{
if (path.Value.Contains("WindowsPowerShell", StringComparison.OrdinalIgnoreCase))
{
return Path.Combine(path.Value, "powershell.exe");
}
}
}
}
return string.Empty;
}
internal CultureInfo PreviousCulture { get; set; } = default!;
}

View File

@@ -33,7 +33,7 @@ internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryP
if (query.TryGetValue("auth_appid", out string? appId) && appId is "webview_gacha")
{
string? queryLanguageCode = query["lang"];
if (metadataOptions.IsCurrentLocale(queryLanguageCode))
if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
{
return new(true, new(queryString));
}

View File

@@ -90,7 +90,7 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
NameValueCollection query = HttpUtility.ParseQueryString(result.TrimEnd("#/log"));
string? queryLanguageCode = query["lang"];
if (metadataOptions.IsCurrentLocale(queryLanguageCode))
if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
{
return new(true, new(result));
}

View File

@@ -37,7 +37,7 @@ internal sealed partial class UIGFImportService : IUIGFImportService
// v2.1 only support CHS
if (version is UIGFVersion.Major2Minor2OrLower)
{
if (!metadataOptions.IsCurrentLocale(uigf.Info.Language))
if (!metadataOptions.LanguageCodeFitsCurrentLocale(uigf.Info.Language))
{
string message = SH.FormatServiceGachaUIGFImportLanguageNotMatch(uigf.Info.Language, metadataOptions.LanguageCode);
ThrowHelper.InvalidOperation(message);

View File

@@ -40,10 +40,6 @@ internal sealed class LaunchOptions : DbStoreOptions
private bool? useStarwardPlayTimeStatistics;
private bool? setDiscordActivityWhenPlaying;
/// <summary>
/// 构造一个新的启动游戏选项
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
public LaunchOptions(IServiceProvider serviceProvider)
: base(serviceProvider)
{
@@ -53,47 +49,66 @@ internal sealed class LaunchOptions : DbStoreOptions
InitializeMonitors(Monitors);
InitializeScreenFps(out primaryScreenFps);
static void InitializeMonitors(List<NameValue<int>> monitors)
{
try
{
// This list can't use foreach
// https://github.com/microsoft/CsWinRT/issues/747
IReadOnlyList<DisplayArea> displayAreas = DisplayArea.FindAll();
for (int i = 0; i < displayAreas.Count; i++)
{
DisplayArea displayArea = displayAreas[i];
int index = i + 1;
monitors.Add(new($"{displayArea.DisplayId.Value:X8}:{index}", index));
}
}
catch
{
monitors.Clear();
}
}
static void InitializeScreenFps(out int fps)
{
HDC hDC = default;
try
{
hDC = GetDC(HWND.Null);
fps = GetDeviceCaps(hDC, GET_DEVICE_CAPS_INDEX.VREFRESH);
}
finally
{
_ = ReleaseDC(HWND.Null, hDC);
}
}
}
/// <summary>
/// 是否启用启动参数
/// </summary>
public bool IsEnabled
{
get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true);
set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value);
}
/// <summary>
/// 是否全屏
/// </summary>
public bool IsFullScreen
{
get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen);
set => SetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen, value);
}
/// <summary>
/// 是否无边框
/// </summary>
public bool IsBorderless
{
get => GetOption(ref isBorderless, SettingEntry.LaunchIsBorderless);
set => SetOption(ref isBorderless, SettingEntry.LaunchIsBorderless, value);
}
/// <summary>
/// 是否独占全屏
/// </summary>
public bool IsExclusive
{
get => GetOption(ref isExclusive, SettingEntry.LaunchIsExclusive);
set => SetOption(ref isExclusive, SettingEntry.LaunchIsExclusive, value);
}
/// <summary>
/// 屏幕宽度
/// </summary>
public int ScreenWidth
{
get => GetOption(ref screenWidth, SettingEntry.LaunchScreenWidth, primaryScreenWidth);
@@ -106,9 +121,6 @@ internal sealed class LaunchOptions : DbStoreOptions
set => SetOption(ref isScreenWidthEnabled, SettingEntry.LaunchIsScreenWidthEnabled, value);
}
/// <summary>
/// 屏幕高度
/// </summary>
public int ScreenHeight
{
get => GetOption(ref screenHeight, SettingEntry.LaunchScreenHeight, primaryScreenHeight);
@@ -121,32 +133,20 @@ internal sealed class LaunchOptions : DbStoreOptions
set => SetOption(ref isScreenHeightEnabled, SettingEntry.LaunchIsScreenHeightEnabled, value);
}
/// <summary>
/// 是否全屏
/// </summary>
public bool UnlockFps
{
get => GetOption(ref unlockFps, SettingEntry.LaunchUnlockFps);
set => SetOption(ref unlockFps, SettingEntry.LaunchUnlockFps, value);
}
/// <summary>
/// 目标帧率
/// </summary>
public int TargetFps
{
get => GetOption(ref targetFps, SettingEntry.LaunchTargetFps, primaryScreenFps);
set => SetOption(ref targetFps, SettingEntry.LaunchTargetFps, value);
}
/// <summary>
/// 所有监视器
/// </summary>
public List<NameValue<int>> Monitors { get; } = [];
/// <summary>
/// 目标帧率
/// </summary>
[AllowNull]
public NameValue<int> Monitor
{
@@ -196,31 +196,4 @@ internal sealed class LaunchOptions : DbStoreOptions
get => GetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, true);
set => SetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, value);
}
private static void InitializeMonitors(List<NameValue<int>> monitors)
{
// This list can't use foreach
// https://github.com/microsoft/CsWinRT/issues/747
IReadOnlyList<DisplayArea> displayAreas = DisplayArea.FindAll();
for (int i = 0; i < displayAreas.Count; i++)
{
DisplayArea displayArea = displayAreas[i];
int index = i + 1;
monitors.Add(new($"{displayArea.DisplayId.Value:X8}:{index}", index));
}
}
private static void InitializeScreenFps(out int fps)
{
HDC hDC = default;
try
{
hDC = GetDC(HWND.Null);
fps = GetDeviceCaps(hDC, GET_DEVICE_CAPS_INDEX.VREFRESH);
}
finally
{
_ = ReleaseDC(HWND.Null, hDC);
}
}
}

View File

@@ -2,141 +2,38 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.Options;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Web.Hutao;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Snap.Hutao.Service.Hutao;
/// <summary>
/// 胡桃用户选项
/// </summary>
[Injection(InjectAs.Singleton)]
internal sealed class HutaoUserOptions : ObservableObject, IOptions<HutaoUserOptions>
internal sealed class HutaoUserOptions : ObservableObject
{
private readonly TaskCompletionSource initializedTaskCompletionSource = new();
private readonly TaskCompletionSource initialization = new();
private string? userName = SH.ViewServiceHutaoUserLoginOrRegisterHint;
private string? token;
private bool isLoggedIn;
private bool isHutaoCloudServiceAllowed;
private bool isCloudServiceAllowed;
private bool isLicensedDeveloper;
private bool isMaintainer;
private string? gachaLogExpireAt;
private string? gachaLogExpireAtSlim;
private bool isMaintainer;
private string? token;
/// <summary>
/// 用户名
/// </summary>
public string? UserName { get => userName; set => SetProperty(ref userName, value); }
/// <summary>
/// 真正的用户名
/// </summary>
public string? ActualUserName { get => IsLoggedIn ? UserName : null; }
/// <summary>
/// 是否已登录
/// </summary>
public bool IsLoggedIn { get => isLoggedIn; set => SetProperty(ref isLoggedIn, value); }
/// <summary>
/// 胡桃云服务是否可用
/// </summary>
public bool IsCloudServiceAllowed { get => isHutaoCloudServiceAllowed; set => SetProperty(ref isHutaoCloudServiceAllowed, value); }
public bool IsCloudServiceAllowed { get => isCloudServiceAllowed; set => SetProperty(ref isCloudServiceAllowed, value); }
/// <summary>
/// 是否为开发者
/// </summary>
public bool IsLicensedDeveloper { get => isLicensedDeveloper; set => SetProperty(ref isLicensedDeveloper, value); }
public bool IsMaintainer { get => isMaintainer; set => SetProperty(ref isMaintainer, value); }
/// <summary>
/// 祈愿记录服务到期时间
/// </summary>
public string? GachaLogExpireAt { get => gachaLogExpireAt; set => SetProperty(ref gachaLogExpireAt, value); }
public string? GachaLogExpireAtSlim { get => gachaLogExpireAtSlim; set => SetProperty(ref gachaLogExpireAtSlim, value); }
/// <inheritdoc/>
public HutaoUserOptions Value { get => this; }
internal string? Token { get => token; set => token = value; }
public async ValueTask<bool> PostLoginSucceedAsync(HutaoPassportClient passportClient, ITaskContext taskContext, string username, string password, string? token)
{
LocalSetting.Set(SettingKeys.PassportUserName, username);
LocalSetting.Set(SettingKeys.PassportPassword, password);
await taskContext.SwitchToMainThreadAsync();
UserName = username;
this.token = token;
IsLoggedIn = true;
initializedTaskCompletionSource.TrySetResult();
await taskContext.SwitchToBackgroundAsync();
Web.Response.Response<UserInfo> userInfoResponse = await passportClient.GetUserInfoAsync(default).ConfigureAwait(false);
if (userInfoResponse.IsOk())
{
await taskContext.SwitchToMainThreadAsync();
UpdateUserInfo(userInfoResponse.Data);
return true;
}
return false;
}
public void LogoutOrUnregister()
{
LocalSetting.Set(SettingKeys.PassportUserName, string.Empty);
LocalSetting.Set(SettingKeys.PassportPassword, string.Empty);
UserName = null;
token = null;
IsLoggedIn = false;
ClearUserInfo();
}
/// <summary>
/// 登录失败
/// </summary>
public void LoginFailed()
{
UserName = SH.ViewServiceHutaoUserLoginFailHint;
initializedTaskCompletionSource.TrySetResult();
}
public void SkipLogin()
{
initializedTaskCompletionSource.TrySetResult();
}
/// <summary>
/// 刷新用户信息
/// </summary>
/// <param name="userInfo">用户信息</param>
public void UpdateUserInfo(UserInfo userInfo)
{
IsLicensedDeveloper = userInfo.IsLicensedDeveloper;
IsMaintainer = userInfo.IsMaintainer;
string unescaped = Regex.Unescape(SH.ServiceHutaoUserGachaLogExpiredAt);
GachaLogExpireAt = string.Format(CultureInfo.CurrentCulture, unescaped, userInfo.GachaLogExpireAt);
GachaLogExpireAtSlim = $"{userInfo.GachaLogExpireAt:yyyy.MM.dd HH:mm:ss}";
IsCloudServiceAllowed = IsLicensedDeveloper || userInfo.GachaLogExpireAt > DateTimeOffset.UtcNow;
}
public async ValueTask<string?> GetTokenAsync()
{
await initializedTaskCompletionSource.Task.ConfigureAwait(false);
return token;
}
private void ClearUserInfo()
{
IsLicensedDeveloper = false;
IsMaintainer = false;
GachaLogExpireAt = null;
GachaLogExpireAtSlim = null;
IsCloudServiceAllowed = false;
}
internal TaskCompletionSource Initialization { get => initialization; }
}

View File

@@ -0,0 +1,88 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Web.Hutao;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Snap.Hutao.Service.Hutao;
internal static class HutaoUserOptionsExtension
{
public static string? GetActualUserName(this HutaoUserOptions options)
{
return options.IsLoggedIn ? options.UserName : null;
}
public static async ValueTask<string?> GetTokenAsync(this HutaoUserOptions options)
{
await options.Initialization.Task.ConfigureAwait(false);
return options.Token;
}
public static async ValueTask<bool> PostLoginSucceedAsync(this HutaoUserOptions options, HutaoPassportClient passportClient, ITaskContext taskContext, string username, string password, string? token)
{
LocalSetting.Set(SettingKeys.PassportUserName, username);
LocalSetting.Set(SettingKeys.PassportPassword, password);
await taskContext.SwitchToMainThreadAsync();
options.UserName = username;
options.Token = token;
options.IsLoggedIn = true;
options.Initialization.TrySetResult();
await taskContext.SwitchToBackgroundAsync();
Web.Response.Response<UserInfo> userInfoResponse = await passportClient.GetUserInfoAsync(default).ConfigureAwait(false);
if (userInfoResponse.IsOk())
{
await taskContext.SwitchToMainThreadAsync();
UpdateUserInfo(options, userInfoResponse.Data);
return true;
}
return false;
static void UpdateUserInfo(HutaoUserOptions options, UserInfo userInfo)
{
options.IsLicensedDeveloper = userInfo.IsLicensedDeveloper;
options.IsMaintainer = userInfo.IsMaintainer;
options.IsCloudServiceAllowed = options.IsLicensedDeveloper || userInfo.GachaLogExpireAt > DateTimeOffset.UtcNow;
string unescaped = Regex.Unescape(SH.ServiceHutaoUserGachaLogExpiredAt);
options.GachaLogExpireAt = string.Format(CultureInfo.CurrentCulture, unescaped, userInfo.GachaLogExpireAt);
options.GachaLogExpireAtSlim = $"{userInfo.GachaLogExpireAt:yyyy.MM.dd HH:mm:ss}";
}
}
public static void PostLogoutOrUnregister(this HutaoUserOptions options)
{
LocalSetting.Set(SettingKeys.PassportUserName, string.Empty);
LocalSetting.Set(SettingKeys.PassportPassword, string.Empty);
options.UserName = null;
options.Token = null;
options.IsLoggedIn = false;
ClearUserInfo(options);
static void ClearUserInfo(HutaoUserOptions options)
{
options.IsLicensedDeveloper = false;
options.IsMaintainer = false;
options.GachaLogExpireAt = null;
options.GachaLogExpireAtSlim = null;
options.IsCloudServiceAllowed = false;
}
}
public static void PostLoginFailed(this HutaoUserOptions options)
{
options.UserName = SH.ViewServiceHutaoUserLoginFailHint;
options.Initialization.TrySetResult();
}
public static void PostLoginSkipped(this HutaoUserOptions options)
{
options.Initialization.TrySetResult();
}
}

View File

@@ -36,7 +36,7 @@ internal sealed partial class HutaoUserService : IHutaoUserService, IHutaoUserSe
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
{
options.SkipLogin();
options.PostLoginSkipped();
}
else
{
@@ -52,7 +52,7 @@ internal sealed partial class HutaoUserService : IHutaoUserService, IHutaoUserSe
else
{
await taskContext.SwitchToMainThreadAsync();
options.LoginFailed();
options.PostLoginFailed();
}
}

View File

@@ -1,19 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Options;
using Snap.Hutao.Core;
using System.Globalization;
using System.IO;
namespace Snap.Hutao.Service.Metadata;
/// <summary>
/// 元数据选项
/// </summary>
[ConstructorGenerated]
[Injection(InjectAs.Singleton)]
internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
internal sealed partial class MetadataOptions
{
private readonly AppOptions appOptions;
private readonly RuntimeOptions hutaoOptions;
@@ -22,9 +17,6 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
private string? fallbackDataFolder;
private string? localizedDataFolder;
/// <summary>
/// 中文数据文件夹
/// </summary>
public string FallbackDataFolder
{
get
@@ -39,9 +31,6 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
}
}
/// <summary>
/// 本地化数据文件夹
/// </summary>
public string LocalizedDataFolder
{
get
@@ -56,17 +45,11 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
}
}
/// <summary>
/// 当前使用的元数据本地化名称
/// </summary>
public string LocaleName
{
get => localeName ??= GetLocaleName(appOptions.CurrentCulture);
get => localeName ??= MetadataOptionsExtension.GetLocaleName(appOptions.CurrentCulture);
}
/// <summary>
/// 当前语言代码
/// </summary>
public string LanguageCode
{
get
@@ -79,63 +62,4 @@ internal sealed partial class MetadataOptions : IOptions<MetadataOptions>
throw new KeyNotFoundException($"Invalid localeName: '{LocaleName}'");
}
}
/// <inheritdoc/>
public MetadataOptions Value { get => this; }
/// <summary>
/// 获取语言名称
/// </summary>
/// <param name="cultureInfo">语言信息</param>
/// <returns>元数据语言名称</returns>
public static string GetLocaleName(CultureInfo cultureInfo)
{
while (true)
{
if (LocaleNames.TryGetLocaleNameFromLanguageName(cultureInfo.Name, out string? localeName))
{
return localeName;
}
else
{
cultureInfo = cultureInfo.Parent;
}
}
}
/// <summary>
/// 检查是否为当前语言名称
/// </summary>
/// <param name="languageCode">语言代码</param>
/// <returns>是否为当前语言名称</returns>
public bool IsCurrentLocale(string? languageCode)
{
if (string.IsNullOrEmpty(languageCode))
{
return false;
}
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageCode);
return GetLocaleName(cultureInfo) == LocaleName;
}
/// <summary>
/// 获取本地的本地化元数据文件
/// </summary>
/// <param name="fileNameWithExtension">文件名</param>
/// <returns>本地的本地化元数据文件</returns>
public string GetLocalizedLocalFile(string fileNameWithExtension)
{
return Path.Combine(LocalizedDataFolder, fileNameWithExtension);
}
/// <summary>
/// 获取服务器上的本地化元数据文件
/// </summary>
/// <param name="fileNameWithExtension">文件名</param>
/// <returns>服务器上的本地化元数据文件</returns>
public string GetLocalizedRemoteFile(string fileNameWithExtension)
{
return Web.HutaoEndpoints.Metadata(LocaleName, fileNameWithExtension);
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Globalization;
using System.IO;
namespace Snap.Hutao.Service.Metadata;
internal static class MetadataOptionsExtension
{
public static string GetLocalizedLocalFile(this MetadataOptions options, string fileNameWithExtension)
{
return Path.Combine(options.LocalizedDataFolder, fileNameWithExtension);
}
public static string GetLocalizedRemoteFile(this MetadataOptions options, string fileNameWithExtension)
{
return Web.HutaoEndpoints.Metadata(options.LocaleName, fileNameWithExtension);
}
public static bool LanguageCodeFitsCurrentLocale(this MetadataOptions options, string? languageCode)
{
if (string.IsNullOrEmpty(languageCode))
{
return false;
}
// We want to make sure code fits in 1 of 15 metadata locales
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageCode);
return GetLocaleName(cultureInfo) == options.LocaleName;
}
internal static string GetLocaleName(CultureInfo cultureInfo)
{
while (true)
{
if (LocaleNames.TryGetLocaleNameFromLanguageName(cultureInfo.Name, out string? localeName))
{
return localeName;
}
else
{
cultureInfo = cultureInfo.Parent;
}
}
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model;
using System.Globalization;
namespace Snap.Hutao.Service;
internal static class SupportedCultures
{
private static readonly List<NameValue<CultureInfo>> Cultures =
[
ToNameValue(CultureInfo.GetCultureInfo("zh-Hans")),
ToNameValue(CultureInfo.GetCultureInfo("zh-Hant")),
ToNameValue(CultureInfo.GetCultureInfo("en")),
ToNameValue(CultureInfo.GetCultureInfo("ko")),
ToNameValue(CultureInfo.GetCultureInfo("ja")),
];
public static List<NameValue<CultureInfo>> Get()
{
return Cultures;
}
private static NameValue<CultureInfo> ToNameValue(CultureInfo info)
{
return new(info.NativeName, info);
}
}

View File

@@ -77,7 +77,7 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
infoBarService.Information(response.GetLocalizationMessageOrMessage());
await taskContext.SwitchToMainThreadAsync();
hutaoUserOptions.LogoutOrUnregister();
hutaoUserOptions.PostLogoutOrUnregister();
}
}
}
@@ -110,7 +110,7 @@ internal sealed partial class HutaoPassportViewModel : Abstraction.ViewModel
[Command("LogoutCommand")]
private void LogoutAsync()
{
hutaoUserOptions.LogoutOrUnregister();
hutaoUserOptions.PostLogoutOrUnregister();
}
[Command("ResetPasswordCommand")]

View File

@@ -8,7 +8,7 @@ internal sealed class IPInformation
public static IPInformation Default { get; } = new()
{
Ip = "Unknown",
Division = "Unknown"
Division = "Unknown",
};
[JsonPropertyName("ip")]

View File

@@ -233,7 +233,7 @@ internal sealed partial class HutaoSpiralAbyssClient
if (spiralAbyssResponse.IsOk())
{
HutaoUserOptions options = serviceProvider.GetRequiredService<HutaoUserOptions>();
return new(userAndUid.Uid.Value, charactersResponse.Data.Avatars, spiralAbyssResponse.Data, options.ActualUserName);
return new(userAndUid.Uid.Value, charactersResponse.Data.Avatars, spiralAbyssResponse.Data, options.GetActualUserName());
}
}
}